diff --git a/.dockerignore b/.dockerignore index 3f674f978db..602ffade5d3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,11 @@ * !invokeai !pyproject.toml +!uv.lock !docker/docker-entrypoint.sh !LICENSE +**/dist **/node_modules **/__pycache__ -**/*.egg-info \ No newline at end of file +**/*.egg-info diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index c6b833cf589..5c04dc964ef 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,5 @@ b3dccfaeb636599c02effc377cdd8a87d658256c 218b6d0546b990fc449c876fb99f44b50c4daa35 +182580ff6970caed400be178c5b888514b75d7f2 +8e9d5c1187b0d36da80571ce4c8ba9b3a37b6c46 +99aac5870e1092b182e6c5f21abcaab6936a4ad1 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index 4c9fc0120e5..6cf175e7c5a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,4 +2,6 @@ # 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 \ No newline at end of file +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 index b979196cc1b..f62b8c90f11 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,32 +1,30 @@ # continuous integration -/.github/workflows/ @lstein @blessedcoolant @hipsterusername @ebr +/.github/workflows/ @lstein @blessedcoolant -# documentation -/docs/ @lstein @blessedcoolant @hipsterusername @Millu -/mkdocs.yml @lstein @blessedcoolant @hipsterusername @Millu +# documentation - anyone with write privileges can review +/docs/ # nodes -/invokeai/app/ @Kyle0654 @blessedcoolant @psychedelicious @brandonrising @hipsterusername +/invokeai/app/ @blessedcoolant @lstein @dunkeroni @JPPhoto # installation and configuration -/pyproject.toml @lstein @blessedcoolant @hipsterusername -/docker/ @lstein @blessedcoolant @hipsterusername @ebr -/scripts/ @ebr @lstein @hipsterusername -/installer/ @lstein @ebr @hipsterusername -/invokeai/assets @lstein @ebr @hipsterusername -/invokeai/configs @lstein @hipsterusername -/invokeai/version @lstein @blessedcoolant @hipsterusername +/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 @psychedelicious @lstein @maryhipp @hipsterusername -/invokeai/backend @blessedcoolant @psychedelicious @lstein @maryhipp @hipsterusername +/invokeai/frontend @blessedcoolant @lstein @dunkeroni # generation, model management, postprocessing -/invokeai/backend @damian0815 @lstein @blessedcoolant @gregghelt2 @StAlKeR7779 @brandonrising @ryanjdick @hipsterusername +/invokeai/backend @lstein @blessedcoolant @dunkeroni @JPPhoto @Pfannkuchensack # front ends -/invokeai/frontend/CLI @lstein @hipsterusername -/invokeai/frontend/install @lstein @ebr @hipsterusername -/invokeai/frontend/merge @lstein @blessedcoolant @hipsterusername -/invokeai/frontend/training @lstein @blessedcoolant @hipsterusername -/invokeai/frontend/web @psychedelicious @blessedcoolant @maryhipp @hipsterusername +/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 index 26e1579f73a..d49271b7d45 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -21,6 +21,20 @@ body: - 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__ @@ -76,8 +90,8 @@ body: attributes: label: Version number description: | - The version of Invoke you have installed. If it is not the latest version, please update and try again to confirm the issue still exists. If you are testing main, please include the commit hash instead. - placeholder: ex. 3.6.1 + 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 @@ -85,17 +99,17 @@ body: id: browser-version attributes: label: Browser - description: Your web browser and version. + description: Your web browser and version, if you do not use the Launcher's provided GUI. placeholder: ex. Firefox 123.0b3 validations: - required: true + required: false - type: textarea id: python-deps attributes: - label: Python dependencies + label: System Information description: | - If the problem occurred during image generation, click the gear icon at the bottom left corner, click "About", click the copy button and then paste here. + Click the gear icon at the bottom left corner, then click "About". Click the copy button and then paste here. validations: required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 306483bfaaf..6febd0917db 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: Project-Documentation - url: https://invoke-ai.github.io/InvokeAI/ + 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 diff --git a/.github/actions/install-frontend-deps/action.yml b/.github/actions/install-frontend-deps/action.yml index 32b49872494..1e6d3e6be80 100644 --- a/.github/actions/install-frontend-deps/action.yml +++ b/.github/actions/install-frontend-deps/action.yml @@ -3,15 +3,15 @@ description: Installs frontend dependencies with pnpm, with caching runs: using: 'composite' steps: - - name: setup node 18 - uses: actions/setup-node@v4 + - name: setup node 22 + uses: actions/setup-node@v6 with: - node-version: '18' + node-version: '22' - name: setup pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v6 with: - version: 8 + version: 10 run_install: false - name: get pnpm store directory @@ -20,7 +20,7 @@ runs: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: setup cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6c8fee470e9..84633de6ce1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,7 +8,7 @@ ## QA Instructions - + ## Merge Plan @@ -18,4 +18,6 @@ - [ ] _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/workflows/build-container.yml b/.github/workflows/build-container.yml index a8bfaa540c1..b5bd8637acd 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -13,6 +13,12 @@ on: 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 @@ -39,27 +45,36 @@ jobs: 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" - sudo swapoff /mnt/swapfile - sudo rm -rf /mnt/swapfile + 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@v3 + uses: actions/checkout@v6 - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v6 with: github-token: ${{ secrets.GITHUB_TOKEN }} images: | ghcr.io/${{ github.repository }} - ${{ env.DOCKERHUB_REPOSITORY }} tags: | type=ref,event=branch type=ref,event=tag @@ -71,50 +86,33 @@ jobs: latest=${{ matrix.gpu-driver == 'cuda' && github.ref == 'refs/heads/main' }} suffix=-${{ matrix.gpu-driver }},onlatest=false - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + 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@v2 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - # - name: Login to Docker Hub - # if: github.event_name != 'pull_request' && vars.DOCKERHUB_REPOSITORY != '' - # uses: docker/login-action@v2 - # with: - # username: ${{ secrets.DOCKERHUB_USERNAME }} - # password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build container timeout-minutes: 40 id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v7 with: context: . file: docker/Dockerfile platforms: ${{ env.PLATFORMS }} - push: ${{ github.ref == 'refs/heads/main' || github.ref_type == 'tag' }} + 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 }} - - # - name: Docker Hub Description - # if: github.ref == 'refs/heads/main' || github.ref == 'refs/tags/*' && vars.DOCKERHUB_REPOSITORY != '' - # uses: peter-evans/dockerhub-description@v3 - # with: - # username: ${{ secrets.DOCKERHUB_USERNAME }} - # password: ${{ secrets.DOCKERHUB_TOKEN }} - # repository: ${{ vars.DOCKERHUB_REPOSITORY }} - # short-description: ${{ github.event.repository.description }} + # 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-installer.yml b/.github/workflows/build-installer.yml deleted file mode 100644 index b517751960c..00000000000 --- a/.github/workflows/build-installer.yml +++ /dev/null @@ -1,45 +0,0 @@ -# Builds and uploads the installer and python build artifacts. - -name: build installer - -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@v4 - - - name: setup python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - 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: create installer - id: create_installer - run: ./create_installer.sh - working-directory: installer - - - name: upload python distribution artifact - uses: actions/upload-artifact@v4 - with: - name: dist - path: ${{ steps.create_installer.outputs.DIST_PATH }} - - - name: upload installer artifact - uses: actions/upload-artifact@v4 - with: - name: installer - path: ${{ steps.create_installer.outputs.INSTALLER_PATH }} 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 index e5acdeab1b9..73d742f3041 100644 --- a/.github/workflows/clean-caches.yml +++ b/.github/workflows/clean-caches.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Cleanup run: | diff --git a/.github/workflows/close-inactive-issues.yml b/.github/workflows/close-inactive-issues.yml index 9636911b2e9..40f75cebb88 100644 --- a/.github/workflows/close-inactive-issues.yml +++ b/.github/workflows/close-inactive-issues.yml @@ -14,7 +14,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v8 + - uses: actions/stale@v10 with: days-before-issue-stale: ${{ env.DAYS_BEFORE_ISSUE_STALE }} days-before-issue-close: ${{ env.DAYS_BEFORE_ISSUE_CLOSE }} @@ -23,6 +23,7 @@ jobs: 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 index da19b74ebcc..b36fbeb650b 100644 --- a/.github/workflows/frontend-checks.yml +++ b/.github/workflows/frontend-checks.yml @@ -39,12 +39,28 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 # expected run time: <2 min steps: - - uses: actions/checkout@v4 + - 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 - uses: tj-actions/changed-files@v42 + # 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: diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 086cff7b3d4..abb1fb8419f 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -39,12 +39,17 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 # expected run time: <2 min steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: check for changed frontend files if: ${{ inputs.always_run != true }} id: changed-files - uses: tj-actions/changed-files@v42 + # 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: diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 1a98512190a..b7689b12021 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: label PRs - uses: actions/labeler@v5 + 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/mkdocs-material.yml b/.github/workflows/mkdocs-material.yml deleted file mode 100644 index 419d87f37bb..00000000000 --- a/.github/workflows/mkdocs-material.yml +++ /dev/null @@ -1,49 +0,0 @@ -# This is a mostly a copy-paste from https://github.com/squidfunk/mkdocs-material/blob/master/docs/publishing-your-site.md - -name: mkdocs - -on: - push: - branches: - - main - workflow_dispatch: - -permissions: - contents: write - -jobs: - deploy: - if: github.event.pull_request.draft == false - runs-on: ubuntu-latest - env: - REPO_URL: '${{ github.server_url }}/${{ github.repository }}' - REPO_NAME: '${{ github.repository }}' - SITE_URL: 'https://${{ github.repository_owner }}.github.io/InvokeAI' - - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: setup python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - cache: pip - cache-dependency-path: pyproject.toml - - - name: set cache id - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - - - name: use cache - uses: actions/cache@v4 - with: - key: mkdocs-material-${{ env.cache_id }} - path: .cache - restore-keys: | - mkdocs-material- - - - name: install dependencies - run: python -m pip install ".[docs]" - - - name: build & deploy - run: mkdocs gh-deploy --force 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 index 94232e15768..b08bc611cec 100644 --- a/.github/workflows/python-checks.yml +++ b/.github/workflows/python-checks.yml @@ -34,16 +34,24 @@ on: 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@v4 + uses: actions/checkout@v6 - name: check for changed python files if: ${{ inputs.always_run != true }} id: changed-files - uses: tj-actions/changed-files@v42 + # 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: @@ -52,25 +60,23 @@ jobs: - '!invokeai/frontend/web/**' - 'tests/**' - - name: setup python + - name: setup uv if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }} - uses: actions/setup-python@v5 + uses: astral-sh/setup-uv@v8.1.0 with: - python-version: '3.10' - cache: pip - cache-dependency-path: pyproject.toml + version: '0.6.10' + enable-cache: true - - name: install ruff + - name: check pypi classifiers if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }} - run: pip install ruff - shell: bash + 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: ruff check --output-format=github . + 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: ruff format --check . + 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 index 8805989a69b..67351c2b387 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -39,28 +39,19 @@ jobs: strategy: matrix: python-version: - - '3.10' - '3.11' + - '3.12' platform: - - linux-cuda-11_7 - - linux-rocm-5_2 - linux-cpu - macos-default - windows-cpu include: - - platform: linux-cuda-11_7 - os: ubuntu-22.04 - github-env: $GITHUB_ENV - - platform: linux-rocm-5_2 - os: ubuntu-22.04 - extra-index-url: 'https://download.pytorch.org/whl/rocm5.2' - github-env: $GITHUB_ENV - platform: linux-cpu - os: ubuntu-22.04 + os: ubuntu-24.04 extra-index-url: 'https://download.pytorch.org/whl/cpu' github-env: $GITHUB_ENV - platform: macos-default - os: macOS-12 + os: macOS-14 github-env: $GITHUB_ENV - platform: windows-cpu os: windows-2022 @@ -70,14 +61,21 @@ jobs: timeout-minutes: 15 # expected run time: 2-6 min, depending on platform env: PIP_USE_PEP517: '1' + steps: - name: checkout - uses: actions/checkout@v4 + # 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 - uses: tj-actions/changed-files@v42 + # 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: @@ -86,21 +84,20 @@ jobs: - '!invokeai/frontend/web/**' - 'tests/**' - - name: setup python + - name: setup uv if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }} - uses: actions/setup-python@v5 + uses: astral-sh/setup-uv@v8.1.0 with: + version: '0.6.10' + enable-cache: true python-version: ${{ matrix.python-version }} - cache: pip - cache-dependency-path: pyproject.toml - name: install dependencies if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }} env: - PIP_EXTRA_INDEX_URL: ${{ matrix.extra-index-url }} - run: > - pip3 install --editable=".[test]" + 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: pytest + run: uv run --no-sync pytest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f09c0b245d..30e87b53dcb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: check python version uses: samuelcolvin/check-python-version@v4 @@ -49,7 +49,7 @@ jobs: always_run: true build: - uses: ./.github/workflows/build-installer.yml + uses: ./.github/workflows/build-wheel.yml publish-testpypi: runs-on: ubuntu-latest @@ -70,7 +70,7 @@ jobs: id-token: write steps: - name: download distribution from build job - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: dist path: dist/ @@ -99,7 +99,7 @@ jobs: id-token: write steps: - name: download distribution from build job - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: dist path: dist/ 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 29d27d78ed5..cc037f09abd 100644 --- a/.gitignore +++ b/.gitignore @@ -79,9 +79,6 @@ instance/ # Scrapy stuff: .scrapy -# Sphinx documentation -docs/_build/ - # PyBuilder .pybuilder/ target/ @@ -145,9 +142,6 @@ ENV/ # Rope project settings .ropeproject -# mkdocs documentation -/site - # mypy .mypy_cache/ .dmypy.json @@ -179,7 +173,9 @@ cython_debug/ # Scratch folder .scratch/ +worktrees/ .vscode/ +.zed/ # source installer files installer/*zip @@ -188,3 +184,9 @@ 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 index 6cff07a959b..f128557bc08 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,21 +4,29 @@ repos: hooks: - id: black name: black - stages: [commit] + stages: [pre-commit] language: system entry: black types: [python] - id: flake8 name: flake8 - stages: [commit] + stages: [pre-commit] language: system entry: flake8 types: [python] - id: isort name: isort - stages: [commit] + stages: [pre-commit] language: system entry: isort - types: [python] \ No newline at end of file + 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/Makefile b/Makefile index e858a89e2b3..9830f7167fc 100644 --- a/Makefile +++ b/Makefile @@ -11,24 +11,29 @@ help: @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 "update-config-docstring Update the app's config docstring so mkdocs can autogenerate it correctly." - @echo "frontend-install Install the pnpm modules needed for the front end" - @echo "frontend-build Build the frontend in order to run on localhost:9090" + @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 "installer-zip Build the installer .zip file for the current version" + @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: - ruff check . --fix - ruff format . + 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 . + ruff format # Runs mypy, using the config in pyproject.toml mypy: @@ -43,10 +48,6 @@ mypy-all: test: pytest ./tests -# Update config docstring -update-config-docstring: - python scripts/update_config_docstring.py - # Install the pnpm modules needed for the front end frontend-install: rm -rf invokeai/frontend/web/node_modules @@ -56,21 +57,52 @@ frontend-install: 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 -# Installer zip file -installer-zip: - cd installer && ./create_installer.sh +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 installer && ./tag_release.sh + 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.md b/README.md index 41de4882ee3..afc0211bd91 100644 --- a/README.md +++ b/README.md @@ -4,38 +4,33 @@ # Invoke - Professional Creative AI Tools for Visual Media -#### To learn more about Invoke, or implement our Business solutions, visit [invoke.com] - [![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] 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. -[Installation and Updates][installation docs] - [Documentation and Tutorials][docs home] - [Bug Reports][github issues] - [Contributing][contributing docs] - -
+- Free to use under a commercially-friendly license +- Download and install on compatible hardware +- Generate, refine, iterate on images, and build workflows ![Highlighted Features - Canvas and Workflows](https://github.com/invoke-ai/InvokeAI/assets/31807370/708f7a82-084f-4860-bfbe-e2588c53548d) -
+--- +> ## 📣 Are you a new or returning InvokeAI user? +> Take our first annual [User's Survey](https://forms.gle/rCE5KuQ7Wfrd1UnS7) -## Quick Start +--- -1. Download and unzip the installer from the bottom of the [latest release][latest release link]. -2. Run the installer script. +# Documentation - - **Windows**: Double-click on the `install.bat` script. - - **macOS**: Open a Terminal window, drag the file `install.sh` from Finder into the Terminal, and press enter. - - **Linux**: Run `install.sh`. +| **Quick Links** | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Installation and Updates][installation docs] - [Documentation and Tutorials][docs home] - [Bug Reports][github issues] - [Contributing][contributing docs] | -3. When prompted, enter a location for the install and select your GPU type. -4. Once the install finishes, find the directory you selected during install. The default location is `C:\Users\Username\invokeai` for Windows or `~/invokeai` for Linux/macOS. -5. Run the launcher script (`invoke.bat` for Windows, `invoke.sh` for macOS and Linux) the same way you ran the installer script in step 2. -6. Select option 1 to start the application. Once it starts up, open your browser and go to . -7. Open the model manager tab to install a starter model and then you'll be ready to generate. +# Installation -More detail, including hardware requirements and manual install instructions, are available in the [installation documentation][installation docs]. +To get started with Invoke, [Download the Launcher](https://github.com/invoke-ai/launcher/releases/latest). ## Troubleshooting, FAQ and Support @@ -57,21 +52,45 @@ The Unified Canvas is a fully integrated canvas implementation with support for ### Workflows & Nodes -Invoke offers a fully featured workflow management solution, enabling users to combine the power of node-based workflows with the easy 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. +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 both ckpt and diffusers models -- SD1.5, SD2.0, and SDXL support +- 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 @@ -87,15 +106,14 @@ Invoke is a combined effort of [passionate and talented people from across the w Original portions of the software are Copyright © 2024 by respective contributors. -[features docs]: https://invoke-ai.github.io/InvokeAI/features/ -[faq]: https://invoke-ai.github.io/InvokeAI/help/FAQ/ -[contributors]: https://invoke-ai.github.io/InvokeAI/other/CONTRIBUTORS/ -[invoke.com]: https://www.invoke.com/about +[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.github.io/InvokeAI -[installation docs]: https://invoke-ai.github.io/InvokeAI/installation/INSTALLATION/ +[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.github.io/InvokeAI/contributing/CONTRIBUTING/ +[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 @@ -114,3 +132,5 @@ Original portions of the software are Copyright © 2024 by respective contributo [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/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/docker/.env.sample b/docker/.env.sample index aeb69bfd27a..7b10af936e1 100644 --- a/docker/.env.sample +++ b/docker/.env.sample @@ -19,8 +19,13 @@ ## INVOKEAI_PORT is the port on which the InvokeAI web interface will be available # INVOKEAI_PORT=9090 -## GPU_DRIVER can be set to either `nvidia` or `rocm` to enable GPU support in the container accordingly. -# GPU_DRIVER=nvidia #| rocm +## 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 index 864bc5eb609..b1b709d54df 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,61 +1,11 @@ # syntax=docker/dockerfile:1.4 -## Builder stage +#### Web UI ------------------------------------ -FROM library/ubuntu:23.04 AS builder - -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,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - apt update && apt-get install -y \ - git \ - python3-venv \ - python3-pip \ - build-essential - -ENV INVOKEAI_SRC=/opt/invokeai -ENV VIRTUAL_ENV=/opt/venv/invokeai - -ENV PATH="$VIRTUAL_ENV/bin:$PATH" -ARG GPU_DRIVER=cuda -ARG TARGETPLATFORM="linux/amd64" -# unused but available -ARG BUILDPLATFORM - -WORKDIR ${INVOKEAI_SRC} - -COPY invokeai ./invokeai -COPY pyproject.toml ./ - -# Editable mode helps use the same image for development: -# the local working copy can be bind-mounted into the image -# at path defined by ${INVOKEAI_SRC} -# NOTE: there are no pytorch builds for arm64 + cuda, only cpu -# x86_64/CUDA is default -RUN --mount=type=cache,target=/root/.cache/pip \ - python3 -m venv ${VIRTUAL_ENV} &&\ - if [ "$TARGETPLATFORM" = "linux/arm64" ] || [ "$GPU_DRIVER" = "cpu" ]; then \ - extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cpu"; \ - elif [ "$GPU_DRIVER" = "rocm" ]; then \ - extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/rocm5.6"; \ - else \ - extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cu121"; \ - fi &&\ - - # xformers + triton fails to install on arm64 - if [ "$GPU_DRIVER" = "cuda" ] && [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - pip install $extra_index_url_arg -e ".[xformers]"; \ - else \ - pip install $extra_index_url_arg -e "."; \ - fi - -# #### Build the Web UI ------------------------------------ - -FROM node:20-slim AS web-builder +FROM docker.io/node:22-slim AS web-builder ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable +RUN corepack use pnpm@10.x && corepack enable WORKDIR /build COPY invokeai/frontend/web/ ./ @@ -63,61 +13,95 @@ RUN --mount=type=cache,target=/pnpm/store \ pnpm install --frozen-lockfile RUN npx vite build -#### Runtime stage --------------------------------------- +## Backend --------------------------------------- -FROM library/ubuntu:23.04 AS runtime +FROM library/ubuntu:24.04 ARG DEBIAN_FRONTEND=noninteractive -ENV PYTHONUNBUFFERED=1 -ENV PYTHONDONTWRITEBYTECODE=1 - -RUN apt update && apt install -y --no-install-recommends \ - git \ - curl \ - vim \ - tmux \ - ncdu \ - iotop \ - bzip2 \ - gosu \ - magic-wormhole \ - libglib2.0-0 \ - libgl1-mesa-glx \ - python3-venv \ - python3-pip \ - build-essential \ - libopencv-dev \ - libstdc++-10-dev &&\ - apt-get clean && apt-get autoclean - - -ENV INVOKEAI_SRC=/opt/invokeai -ENV VIRTUAL_ENV=/opt/venv/invokeai -ENV INVOKEAI_ROOT=/invokeai -ENV INVOKEAI_HOST=0.0.0.0 -ENV INVOKEAI_PORT=9090 -ENV PATH="$VIRTUAL_ENV/bin:$INVOKEAI_SRC:$PATH" -ENV CONTAINER_UID=${CONTAINER_UID:-1000} -ENV CONTAINER_GID=${CONTAINER_GID:-1000} - -# --link requires buldkit w/ dockerfile syntax 1.4 -COPY --link --from=builder ${INVOKEAI_SRC} ${INVOKEAI_SRC} -COPY --link --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} -COPY --link --from=web-builder /build/dist ${INVOKEAI_SRC}/invokeai/frontend/web/dist +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" - -WORKDIR ${INVOKEAI_SRC} + 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 python3 -c "from patchmatch import patch_match" +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 index 9e7ac15145d..b9c7c010f4a 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,41 +1,88 @@ -# InvokeAI Containerized +# Invoke in Docker -All commands should be run within the `docker` directory: `cd docker` +First things first: -## Quickstart :rocket: +- 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. -On a known working Linux+Docker+CUDA (Nvidia) system, execute `./run.sh` in this directory. It will take a few minutes - depending on your internet speed - to install the core models. Once the application starts up, open `http://localhost:9090` in your browser to Invoke! +## Quickstart -For more configuration options (using an AMD GPU, custom root directory location, etc): read on. +No `docker compose`, no persistence, single command, using the official images: -## Detailed setup +**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 builkit is enabled in the Docker daemon settings (`/etc/docker/daemon.json`) +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 continues to work for now. + - 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. - - You may need to install [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) + - [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 +This is done via Docker Desktop preferences. -### Configure Invoke environment +### 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: - a. the desired location of the InvokeAI runtime directory, or - b. an existing, v3.0.0 compatible runtime directory. +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`. The runtime directory will be populated with the base configs and models necessary to start generating. +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 @@ -43,9 +90,9 @@ The runtime directory (holding models and outputs) will be created in the locati - 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 documentation for the most up-to-date instructions for using your GPU with Docker. +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. +To use an AMD GPU, set `GPU_DRIVER=rocm` in your `.env` file before running `./run.sh`. ## Customize @@ -59,30 +106,12 @@ Values are optional, but setting `INVOKEAI_ROOT` is highly recommended. The defa INVOKEAI_ROOT=/Volumes/WorkDrive/invokeai HUGGINGFACE_TOKEN=the_actual_token CONTAINER_UID=1000 -GPU_DRIVER=nvidia +GPU_DRIVER=cuda ``` -Any environment variables supported by InvokeAI can be set here - please see the [Configuration docs](https://invoke-ai.github.io/InvokeAI/features/CONFIGURATION/) for further detail. +Any environment variables supported by InvokeAI can be set here. See the [Configuration docs](https://invoke.ai/configuration/invokeai-yaml/) for further detail. -## Even More Customizing! +--- -See the `docker-compose.yml` file. The `command` instruction can be uncommented and used to run arbitrary startup commands. Some examples below. - -### Reconfigure the runtime directory - -Can be used to download additional models from the supported model list - -In conjunction with `INVOKEAI_ROOT` can be also used to initialize a runtime directory - -```yaml -command: - - invokeai-configure - - --yes -``` - -Or install models: - -```yaml -command: - - invokeai-model-install -``` +[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 index 2ad50e74a17..2e5bc91f260 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,9 +1,7 @@ # Copyright (c) 2023 Eugene Brodsky https://github.com/ebr -version: '3.8' - x-invokeai: &invokeai - image: "local/invokeai:latest" + image: "ghcr.io/invoke-ai/invokeai:latest" build: context: .. dockerfile: docker/Dockerfile @@ -32,7 +30,7 @@ x-invokeai: &invokeai services: - invokeai-nvidia: + invokeai-cuda: <<: *invokeai deploy: resources: @@ -49,8 +47,9 @@ services: invokeai-rocm: <<: *invokeai - devices: - - /dev/kfd:/dev/kfd - - /dev/dri:/dev/dri + 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 index 7fb52f3af9e..12f24a93527 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -16,26 +16,42 @@ set -e -o pipefail 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 + 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}" +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 index d413e53453b..272d4e44a5c 100755 --- a/docker/run.sh +++ b/docker/run.sh @@ -8,11 +8,15 @@ run() { 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)" + profile="$(awk -F '=' '/GPU_DRIVER=/ {print $2}' .env)" - [[ -z "$profile" ]] && profile="nvidia" + # default to 'cuda' profile + [[ -z "$profile" ]] && profile="cuda" local service_name="invokeai-$profile" @@ -26,7 +30,7 @@ run() { printf "%s\n" "starting service $service_name" docker compose --profile "$profile" up -d "$service_name" - docker compose logs -f + docker compose --profile "$profile" logs -f } run 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/CHANGELOG.md b/docs/CHANGELOG.md deleted file mode 100644 index 24bd5ad7dd2..00000000000 --- a/docs/CHANGELOG.md +++ /dev/null @@ -1,815 +0,0 @@ ---- -title: Changelog ---- - -# :octicons-log-16: **Changelog** - -## v2.3.5 (22 May 2023) - -This release (along with the post1 and post2 follow-on releases) expands support for additional LoRA and LyCORIS models, upgrades diffusers versions, and fixes a few bugs. - -### LoRA and LyCORIS Support Improvement - - A number of LoRA/LyCORIS fine-tune files (those which alter the text encoder as well as the unet model) were not having the desired effect in InvokeAI. This bug has now been fixed. Full documentation of LoRA support is available at InvokeAI LoRA Support. - Previously, InvokeAI did not distinguish between LoRA/LyCORIS models based on Stable Diffusion v1.5 vs those based on v2.0 and 2.1, leading to a crash when an incompatible model was loaded. This has now been fixed. In addition, the web pulldown menus for LoRA and Textual Inversion selection have been enhanced to show only those files that are compatible with the currently-selected Stable Diffusion model. - Support for the newer LoKR LyCORIS files has been added. - -### Library Updates and Speed/Reproducibility Advancements -The major enhancement in this version is that NVIDIA users no longer need to decide between speed and reproducibility. Previously, if you activated the Xformers library, you would see improvements in speed and memory usage, but multiple images generated with the same seed and other parameters would be slightly different from each other. This is no longer the case. Relative to 2.3.5 you will see improved performance when running without Xformers, and even better performance when Xformers is activated. In both cases, images generated with the same settings will be identical. - -Here are the new library versions: -Library Version -Torch 2.0.0 -Diffusers 0.16.1 -Xformers 0.0.19 -Compel 1.1.5 -Other Improvements - -### Performance Improvements - - When a model is loaded for the first time, InvokeAI calculates its checksum for incorporation into the PNG metadata. This process could take up to a minute on network-mounted disks and WSL mounts. This release noticeably speeds up the process. - -### Bug Fixes - - The "import models from directory" and "import from URL" functionality in the console-based model installer has now been fixed. - When running the WebUI, we have reduced the number of times that InvokeAI reaches out to HuggingFace to fetch the list of embeddable Textual Inversion models. We have also caught and fixed a problem with the updater not correctly detecting when another instance of the updater is running - - -## v2.3.4 (7 April 2023) - -What's New in 2.3.4 - -This features release adds support for LoRA (Low-Rank Adaptation) and LyCORIS (Lora beYond Conventional) models, as well as some minor bug fixes. -### LoRA and LyCORIS Support - -LoRA files contain fine-tuning weights that enable particular styles, subjects or concepts to be applied to generated images. LyCORIS files are an extended variant of LoRA. InvokeAI supports the most common LoRA/LyCORIS format, which ends in the suffix .safetensors. You will find numerous LoRA and LyCORIS models for download at Civitai, and a small but growing number at Hugging Face. Full documentation of LoRA support is available at InvokeAI LoRA Support.( Pre-release note: this page will only be available after release) - -To use LoRA/LyCORIS models in InvokeAI: - - Download the .safetensors files of your choice and place in /path/to/invokeai/loras. This directory was not present in earlier version of InvokeAI but will be created for you the first time you run the command-line or web client. You can also create the directory manually. - - Add withLora(lora-file,weight) to your prompts. The weight is optional and will default to 1.0. A few examples, assuming that a LoRA file named loras/sushi.safetensors is present: - -family sitting at dinner table eating sushi withLora(sushi,0.9) -family sitting at dinner table eating sushi withLora(sushi, 0.75) -family sitting at dinner table eating sushi withLora(sushi) - -Multiple withLora() prompt fragments are allowed. The weight can be arbitrarily large, but the useful range is roughly 0.5 to 1.0. Higher weights make the LoRA's influence stronger. Negative weights are also allowed, which can lead to some interesting effects. - - Generate as you usually would! If you find that the image is too "crisp" try reducing the overall CFG value or reducing individual LoRA weights. As is the case with all fine-tunes, you'll get the best results when running the LoRA on top of the model similar to, or identical with, the one that was used during the LoRA's training. Don't try to load a SD 1.x-trained LoRA into a SD 2.x model, and vice versa. This will trigger a non-fatal error message and generation will not proceed. - - You can change the location of the loras directory by passing the --lora_directory option to `invokeai. - -### New WebUI LoRA and Textual Inversion Buttons - -This version adds two new web interface buttons for inserting LoRA and Textual Inversion triggers into the prompt as shown in the screenshot below. - -Clicking on one or the other of the buttons will bring up a menu of available LoRA/LyCORIS or Textual Inversion trigger terms. Select a menu item to insert the properly-formatted withLora() or prompt fragment into the positive prompt. The number in parentheses indicates the number of trigger terms currently in the prompt. You may click the button again and deselect the LoRA or trigger to remove it from the prompt, or simply edit the prompt directly. - -Currently terms are inserted into the positive prompt textbox only. However, some textual inversion embeddings are designed to be used with negative prompts. To move a textual inversion trigger into the negative prompt, simply cut and paste it. - -By default the Textual Inversion menu only shows locally installed models found at startup time in /path/to/invokeai/embeddings. However, InvokeAI has the ability to dynamically download and install additional Textual Inversion embeddings from the HuggingFace Concepts Library. You may choose to display the most popular of these (with five or more likes) in the Textual Inversion menu by going to Settings and turning on "Show Textual Inversions from HF Concepts Library." When this option is activated, the locally-installed TI embeddings will be shown first, followed by uninstalled terms from Hugging Face. See The Hugging Face Concepts Library and Importing Textual Inversion files for more information. -### Minor features and fixes - -This release changes model switching behavior so that the command-line and Web UIs save the last model used and restore it the next time they are launched. It also improves the behavior of the installer so that the pip utility is kept up to date. - -### Known Bugs in 2.3.4 - -These are known bugs in the release. - - The Ancestral DPMSolverMultistepScheduler (k_dpmpp_2a) sampler is not yet implemented for diffusers models and will disappear from the WebUI Sampler menu when a diffusers model is selected. - Windows Defender will sometimes raise Trojan or backdoor alerts for the codeformer.pth face restoration model, as well as the CIDAS/clipseg and runwayml/stable-diffusion-v1.5 models. These are false positives and can be safely ignored. InvokeAI performs a malware scan on all models as they are loaded. For additional security, you should use safetensors models whenever they are available. - - -## v2.3.3 (28 March 2023) - -This is a bugfix and minor feature release. -### Bugfixes - -Since version 2.3.2 the following bugs have been fixed: -Bugs - - When using legacy checkpoints with an external VAE, the VAE file is now scanned for malware prior to loading. Previously only the main model weights file was scanned. - Textual inversion will select an appropriate batchsize based on whether xformers is active, and will default to xformers enabled if the library is detected. - The batch script log file names have been fixed to be compatible with Windows. - Occasional corruption of the .next_prefix file (which stores the next output file name in sequence) on Windows systems is now detected and corrected. - Support loading of legacy config files that have no personalization (textual inversion) section. - An infinite loop when opening the developer's console from within the invoke.sh script has been corrected. - Documentation fixes, including a recipe for detecting and fixing problems with the AMD GPU ROCm driver. - -Enhancements - - It is now possible to load and run several community-contributed SD-2.0 based models, including the often-requested "Illuminati" model. - The "NegativePrompts" embedding file, and others like it, can now be loaded by placing it in the InvokeAI embeddings directory. - If no --model is specified at launch time, InvokeAI will remember the last model used and restore it the next time it is launched. - On Linux systems, the invoke.sh launcher now uses a prettier console-based interface. To take advantage of it, install the dialog package using your package manager (e.g. sudo apt install dialog). - When loading legacy models (safetensors/ckpt) you can specify a custom config file and/or a VAE by placing like-named files in the same directory as the model following this example: - -my-favorite-model.ckpt -my-favorite-model.yaml -my-favorite-model.vae.pt # or my-favorite-model.vae.safetensors - -### Known Bugs in 2.3.3 - -These are known bugs in the release. - - The Ancestral DPMSolverMultistepScheduler (k_dpmpp_2a) sampler is not yet implemented for diffusers models and will disappear from the WebUI Sampler menu when a diffusers model is selected. - Windows Defender will sometimes raise Trojan or backdoor alerts for the codeformer.pth face restoration model, as well as the CIDAS/clipseg and runwayml/stable-diffusion-v1.5 models. These are false positives and can be safely ignored. InvokeAI performs a malware scan on all models as they are loaded. For additional security, you should use safetensors models whenever they are available. - - -## v2.3.2 (11 March 2023) -This is a bugfix and minor feature release. - -### Bugfixes - -Since version 2.3.1 the following bugs have been fixed: - - Black images appearing for potential NSFW images when generating with legacy checkpoint models and both --no-nsfw_checker and --ckpt_convert turned on. - Black images appearing when generating from models fine-tuned on Stable-Diffusion-2-1-base. When importing V2-derived models, you may be asked to select whether the model was derived from a "base" model (512 pixels) or the 768-pixel SD-2.1 model. - The "Use All" button was not restoring the Hi-Res Fix setting on the WebUI - When using the model installer console app, models failed to import correctly when importing from directories with spaces in their names. A similar issue with the output directory was also fixed. - Crashes that occurred during model merging. - Restore previous naming of Stable Diffusion base and 768 models. - Upgraded to latest versions of diffusers, transformers, safetensors and accelerate libraries upstream. We hope that this will fix the assertion NDArray > 2**32 issue that MacOS users have had when generating images larger than 768x768 pixels. Please report back. - -As part of the upgrade to diffusers, the location of the diffusers-based models has changed from models/diffusers to models/hub. When you launch InvokeAI for the first time, it will prompt you to OK a one-time move. This should be quick and harmless, but if you have modified your models/diffusers directory in some way, for example using symlinks, you may wish to cancel the migration and make appropriate adjustments. -New "Invokeai-batch" script - -### Invoke AI Batch -2.3.2 introduces a new command-line only script called invokeai-batch that can be used to generate hundreds of images from prompts and settings that vary systematically. This can be used to try the same prompt across multiple combinations of models, steps, CFG settings and so forth. It also allows you to template prompts and generate a combinatorial list like: - -a shack in the mountains, photograph -a shack in the mountains, watercolor -a shack in the mountains, oil painting -a chalet in the mountains, photograph -a chalet in the mountains, watercolor -a chalet in the mountains, oil painting -a shack in the desert, photograph -... - -If you have a system with multiple GPUs, or a single GPU with lots of VRAM, you can parallelize generation across the combinatorial set, reducing wait times and using your system's resources efficiently (make sure you have good GPU cooling). - -To try invokeai-batch out. Launch the "developer's console" using the invoke launcher script, or activate the invokeai virtual environment manually. From the console, give the command invokeai-batch --help in order to learn how the script works and create your first template file for dynamic prompt generation. - - -### Known Bugs in 2.3.2 - -These are known bugs in the release. - - The Ancestral DPMSolverMultistepScheduler (k_dpmpp_2a) sampler is not yet implemented for diffusers models and will disappear from the WebUI Sampler menu when a diffusers model is selected. - Windows Defender will sometimes raise a Trojan alert for the codeformer.pth face restoration model. As far as we have been able to determine, this is a false positive and can be safely whitelisted. - - -## v2.3.1 (22 February 2023) -This is primarily a bugfix release, but it does provide several new features that will improve the user experience. - -### Enhanced support for model management - -InvokeAI now makes it convenient to add, remove and modify models. You can individually import models that are stored on your local system, scan an entire folder and its subfolders for models and import them automatically, and even directly import models from the internet by providing their download URLs. You also have the option of designating a local folder to scan for new models each time InvokeAI is restarted. - -There are three ways of accessing the model management features: - - From the WebUI, click on the cube to the right of the model selection menu. This will bring up a form that allows you to import models individually from your local disk or scan a directory for models to import. - - Using the Model Installer App - -Choose option (5) download and install models from the invoke launcher script to start a new console-based application for model management. You can use this to select from a curated set of starter models, or import checkpoint, safetensors, and diffusers models from a local disk or the internet. The example below shows importing two checkpoint URLs from popular SD sites and a HuggingFace diffusers model using its Repository ID. It also shows how to designate a folder to be scanned at startup time for new models to import. - -Command-line users can start this app using the command invokeai-model-install. - - Using the Command Line Client (CLI) - -The !install_model and !convert_model commands have been enhanced to allow entering of URLs and local directories to scan and import. The first command installs .ckpt and .safetensors files as-is. The second one converts them into the faster diffusers format before installation. - -Internally InvokeAI is able to probe the contents of a .ckpt or .safetensors file to distinguish among v1.x, v2.x and inpainting models. This means that you do not need to include "inpaint" in your model names to use an inpainting model. Note that Stable Diffusion v2.x models will be autoconverted into a diffusers model the first time you use it. - -Please see INSTALLING MODELS for more information on model management. - -### An Improved Installer Experience - -The installer now launches a console-based UI for setting and changing commonly-used startup options: - -After selecting the desired options, the installer installs several support models needed by InvokeAI's face reconstruction and upscaling features and then launches the interface for selecting and installing models shown earlier. At any time, you can edit the startup options by launching invoke.sh/invoke.bat and entering option (6) change InvokeAI startup options - -Command-line users can launch the new configure app using invokeai-configure. - -This release also comes with a renewed updater. To do an update without going through a whole reinstallation, launch invoke.sh or invoke.bat and choose option (9) update InvokeAI . This will bring you to a screen that prompts you to update to the latest released version, to the most current development version, or any released or unreleased version you choose by selecting the tag or branch of the desired version. - -Command-line users can run this interface by typing invokeai-configure - -### Image Symmetry Options - -There are now features to generate horizontal and vertical symmetry during generation. The way these work is to wait until a selected step in the generation process and then to turn on a mirror image effect. In addition to generating some cool images, you can also use this to make side-by-side comparisons of how an image will look with more or fewer steps. Access this option from the WebUI by selecting Symmetry from the image generation settings, or within the CLI by using the options --h_symmetry_time_pct and --v_symmetry_time_pct (these can be abbreviated to --h_sym and --v_sym like all other options). - -### A New Unified Canvas Look - -This release introduces a beta version of the WebUI Unified Canvas. To try it out, open up the settings dialogue in the WebUI (gear icon) and select Use Canvas Beta Layout: - -Refresh the screen and go to to Unified Canvas (left side of screen, third icon from the top). The new layout is designed to provide more space to work in and to keep the image controls close to the image itself: - -Model conversion and merging within the WebUI - -The WebUI now has an intuitive interface for model merging, as well as for permanent conversion of models from legacy .ckpt/.safetensors formats into diffusers format. These options are also available directly from the invoke.sh/invoke.bat scripts. -An easier way to contribute translations to the WebUI - -We have migrated our translation efforts to Weblate, a FOSS translation product. Maintaining the growing project's translations is now far simpler for the maintainers and community. Please review our brief translation guide for more information on how to contribute. -Numerous internal bugfixes and performance issues - -### Bug Fixes -This releases quashes multiple bugs that were reported in 2.3.0. Major internal changes include upgrading to diffusers 0.13.0, and using the compel library for prompt parsing. See Detailed Change Log for a detailed list of bugs caught and squished. -Summary of InvokeAI command line scripts (all accessible via the launcher menu) -Command Description -invokeai Command line interface -invokeai --web Web interface -invokeai-model-install Model installer with console forms-based front end -invokeai-ti --gui Textual inversion, with a console forms-based front end -invokeai-merge --gui Model merging, with a console forms-based front end -invokeai-configure Startup configuration; can also be used to reinstall support models -invokeai-update InvokeAI software updater - -### Known Bugs in 2.3.1 - -These are known bugs in the release. - MacOS users generating 768x768 pixel images or greater using diffusers models may experience a hard crash with assertion NDArray > 2**32 This appears to be an issu... - - - -## v2.3.0 (15 January 2023) - -**Transition to diffusers - -Version 2.3 provides support for both the traditional `.ckpt` weight -checkpoint files as well as the HuggingFace `diffusers` format. This -introduces several changes you should know about. - -1. The models.yaml format has been updated. There are now two - different type of configuration stanza. The traditional ckpt - one will look like this, with a `format` of `ckpt` and a - `weights` field that points to the absolute or ROOTDIR-relative - location of the ckpt file. - - ``` - inpainting-1.5: - description: RunwayML SD 1.5 model optimized for inpainting (4.27 GB) - repo_id: runwayml/stable-diffusion-inpainting - format: ckpt - width: 512 - height: 512 - weights: models/ldm/stable-diffusion-v1/sd-v1-5-inpainting.ckpt - config: configs/stable-diffusion/v1-inpainting-inference.yaml - vae: models/ldm/stable-diffusion-v1/vae-ft-mse-840000-ema-pruned.ckpt - ``` - - A configuration stanza for a diffusers model hosted at HuggingFace will look like this, - with a `format` of `diffusers` and a `repo_id` that points to the - repository ID of the model on HuggingFace: - - ``` - stable-diffusion-2.1: - description: Stable Diffusion version 2.1 diffusers model (5.21 GB) - repo_id: stabilityai/stable-diffusion-2-1 - format: diffusers - ``` - - A configuration stanza for a diffuers model stored locally should - look like this, with a `format` of `diffusers`, but a `path` field - that points at the directory that contains `model_index.json`: - - ``` - waifu-diffusion: - description: Latest waifu diffusion 1.4 - format: diffusers - path: models/diffusers/hakurei-haifu-diffusion-1.4 - ``` - -2. In order of precedence, InvokeAI will now use HF_HOME, then - XDG_CACHE_HOME, then finally default to `ROOTDIR/models` to - store HuggingFace diffusers models. - - Consequently, the format of the models directory has changed to - mimic the HuggingFace cache directory. When HF_HOME and XDG_HOME - are not set, diffusers models are now automatically downloaded - and retrieved from the directory `ROOTDIR/models/diffusers`, - while other models are stored in the directory - `ROOTDIR/models/hub`. This organization is the same as that used - by HuggingFace for its cache management. - - This allows you to share diffusers and ckpt model files easily with - other machine learning applications that use the HuggingFace - libraries. To do this, set the environment variable HF_HOME - before starting up InvokeAI to tell it what directory to - cache models in. To tell InvokeAI to use the standard HuggingFace - cache directory, you would set HF_HOME like this (Linux/Mac): - - `export HF_HOME=~/.cache/huggingface` - - Both HuggingFace and InvokeAI will fall back to the XDG_CACHE_HOME - environment variable if HF_HOME is not set; this path - takes precedence over `ROOTDIR/models` to allow for the same sharing - with other machine learning applications that use HuggingFace - libraries. - -3. If you upgrade to InvokeAI 2.3.* from an earlier version, there - will be a one-time migration from the old models directory format - to the new one. You will see a message about this the first time - you start `invoke.py`. - -4. Both the front end back ends of the model manager have been - rewritten to accommodate diffusers. You can import models using - their local file path, using their URLs, or their HuggingFace - repo_ids. On the command line, all these syntaxes work: - - ``` - !import_model stabilityai/stable-diffusion-2-1-base - !import_model /opt/sd-models/sd-1.4.ckpt - !import_model https://huggingface.co/Fictiverse/Stable_Diffusion_PaperCut_Model/blob/main/PaperCut_v1.ckpt - ``` - -**KNOWN BUGS (15 January 2023) - -1. On CUDA systems, the 768 pixel stable-diffusion-2.0 and - stable-diffusion-2.1 models can only be run as `diffusers` models - when the `xformer` library is installed and configured. Without - `xformers`, InvokeAI returns black images. - -2. Inpainting and outpainting have regressed in quality. - -Both these issues are being actively worked on. - -## v2.2.4 (11 December 2022) - -**the `invokeai` directory** - -Previously there were two directories to worry about, the directory that -contained the InvokeAI source code and the launcher scripts, and the `invokeai` -directory that contained the models files, embeddings, configuration and -outputs. With the 2.2.4 release, this dual system is done away with, and -everything, including the `invoke.bat` and `invoke.sh` launcher scripts, now -live in a directory named `invokeai`. By default this directory is located in -your home directory (e.g. `\Users\yourname` on Windows), but you can select -where it goes at install time. - -After installation, you can delete the install directory (the one that the zip -file creates when it unpacks). Do **not** delete or move the `invokeai` -directory! - -**Initialization file `invokeai/invokeai.init`** - -You can place frequently-used startup options in this file, such as the default -number of steps or your preferred sampler. To keep everything in one place, this -file has now been moved into the `invokeai` directory and is named -`invokeai.init`. - -**To update from Version 2.2.3** - -The easiest route is to download and unpack one of the 2.2.4 installer files. -When it asks you for the location of the `invokeai` runtime directory, respond -with the path to the directory that contains your 2.2.3 `invokeai`. That is, if -`invokeai` lives at `C:\Users\fred\invokeai`, then answer with `C:\Users\fred` -and answer "Y" when asked if you want to reuse the directory. - -The `update.sh` (`update.bat`) script that came with the 2.2.3 source installer -does not know about the new directory layout and won't be fully functional. - -**To update to 2.2.5 (and beyond) there's now an update path** - -As they become available, you can update to more recent versions of InvokeAI -using an `update.sh` (`update.bat`) script located in the `invokeai` directory. -Running it without any arguments will install the most recent version of -InvokeAI. Alternatively, you can get set releases by running the `update.sh` -script with an argument in the command shell. This syntax accepts the path to -the desired release's zip file, which you can find by clicking on the green -"Code" button on this repository's home page. - -**Other 2.2.4 Improvements** - -- Fix InvokeAI GUI initialization by @addianto in #1687 -- fix link in documentation by @lstein in #1728 -- Fix broken link by @ShawnZhong in #1736 -- Remove reference to binary installer by @lstein in #1731 -- documentation fixes for 2.2.3 by @lstein in #1740 -- Modify installer links to point closer to the source installer by @ebr in - #1745 -- add documentation warning about 1650/60 cards by @lstein in #1753 -- Fix Linux source URL in installation docs by @andybearman in #1756 -- Make install instructions discoverable in readme by @damian0815 in #1752 -- typo fix by @ofirkris in #1755 -- Non-interactive model download (support HUGGINGFACE_TOKEN) by @ebr in #1578 -- fix(srcinstall): shell installer - cp scripts instead of linking by @tildebyte - in #1765 -- stability and usage improvements to binary & source installers by @lstein in - #1760 -- fix off-by-one bug in cross-attention-control by @damian0815 in #1774 -- Eventually update APP_VERSION to 2.2.3 by @spezialspezial in #1768 -- invoke script cds to its location before running by @lstein in #1805 -- Make PaperCut and VoxelArt models load again by @lstein in #1730 -- Fix --embedding_directory / --embedding_path not working by @blessedcoolant in - #1817 -- Clean up readme by @hipsterusername in #1820 -- Optimized Docker build with support for external working directory by @ebr in - #1544 -- disable pushing the cloud container by @mauwii in #1831 -- Fix docker push github action and expand with additional metadata by @ebr in - #1837 -- Fix Broken Link To Notebook by @VedantMadane in #1821 -- Account for flat models by @spezialspezial in #1766 -- Update invoke.bat.in isolate environment variables by @lynnewu in #1833 -- Arch Linux Specific PatchMatch Instructions & fixing conda install on linux by - @SammCheese in #1848 -- Make force free GPU memory work in img2img by @addianto in #1844 -- New installer by @lstein - -## v2.2.3 (2 December 2022) - -!!! Note - - This point release removes references to the binary installer from the - installation guide. The binary installer is not stable at the current - time. First time users are encouraged to use the "source" installer as - described in [Installing InvokeAI with the Source Installer](installation/deprecated_documentation/INSTALL_SOURCE.md) - -With InvokeAI 2.2, this project now provides enthusiasts and professionals a -robust workflow solution for creating AI-generated and human facilitated -compositions. Additional enhancements have been made as well, improving safety, -ease of use, and installation. - -Optimized for efficiency, InvokeAI needs only ~3.5GB of VRAM to generate a -512x768 image (and less for smaller images), and is compatible with -Windows/Linux/Mac (M1 & M2). - -You can see the [release video](https://youtu.be/hIYBfDtKaus) here, which -introduces the main WebUI enhancement for version 2.2 - -[The Unified Canvas](features/UNIFIED_CANVAS.md). This new workflow is the -biggest enhancement added to the WebUI to date, and unlocks a stunning amount of -potential for users to create and iterate on their creations. The following -sections describe what's new for InvokeAI. - -## v2.2.2 (30 November 2022) - -!!! note - - The binary installer is not ready for prime time. First time users are recommended to install via the "source" installer accessible through the links at the bottom of this page.**** - -With InvokeAI 2.2, this project now provides enthusiasts and professionals a -robust workflow solution for creating AI-generated and human facilitated -compositions. Additional enhancements have been made as well, improving safety, -ease of use, and installation. - -Optimized for efficiency, InvokeAI needs only ~3.5GB of VRAM to generate a -512x768 image (and less for smaller images), and is compatible with -Windows/Linux/Mac (M1 & M2). - -You can see the [release video](https://youtu.be/hIYBfDtKaus) here, which -introduces the main WebUI enhancement for version 2.2 - -[The Unified Canvas](https://invoke-ai.github.io/InvokeAI/features/UNIFIED_CANVAS/). -This new workflow is the biggest enhancement added to the WebUI to date, and -unlocks a stunning amount of potential for users to create and iterate on their -creations. The following sections describe what's new for InvokeAI. - -## v2.2.0 (2 December 2022) - -With InvokeAI 2.2, this project now provides enthusiasts and professionals a -robust workflow solution for creating AI-generated and human facilitated -compositions. Additional enhancements have been made as well, improving safety, -ease of use, and installation. - -Optimized for efficiency, InvokeAI needs only ~3.5GB of VRAM to generate a -512x768 image (and less for smaller images), and is compatible with -Windows/Linux/Mac (M1 & M2). - -You can see the [release video](https://youtu.be/hIYBfDtKaus) here, which -introduces the main WebUI enhancement for version 2.2 - -[The Unified Canvas](features/UNIFIED_CANVAS.md). This new workflow is the -biggest enhancement added to the WebUI to date, and unlocks a stunning amount of -potential for users to create and iterate on their creations. The following -sections describe what's new for InvokeAI. - -## v2.1.3 (13 November 2022) - -- A choice of installer scripts that automate installation and configuration. - See - [Installation](installation/INSTALLATION.md). -- A streamlined manual installation process that works for both Conda and - PIP-only installs. See - [Manual Installation](installation/020_INSTALL_MANUAL.md). -- The ability to save frequently-used startup options (model to load, steps, - sampler, etc) in a `.invokeai` file. See - [Client](deprecated/CLI.md) -- Support for AMD GPU cards (non-CUDA) on Linux machines. -- Multiple bugs and edge cases squashed. - -## v2.1.0 (2 November 2022) - -- update mac instructions to use invokeai for env name by @willwillems in #1030 -- Update .gitignore by @blessedcoolant in #1040 -- reintroduce fix for m1 from #579 missing after merge by @skurovec in #1056 -- Update Stable_Diffusion_AI_Notebook.ipynb (Take 2) by @ChloeL19 in #1060 -- Print out the device type which is used by @manzke in #1073 -- Hires Addition by @hipsterusername in #1063 -- fix for "1 leaked semaphore objects to clean up at shutdown" on M1 by - @skurovec in #1081 -- Forward dream.py to invoke.py using the same interpreter, add deprecation - warning by @db3000 in #1077 -- fix noisy images at high step counts by @lstein in #1086 -- Generalize facetool strength argument by @db3000 in #1078 -- Enable fast switching among models at the invoke> command line by @lstein in - #1066 -- Fix Typo, committed changing ldm environment to invokeai by @jdries3 in #1095 -- Update generate.py by @unreleased in #1109 -- Update 'ldm' env to 'invokeai' in troubleshooting steps by @19wolf in #1125 -- Fixed documentation typos and resolved merge conflicts by @rupeshs in #1123 -- Fix broken doc links, fix malaprop in the project subtitle by @majick in #1131 -- Only output facetool parameters if enhancing faces by @db3000 in #1119 -- Update gitignore to ignore codeformer weights at new location by - @spezialspezial in #1136 -- fix links to point to invoke-ai.github.io #1117 by @mauwii in #1143 -- Rework-mkdocs by @mauwii in #1144 -- add option to CLI and pngwriter that allows user to set PNG compression level - by @lstein in #1127 -- Fix img2img DDIM index out of bound by @wfng92 in #1137 -- Fix gh actions by @mauwii in #1128 -- update mac instructions to use invokeai for env name by @willwillems in #1030 -- Update .gitignore by @blessedcoolant in #1040 -- reintroduce fix for m1 from #579 missing after merge by @skurovec in #1056 -- Update Stable_Diffusion_AI_Notebook.ipynb (Take 2) by @ChloeL19 in #1060 -- Print out the device type which is used by @manzke in #1073 -- Hires Addition by @hipsterusername in #1063 -- fix for "1 leaked semaphore objects to clean up at shutdown" on M1 by - @skurovec in #1081 -- Forward dream.py to invoke.py using the same interpreter, add deprecation - warning by @db3000 in #1077 -- fix noisy images at high step counts by @lstein in #1086 -- Generalize facetool strength argument by @db3000 in #1078 -- Enable fast switching among models at the invoke> command line by @lstein in - #1066 -- Fix Typo, committed changing ldm environment to invokeai by @jdries3 in #1095 -- Fixed documentation typos and resolved merge conflicts by @rupeshs in #1123 -- Only output facetool parameters if enhancing faces by @db3000 in #1119 -- add option to CLI and pngwriter that allows user to set PNG compression level - by @lstein in #1127 -- Fix img2img DDIM index out of bound by @wfng92 in #1137 -- Add text prompt to inpaint mask support by @lstein in #1133 -- Respect http[s] protocol when making socket.io middleware by @damian0815 in - #976 -- WebUI: Adds Codeformer support by @psychedelicious in #1151 -- Skips normalizing prompts for web UI metadata by @psychedelicious in #1165 -- Add Asymmetric Tiling by @carson-katri in #1132 -- Web UI: Increases max CFG Scale to 200 by @psychedelicious in #1172 -- Corrects color channels in face restoration; Fixes #1167 by @psychedelicious - in #1175 -- Flips channels using array slicing instead of using OpenCV by @psychedelicious - in #1178 -- Fix typo in docs: s/Formally/Formerly by @noodlebox in #1176 -- fix clipseg loading problems by @lstein in #1177 -- Correct color channels in upscale using array slicing by @wfng92 in #1181 -- Web UI: Filters existing images when adding new images; Fixes #1085 by - @psychedelicious in #1171 -- fix a number of bugs in textual inversion by @lstein in #1190 -- Improve !fetch, add !replay command by @ArDiouscuros in #882 -- Fix generation of image with s>1000 by @holstvoogd in #951 -- Web UI: Gallery improvements by @psychedelicious in #1198 -- Update CLI.md by @krummrey in #1211 -- outcropping improvements by @lstein in #1207 -- add support for loading VAE autoencoders by @lstein in #1216 -- remove duplicate fix_func for MPS by @wfng92 in #1210 -- Metadata storage and retrieval fixes by @lstein in #1204 -- nix: add shell.nix file by @Cloudef in #1170 -- Web UI: Changes vite dist asset paths to relative by @psychedelicious in #1185 -- Web UI: Removes isDisabled from PromptInput by @psychedelicious in #1187 -- Allow user to generate images with initial noise as on M1 / mps system by - @ArDiouscuros in #981 -- feat: adding filename format template by @plucked in #968 -- Web UI: Fixes broken bundle by @psychedelicious in #1242 -- Support runwayML custom inpainting model by @lstein in #1243 -- Update IMG2IMG.md by @talitore in #1262 -- New dockerfile - including a build- and a run- script as well as a GH-Action - by @mauwii in #1233 -- cut over from karras to model noise schedule for higher steps by @lstein in - #1222 -- Prompt tweaks by @lstein in #1268 -- Outpainting implementation by @Kyle0654 in #1251 -- fixing aspect ratio on hires by @tjennings in #1249 -- Fix-build-container-action by @mauwii in #1274 -- handle all unicode characters by @damian0815 in #1276 -- adds models.user.yml to .gitignore by @JakeHL in #1281 -- remove debug branch, set fail-fast to false by @mauwii in #1284 -- Protect-secrets-on-pr by @mauwii in #1285 -- Web UI: Adds initial inpainting implementation by @psychedelicious in #1225 -- fix environment-mac.yml - tested on x64 and arm64 by @mauwii in #1289 -- Use proper authentication to download model by @mauwii in #1287 -- Prevent indexing error for mode RGB by @spezialspezial in #1294 -- Integrate sd-v1-5 model into test matrix (easily expandable), remove - unecesarry caches by @mauwii in #1293 -- add --no-interactive to configure_invokeai step by @mauwii in #1302 -- 1-click installer and updater. Uses micromamba to install git and conda into a - contained environment (if necessary) before running the normal installation - script by @cmdr2 in #1253 -- configure_invokeai.py script downloads the weight files by @lstein in #1290 - -## v2.0.1 (13 October 2022) - -- fix noisy images at high step count when using k\* samplers -- dream.py script now calls invoke.py module directly rather than via a new - python process (which could break the environment) - -## v2.0.0 (9 October 2022) - -- `dream.py` script renamed `invoke.py`. A `dream.py` script wrapper remains for - backward compatibility. -- Completely new WebGUI - launch with `python3 scripts/invoke.py --web` -- img2img runs on all k\* samplers -- Support for - [negative prompts](features/PROMPTS.md#negative-and-unconditioned-prompts) -- Support for CodeFormer face reconstruction -- Support for Textual Inversion on Macintoshes -- Support in both WebGUI and CLI for - [post-processing of previously-generated images](features/POSTPROCESS.md) - using facial reconstruction, ESRGAN upscaling, outcropping (similar to DALL-E - infinite canvas), and "embiggen" upscaling. See the `!fix` command. -- New `--hires` option on `invoke>` line allows - [larger images to be created without duplicating elements](deprecated/CLI.md#this-is-an-example-of-txt2img), - at the cost of some performance. -- New `--perlin` and `--threshold` options allow you to add and control - variation during image generation (see - [Thresholding and Perlin Noise Initialization](features/OTHER.md#thresholding-and-perlin-noise-initialization-options)) -- Extensive metadata now written into PNG files, allowing reliable regeneration - of images and tweaking of previous settings. -- Command-line completion in `invoke.py` now works on Windows, Linux and Mac - platforms. -- Improved [command-line completion behavior](deprecated/CLI.md) New commands - added: - - List command-line history with `!history` - - Search command-line history with `!search` - - Clear history with `!clear` -- Deprecated `--full_precision` / `-F`. Simply omit it and `invoke.py` will auto - configure. To switch away from auto use the new flag like - `--precision=float32`. - -## v1.14 (11 September 2022) - -- Memory optimizations for small-RAM cards. 512x512 now possible on 4 GB GPUs. -- Full support for Apple hardware with M1 or M2 chips. -- Add "seamless mode" for circular tiling of image. Generates beautiful effects. - ([prixt](https://github.com/prixt)). -- Inpainting support. -- Improved web server GUI. -- Lots of code and documentation cleanups. - -## v1.13 (3 September 2022) - -- Support image variations (see [VARIATIONS](deprecated/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 invoke.py command line as the default. -- Miscellaneous internal bug and stability fixes. -- Works on M1 Apple hardware. -- Multiple bug 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 invoke.py script. Invoke by adding - --web to the invoke.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 invoke> 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-invoke** 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 invoke> 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 invoke> 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" to load the k_lms - dependencies!!** - ---- - -## v1.01 (21 August 2022) - -- added k_lms sampling. **Please run "conda env update" 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 invoke.py to get - slower but more accurate image generation - ---- - -## Links - -- **[Read Me](index.md)** diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md deleted file mode 100644 index d68cdf98c83..00000000000 --- a/docs/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,128 +0,0 @@ -# Contributor Covenant 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/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/RELEASE.md b/docs/RELEASE.md deleted file mode 100644 index 56d27eb76ce..00000000000 --- a/docs/RELEASE.md +++ /dev/null @@ -1,173 +0,0 @@ -# Release Process - -The app is published in twice, in different build formats. - -- A [PyPI] distribution. This includes both a source distribution and built distribution (a wheel). Users install with `pip install invokeai`. The updater uses this build. -- An installer on the [InvokeAI Releases Page]. This is a zip file with install scripts and a wheel. This is only used for new installs. - -## General Prep - -Make a developer call-out for PRs to merge. Merge and test things out. - -While the release workflow does not include end-to-end tests, it does pause before publishing so you can download and test the final build. - -## 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*`. It doesn't matter if you've prepped a release branch like `release/v3.5.0` or are releasing from `main` - it works the same. - -> Because commits are reference-counted, it is safe to create a release branch, tag it, let the workflow run, then delete the branch. So long as the tag exists, that commit will exist. - -### Triggering the Workflow - -Run `make tag-release` to tag the current commit and kick off the workflow. - -The release may also be dispatched [manually]. - -### Workflow Jobs and Process - -The workflow consists of a number of concurrently-run jobs, and two final publish jobs. - -The publish jobs require manual approval and are only run if the other jobs succeed. - -#### `check-version` Job - -This job checks that the git ref matches the app version. It matches the ref against the `__version__` variable in `invokeai/version/invokeai_version.py`. - -When the workflow is triggered by tag push, the ref is the tag. If the workflow is run manually, the ref is the target selected from the **Use workflow from** dropdown. - -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 - -- **`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) - -> **TODO** We should add `mypy` or `pyright` to the **`check-python`** job. - -> **TODO** We should add an end-to-end test job that generates an image. - -#### `build-installer` Job - -This sets up both python and frontend dependencies and builds the python package. Internally, this runs `installer/create_installer.sh` and uploads two artifacts: - -- **`dist`**: the python distribution, to be published on PyPI -- **`InvokeAI-installer-${VERSION}.zip`**: the installer to be included in the GitHub release - -#### Sanity Check & Smoke Test - -At this point, the release workflow pauses as the remaining publish jobs require approval. Time to test the installer. - -Because the installer pulls from PyPI, and we haven't published to PyPI yet, you will need to install from the wheel: - -- Download and unzip `dist.zip` and the installer from the **Summary** tab of the workflow -- Run the installer script using the `--wheel` CLI arg, pointing at the wheel: - - ```sh - ./install.sh --wheel ../InvokeAI-4.0.0rc6-py3-none-any.whl - ``` - -- Install to a temporary directory so you get the new user experience -- Download a model and generate - -> The same wheel file is bundled in the installer and in the `dist` artifact, which is uploaded to PyPI. You should end up with the exactly the same installation as if the installer got the wheel from PyPI. - -##### 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?). - -Now you can start from the top: - -- Fix the issues and PR the fixes per usual -- Get the PR approved and merged per usual -- Switch to `main` and pull in the fixes -- Run `make tag-release` to move the tag to `HEAD` (which has the fixes) and kick off the release workflow again -- Re-do the sanity check - -#### PyPI Publish Jobs - -The publish jobs will run if any of the previous jobs fail. - -They use [GitHub environments], which are configured as [trusted publishers] on PyPI. - -Both jobs require a maintainer to approve them from the workflow's **Summary** tab. - -- Click the **Review deployments** button -- Select the environment (either `testpypi` or `pypi`) -- 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 @hipsterusername or @lstein, 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. - -If approved and successful, you could try out the test release like this: - -```sh -# Create a new virtual environment -python -m venv ~/.test-invokeai-dist --prompt test-invokeai-dist -# Install the distribution from Test PyPI -pip install --index-url https://test.pypi.org/simple/ invokeai -# Run and test the app -invokeai-web -# Cleanup -deactivate -rm -rf ~/.test-invokeai-dist -``` - -#### `publish-pypi` Job - -Publishes the distribution on the production PyPI index, using the `pypi` GitHub environment. - -## Publish the GitHub Release with installer - -Once the release is published to PyPI, it's time to publish the GitHub release. - -1. [Draft a new release] on GitHub, choosing the tag that triggered the release. -1. Write the release notes, describing important changes. The **Generate release notes** button automatically inserts the changelog and new contributors, and you can copy/paste the intro from previous releases. -1. Use `scripts/get_external_contributions.py` to get a list of external contributions to shout out in the release notes. -1. Upload the zip file created in **`build`** job into the Assets section of the release notes. -1. Check **Set as a pre-release** if it's a pre-release. -1. Check **Create a discussion for this release**. -1. Publish the release. -1. Announce the release in Discord. - -> **TODO** Workflows can create a GitHub release from a template and upload release assets. One popular action to handle this is [ncipollo/release-action]. A future enhancement to the release process could set this up. - -## Manual Build - -The `build installer` workflow can be dispatched manually. This is useful to test the installer for a given branch or tag. - -No checks are run, it just builds. - -## 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. - -[InvokeAI Releases Page]: https://github.com/invoke-ai/InvokeAI/releases -[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/ -[ncipollo/release-action]: https://github.com/ncipollo/release-action -[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/assets/Lincoln-and-Parrot-512-transparent.png b/docs/assets/Lincoln-and-Parrot-512-transparent.png deleted file mode 100755 index 363f3cced3f..00000000000 Binary files a/docs/assets/Lincoln-and-Parrot-512-transparent.png and /dev/null differ diff --git a/docs/assets/Lincoln-and-Parrot-512.png b/docs/assets/Lincoln-and-Parrot-512.png deleted file mode 100644 index acabe0f27c1..00000000000 Binary files a/docs/assets/Lincoln-and-Parrot-512.png and /dev/null differ diff --git a/docs/assets/canvas/biker_granny.png b/docs/assets/canvas/biker_granny.png deleted file mode 100644 index 70385014da8..00000000000 Binary files a/docs/assets/canvas/biker_granny.png and /dev/null differ diff --git a/docs/assets/canvas/biker_jacket_granny.png b/docs/assets/canvas/biker_jacket_granny.png deleted file mode 100644 index 3a46b8a49ce..00000000000 Binary files a/docs/assets/canvas/biker_jacket_granny.png and /dev/null differ diff --git a/docs/assets/canvas/mask_granny.png b/docs/assets/canvas/mask_granny.png deleted file mode 100644 index 041a0317c92..00000000000 Binary files a/docs/assets/canvas/mask_granny.png and /dev/null differ diff --git a/docs/assets/canvas/staging_area.png b/docs/assets/canvas/staging_area.png deleted file mode 100644 index 0e9d4ba0de9..00000000000 Binary files a/docs/assets/canvas/staging_area.png and /dev/null differ diff --git a/docs/assets/canvas_preview.png b/docs/assets/canvas_preview.png deleted file mode 100644 index dba4ee2ca29..00000000000 Binary files a/docs/assets/canvas_preview.png and /dev/null differ diff --git a/docs/assets/colab_notebook.png b/docs/assets/colab_notebook.png deleted file mode 100644 index 933664a86f7..00000000000 Binary files a/docs/assets/colab_notebook.png and /dev/null differ diff --git a/docs/assets/concepts/image1.png b/docs/assets/concepts/image1.png deleted file mode 100644 index f8c93efcf93..00000000000 Binary files a/docs/assets/concepts/image1.png and /dev/null differ diff --git a/docs/assets/concepts/image2.png b/docs/assets/concepts/image2.png deleted file mode 100644 index a22411492e7..00000000000 Binary files a/docs/assets/concepts/image2.png and /dev/null differ diff --git a/docs/assets/concepts/image3.png b/docs/assets/concepts/image3.png deleted file mode 100644 index e2213fc7079..00000000000 Binary files a/docs/assets/concepts/image3.png and /dev/null differ diff --git a/docs/assets/concepts/image4.png b/docs/assets/concepts/image4.png deleted file mode 100644 index 052479019c1..00000000000 Binary files a/docs/assets/concepts/image4.png and /dev/null differ diff --git a/docs/assets/concepts/image5.png b/docs/assets/concepts/image5.png deleted file mode 100644 index f3a4f764705..00000000000 Binary files a/docs/assets/concepts/image5.png and /dev/null differ diff --git a/docs/assets/control-panel-2.png b/docs/assets/control-panel-2.png deleted file mode 100644 index d7767d524a4..00000000000 Binary files a/docs/assets/control-panel-2.png and /dev/null differ diff --git a/docs/assets/dream-py-demo.png b/docs/assets/dream-py-demo.png deleted file mode 100644 index c6945bf07c9..00000000000 Binary files a/docs/assets/dream-py-demo.png and /dev/null differ diff --git a/docs/assets/dream_web_server.png b/docs/assets/dream_web_server.png deleted file mode 100644 index c8ec9756c59..00000000000 Binary files a/docs/assets/dream_web_server.png and /dev/null differ diff --git a/docs/assets/features/restoration-montage.png b/docs/assets/features/restoration-montage.png deleted file mode 100644 index 825a89a8dd2..00000000000 Binary files a/docs/assets/features/restoration-montage.png and /dev/null differ diff --git a/docs/assets/features/upscale-dialog.png b/docs/assets/features/upscale-dialog.png deleted file mode 100644 index fd91f90a65f..00000000000 Binary files a/docs/assets/features/upscale-dialog.png and /dev/null differ diff --git a/docs/assets/features/upscaling-montage.png b/docs/assets/features/upscaling-montage.png deleted file mode 100644 index 6b3eeba3473..00000000000 Binary files a/docs/assets/features/upscaling-montage.png and /dev/null differ diff --git a/docs/assets/img2img/000019.1592514025.png b/docs/assets/img2img/000019.1592514025.png deleted file mode 100644 index 2bc2d63ffa0..00000000000 Binary files a/docs/assets/img2img/000019.1592514025.png and /dev/null differ diff --git a/docs/assets/img2img/000019.steps.png b/docs/assets/img2img/000019.steps.png deleted file mode 100644 index 28899e91111..00000000000 Binary files a/docs/assets/img2img/000019.steps.png and /dev/null differ diff --git a/docs/assets/img2img/000030.1592514025.png b/docs/assets/img2img/000030.1592514025.png deleted file mode 100644 index 0e1641f7eb0..00000000000 Binary files a/docs/assets/img2img/000030.1592514025.png and /dev/null differ diff --git a/docs/assets/img2img/000030.step-0.png b/docs/assets/img2img/000030.step-0.png deleted file mode 100644 index 81beb074ec0..00000000000 Binary files a/docs/assets/img2img/000030.step-0.png and /dev/null differ diff --git a/docs/assets/img2img/000030.steps.gravity.png b/docs/assets/img2img/000030.steps.gravity.png deleted file mode 100644 index 2bda935a5f3..00000000000 Binary files a/docs/assets/img2img/000030.steps.gravity.png and /dev/null differ diff --git a/docs/assets/img2img/000032.1592514025.png b/docs/assets/img2img/000032.1592514025.png deleted file mode 100644 index 0ed2106ec48..00000000000 Binary files a/docs/assets/img2img/000032.1592514025.png and /dev/null differ diff --git a/docs/assets/img2img/000032.step-0.png b/docs/assets/img2img/000032.step-0.png deleted file mode 100644 index cc2da68ee43..00000000000 Binary files a/docs/assets/img2img/000032.step-0.png and /dev/null differ diff --git a/docs/assets/img2img/000032.steps.gravity.png b/docs/assets/img2img/000032.steps.gravity.png deleted file mode 100644 index 79058c1227a..00000000000 Binary files a/docs/assets/img2img/000032.steps.gravity.png and /dev/null differ diff --git a/docs/assets/img2img/000034.1592514025.png b/docs/assets/img2img/000034.1592514025.png deleted file mode 100644 index 43751da5728..00000000000 Binary files a/docs/assets/img2img/000034.1592514025.png and /dev/null differ diff --git a/docs/assets/img2img/000034.steps.png b/docs/assets/img2img/000034.steps.png deleted file mode 100644 index 216213162f5..00000000000 Binary files a/docs/assets/img2img/000034.steps.png and /dev/null differ diff --git a/docs/assets/img2img/000035.1592514025.png b/docs/assets/img2img/000035.1592514025.png deleted file mode 100644 index d298895080b..00000000000 Binary files a/docs/assets/img2img/000035.1592514025.png and /dev/null differ diff --git a/docs/assets/img2img/000035.steps.gravity.png b/docs/assets/img2img/000035.steps.gravity.png deleted file mode 100644 index 122c729e87c..00000000000 Binary files a/docs/assets/img2img/000035.steps.gravity.png and /dev/null differ diff --git a/docs/assets/img2img/000045.1592514025.png b/docs/assets/img2img/000045.1592514025.png deleted file mode 100644 index 5e70f1a5bfc..00000000000 Binary files a/docs/assets/img2img/000045.1592514025.png and /dev/null differ diff --git a/docs/assets/img2img/000045.steps.gravity.png b/docs/assets/img2img/000045.steps.gravity.png deleted file mode 100644 index 39e2a9b7111..00000000000 Binary files a/docs/assets/img2img/000045.steps.gravity.png and /dev/null differ diff --git a/docs/assets/img2img/000046.1592514025.png b/docs/assets/img2img/000046.1592514025.png deleted file mode 100644 index 70d248eb613..00000000000 Binary files a/docs/assets/img2img/000046.1592514025.png and /dev/null differ diff --git a/docs/assets/img2img/000046.steps.gravity.png b/docs/assets/img2img/000046.steps.gravity.png deleted file mode 100644 index d801a487015..00000000000 Binary files a/docs/assets/img2img/000046.steps.gravity.png and /dev/null differ diff --git a/docs/assets/img2img/fire-drawing.png b/docs/assets/img2img/fire-drawing.png deleted file mode 100644 index 36e2f111fa8..00000000000 Binary files a/docs/assets/img2img/fire-drawing.png and /dev/null differ diff --git a/docs/assets/inpainting/000019.curly.hair.deselected.png b/docs/assets/inpainting/000019.curly.hair.deselected.png deleted file mode 100644 index 54f2285550c..00000000000 Binary files a/docs/assets/inpainting/000019.curly.hair.deselected.png and /dev/null differ diff --git a/docs/assets/inpainting/000019.curly.hair.masked.png b/docs/assets/inpainting/000019.curly.hair.masked.png deleted file mode 100644 index a221c522f3e..00000000000 Binary files a/docs/assets/inpainting/000019.curly.hair.masked.png and /dev/null differ diff --git a/docs/assets/inpainting/000019.curly.hair.selected.png b/docs/assets/inpainting/000019.curly.hair.selected.png deleted file mode 100644 index e25bb4340c5..00000000000 Binary files a/docs/assets/inpainting/000019.curly.hair.selected.png and /dev/null differ diff --git a/docs/assets/inpainting/000024.801380492.png b/docs/assets/inpainting/000024.801380492.png deleted file mode 100644 index 9c72eb06b8b..00000000000 Binary files a/docs/assets/inpainting/000024.801380492.png and /dev/null differ diff --git a/docs/assets/installer-walkthrough/choose-gpu.png b/docs/assets/installer-walkthrough/choose-gpu.png deleted file mode 100644 index 3db23ef207b..00000000000 Binary files a/docs/assets/installer-walkthrough/choose-gpu.png and /dev/null differ diff --git a/docs/assets/installer-walkthrough/confirm-directory.png b/docs/assets/installer-walkthrough/confirm-directory.png deleted file mode 100644 index bc3099bbb35..00000000000 Binary files a/docs/assets/installer-walkthrough/confirm-directory.png and /dev/null differ diff --git a/docs/assets/installer-walkthrough/downloading-models.png b/docs/assets/installer-walkthrough/downloading-models.png deleted file mode 100644 index 975a6e39fe9..00000000000 Binary files a/docs/assets/installer-walkthrough/downloading-models.png and /dev/null differ diff --git a/docs/assets/installer-walkthrough/installing-models.png b/docs/assets/installer-walkthrough/installing-models.png deleted file mode 100644 index b2f6190bf5c..00000000000 Binary files a/docs/assets/installer-walkthrough/installing-models.png and /dev/null differ diff --git a/docs/assets/installer-walkthrough/settings-form.png b/docs/assets/installer-walkthrough/settings-form.png deleted file mode 100644 index 84e936ed56b..00000000000 Binary files a/docs/assets/installer-walkthrough/settings-form.png and /dev/null differ diff --git a/docs/assets/installer-walkthrough/unpacked-zipfile.png b/docs/assets/installer-walkthrough/unpacked-zipfile.png deleted file mode 100644 index 0e7e5f681f2..00000000000 Binary files a/docs/assets/installer-walkthrough/unpacked-zipfile.png and /dev/null differ diff --git a/docs/assets/installing-models/model-installer-controlnet.png b/docs/assets/installing-models/model-installer-controlnet.png deleted file mode 100644 index 09dfdb269fb..00000000000 Binary files a/docs/assets/installing-models/model-installer-controlnet.png and /dev/null differ diff --git a/docs/assets/installing-models/webui-models-1.png b/docs/assets/installing-models/webui-models-1.png deleted file mode 100644 index 648791ca270..00000000000 Binary files a/docs/assets/installing-models/webui-models-1.png and /dev/null differ diff --git a/docs/assets/installing-models/webui-models-2.png b/docs/assets/installing-models/webui-models-2.png deleted file mode 100644 index e58f5b9752c..00000000000 Binary files a/docs/assets/installing-models/webui-models-2.png and /dev/null differ diff --git a/docs/assets/installing-models/webui-models-3.png b/docs/assets/installing-models/webui-models-3.png deleted file mode 100644 index 6797a6b9076..00000000000 Binary files a/docs/assets/installing-models/webui-models-3.png and /dev/null differ diff --git a/docs/assets/installing-models/webui-models-4.png b/docs/assets/installing-models/webui-models-4.png deleted file mode 100644 index 75aa7ad7f16..00000000000 Binary files a/docs/assets/installing-models/webui-models-4.png and /dev/null differ diff --git a/docs/assets/invoke-control-panel-1.png b/docs/assets/invoke-control-panel-1.png deleted file mode 100644 index 09046676007..00000000000 Binary files a/docs/assets/invoke-control-panel-1.png and /dev/null differ diff --git a/docs/assets/invoke-web-server-2.png b/docs/assets/invoke-web-server-2.png deleted file mode 100644 index 361c113159c..00000000000 Binary files a/docs/assets/invoke-web-server-2.png and /dev/null differ diff --git a/docs/assets/invoke-web-server-3.png b/docs/assets/invoke-web-server-3.png deleted file mode 100644 index 7d392cfecca..00000000000 Binary files a/docs/assets/invoke-web-server-3.png and /dev/null differ diff --git a/docs/assets/invoke-web-server-4.png b/docs/assets/invoke-web-server-4.png deleted file mode 100644 index 2690356b8ac..00000000000 Binary files a/docs/assets/invoke-web-server-4.png and /dev/null differ diff --git a/docs/assets/invoke-web-server-5.png b/docs/assets/invoke-web-server-5.png deleted file mode 100644 index 1923e641294..00000000000 Binary files a/docs/assets/invoke-web-server-5.png and /dev/null differ diff --git a/docs/assets/invoke-web-server-6.png b/docs/assets/invoke-web-server-6.png deleted file mode 100644 index 0e8f703cb5a..00000000000 Binary files a/docs/assets/invoke-web-server-6.png and /dev/null differ diff --git a/docs/assets/invoke-web-server-7.png b/docs/assets/invoke-web-server-7.png deleted file mode 100644 index 19769dea93e..00000000000 Binary files a/docs/assets/invoke-web-server-7.png and /dev/null differ diff --git a/docs/assets/invoke-web-server-8.png b/docs/assets/invoke-web-server-8.png deleted file mode 100644 index dc708b11177..00000000000 Binary files a/docs/assets/invoke-web-server-8.png and /dev/null differ diff --git a/docs/assets/invoke-web-server-9.png b/docs/assets/invoke-web-server-9.png deleted file mode 100644 index 9d0d31bec0c..00000000000 Binary files a/docs/assets/invoke-web-server-9.png and /dev/null differ diff --git a/docs/assets/invoke_web_dark.png b/docs/assets/invoke_web_dark.png deleted file mode 100644 index 9141ab40f37..00000000000 Binary files a/docs/assets/invoke_web_dark.png and /dev/null differ diff --git a/docs/assets/invoke_web_light.png b/docs/assets/invoke_web_light.png deleted file mode 100644 index 98311ccafd6..00000000000 Binary files a/docs/assets/invoke_web_light.png and /dev/null differ diff --git a/docs/assets/invoke_web_server.png b/docs/assets/invoke_web_server.png deleted file mode 100644 index cdb34a0835b..00000000000 Binary files a/docs/assets/invoke_web_server.png and /dev/null differ diff --git a/docs/assets/join-us-on-discord-image.png b/docs/assets/join-us-on-discord-image.png deleted file mode 100644 index 53e4ee0fe05..00000000000 Binary files a/docs/assets/join-us-on-discord-image.png and /dev/null differ diff --git a/docs/assets/logo.png b/docs/assets/logo.png deleted file mode 100644 index b6eb33a6db5..00000000000 Binary files a/docs/assets/logo.png and /dev/null differ diff --git a/docs/assets/lora-example-0.png b/docs/assets/lora-example-0.png deleted file mode 100644 index f98fa53ca41..00000000000 Binary files a/docs/assets/lora-example-0.png and /dev/null differ diff --git a/docs/assets/lora-example-1.png b/docs/assets/lora-example-1.png deleted file mode 100644 index 29ea46e970b..00000000000 Binary files a/docs/assets/lora-example-1.png and /dev/null differ diff --git a/docs/assets/lora-example-2.png b/docs/assets/lora-example-2.png deleted file mode 100644 index 40eecdce849..00000000000 Binary files a/docs/assets/lora-example-2.png and /dev/null differ diff --git a/docs/assets/lora-example-3.png b/docs/assets/lora-example-3.png deleted file mode 100644 index be4c505d439..00000000000 Binary files a/docs/assets/lora-example-3.png and /dev/null differ diff --git a/docs/assets/negative_prompt_walkthru/step1.png b/docs/assets/negative_prompt_walkthru/step1.png deleted file mode 100644 index 6f94d7d035a..00000000000 Binary files a/docs/assets/negative_prompt_walkthru/step1.png and /dev/null differ diff --git a/docs/assets/negative_prompt_walkthru/step2.png b/docs/assets/negative_prompt_walkthru/step2.png deleted file mode 100644 index 0ff90eca3c1..00000000000 Binary files a/docs/assets/negative_prompt_walkthru/step2.png and /dev/null differ diff --git a/docs/assets/negative_prompt_walkthru/step3.png b/docs/assets/negative_prompt_walkthru/step3.png deleted file mode 100644 index f6676de3868..00000000000 Binary files a/docs/assets/negative_prompt_walkthru/step3.png and /dev/null differ diff --git a/docs/assets/negative_prompt_walkthru/step4.png b/docs/assets/negative_prompt_walkthru/step4.png deleted file mode 100644 index 2e73532629d..00000000000 Binary files a/docs/assets/negative_prompt_walkthru/step4.png and /dev/null differ diff --git a/docs/assets/outpainting/curly-outcrop-2.png b/docs/assets/outpainting/curly-outcrop-2.png deleted file mode 100644 index 595f011f27e..00000000000 Binary files a/docs/assets/outpainting/curly-outcrop-2.png and /dev/null differ diff --git a/docs/assets/outpainting/curly-outcrop.png b/docs/assets/outpainting/curly-outcrop.png deleted file mode 100644 index ae8d8dacd3d..00000000000 Binary files a/docs/assets/outpainting/curly-outcrop.png and /dev/null differ diff --git a/docs/assets/outpainting/curly-outpaint.png b/docs/assets/outpainting/curly-outpaint.png deleted file mode 100644 index 9f4a2ee431e..00000000000 Binary files a/docs/assets/outpainting/curly-outpaint.png and /dev/null differ diff --git a/docs/assets/outpainting/curly.png b/docs/assets/outpainting/curly.png deleted file mode 100644 index d9a4cb257ee..00000000000 Binary files a/docs/assets/outpainting/curly.png and /dev/null differ diff --git a/docs/assets/prompt-blending/blue-sphere-0.25-red-cube-0.75-hybrid.png b/docs/assets/prompt-blending/blue-sphere-0.25-red-cube-0.75-hybrid.png deleted file mode 100644 index 43d780a3439..00000000000 Binary files a/docs/assets/prompt-blending/blue-sphere-0.25-red-cube-0.75-hybrid.png and /dev/null differ diff --git a/docs/assets/prompt-blending/blue-sphere-0.5-red-cube-0.5-hybrid.png b/docs/assets/prompt-blending/blue-sphere-0.5-red-cube-0.5-hybrid.png deleted file mode 100644 index c131ae03f4d..00000000000 Binary files a/docs/assets/prompt-blending/blue-sphere-0.5-red-cube-0.5-hybrid.png and /dev/null differ diff --git a/docs/assets/prompt-blending/blue-sphere-0.5-red-cube-0.5.png b/docs/assets/prompt-blending/blue-sphere-0.5-red-cube-0.5.png deleted file mode 100644 index d0cb8e389ee..00000000000 Binary files a/docs/assets/prompt-blending/blue-sphere-0.5-red-cube-0.5.png and /dev/null differ diff --git a/docs/assets/prompt-blending/blue-sphere-0.75-red-cube-0.25-hybrid.png b/docs/assets/prompt-blending/blue-sphere-0.75-red-cube-0.25-hybrid.png deleted file mode 100644 index b7ddf6658bf..00000000000 Binary files a/docs/assets/prompt-blending/blue-sphere-0.75-red-cube-0.25-hybrid.png and /dev/null differ diff --git a/docs/assets/prompt-blending/blue-sphere-red-cube-hybrid.png b/docs/assets/prompt-blending/blue-sphere-red-cube-hybrid.png deleted file mode 100644 index 3ec14564e61..00000000000 Binary files a/docs/assets/prompt-blending/blue-sphere-red-cube-hybrid.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/apricots--1.png b/docs/assets/prompt_syntax/apricots--1.png deleted file mode 100644 index 0f0c17f08b0..00000000000 Binary files a/docs/assets/prompt_syntax/apricots--1.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/apricots--2.png b/docs/assets/prompt_syntax/apricots--2.png deleted file mode 100644 index 5c519b09aed..00000000000 Binary files a/docs/assets/prompt_syntax/apricots--2.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/apricots--3.png b/docs/assets/prompt_syntax/apricots--3.png deleted file mode 100644 index c98ffd8b071..00000000000 Binary files a/docs/assets/prompt_syntax/apricots--3.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/apricots-0.png b/docs/assets/prompt_syntax/apricots-0.png deleted file mode 100644 index f8ead74db2f..00000000000 Binary files a/docs/assets/prompt_syntax/apricots-0.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/apricots-1.png b/docs/assets/prompt_syntax/apricots-1.png deleted file mode 100644 index 75ff7a24a3d..00000000000 Binary files a/docs/assets/prompt_syntax/apricots-1.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/apricots-2.png b/docs/assets/prompt_syntax/apricots-2.png deleted file mode 100644 index e24ced76375..00000000000 Binary files a/docs/assets/prompt_syntax/apricots-2.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/apricots-3.png b/docs/assets/prompt_syntax/apricots-3.png deleted file mode 100644 index d6edf5073c9..00000000000 Binary files a/docs/assets/prompt_syntax/apricots-3.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/apricots-4.png b/docs/assets/prompt_syntax/apricots-4.png deleted file mode 100644 index 291c5a1b03b..00000000000 Binary files a/docs/assets/prompt_syntax/apricots-4.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/apricots-5.png b/docs/assets/prompt_syntax/apricots-5.png deleted file mode 100644 index c71b8578375..00000000000 Binary files a/docs/assets/prompt_syntax/apricots-5.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/mountain-man.png b/docs/assets/prompt_syntax/mountain-man.png deleted file mode 100644 index 4bfa5817b8c..00000000000 Binary files a/docs/assets/prompt_syntax/mountain-man.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/mountain-man1.png b/docs/assets/prompt_syntax/mountain-man1.png deleted file mode 100644 index 5ed98162d35..00000000000 Binary files a/docs/assets/prompt_syntax/mountain-man1.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/mountain-man2.png b/docs/assets/prompt_syntax/mountain-man2.png deleted file mode 100644 index d4466514ded..00000000000 Binary files a/docs/assets/prompt_syntax/mountain-man2.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/mountain-man3.png b/docs/assets/prompt_syntax/mountain-man3.png deleted file mode 100644 index 3196c5ca96f..00000000000 Binary files a/docs/assets/prompt_syntax/mountain-man3.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/mountain-man4.png b/docs/assets/prompt_syntax/mountain-man4.png deleted file mode 100644 index 69522dba232..00000000000 Binary files a/docs/assets/prompt_syntax/mountain-man4.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/mountain1-man.png b/docs/assets/prompt_syntax/mountain1-man.png deleted file mode 100644 index b5952d02f93..00000000000 Binary files a/docs/assets/prompt_syntax/mountain1-man.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/mountain2-man.png b/docs/assets/prompt_syntax/mountain2-man.png deleted file mode 100644 index 8ab55ff2a7e..00000000000 Binary files a/docs/assets/prompt_syntax/mountain2-man.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/mountain3-man.png b/docs/assets/prompt_syntax/mountain3-man.png deleted file mode 100644 index c1024b0a635..00000000000 Binary files a/docs/assets/prompt_syntax/mountain3-man.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/sdxl-prompt-concatenated.png b/docs/assets/prompt_syntax/sdxl-prompt-concatenated.png deleted file mode 100644 index 8d5336da3d6..00000000000 Binary files a/docs/assets/prompt_syntax/sdxl-prompt-concatenated.png and /dev/null differ diff --git a/docs/assets/prompt_syntax/sdxl-prompt.png b/docs/assets/prompt_syntax/sdxl-prompt.png deleted file mode 100644 index b85464c5ad8..00000000000 Binary files a/docs/assets/prompt_syntax/sdxl-prompt.png and /dev/null differ diff --git a/docs/assets/sdxl-graphs/sdxl-base-example1.json b/docs/assets/sdxl-graphs/sdxl-base-example1.json deleted file mode 100644 index af162ba3e91..00000000000 --- a/docs/assets/sdxl-graphs/sdxl-base-example1.json +++ /dev/null @@ -1 +0,0 @@ -{"nodes":[{"width":387,"height":565,"dragHandle":".node-drag-handle","id":"800b3166-5044-4987-ba13-f839963fec96","type":"invocation","position":{"x":-169.10829044927982,"y":-272.36451106154334},"data":{"id":"800b3166-5044-4987-ba13-f839963fec96","type":"sdxl_compel_prompt","inputs":{"prompt":{"id":"8e76febd-6692-4a40-8bba-cbac54bccf1a","name":"prompt","type":"string","value":"bluebird in a sakura tree"},"style":{"id":"cd0e7434-04d8-4e36-b08b-3342d6ab4caa","name":"style","type":"string","value":""},"original_width":{"id":"cd259354-c92c-4e1c-815a-205fb38f8fec","name":"original_width","type":"integer","value":1024},"original_height":{"id":"b5cebf0e-97ce-4cd3-92be-fad37472daf2","name":"original_height","type":"integer","value":1024},"crop_top":{"id":"e6763334-676e-42fd-9fbf-0537ff002013","name":"crop_top","type":"integer","value":0},"crop_left":{"id":"00042856-acd5-4f99-a0e1-2d0b567ebf0f","name":"crop_left","type":"integer","value":0},"target_width":{"id":"800e17aa-1c15-4cdc-bceb-1f3f3ef5d244","name":"target_width","type":"integer","value":1024},"target_height":{"id":"91079f90-886b-440f-8236-236a239af747","name":"target_height","type":"integer","value":1024},"clip":{"id":"b32c84f8-b39e-4c23-8a77-fc62944ef942","name":"clip","type":"clip"},"clip2":{"id":"80db777a-3e3d-4132-ba0b-75a0b2135809","name":"clip2","type":"clip"}},"outputs":{"conditioning":{"id":"ea368e7a-bc93-4492-8fc6-e1d2fe2f5e2a","name":"conditioning","type":"conditioning"}}},"selected":false,"positionAbsolute":{"x":-169.10829044927982,"y":-272.36451106154334},"dragging":false},{"width":387,"height":565,"dragHandle":".node-drag-handle","id":"f3b94b55-bd55-4244-87b2-bf92e4ebbd2a","type":"invocation","position":{"x":-173.7213091998297,"y":346.42482955878256},"data":{"id":"f3b94b55-bd55-4244-87b2-bf92e4ebbd2a","type":"sdxl_compel_prompt","inputs":{"prompt":{"id":"d718b007-51f9-4385-bbf9-fa99756d5a4c","name":"prompt","type":"string","value":""},"style":{"id":"6476aafe-9e17-48b2-b66c-fbb8ff555926","name":"style","type":"string","value":""},"original_width":{"id":"7f4cf0bd-d5ab-460f-9484-375160f87620","name":"original_width","type":"integer","value":1024},"original_height":{"id":"8c1695a7-2b1b-44ba-8644-da207c551366","name":"original_height","type":"integer","value":1024},"crop_top":{"id":"5e7e7434-e966-4592-a422-924784192719","name":"crop_top","type":"integer","value":0},"crop_left":{"id":"cbbbc129-c481-4a28-a944-ad894dcec8b1","name":"crop_left","type":"integer","value":0},"target_width":{"id":"18bb5fb9-d488-41c3-9d8b-1b0e5b163676","name":"target_width","type":"integer","value":1024},"target_height":{"id":"44cbc645-b582-48c7-9e37-8aabd2615355","name":"target_height","type":"integer","value":1024},"clip":{"id":"65f8cad6-9aaf-4d21-91af-e9aeb9657bf2","name":"clip","type":"clip"},"clip2":{"id":"db5cdf20-9968-4932-b7de-adbc2460ca31","name":"clip2","type":"clip"}},"outputs":{"conditioning":{"id":"4f0236e9-0bf1-4410-867d-7241f1fe3e4c","name":"conditioning","type":"conditioning"}}},"selected":false,"positionAbsolute":{"x":-173.7213091998297,"y":346.42482955878256},"dragging":false},{"width":334,"height":269,"dragHandle":".node-drag-handle","id":"e29a9d29-2f63-487b-89b8-4184b1871c8f","type":"invocation","position":{"x":-652.2340297939481,"y":338.43526096363286},"data":{"id":"e29a9d29-2f63-487b-89b8-4184b1871c8f","type":"sdxl_model_loader","inputs":{"model":{"id":"4b19dda6-31fe-4317-a741-a19f8d8ec18f","name":"model","type":"model","value":{"model_name":"stable-diffusion-xl-base-0.9","base_model":"sdxl"}}},"outputs":{"unet":{"id":"d816da3a-7cb9-4d0d-8b1a-cea4e596c11e","name":"unet","type":"unet"},"clip":{"id":"35af0240-3014-4a2e-b3f2-f79ddffaca29","name":"clip","type":"clip"},"clip2":{"id":"25360ee2-ce84-4d04-95d6-a6e63db7e87c","name":"clip2","type":"clip"},"vae":{"id":"e8f15338-cbbe-45a1-8248-059b3601b058","name":"vae","type":"vae"}}},"selected":false,"positionAbsolute":{"x":-652.2340297939481,"y":338.43526096363286},"dragging":false},{"width":384,"height":519,"dragHandle":".node-drag-handle","id":"41f3e341-a2ae-4741-b0cd-dea20abde273","type":"invocation","position":{"x":796.6378578223604,"y":-192.53063049405188},"data":{"id":"41f3e341-a2ae-4741-b0cd-dea20abde273","type":"t2l_sdxl","inputs":{"positive_conditioning":{"id":"2b679eb8-3d15-4b4c-9a68-334bf43ea3d3","name":"positive_conditioning","type":"conditioning"},"negative_conditioning":{"id":"d3cfcf4b-8b31-4f88-98ed-3486dd60fa6f","name":"negative_conditioning","type":"conditioning"},"noise":{"id":"ed624337-7042-4a28-adf1-c8a6def87b4c","name":"noise","type":"latents"},"steps":{"id":"92f299da-c6ec-45fc-ad3d-d6af0f9862e6","name":"steps","type":"integer","value":10},"cfg_scale":{"id":"b129fa4e-c772-48cd-9dad-9b7223ddfcf5","name":"cfg_scale","type":"float","value":7.5},"scheduler":{"id":"d5e86735-8301-42f4-8fc4-276120e444cd","name":"scheduler","type":"enum","value":"euler"},"unet":{"id":"9affc51f-37a9-4468-90c9-bec97efda3f4","name":"unet","type":"unet"},"denoising_end":{"id":"91f41083-910e-4955-b3ae-d93ad3d1d801","name":"denoising_end","type":"float","value":1}},"outputs":{"latents":{"id":"f6b119cc-cf8d-46c0-8f43-bf712c509938","name":"latents","type":"latents"},"width":{"id":"cfcced65-62b2-40c7-b2bf-6534220eeaa1","name":"width","type":"integer"},"height":{"id":"819fbf37-8db1-4f5c-ad6b-a5cdb71e8c9a","name":"height","type":"integer"}}},"selected":false,"positionAbsolute":{"x":796.6378578223604,"y":-192.53063049405188},"dragging":false},{"width":333,"height":344,"dragHandle":".node-drag-handle","id":"357773ee-386a-423a-a712-a23812554cbb","type":"invocation","position":{"x":401.8907147451813,"y":576.7882372082732},"data":{"id":"357773ee-386a-423a-a712-a23812554cbb","type":"noise","inputs":{"seed":{"id":"9eaa6f6f-bde4-42b6-945a-a09b6cc46531","name":"seed","type":"integer","value":10},"width":{"id":"241032d2-6d05-47c1-bf9e-1a13ef95956b","name":"width","type":"integer","value":1024},"height":{"id":"71eda9a5-4460-4e7e-ba7a-2d99fa47ddf5","name":"height","type":"integer","value":1024},"use_cpu":{"id":"1dd54a91-deaa-4b83-be89-230c517d7b6b","name":"use_cpu","type":"boolean","value":true}},"outputs":{"noise":{"id":"321604ef-b33b-4529-91af-0d8d03d23f2f","name":"noise","type":"latents"},"width":{"id":"fc1f68f3-720f-4dbe-a9c9-b8a08cca768a","name":"width","type":"integer"},"height":{"id":"61022078-48f5-4faf-923e-9cddb508be30","name":"height","type":"integer"}}},"selected":false,"positionAbsolute":{"x":401.8907147451813,"y":576.7882372082732},"dragging":false},{"width":250,"height":323,"dragHandle":".node-drag-handle","id":"3e712379-88e7-465d-94d1-e06160b7cfae","type":"invocation","position":{"x":1237.2519670580534,"y":257.58346014925485},"data":{"id":"3e712379-88e7-465d-94d1-e06160b7cfae","type":"l2i","inputs":{"latents":{"id":"453d1aac-7bfe-480b-bc6f-2c3b110cd9b8","name":"latents","type":"latents"},"vae":{"id":"c7c38097-3620-4eb8-aec5-4ce1b043bb83","name":"vae","type":"vae"},"tiled":{"id":"2cab559e-fbaa-4cbf-ae9b-e5e43df38369","name":"tiled","type":"boolean","value":false},"fp32":{"id":"daf6c8df-325a-4cf6-9e86-d8962d838f17","name":"fp32","type":"boolean","value":true}},"outputs":{"image":{"id":"6ead6575-d4ad-4dd2-b6e0-5b70f9cce726","name":"image","type":"image"},"width":{"id":"734a4436-2c6f-4c57-b1cb-25a01548031d","name":"width","type":"integer"},"height":{"id":"7a03a4af-ab06-43f3-bb4d-1789b6f2c5e9","name":"height","type":"integer"}}},"selected":false,"positionAbsolute":{"x":1237.2519670580534,"y":257.58346014925485},"dragging":false},{"width":320,"height":187,"dragHandle":".node-drag-handle","id":"2423e526-ba3c-448e-8240-7442508d4609","type":"invocation","position":{"x":-33.59691285742994,"y":945.2263448868741},"data":{"id":"2423e526-ba3c-448e-8240-7442508d4609","type":"rand_int","inputs":{"low":{"id":"70ffaec8-3064-45dc-8b5c-6b4f43cc3b62","name":"low","type":"integer","value":0},"high":{"id":"82e10321-ce31-4e86-9e21-077c16a18e3d","name":"high","type":"integer","value":2147483647}},"outputs":{"a":{"id":"bba34654-8a92-40db-9810-7f75ddad8ae9","name":"a","type":"integer"}}},"selected":true,"positionAbsolute":{"x":-33.59691285742994,"y":945.2263448868741},"dragging":false}],"edges":[{"source":"41f3e341-a2ae-4741-b0cd-dea20abde273","sourceHandle":"latents","target":"3e712379-88e7-465d-94d1-e06160b7cfae","targetHandle":"latents","id":"reactflow__edge-41f3e341-a2ae-4741-b0cd-dea20abde273latents-3e712379-88e7-465d-94d1-e06160b7cfaelatents"},{"source":"800b3166-5044-4987-ba13-f839963fec96","sourceHandle":"conditioning","target":"41f3e341-a2ae-4741-b0cd-dea20abde273","targetHandle":"positive_conditioning","id":"reactflow__edge-800b3166-5044-4987-ba13-f839963fec96conditioning-41f3e341-a2ae-4741-b0cd-dea20abde273positive_conditioning"},{"source":"f3b94b55-bd55-4244-87b2-bf92e4ebbd2a","sourceHandle":"conditioning","target":"41f3e341-a2ae-4741-b0cd-dea20abde273","targetHandle":"negative_conditioning","id":"reactflow__edge-f3b94b55-bd55-4244-87b2-bf92e4ebbd2aconditioning-41f3e341-a2ae-4741-b0cd-dea20abde273negative_conditioning"},{"source":"357773ee-386a-423a-a712-a23812554cbb","sourceHandle":"noise","target":"41f3e341-a2ae-4741-b0cd-dea20abde273","targetHandle":"noise","id":"reactflow__edge-357773ee-386a-423a-a712-a23812554cbbnoise-41f3e341-a2ae-4741-b0cd-dea20abde273noise"},{"source":"e29a9d29-2f63-487b-89b8-4184b1871c8f","sourceHandle":"vae","target":"3e712379-88e7-465d-94d1-e06160b7cfae","targetHandle":"vae","id":"reactflow__edge-e29a9d29-2f63-487b-89b8-4184b1871c8fvae-3e712379-88e7-465d-94d1-e06160b7cfaevae"},{"source":"e29a9d29-2f63-487b-89b8-4184b1871c8f","sourceHandle":"clip","target":"800b3166-5044-4987-ba13-f839963fec96","targetHandle":"clip","id":"reactflow__edge-e29a9d29-2f63-487b-89b8-4184b1871c8fclip-800b3166-5044-4987-ba13-f839963fec96clip"},{"source":"e29a9d29-2f63-487b-89b8-4184b1871c8f","sourceHandle":"clip2","target":"800b3166-5044-4987-ba13-f839963fec96","targetHandle":"clip2","id":"reactflow__edge-e29a9d29-2f63-487b-89b8-4184b1871c8fclip2-800b3166-5044-4987-ba13-f839963fec96clip2"},{"source":"e29a9d29-2f63-487b-89b8-4184b1871c8f","sourceHandle":"clip","target":"f3b94b55-bd55-4244-87b2-bf92e4ebbd2a","targetHandle":"clip","id":"reactflow__edge-e29a9d29-2f63-487b-89b8-4184b1871c8fclip-f3b94b55-bd55-4244-87b2-bf92e4ebbd2aclip"},{"source":"e29a9d29-2f63-487b-89b8-4184b1871c8f","sourceHandle":"clip2","target":"f3b94b55-bd55-4244-87b2-bf92e4ebbd2a","targetHandle":"clip2","id":"reactflow__edge-e29a9d29-2f63-487b-89b8-4184b1871c8fclip2-f3b94b55-bd55-4244-87b2-bf92e4ebbd2aclip2"},{"source":"e29a9d29-2f63-487b-89b8-4184b1871c8f","sourceHandle":"unet","target":"41f3e341-a2ae-4741-b0cd-dea20abde273","targetHandle":"unet","id":"reactflow__edge-e29a9d29-2f63-487b-89b8-4184b1871c8funet-41f3e341-a2ae-4741-b0cd-dea20abde273unet"},{"source":"2423e526-ba3c-448e-8240-7442508d4609","sourceHandle":"a","target":"357773ee-386a-423a-a712-a23812554cbb","targetHandle":"seed","id":"reactflow__edge-2423e526-ba3c-448e-8240-7442508d4609a-357773ee-386a-423a-a712-a23812554cbbseed"}],"viewport":{"x":473.83885376565604,"y":374.3473116493717,"zoom":0.5904963307147653}} \ No newline at end of file diff --git a/docs/assets/sdxl-graphs/sdxl-base-refine-example1.json b/docs/assets/sdxl-graphs/sdxl-base-refine-example1.json deleted file mode 100644 index 6935c15fd0d..00000000000 --- a/docs/assets/sdxl-graphs/sdxl-base-refine-example1.json +++ /dev/null @@ -1 +0,0 @@ -{"nodes":[{"width":309,"height":138,"dragHandle":".node-drag-handle","id":"2daddd1d-a468-4841-9ff4-57befb3518a1","type":"invocation","position":{"x":-352.03597532489465,"y":-853.3432520441677},"data":{"id":"2daddd1d-a468-4841-9ff4-57befb3518a1","type":"param_string","inputs":{"text":{"id":"aa4e1128-b6e0-4d8b-831b-723e9879b307","name":"text","type":"string","value":"bluebird in a sakura tree"}},"outputs":{"text":{"id":"8605a8bb-ea46-402d-a0cf-658ad66aa589","name":"text","type":"string"}}},"selected":false,"positionAbsolute":{"x":-352.03597532489465,"y":-853.3432520441677},"dragging":false},{"width":309,"height":138,"dragHandle":".node-drag-handle","id":"0cee1a14-34fa-4b5e-b361-9ebfaba62844","type":"invocation","position":{"x":-350.69241780355867,"y":-671.6645885606935},"data":{"id":"0cee1a14-34fa-4b5e-b361-9ebfaba62844","type":"param_string","inputs":{"text":{"id":"55a39e14-81ad-4112-94b3-62faba370317","name":"text","type":"string","value":"classical chinese painting"}},"outputs":{"text":{"id":"60a32b20-0e81-4936-bf68-38f65c13cfab","name":"text","type":"string"}}},"selected":false,"positionAbsolute":{"x":-350.69241780355867,"y":-671.6645885606935},"dragging":false},{"width":309,"height":138,"dragHandle":".node-drag-handle","id":"3898e2dd-0efa-44cf-8d6c-0d23ef184419","type":"invocation","position":{"x":-353.4581776953401,"y":-483.26349757856553},"data":{"id":"3898e2dd-0efa-44cf-8d6c-0d23ef184419","type":"param_string","inputs":{"text":{"id":"79c9244d-b40b-45f7-8351-a0e3aad9c203","name":"text","type":"string","value":""}},"outputs":{"text":{"id":"c49253f2-57d2-4ea8-920e-2d0107834c3a","name":"text","type":"string"}}},"selected":false,"positionAbsolute":{"x":-353.4581776953401,"y":-483.26349757856553},"dragging":false},{"width":309,"height":138,"dragHandle":".node-drag-handle","id":"96935e84-c864-48a4-b6c0-c940c71c43ae","type":"invocation","position":{"x":-355.15166832006526,"y":-312.2209444813352},"data":{"id":"96935e84-c864-48a4-b6c0-c940c71c43ae","type":"param_string","inputs":{"text":{"id":"a57a792a-3621-489e-bae4-0a413510caef","name":"text","type":"string","value":""}},"outputs":{"text":{"id":"40c229fc-b044-40cd-8b31-31f0562ca7ae","name":"text","type":"string"}}},"selected":false,"positionAbsolute":{"x":-355.15166832006526,"y":-312.2209444813352},"dragging":false},{"width":334,"height":269,"dragHandle":".node-drag-handle","id":"f3cd6592-f3a4-4d4e-a833-deb36314716f","type":"invocation","position":{"x":-405.6072492884527,"y":-90.55508106907858},"data":{"id":"f3cd6592-f3a4-4d4e-a833-deb36314716f","type":"sdxl_model_loader","inputs":{"model":{"id":"e54a5f62-7fb2-46ab-bec2-e1d1de26245b","name":"model","type":"model","value":{"model_name":"stable-diffusion-xl-base-0.9","base_model":"sdxl"}}},"outputs":{"unet":{"id":"59d159bd-42e0-40ee-8c5e-0021158d2f2e","name":"unet","type":"unet"},"clip":{"id":"232c0412-142b-4f61-b3f2-673e8375452c","name":"clip","type":"clip"},"clip2":{"id":"bfaaf2ed-d5fb-419f-8f4d-1c47375b23d9","name":"clip2","type":"clip"},"vae":{"id":"b7699621-12fd-4364-b68d-27fb322fcc2b","name":"vae","type":"vae"}}},"selected":false,"positionAbsolute":{"x":-405.6072492884527,"y":-90.55508106907858},"dragging":false},{"width":387,"height":565,"dragHandle":".node-drag-handle","id":"c1c4e1e7-ae79-4fc8-9a98-246175e7c155","type":"invocation","position":{"x":81.8860645633394,"y":-680.6110236371882},"data":{"id":"c1c4e1e7-ae79-4fc8-9a98-246175e7c155","type":"sdxl_compel_prompt","inputs":{"prompt":{"id":"9df5bf37-ff52-4deb-b8b2-59670578bd74","name":"prompt","type":"string","value":""},"style":{"id":"66b685eb-2c97-4dee-be34-fd10bda699fb","name":"style","type":"string","value":""},"original_width":{"id":"0dada1d1-305f-4b46-b342-e72150cf135d","name":"original_width","type":"integer","value":1024},"original_height":{"id":"fcbc471b-982e-4fed-9882-981d5490ab24","name":"original_height","type":"integer","value":1024},"crop_top":{"id":"7b891ead-37b4-4fcf-a642-1b27589ef8b8","name":"crop_top","type":"integer","value":0},"crop_left":{"id":"3d7d7f57-62af-42a6-ac55-80a5dcc6a6a1","name":"crop_left","type":"integer","value":0},"target_width":{"id":"e51a406e-185b-4447-82a9-752118d86ded","name":"target_width","type":"integer","value":1024},"target_height":{"id":"86aac99d-8671-4cdf-9f4b-fbd9261a1a43","name":"target_height","type":"integer","value":1024},"clip":{"id":"a6c1cdc5-ea91-4aaa-8b27-d5b0ea149d2c","name":"clip","type":"clip"},"clip2":{"id":"4af4c3c4-d23a-4940-8597-e1181b54e2b1","name":"clip2","type":"clip"}},"outputs":{"conditioning":{"id":"f8a844c4-8395-4645-87b8-7b8f02057302","name":"conditioning","type":"conditioning"}}},"selected":false,"positionAbsolute":{"x":81.8860645633394,"y":-680.6110236371882},"dragging":false},{"width":387,"height":565,"dragHandle":".node-drag-handle","id":"addd5054-97d8-466c-ab3c-41f5e039af02","type":"invocation","position":{"x":82.9601624843504,"y":-78.81640327238692},"data":{"id":"addd5054-97d8-466c-ab3c-41f5e039af02","type":"sdxl_compel_prompt","inputs":{"prompt":{"id":"d6eee212-c125-4c35-8028-a8c980496ee2","name":"prompt","type":"string","value":""},"style":{"id":"88c7d42e-5868-4e75-ab37-0b05a4cfc84d","name":"style","type":"string","value":""},"original_width":{"id":"21897c23-5bb2-46c0-b0e3-9e481c6828ab","name":"original_width","type":"integer","value":1024},"original_height":{"id":"70b41031-48b6-4c3b-9683-8332b344cc88","name":"original_height","type":"integer","value":1024},"crop_top":{"id":"a1e7aad9-1088-47c9-9956-eed7ae857a15","name":"crop_top","type":"integer","value":0},"crop_left":{"id":"9a237728-23ea-42fd-98c3-1b3ee571d8de","name":"crop_left","type":"integer","value":0},"target_width":{"id":"48ee0274-c952-4a8f-8b11-17c6cbad3546","name":"target_width","type":"integer","value":1024},"target_height":{"id":"4c75e60f-fd59-46f0-95dc-825a43fb329f","name":"target_height","type":"integer","value":1024},"clip":{"id":"1cb885c0-efc8-451c-9208-765f5c436950","name":"clip","type":"clip"},"clip2":{"id":"0b98f464-3f10-4dc3-aa14-22e25ac1a301","name":"clip2","type":"clip"}},"outputs":{"conditioning":{"id":"6cdd9112-92ec-4b34-9c10-c185f66d3d2a","name":"conditioning","type":"conditioning"}}},"selected":false,"positionAbsolute":{"x":82.9601624843504,"y":-78.81640327238692},"dragging":false},{"width":394,"height":425,"dragHandle":".node-drag-handle","id":"0af8fc93-be59-4002-9956-c700fb778d4a","type":"invocation","position":{"x":1100.1437566814066,"y":-851.6600425944865},"data":{"id":"0af8fc93-be59-4002-9956-c700fb778d4a","type":"sdxl_refiner_compel_prompt","inputs":{"style":{"id":"fda33c4f-84ef-4d82-bff1-b1b4b7c0f272","name":"style","type":"string","value":""},"original_width":{"id":"daf3c09d-d0bf-441f-ab05-5e8c16c8e136","name":"original_width","type":"integer","value":1024},"original_height":{"id":"d20c06c8-db5b-48db-92d9-79ee8ccd44f8","name":"original_height","type":"integer","value":1024},"crop_top":{"id":"29ad28e3-a497-4949-8ee5-ddb9645ca38f","name":"crop_top","type":"integer","value":0},"crop_left":{"id":"c4bdccc3-2f11-420a-abbe-2b4bd66a11c6","name":"crop_left","type":"integer","value":0},"aesthetic_score":{"id":"342061a0-eb9b-43fc-ac41-707b10355256","name":"aesthetic_score","type":"float","value":6},"clip2":{"id":"9fc7860d-aa7f-47fe-b8c9-1add0aee25bc","name":"clip2","type":"clip"}},"outputs":{"conditioning":{"id":"87cc45e3-839e-4a20-b93f-7892dbd53d75","name":"conditioning","type":"conditioning"}}},"selected":false,"positionAbsolute":{"x":1100.1437566814066,"y":-851.6600425944865},"dragging":false},{"width":334,"height":236,"dragHandle":".node-drag-handle","id":"d9e4c4e6-de63-4ee1-b2e7-ad11d874bcae","type":"invocation","position":{"x":542.9067581903546,"y":-677.6764639738383},"data":{"id":"d9e4c4e6-de63-4ee1-b2e7-ad11d874bcae","type":"sdxl_refiner_model_loader","inputs":{"model":{"id":"75b59470-6fac-48c6-a2c8-957ecb24abc9","name":"model","type":"model","value":{"model_name":"stable-diffusion-xl-refiner-0.9","base_model":"sdxl-refiner"}}},"outputs":{"unet":{"id":"8c78fcbd-a23d-4434-944c-5d1798156ae4","name":"unet","type":"unet"},"clip2":{"id":"9456e151-1b09-4853-9855-ad6c9f659720","name":"clip2","type":"clip"},"vae":{"id":"a86160bc-037c-4516-8e74-c6298e4d1f15","name":"vae","type":"vae"}}},"selected":true,"positionAbsolute":{"x":542.9067581903546,"y":-677.6764639738383},"dragging":false},{"width":394,"height":425,"dragHandle":".node-drag-handle","id":"c5b420c9-82cd-4863-b416-a620009f2d36","type":"invocation","position":{"x":1090.7741649651036,"y":-368.4448651589644},"data":{"id":"c5b420c9-82cd-4863-b416-a620009f2d36","type":"sdxl_refiner_compel_prompt","inputs":{"style":{"id":"90b996b2-e9f2-4b9b-acf9-d19804fe0228","name":"style","type":"string","value":""},"original_width":{"id":"7d511718-55c7-4c78-9bd5-89c6e293c1c2","name":"original_width","type":"integer","value":1024},"original_height":{"id":"1ec9ea13-ea31-4abd-af49-32d288313291","name":"original_height","type":"integer","value":1024},"crop_top":{"id":"ea1db4fd-521c-4439-9f57-d7d4f16175e7","name":"crop_top","type":"integer","value":0},"crop_left":{"id":"6e7af04e-0281-4f68-a393-1ec5b3af54c1","name":"crop_left","type":"integer","value":0},"aesthetic_score":{"id":"6c270052-bea0-4b08-a6b3-6601457f47bc","name":"aesthetic_score","type":"float","value":6},"clip2":{"id":"813f23b9-533e-4f65-82b3-c5d6cda09a22","name":"clip2","type":"clip"}},"outputs":{"conditioning":{"id":"46e6250e-daea-4d8d-ba32-d3405670cdae","name":"conditioning","type":"conditioning"}}},"selected":false,"positionAbsolute":{"x":1090.7741649651036,"y":-368.4448651589644},"dragging":false},{"width":333,"height":344,"dragHandle":".node-drag-handle","id":"fface4ab-25e3-4714-aca9-bb6c67842cd5","type":"invocation","position":{"x":572.6787481456306,"y":464.11370246551087},"data":{"id":"fface4ab-25e3-4714-aca9-bb6c67842cd5","type":"noise","inputs":{"seed":{"id":"c820a274-fb57-408d-b2b6-a1b1e2db4c39","name":"seed","type":"integer","value":0},"width":{"id":"533435f9-a82d-4841-8a17-33e6c766cd03","name":"width","type":"integer","value":1024},"height":{"id":"8d350b62-ed7a-484c-9cf2-ffe54a96465d","name":"height","type":"integer","value":1024},"use_cpu":{"id":"2cb494a6-5550-43da-802b-4809314615d4","name":"use_cpu","type":"boolean","value":true}},"outputs":{"noise":{"id":"e5a7a90f-656b-474a-b4cb-475d904e82c7","name":"noise","type":"latents"},"width":{"id":"e5b9de92-07f1-4dc5-95de-3096eca8db30","name":"width","type":"integer"},"height":{"id":"8489a4ce-41e9-4d69-bd8d-f0b862d84a3e","name":"height","type":"integer"}}},"selected":false,"positionAbsolute":{"x":572.6787481456306,"y":464.11370246551087},"dragging":false},{"width":384,"height":519,"dragHandle":".node-drag-handle","id":"0d025980-24fa-4b38-a5e6-e38c40ba23cf","type":"invocation","position":{"x":1596.8574685369997,"y":277.26040215989804},"data":{"id":"0d025980-24fa-4b38-a5e6-e38c40ba23cf","type":"t2l_sdxl","inputs":{"positive_conditioning":{"id":"77c4bda4-2d88-4d7c-bfc4-a0843a850109","name":"positive_conditioning","type":"conditioning"},"negative_conditioning":{"id":"4aad4196-2211-401a-82cf-0a66dc8b8374","name":"negative_conditioning","type":"conditioning"},"noise":{"id":"854bd0c1-a480-4c3a-a57a-0bce2d862c89","name":"noise","type":"latents"},"steps":{"id":"828a4cb3-746d-4f21-ab48-ff25b4f4b9f7","name":"steps","type":"integer","value":20},"cfg_scale":{"id":"77ec37ee-775b-49e9-8c6b-96dd81a8b561","name":"cfg_scale","type":"float","value":7.5},"scheduler":{"id":"12cd8ca4-b973-4875-a3e0-e17898ec514a","name":"scheduler","type":"enum","value":"euler"},"unet":{"id":"3465ea9c-f80b-46da-aa2d-6c550dcf4312","name":"unet","type":"unet"},"denoising_end":{"id":"717c3255-3aff-4eb5-9935-2f9b6794571e","name":"denoising_end","type":"float","value":1}},"outputs":{"latents":{"id":"eab9d5f1-aa6e-4ef5-87e6-1a5240e6638a","name":"latents","type":"latents"},"width":{"id":"957af5db-5c7d-4838-ae23-b4da184e4d92","name":"width","type":"integer"},"height":{"id":"337f2d41-78e7-4692-9f9e-7db102f97f8e","name":"height","type":"integer"}}},"selected":false,"positionAbsolute":{"x":1596.8574685369997,"y":277.26040215989804},"dragging":false},{"width":391,"height":610,"dragHandle":".node-drag-handle","id":"64fb34f7-6e0f-4fcf-80c3-9ca436c3623f","type":"invocation","position":{"x":2044.2621231418175,"y":-374.36499568795915},"data":{"id":"64fb34f7-6e0f-4fcf-80c3-9ca436c3623f","type":"l2l_sdxl","inputs":{"positive_conditioning":{"id":"561c26bd-5f7a-4a5b-83ef-a0380248a4b6","name":"positive_conditioning","type":"conditioning"},"negative_conditioning":{"id":"9f61e901-d8a7-4d61-ae69-ff00e8dba6da","name":"negative_conditioning","type":"conditioning"},"noise":{"id":"622b98db-33d9-495b-bc60-d0c1cf268573","name":"noise","type":"latents"},"steps":{"id":"4176611a-7071-4138-9a31-6ff5a7fae44a","name":"steps","type":"integer","value":20},"cfg_scale":{"id":"5223049f-672e-4782-8b75-d80ea4d6116b","name":"cfg_scale","type":"float","value":7.5},"scheduler":{"id":"0368e8b4-eafd-4efc-9e8f-4b0587977a62","name":"scheduler","type":"enum","value":"euler"},"unet":{"id":"f3a6510b-9e5d-4fbc-bcf8-f66d6ad13508","name":"unet","type":"unet"},"latents":{"id":"5aa7d118-7b02-4efd-8010-e72f422088b7","name":"latents","type":"latents"},"denoising_start":{"id":"1e12482b-4c8b-45f2-9f03-b6018e8db6fb","name":"denoising_start","type":"float","value":0.7},"denoising_end":{"id":"59d159d3-0839-41c6-a57f-a1bd4d655634","name":"denoising_end","type":"float","value":1}},"outputs":{"latents":{"id":"990cd073-3cd6-49d2-b27b-e6d783db56e1","name":"latents","type":"latents"},"width":{"id":"310659f0-5ee9-4093-ba93-83d8abdc879f","name":"width","type":"integer"},"height":{"id":"a613cafe-fdb4-4347-a231-e77b3e2ba975","name":"height","type":"integer"}}},"selected":false,"positionAbsolute":{"x":2044.2621231418175,"y":-374.36499568795915},"dragging":false},{"width":250,"height":323,"dragHandle":".node-drag-handle","id":"d1507d22-215b-4197-a893-c070f84c1eb2","type":"invocation","position":{"x":2564.7859493061546,"y":-740.6258381907046},"data":{"id":"d1507d22-215b-4197-a893-c070f84c1eb2","type":"l2i","inputs":{"latents":{"id":"206b269c-f27c-4ab9-9d97-80a26e6b5cc1","name":"latents","type":"latents"},"vae":{"id":"44e700f0-fe33-4d2d-ab74-3512f0625159","name":"vae","type":"vae"},"tiled":{"id":"0441b8f5-c6af-4461-8d9c-39abef4045f7","name":"tiled","type":"boolean","value":true},"fp32":{"id":"dcfa7f96-9d73-44cc-9cf5-418fd796d70c","name":"fp32","type":"boolean","value":true}},"outputs":{"image":{"id":"30c13692-27f8-4560-9ee8-188c4e4be266","name":"image","type":"image"},"width":{"id":"f12d0aee-6b61-4910-a4ef-14764a1dbaa3","name":"width","type":"integer"},"height":{"id":"3851e7e3-702a-4be9-9f51-176e0697c865","name":"height","type":"integer"}}},"selected":false,"positionAbsolute":{"x":2564.7859493061546,"y":-740.6258381907046},"dragging":false},{"width":320,"height":187,"dragHandle":".node-drag-handle","id":"1d7b5526-021f-4ade-bc7b-20173dd5e6f1","type":"invocation","position":{"x":110.88284993961776,"y":580.9044019817862},"data":{"id":"1d7b5526-021f-4ade-bc7b-20173dd5e6f1","type":"rand_int","inputs":{"low":{"id":"b05a55e0-ad8f-4629-9986-6ce27802c590","name":"low","type":"integer","value":0},"high":{"id":"4797e9b7-5d12-49e3-8f2c-64d80f203d42","name":"high","type":"integer","value":2147483647}},"outputs":{"a":{"id":"45fc7618-6803-4641-a976-36629852652d","name":"a","type":"integer"}}},"selected":false,"positionAbsolute":{"x":110.88284993961776,"y":580.9044019817862},"dragging":false},{"width":331,"height":138,"dragHandle":".node-drag-handle","id":"081ba10b-1fd0-4ad6-b4cc-b5aad5fec68e","type":"invocation","position":{"x":979.9163444732269,"y":267.53735270001346},"data":{"id":"081ba10b-1fd0-4ad6-b4cc-b5aad5fec68e","type":"param_float","inputs":{"param":{"id":"50bb0487-8138-415c-b3d4-54eedd70b980","name":"param","type":"float","value":0.7}},"outputs":{"param":{"id":"d3cf0272-d0fb-4085-bb52-9094352471a2","name":"param","type":"float"}}},"selected":false,"positionAbsolute":{"x":979.9163444732269,"y":267.53735270001346},"dragging":false}],"edges":[{"source":"2daddd1d-a468-4841-9ff4-57befb3518a1","sourceHandle":"text","target":"0af8fc93-be59-4002-9956-c700fb778d4a","targetHandle":"style","id":"reactflow__edge-2daddd1d-a468-4841-9ff4-57befb3518a1text-0af8fc93-be59-4002-9956-c700fb778d4astyle"},{"source":"2daddd1d-a468-4841-9ff4-57befb3518a1","sourceHandle":"text","target":"c1c4e1e7-ae79-4fc8-9a98-246175e7c155","targetHandle":"prompt","id":"reactflow__edge-2daddd1d-a468-4841-9ff4-57befb3518a1text-c1c4e1e7-ae79-4fc8-9a98-246175e7c155prompt"},{"source":"0cee1a14-34fa-4b5e-b361-9ebfaba62844","sourceHandle":"text","target":"c1c4e1e7-ae79-4fc8-9a98-246175e7c155","targetHandle":"style","id":"reactflow__edge-0cee1a14-34fa-4b5e-b361-9ebfaba62844text-c1c4e1e7-ae79-4fc8-9a98-246175e7c155style"},{"source":"3898e2dd-0efa-44cf-8d6c-0d23ef184419","sourceHandle":"text","target":"c5b420c9-82cd-4863-b416-a620009f2d36","targetHandle":"style","id":"reactflow__edge-3898e2dd-0efa-44cf-8d6c-0d23ef184419text-c5b420c9-82cd-4863-b416-a620009f2d36style"},{"source":"3898e2dd-0efa-44cf-8d6c-0d23ef184419","sourceHandle":"text","target":"addd5054-97d8-466c-ab3c-41f5e039af02","targetHandle":"prompt","id":"reactflow__edge-3898e2dd-0efa-44cf-8d6c-0d23ef184419text-addd5054-97d8-466c-ab3c-41f5e039af02prompt"},{"source":"96935e84-c864-48a4-b6c0-c940c71c43ae","sourceHandle":"text","target":"addd5054-97d8-466c-ab3c-41f5e039af02","targetHandle":"style","id":"reactflow__edge-96935e84-c864-48a4-b6c0-c940c71c43aetext-addd5054-97d8-466c-ab3c-41f5e039af02style"},{"source":"f3cd6592-f3a4-4d4e-a833-deb36314716f","sourceHandle":"unet","target":"0d025980-24fa-4b38-a5e6-e38c40ba23cf","targetHandle":"unet","id":"reactflow__edge-f3cd6592-f3a4-4d4e-a833-deb36314716funet-0d025980-24fa-4b38-a5e6-e38c40ba23cfunet"},{"source":"f3cd6592-f3a4-4d4e-a833-deb36314716f","sourceHandle":"clip","target":"c1c4e1e7-ae79-4fc8-9a98-246175e7c155","targetHandle":"clip","id":"reactflow__edge-f3cd6592-f3a4-4d4e-a833-deb36314716fclip-c1c4e1e7-ae79-4fc8-9a98-246175e7c155clip"},{"source":"f3cd6592-f3a4-4d4e-a833-deb36314716f","sourceHandle":"clip","target":"addd5054-97d8-466c-ab3c-41f5e039af02","targetHandle":"clip","id":"reactflow__edge-f3cd6592-f3a4-4d4e-a833-deb36314716fclip-addd5054-97d8-466c-ab3c-41f5e039af02clip"},{"source":"f3cd6592-f3a4-4d4e-a833-deb36314716f","sourceHandle":"clip2","target":"c1c4e1e7-ae79-4fc8-9a98-246175e7c155","targetHandle":"clip2","id":"reactflow__edge-f3cd6592-f3a4-4d4e-a833-deb36314716fclip2-c1c4e1e7-ae79-4fc8-9a98-246175e7c155clip2"},{"source":"f3cd6592-f3a4-4d4e-a833-deb36314716f","sourceHandle":"clip2","target":"addd5054-97d8-466c-ab3c-41f5e039af02","targetHandle":"clip2","id":"reactflow__edge-f3cd6592-f3a4-4d4e-a833-deb36314716fclip2-addd5054-97d8-466c-ab3c-41f5e039af02clip2"},{"source":"c1c4e1e7-ae79-4fc8-9a98-246175e7c155","sourceHandle":"conditioning","target":"0d025980-24fa-4b38-a5e6-e38c40ba23cf","targetHandle":"positive_conditioning","id":"reactflow__edge-c1c4e1e7-ae79-4fc8-9a98-246175e7c155conditioning-0d025980-24fa-4b38-a5e6-e38c40ba23cfpositive_conditioning"},{"source":"addd5054-97d8-466c-ab3c-41f5e039af02","sourceHandle":"conditioning","target":"0d025980-24fa-4b38-a5e6-e38c40ba23cf","targetHandle":"negative_conditioning","id":"reactflow__edge-addd5054-97d8-466c-ab3c-41f5e039af02conditioning-0d025980-24fa-4b38-a5e6-e38c40ba23cfnegative_conditioning"},{"source":"fface4ab-25e3-4714-aca9-bb6c67842cd5","sourceHandle":"noise","target":"0d025980-24fa-4b38-a5e6-e38c40ba23cf","targetHandle":"noise","id":"reactflow__edge-fface4ab-25e3-4714-aca9-bb6c67842cd5noise-0d025980-24fa-4b38-a5e6-e38c40ba23cfnoise"},{"source":"1d7b5526-021f-4ade-bc7b-20173dd5e6f1","sourceHandle":"a","target":"fface4ab-25e3-4714-aca9-bb6c67842cd5","targetHandle":"seed","id":"reactflow__edge-1d7b5526-021f-4ade-bc7b-20173dd5e6f1a-fface4ab-25e3-4714-aca9-bb6c67842cd5seed"},{"source":"d9e4c4e6-de63-4ee1-b2e7-ad11d874bcae","sourceHandle":"unet","target":"64fb34f7-6e0f-4fcf-80c3-9ca436c3623f","targetHandle":"unet","id":"reactflow__edge-d9e4c4e6-de63-4ee1-b2e7-ad11d874bcaeunet-64fb34f7-6e0f-4fcf-80c3-9ca436c3623funet"},{"source":"0af8fc93-be59-4002-9956-c700fb778d4a","sourceHandle":"conditioning","target":"64fb34f7-6e0f-4fcf-80c3-9ca436c3623f","targetHandle":"positive_conditioning","id":"reactflow__edge-0af8fc93-be59-4002-9956-c700fb778d4aconditioning-64fb34f7-6e0f-4fcf-80c3-9ca436c3623fpositive_conditioning"},{"source":"c5b420c9-82cd-4863-b416-a620009f2d36","sourceHandle":"conditioning","target":"64fb34f7-6e0f-4fcf-80c3-9ca436c3623f","targetHandle":"negative_conditioning","id":"reactflow__edge-c5b420c9-82cd-4863-b416-a620009f2d36conditioning-64fb34f7-6e0f-4fcf-80c3-9ca436c3623fnegative_conditioning"},{"source":"0d025980-24fa-4b38-a5e6-e38c40ba23cf","sourceHandle":"latents","target":"64fb34f7-6e0f-4fcf-80c3-9ca436c3623f","targetHandle":"latents","id":"reactflow__edge-0d025980-24fa-4b38-a5e6-e38c40ba23cflatents-64fb34f7-6e0f-4fcf-80c3-9ca436c3623flatents"},{"source":"64fb34f7-6e0f-4fcf-80c3-9ca436c3623f","sourceHandle":"latents","target":"d1507d22-215b-4197-a893-c070f84c1eb2","targetHandle":"latents","id":"reactflow__edge-64fb34f7-6e0f-4fcf-80c3-9ca436c3623flatents-d1507d22-215b-4197-a893-c070f84c1eb2latents"},{"source":"d9e4c4e6-de63-4ee1-b2e7-ad11d874bcae","sourceHandle":"vae","target":"d1507d22-215b-4197-a893-c070f84c1eb2","targetHandle":"vae","id":"reactflow__edge-d9e4c4e6-de63-4ee1-b2e7-ad11d874bcaevae-d1507d22-215b-4197-a893-c070f84c1eb2vae"},{"source":"d9e4c4e6-de63-4ee1-b2e7-ad11d874bcae","sourceHandle":"clip2","target":"0af8fc93-be59-4002-9956-c700fb778d4a","targetHandle":"clip2","id":"reactflow__edge-d9e4c4e6-de63-4ee1-b2e7-ad11d874bcaeclip2-0af8fc93-be59-4002-9956-c700fb778d4aclip2"},{"source":"d9e4c4e6-de63-4ee1-b2e7-ad11d874bcae","sourceHandle":"clip2","target":"c5b420c9-82cd-4863-b416-a620009f2d36","targetHandle":"clip2","id":"reactflow__edge-d9e4c4e6-de63-4ee1-b2e7-ad11d874bcaeclip2-c5b420c9-82cd-4863-b416-a620009f2d36clip2"},{"source":"081ba10b-1fd0-4ad6-b4cc-b5aad5fec68e","sourceHandle":"param","target":"0d025980-24fa-4b38-a5e6-e38c40ba23cf","targetHandle":"denoising_end","id":"reactflow__edge-081ba10b-1fd0-4ad6-b4cc-b5aad5fec68eparam-0d025980-24fa-4b38-a5e6-e38c40ba23cfdenoising_end"},{"source":"081ba10b-1fd0-4ad6-b4cc-b5aad5fec68e","sourceHandle":"param","target":"64fb34f7-6e0f-4fcf-80c3-9ca436c3623f","targetHandle":"denoising_start","id":"reactflow__edge-081ba10b-1fd0-4ad6-b4cc-b5aad5fec68eparam-64fb34f7-6e0f-4fcf-80c3-9ca436c3623fdenoising_start"}],"viewport":{"x":388.5523754198109,"y":521.3299012520417,"zoom":0.5336411821766158}} \ No newline at end of file diff --git a/docs/assets/send-to-icon.png b/docs/assets/send-to-icon.png deleted file mode 100644 index 6ff1c9065b9..00000000000 Binary files a/docs/assets/send-to-icon.png and /dev/null differ diff --git a/docs/assets/stable-samples/img2img/mountains-2.png b/docs/assets/stable-samples/img2img/mountains-2.png deleted file mode 100644 index e9f4e708535..00000000000 Binary files a/docs/assets/stable-samples/img2img/mountains-2.png and /dev/null differ diff --git a/docs/assets/stable-samples/img2img/mountains-3.png b/docs/assets/stable-samples/img2img/mountains-3.png deleted file mode 100644 index 017de3012c2..00000000000 Binary files a/docs/assets/stable-samples/img2img/mountains-3.png and /dev/null differ diff --git a/docs/assets/stable-samples/img2img/sketch-mountains-input.jpg b/docs/assets/stable-samples/img2img/sketch-mountains-input.jpg deleted file mode 100644 index 79d652b8003..00000000000 Binary files a/docs/assets/stable-samples/img2img/sketch-mountains-input.jpg and /dev/null differ diff --git a/docs/assets/stable-samples/txt2img/merged-0005.png b/docs/assets/stable-samples/txt2img/merged-0005.png deleted file mode 100644 index ca0a1af2065..00000000000 Binary files a/docs/assets/stable-samples/txt2img/merged-0005.png and /dev/null differ diff --git a/docs/assets/stable-samples/txt2img/merged-0006.png b/docs/assets/stable-samples/txt2img/merged-0006.png deleted file mode 100644 index 999f3703230..00000000000 Binary files a/docs/assets/stable-samples/txt2img/merged-0006.png and /dev/null differ diff --git a/docs/assets/stable-samples/txt2img/merged-0007.png b/docs/assets/stable-samples/txt2img/merged-0007.png deleted file mode 100644 index af390acaf60..00000000000 Binary files a/docs/assets/stable-samples/txt2img/merged-0007.png and /dev/null differ diff --git a/docs/assets/step1.png b/docs/assets/step1.png deleted file mode 100644 index 6309f41f206..00000000000 Binary files a/docs/assets/step1.png and /dev/null differ diff --git a/docs/assets/step2.png b/docs/assets/step2.png deleted file mode 100644 index 06027289b2e..00000000000 Binary files a/docs/assets/step2.png and /dev/null differ diff --git a/docs/assets/step4.png b/docs/assets/step4.png deleted file mode 100644 index c24db6b4702..00000000000 Binary files a/docs/assets/step4.png and /dev/null differ diff --git a/docs/assets/step5.png b/docs/assets/step5.png deleted file mode 100644 index b4e9b50576c..00000000000 Binary files a/docs/assets/step5.png and /dev/null differ diff --git a/docs/assets/step6.png b/docs/assets/step6.png deleted file mode 100644 index c43140c1aab..00000000000 Binary files a/docs/assets/step6.png and /dev/null differ diff --git a/docs/assets/step7.png b/docs/assets/step7.png deleted file mode 100644 index a575af28b22..00000000000 Binary files a/docs/assets/step7.png and /dev/null differ diff --git a/docs/assets/still-life-inpainted.png b/docs/assets/still-life-inpainted.png deleted file mode 100644 index ab8c7bd69a7..00000000000 Binary files a/docs/assets/still-life-inpainted.png and /dev/null differ diff --git a/docs/assets/still-life-scaled.jpg b/docs/assets/still-life-scaled.jpg deleted file mode 100644 index ba9c86be009..00000000000 Binary files a/docs/assets/still-life-scaled.jpg and /dev/null differ diff --git a/docs/assets/textual-inversion/ti-frontend.png b/docs/assets/textual-inversion/ti-frontend.png deleted file mode 100644 index 0500e9b1329..00000000000 Binary files a/docs/assets/textual-inversion/ti-frontend.png and /dev/null differ diff --git a/docs/assets/troubleshooting/broken-dependency.png b/docs/assets/troubleshooting/broken-dependency.png deleted file mode 100644 index 28415089204..00000000000 Binary files a/docs/assets/troubleshooting/broken-dependency.png and /dev/null differ diff --git a/docs/assets/truncation_comparison.jpg b/docs/assets/truncation_comparison.jpg deleted file mode 100644 index a39a804beb7..00000000000 Binary files a/docs/assets/truncation_comparison.jpg and /dev/null differ diff --git a/docs/assets/upscaling.png b/docs/assets/upscaling.png deleted file mode 100644 index e58a538e5ee..00000000000 Binary files a/docs/assets/upscaling.png and /dev/null differ diff --git a/docs/assets/v1-variants-scores.jpg b/docs/assets/v1-variants-scores.jpg deleted file mode 100644 index 9201b985d45..00000000000 Binary files a/docs/assets/v1-variants-scores.jpg and /dev/null differ diff --git a/docs/assets/variation_walkthru/000001.3357757885.png b/docs/assets/variation_walkthru/000001.3357757885.png deleted file mode 100644 index b9aa4a78edf..00000000000 Binary files a/docs/assets/variation_walkthru/000001.3357757885.png and /dev/null differ diff --git a/docs/assets/variation_walkthru/000002.1614299449.png b/docs/assets/variation_walkthru/000002.1614299449.png deleted file mode 100644 index 0db167ae6c1..00000000000 Binary files a/docs/assets/variation_walkthru/000002.1614299449.png and /dev/null differ diff --git a/docs/assets/variation_walkthru/000002.3647897225.png b/docs/assets/variation_walkthru/000002.3647897225.png deleted file mode 100644 index 7fe1f29227c..00000000000 Binary files a/docs/assets/variation_walkthru/000002.3647897225.png and /dev/null differ diff --git a/docs/assets/variation_walkthru/000003.1614299449.png b/docs/assets/variation_walkthru/000003.1614299449.png deleted file mode 100644 index b7f6ae76139..00000000000 Binary files a/docs/assets/variation_walkthru/000003.1614299449.png and /dev/null differ diff --git a/docs/assets/variation_walkthru/000004.3747154981.png b/docs/assets/variation_walkthru/000004.3747154981.png deleted file mode 100644 index e6ac5f3bc98..00000000000 Binary files a/docs/assets/variation_walkthru/000004.3747154981.png and /dev/null differ 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/docs/contributing/ARCHITECTURE.md b/docs/contributing/ARCHITECTURE.md deleted file mode 100644 index d74df94492c..00000000000 --- a/docs/contributing/ARCHITECTURE.md +++ /dev/null @@ -1,93 +0,0 @@ -# Invoke.AI Architecture - -```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 `/frontend` and the backend code is found in `/ldm/invoke/app/api_app.py` and `/ldm/invoke/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 `/ldm/invoke/app/cli_app.py`. - -## 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 (`/ldm/invoke/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 `/ldm/invoke/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](INVOCATIONS.md) 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 `/ldm/invoke/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 `/ldm/invoke/app/`). diff --git a/docs/contributing/CONTRIBUTING.md b/docs/contributing/CONTRIBUTING.md deleted file mode 100644 index ccaf4f25610..00000000000 --- a/docs/contributing/CONTRIBUTING.md +++ /dev/null @@ -1,60 +0,0 @@ -# Contributing - -Invoke AI originated as a project built by the community, and that vision carries forward today as we aim to build the best pro-grade tools available. We work together to incorporate the latest in AI/ML research, making these tools available in over 20 languages to artists and creatives around the world as part of our fully permissive OSS project designed for individual users to self-host and use. - - -# Methods of Contributing to Invoke AI -Anyone who wishes to contribute to InvokeAI, whether features, bug fixes, code cleanup, testing, code reviews, documentation or translation is very much encouraged to do so. - -## Development -If you’d like to help with development, please see our [development guide](contribution_guides/development.md). - -**New Contributors:** If you’re unfamiliar with contributing to open source projects, take a look at our [new contributor guide](contribution_guides/newContributorChecklist.md). - -## Nodes -If you’d like to add a Node, please see our [nodes contribution guide](../nodes/contributingNodes.md). - -## 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 [documentation guide](contribution_guides/documentation.md). - -## Translation -If you'd like to help with translation, please see our [translation guide](contribution_guides/translation.md). - -## Tutorials -Please reach out to @imic or @hipsterusername on [Discord](https://discord.gg/ZmtBAhwWhy) to help create tutorials for InvokeAI. - -We hope you enjoy using our software as much as we enjoy creating it, and we hope that some of those of you who are reading this will elect to become part of our contributor community. - - -# Contributors - -This project is a combined effort of dedicated people from across the world. [Check out the list of all these amazing people](https://invoke-ai.github.io/InvokeAI/other/CONTRIBUTORS/). We thank them for their time, hard work and effort. - -# Code of Conduct - -The InvokeAI community is a welcoming place, and we want your help in maintaining that. Please review our [Code of Conduct](https://github.com/invoke-ai/InvokeAI/blob/main/CODE_OF_CONDUCT.md) to learn more - it's essential to maintaining a respectful and inclusive environment. - -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. -# Support - -For support, please use this repository's [GitHub Issues](https://github.com/invoke-ai/InvokeAI/issues), or join the [Discord](https://discord.gg/ZmtBAhwWhy). - -Original portions of the software are Copyright (c) 2023 by respective contributors. - ---- - -Remember, your contributions help make this project great. We're excited to see what you'll bring to our community! diff --git a/docs/contributing/DOWNLOAD_QUEUE.md b/docs/contributing/DOWNLOAD_QUEUE.md deleted file mode 100644 index 960180961e9..00000000000 --- a/docs/contributing/DOWNLOAD_QUEUE.md +++ /dev/null @@ -1,334 +0,0 @@ -# The InvokeAI Download Queue - -The DownloadQueueService provides a multithreaded parallel download -queue for arbitrary URLs, with queue prioritization, event handling, -and restart capabilities. - -## Simple Example - -``` -from invokeai.app.services.download import DownloadQueueService, TqdmProgress - -download_queue = DownloadQueueService() -for url in ['https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/assets/a-painting-of-a-fire.png?raw=true', - 'https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/assets/birdhouse.png?raw=true', - 'https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/assets/missing.png', - 'https://civitai.com/api/download/models/152309?type=Model&format=SafeTensor', - ]: - - # urls start downloading as soon as download() is called - download_queue.download(source=url, - dest='/tmp/downloads', - on_progress=TqdmProgress().update - ) - -download_queue.join() # wait for all downloads to finish -for job in download_queue.list_jobs(): - print(job.model_dump_json(exclude_none=True, indent=4),"\n") -``` - -Output: - -``` -{ - "source": "https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/assets/a-painting-of-a-fire.png?raw=true", - "dest": "/tmp/downloads", - "id": 0, - "priority": 10, - "status": "completed", - "download_path": "/tmp/downloads/a-painting-of-a-fire.png", - "job_started": "2023-12-04T05:34:41.742174", - "job_ended": "2023-12-04T05:34:42.592035", - "bytes": 666734, - "total_bytes": 666734 -} - -{ - "source": "https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/assets/birdhouse.png?raw=true", - "dest": "/tmp/downloads", - "id": 1, - "priority": 10, - "status": "completed", - "download_path": "/tmp/downloads/birdhouse.png", - "job_started": "2023-12-04T05:34:41.741975", - "job_ended": "2023-12-04T05:34:42.652841", - "bytes": 774949, - "total_bytes": 774949 -} - -{ - "source": "https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/assets/missing.png", - "dest": "/tmp/downloads", - "id": 2, - "priority": 10, - "status": "error", - "job_started": "2023-12-04T05:34:41.742079", - "job_ended": "2023-12-04T05:34:42.147625", - "bytes": 0, - "total_bytes": 0, - "error_type": "HTTPError(Not Found)", - "error": "Traceback (most recent call last):\n File \"/home/lstein/Projects/InvokeAI/invokeai/app/services/download/download_default.py\", line 182, in _download_next_item\n self._do_download(job)\n File \"/home/lstein/Projects/InvokeAI/invokeai/app/services/download/download_default.py\", line 206, in _do_download\n raise HTTPError(resp.reason)\nrequests.exceptions.HTTPError: Not Found\n" -} - -{ - "source": "https://civitai.com/api/download/models/152309?type=Model&format=SafeTensor", - "dest": "/tmp/downloads", - "id": 3, - "priority": 10, - "status": "completed", - "download_path": "/tmp/downloads/xl_more_art-full_v1.safetensors", - "job_started": "2023-12-04T05:34:42.147645", - "job_ended": "2023-12-04T05:34:43.735990", - "bytes": 719020768, - "total_bytes": 719020768 -} -``` - -## The API - -The default download queue is `DownloadQueueService`, an -implementation of ABC `DownloadQueueServiceBase`. It juggles multiple -background download requests and provides facilities for interrogating -and cancelling the requests. Access to a current or past download task -is mediated via `DownloadJob` objects which report the current status -of a job request - -### The Queue Object - -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. - -``` -queue = DownloadQueueService(event_bus=events) -``` - -`DownloadQueueService()` takes three optional arguments: - -| **Argument** | **Type** | **Default** | **Description** | -|----------------|-----------------|---------------|-----------------| -| `max_parallel_dl` | int | 5 | Maximum number of simultaneous downloads allowed | -| `event_bus` | EventServiceBase | None | System-wide FastAPI event bus for reporting download events | -| `requests_session` | requests.sessions.Session | None | An alternative requests Session object to use for the download | - -`max_parallel_dl` specifies how many download jobs are allowed to run -simultaneously. Each will run in a different thread of execution. - -`event_bus` is an EventServiceBase, typically the one created at -InvokeAI startup. If present, download events are periodically emitted -on this bus to allow clients to follow download progress. - -`requests_session` is a url library requests Session object. It is -used for testing. - -### The Job object - -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. - -Two job types are defined. `DownloadJob` and -`MultiFileDownloadJob`. The former is a pydantic object with the -following fields: - -| **Field** | **Type** | **Default** | **Description** | -|----------------|-----------------|---------------|-----------------| -| _Fields passed in at job creation time_ | -| `source` | AnyHttpUrl | | Where to download from | -| `dest` | Path | | Where to download to | -| `access_token` | str | | [optional] string containing authentication token for access | -| `on_start` | Callable | | [optional] callback when the download starts | -| `on_progress` | Callable | | [optional] callback called at intervals during download progress | -| `on_complete` | Callable | | [optional] callback called after successful download completion | -| `on_error` | Callable | | [optional] callback called after an error occurs | -| `id` | int | auto assigned | Job ID, an integer >= 0 | -| `priority` | int | 10 | Job priority. Lower priorities run before higher priorities | -| | -| _Fields updated over the course of the download task_ -| `status` | DownloadJobStatus| | Status code | -| `download_path` | Path | | Path to the location of the downloaded file | -| `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 | -| `bytes` | int | 0 | Bytes downloaded so far | -| `total_bytes` | int | 0 | Total size of the file at the remote site | -| `error_type` | str | | String version of the exception that caused an error during download | -| `error` | str | | String version of the traceback associated with an error | -| `cancelled` | bool | False | Set to true if the job was cancelled by the caller| - -When you create a job, you can assign it a `priority`. If multiple -jobs are queued, the job with the lowest priority runs first. - -Every job has a `source` and a `dest`. `source` is a pydantic.networks AnyHttpUrl object. -The `dest` is a path on the local filesystem that specifies the -destination for the downloaded object. Its semantics are -described below. - -When the job is submitted, it is assigned a numeric `id`. The id can -then be used to fetch the job object from the queue. - -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 ** | -|--------------|---------------------|-------------------| -| `WAITING` | waiting | Job is on the queue but not yet running| -| `RUNNING` | running | The download is started | -| `COMPLETED` | completed | Job has finished its work without an error | -| `ERROR` | error | Job encountered an error and will not run again| - -`job_started` and `job_ended` indicate when the job -was started (using a python timestamp) and when it completed. - -In case of an error, the job's status will be set to `DownloadJobStatus.ERROR`, the text of the -Exception that caused the error will be placed in the `error_type` -field and the traceback that led to the error will be in `error`. - -A cancelled job will have status `DownloadJobStatus.ERROR` and an -`error_type` field of "DownloadJobCancelledException". In addition, -the job's `cancelled` property will be set to True. - -The `MultiFileDownloadJob` is used for diffusers model downloads, -which contain multiple files and directories under a common root: - -| **Field** | **Type** | **Default** | **Description** | -|----------------|-----------------|---------------|-----------------| -| _Fields passed in at job creation time_ | -| `download_parts` | Set[DownloadJob]| | Component download jobs | -| `dest` | Path | | Where to download to | -| `on_start` | Callable | | [optional] callback when the download starts | -| `on_progress` | Callable | | [optional] callback called at intervals during download progress | -| `on_complete` | Callable | | [optional] callback called after successful download completion | -| `on_error` | Callable | | [optional] callback called after an error occurs | -| `id` | int | auto assigned | Job ID, an integer >= 0 | -| _Fields updated over the course of the download task_ -| `status` | DownloadJobStatus| | Status code | -| `download_path` | Path | | Path to the root of the downloaded files | -| `bytes` | int | 0 | Bytes downloaded so far | -| `total_bytes` | int | 0 | Total size of the file at the remote site | -| `error_type` | str | | String version of the exception that caused an error during download | -| `error` | str | | String version of the traceback associated with an error | -| `cancelled` | bool | False | Set to true if the job was cancelled by the caller| - -Note that the MultiFileDownloadJob does not support the `priority`, -`job_started`, `job_ended` or `content_type` attributes. You can get -these from the individual download jobs in `download_parts`. - - -### Callbacks - -Download jobs can be associated with a series of callbacks, each with -the signature `Callable[["DownloadJob"], None]`. The callbacks are assigned -using optional arguments `on_start`, `on_progress`, `on_complete` and -`on_error`. When the corresponding event occurs, the callback wil be -invoked and passed the job. The callback will be run in a `try:` -context in the same thread as the download job. Any exceptions that -occur during execution of the callback will be caught and converted -into a log error message, thereby allowing the download to continue. - -#### `TqdmProgress` - -The `invokeai.app.services.download.download_default` module defines a -class named `TqdmProgress` which can be used as an `on_progress` -handler to display a completion bar in the console. Use as follows: - -``` -from invokeai.app.services.download import TqdmProgress - -download_queue.download(source='http://some.server.somewhere/some_file', - dest='/tmp/downloads', - on_progress=TqdmProgress().update - ) - -``` - -### Events - -If the queue was initialized with the InvokeAI event bus (the case -when using `ApiDependencies.invoker.services.download_queue`), then -download events will also be issued on the bus. The events are: - -* `download_started` -- This is issued when a job is taken off the -queue and a request is made to the remote server for the URL headers, but before any data -has been downloaded. The event payload will contain the keys `source` -and `download_path`. The latter contains the path that the URL will be -downloaded to. - -* `download_progress -- This is issued periodically as the download -runs. The payload contains the keys `source`, `download_path`, -`current_bytes` and `total_bytes`. The latter two fields can be -used to display the percent complete. - -* `download_complete` -- This is issued when the download completes -successfully. The payload contains the keys `source`, `download_path` -and `total_bytes`. - -* `download_error` -- This is issued when the download stops because -of an error condition. The payload contains the fields `error_type` -and `error`. The former is the text representation of the exception, -and the latter is a traceback showing where the error occurred. - -### Job control - -To create a job call the queue's `download()` method. You can list all -jobs using `list_jobs()`, fetch a single job by its with -`id_to_job()`, cancel a running job with `cancel_job()`, cancel all -running jobs with `cancel_all_jobs()`, and wait for all jobs to finish -with `join()`. - -#### job = queue.download(source, dest, priority, access_token, on_start, on_progress, on_complete, on_cancelled, on_error) - -Create a new download job and put it on the queue, returning the -DownloadJob object. - -#### multifile_job = queue.multifile_download(parts, dest, access_token, on_start, on_progress, on_complete, on_cancelled, on_error) - -This is similar to download(), but instead of taking a single source, -it accepts a `parts` argument consisting of a list of -`RemoteModelFile` objects. Each part corresponds to a URL/Path pair, -where the URL is the location of the remote file, and the Path is the -destination. - -`RemoteModelFile` can be imported from `invokeai.backend.model_manager.metadata`, and -consists of a url/path pair. Note that the path *must* be relative. - -The method returns a `MultiFileDownloadJob`. - - -``` -from invokeai.backend.model_manager.metadata import RemoteModelFile -remote_file_1 = RemoteModelFile(url='http://www.foo.bar/my/pytorch_model.safetensors'', - path='my_model/textencoder/pytorch_model.safetensors' - ) -remote_file_2 = RemoteModelFile(url='http://www.bar.baz/vae.ckpt', - path='my_model/vae/diffusers_model.safetensors' - ) -job = queue.multifile_download(parts=[remote_file_1, remote_file_2], - dest='/tmp/downloads', - on_progress=TqdmProgress().update) -queue.wait_for_job(job) -print(f"The files were downloaded to {job.download_path}") -``` - -#### jobs = queue.list_jobs() - -Return a list of all active and inactive `DownloadJob`s. - -#### job = queue.id_to_job(id) - -Return the job corresponding to given ID. - -Return a list of all active and inactive `DownloadJob`s. - -#### queue.prune_jobs() - -Remove inactive (complete or errored) jobs from the listing returned -by `list_jobs()`. - -#### queue.join() - -Block until all pending jobs have run to completion or errored out. - diff --git a/docs/contributing/INVOCATIONS.md b/docs/contributing/INVOCATIONS.md deleted file mode 100644 index ce1ee9e808a..00000000000 --- a/docs/contributing/INVOCATIONS.md +++ /dev/null @@ -1,395 +0,0 @@ -# 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: - -```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 -``` - -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 -from .cool_node import CoolInvocation -``` - -## 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 - - -- Create a new class that derives from a predefined parent class called - `BaseInvocation`. -- Every Invocation must have a `docstring` that describes what this Invocation - does. -- While not strictly required, we suggest every invocation class name ends in - "Invocation", eg "CropImageInvocation". -- Every Invocation must use the `@invocation` decorator to provide its unique - invocation type. You may also provide its title, tags and category using the - decorator. -- 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. - -```python -from invokeai.app.invocations.baseinvocation 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. - -```python -from invokeai.app.invocations.baseinvocation import BaseInvocation, InputField, invocation -from invokeai.app.invocations.primitives import ImageField - -@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 types for our field. Indicates that the image must be an `ImageField` type. | -| Field | `InputField(description="The input image")` | The image variable is an `InputField` which needs a description. | - -Great. Now let us create our other inputs for `width` and `height` - -```python -from invokeai.app.invocations.baseinvocation import BaseInvocation, InputField, invocation -from invokeai.app.invocations.primitives import ImageField - -@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") -``` - -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 contraints 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. - -```python -from invokeai.app.invocations.baseinvocation import BaseInvocation, InputField, invocation, InvocationContext -from invokeai.app.invocations.primitives import ImageField - -@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. - -```python -from invokeai.app.invocations.baseinvocation import BaseInvocation, InputField, invocation, InvocationContext -from invokeai.app.invocations.primitives import ImageField -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. - -```python -from invokeai.app.invocations.baseinvocation import BaseInvocation, InputField, invocation, InvocationContext -from invokeai.app.invocations.primitives import ImageField -from invokeai.app.invocations.image import ImageOutput, ResourceOrigin, ImageCategory - -@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. - -```python -@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") - ... -``` - -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/contributing/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/contributing/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](nodes/communityNodes) list. If you're not sure how to do that, -take a look a at our [contributing nodes overview](contributingNodes). - -## 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. - -```python -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 a class that derive from the `BaseModel` type from `pydantic`. -So let's create one. - -```python -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. - -> Technically, the properties are _also_ called fields - but in this case, it -> refers to a `pydantic` field. - -```python -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/contributing/LOCAL_DEVELOPMENT.md b/docs/contributing/LOCAL_DEVELOPMENT.md deleted file mode 100644 index 0136fbfcd08..00000000000 --- a/docs/contributing/LOCAL_DEVELOPMENT.md +++ /dev/null @@ -1,278 +0,0 @@ -# Local Development - -If you are looking to contribute you will need to have a local development -environment. See the -[Developer Install](../installation/020_INSTALL_MANUAL.md#developer-install) for -full details. - -Broadly this involves cloning the repository, installing the pre-reqs, and -InvokeAI (in editable form). Assuming this is working, choose your area of -focus. - -## Documentation - -We use [mkdocs](https://www.mkdocs.org) for our documentation with the -[material theme](https://squidfunk.github.io/mkdocs-material/). Documentation is -written in markdown files under the `./docs` folder and then built into a static -website for hosting with GitHub Pages at -[invoke-ai.github.io/InvokeAI](https://invoke-ai.github.io/InvokeAI). - -To contribute to the documentation you'll need to install the dependencies. Note -the use of `"`. - -```zsh -pip install ".[docs]" -``` - -Now, to run the documentation locally with hot-reloading for changes made. - -```zsh -mkdocs serve -``` - -You'll then be prompted to connect to `http://127.0.0.1:8080` in order to -access. - -## Backend - -The backend is contained within the `./invokeai/backend` and `./invokeai/app` directories. -To get started please install the development dependencies. - -From the root of the repository run the following command. Note the use of `"`. - -```zsh -pip install ".[dev,test]" -``` - -These are optional groups of packages which are defined within the `pyproject.toml` -and will be required for testing the changes you make to the code. - -### Tests - -See the [tests documentation](./TESTS.md) for information about running and writing tests. -### Reloading Changes - -Experimenting with changes to the Python source code is a drag if you have to re-start the server — -and re-load those 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. - - -## Front End - - - ---8<-- "invokeai/frontend/web/README.md" - -## Developing InvokeAI in VSCode - -VSCode offers some nice tools: - -- python debugger -- automatic `venv` activation -- remote dev (e.g. run InvokeAI on a beefy linux desktop while you type in - comfort on your macbook) - -### Setup - -You'll need the -[Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) -and -[Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) -extensions installed first. - -It's also really handy to install the `Jupyter` extensions: - -- [Jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) -- [Jupyter Cell Tags](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-jupyter-cell-tags) -- [Jupyter Notebook Renderers](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter-renderers) -- [Jupyter Slide Show](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-jupyter-slideshow) - -#### InvokeAI workspace - -Creating a VSCode workspace for working on InvokeAI is highly recommended. It -can hold InvokeAI-specific settings and configs. - -To make a workspace: - -- Open the InvokeAI repo dir in VSCode -- `File` > `Save Workspace As` > save it _outside_ the repo - -#### Default python interpreter (i.e. automatic virtual environment activation) - -- Use command palette to run command - `Preferences: Open Workspace Settings (JSON)` -- Add `python.defaultInterpreterPath` to `settings`, pointing to your `venv`'s - python - -Should look something like this: - -```jsonc -{ - // I like to have all InvokeAI-related folders in my workspace - "folders": [ - { - // repo root - "path": "InvokeAI" - }, - { - // InvokeAI root dir, where `invokeai.yaml` lives - "path": "/path/to/invokeai_root" - } - ], - "settings": { - // Where your InvokeAI `venv`'s python executable lives - "python.defaultInterpreterPath": "/path/to/invokeai_root/.venv/bin/python" - } -} -``` - -Now when you open the VSCode integrated terminal, or do anything that needs to -run python, it will automatically be in your InvokeAI virtual environment. - -Bonus: When you create a Jupyter notebook, when you run it, you'll be prompted -for the python interpreter to run in. This will default to your `venv` python, -and so you'll have access to the same python environment as the InvokeAI app. - -This is _super_ handy. - -#### Enabling Type-Checking with Pylance - -We use python's typing system in InvokeAI. PR reviews will include checking that types are present and correct. We don't enforce types with `mypy` at this time, but that is on the horizon. - -Using a code analysis tool to automatically type check your code (and types) is very important when writing with types. These tools provide immediate feedback in your editor when types are incorrect, and following their suggestions lead to fewer runtime bugs. - -Pylance, installed at the beginning of this guide, is the de-facto python LSP (language server protocol). It provides type checking in the editor (among many other features). Once installed, you do need to enable type checking manually: - -- Open a python file -- Look along the status bar in VSCode for `{ } Python` -- Click the `{ }` -- Turn type checking on - basic is fine - -You'll now see red squiggly lines where type issues are detected. Hover your cursor over the indicated symbols to see what's wrong. - -In 99% of cases when the type checker says there is a problem, there really is a problem, and you should take some time to understand and resolve what it is pointing out. - -#### Debugging configs with `launch.json` - -Debugging configs are managed in a `launch.json` file. Like most VSCode configs, -these can be scoped to a workspace or folder. - -Follow the [official guide](https://code.visualstudio.com/docs/python/debugging) -to set up your `launch.json` and try it out. - -Now we can create the InvokeAI debugging configs: - -```jsonc -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - // Run the InvokeAI backend & serve the pre-built UI - "name": "InvokeAI Web", - "type": "python", - "request": "launch", - "program": "scripts/invokeai-web.py", - "args": [ - // Your InvokeAI root dir (where `invokeai.yaml` lives) - "--root", - "/path/to/invokeai_root", - // Access the app from anywhere on your local network - "--host", - "0.0.0.0" - ], - "justMyCode": true - }, - { - // Run the nodes-based CLI - "name": "InvokeAI CLI", - "type": "python", - "request": "launch", - "program": "scripts/invokeai-cli.py", - "justMyCode": true - }, - { - // Run tests - "name": "InvokeAI Test", - "type": "python", - "request": "launch", - "module": "pytest", - "args": ["--capture=no"], - "justMyCode": true - }, - { - // Run a single test - "name": "InvokeAI Single Test", - "type": "python", - "request": "launch", - "module": "pytest", - "args": [ - // Change this to point to the specific test you are working on - "tests/nodes/test_invoker.py" - ], - "justMyCode": true - }, - { - // This is the default, useful to just run a single file - "name": "Python: File", - "type": "python", - "request": "launch", - "program": "${file}", - "justMyCode": true - } - ] -} -``` - -You'll see these configs in the debugging configs drop down. Running them will -start InvokeAI with attached debugger, in the correct environment, and work just -like the normal app. - -Enjoy debugging InvokeAI with ease (not that we have any bugs of course). - -#### Remote dev - -This is very easy to set up and provides the same very smooth experience as -local development. Environments and debugging, as set up above, just work, -though you'd need to recreate the workspace and debugging configs on the remote. - -Consult the -[official guide](https://code.visualstudio.com/docs/remote/remote-overview) to -get it set up. - -Suggest using VSCode's included settings sync so that your remote dev host has -all the same app settings and extensions automagically. - -##### One remote dev gotcha - -I've found the automatic port forwarding to be very flakey. You can disable it -in `Preferences: Open Remote Settings (ssh: hostname)`. Search for -`remote.autoForwardPorts` and untick the box. - -To forward ports very reliably, use SSH on the remote dev client (e.g. your -macbook). Here's how to forward both backend API port (`9090`) and the frontend -live dev server port (`5173`): - -```bash -ssh \ - -L 9090:localhost:9090 \ - -L 5173:localhost:5173 \ - user@remote-dev-host -``` - -The forwarding stops when you close the terminal window, so suggest to do this -_outside_ the VSCode integrated terminal in case you need to restart VSCode for -an extension update or something - -Now, on your remote dev client, you can open `localhost:9090` and access the UI, -now served from the remote dev host, just the same as if it was running on the -client. diff --git a/docs/contributing/MODEL_MANAGER.md b/docs/contributing/MODEL_MANAGER.md deleted file mode 100644 index 9699db4f1a6..00000000000 --- a/docs/contributing/MODEL_MANAGER.md +++ /dev/null @@ -1,1659 +0,0 @@ -# Introduction to the Model Manager V2 - -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_records/` -* `invokeai/app/services/model_install/` -* `invokeai/app/services/downloads/` -* `invokeai/app/services/model_load/` - -Code related to the FastAPI web API can be found in -`invokeai/app/api/routers/model_manager_v2.py`. - -*** - -## 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. - -`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`: - -``` -from invokeai.app.services.model_records import ModelType, ModelFormat, BaseModelType -``` - -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. - -### 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: - -``` -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: - -``` -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: - -``` -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: - -``` -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: - -``` -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: - -``` -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 databsae. - -#### 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: - -``` -# 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: - -``` -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: - -``` -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) -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: - -``` -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 overriden 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: - -``` -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 intialization 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 me! 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: - -* enqueued -* running -* running -* running -* 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: - -* enqueued -* running -* running -* paused -* running -* 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. - -``` -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: - -``` -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: - -``` -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: - -``` -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: - -``` -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 - -``` -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: - -``` -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) | -| `locker` | ModelLockerBase | A context manager that mediates the movement of the model into VRAM | - -### get_model_by_key(key, [submodel]) -> LoadedModel - -The `get_model_by_key()` method will retrieve the model using its -unique database key. For example: - -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: - -``` -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: - -``` -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: - -``` -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: - -``` -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: - -``` -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 that 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: - -``` -_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: - -``` -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: - -``` -configs = mm.store.get_model_by_attr(name='stable-diffusion-v1-5') -``` - -### mm.install - -This retrieves the `ModelInstallService` associated with the manager. -Example: - -``` -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: - -``` -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/contributing/TESTS.md b/docs/contributing/TESTS.md deleted file mode 100644 index 8d823bb4e97..00000000000 --- a/docs/contributing/TESTS.md +++ /dev/null @@ -1,89 +0,0 @@ -# InvokeAI Backend Tests - -We use `pytest` to run the backend python tests. (See [pyproject.toml](/pyproject.toml) for the default `pytest` options.) - -## Fast vs. Slow -All tests are categorized as either 'fast' (no test annotation) or 'slow' (annotated with the `@pytest.mark.slow` decorator). - -'Fast' tests are run to validate every PR, and are fast enough that they can be run routinely during development. - -'Slow' tests are currently only run manually on an ad-hoc basis. In the future, they may be automated to run nightly. Most developers are only expected to run the 'slow' tests that directly relate to the feature(s) that they are working on. - -As a rule of thumb, tests should be marked as 'slow' if there is a chance that they take >1s (e.g. on a CPU-only machine with slow internet connection). Common examples of slow tests are tests that depend on downloading a model, or running model inference. - -## Running Tests - -Below are some common test commands: -```bash -# Run the fast tests. (This implicitly uses the configured default option: `-m "not slow"`.) -pytest tests/ - -# Equivalent command to run the fast tests. -pytest tests/ -m "not slow" - -# Run the slow tests. -pytest tests/ -m "slow" - -# Run the slow tests from a specific file. -pytest tests/path/to/slow_test.py -m "slow" - -# Run all tests (fast and slow). -pytest tests -m "" -``` - -## Test Organization - -All backend tests are in the [`tests/`](/tests/) directory. This directory mirrors the organization of the `invokeai/` directory. For example, tests for `invokeai/model_management/model_manager.py` would be found in `tests/model_management/test_model_manager.py`. - -TODO: The above statement is aspirational. A re-organization of legacy tests is required to make it true. - -## Tests that depend on models - -There are a few things to keep in mind when adding tests that depend on models. - -1. If a required model is not already present, it should automatically be downloaded as part of the test setup. -2. If a model is already downloaded, it should not be re-downloaded unnecessarily. -3. Take reasonable care to keep the total number of models required for the tests low. Whenever possible, re-use models that are already required for other tests. If you are adding a new model, consider including a comment to explain why it is required/unique. - -There are several utilities to help with model setup for tests. Here is a sample test that depends on a model: -```python -import pytest -import torch - -from invokeai.backend.model_management.models.base import BaseModelType, ModelType -from invokeai.backend.util.test_utils import install_and_load_model - -@pytest.mark.slow -def test_model(model_installer, torch_device): - model_info = install_and_load_model( - model_installer=model_installer, - model_path_id_or_url="HF/dummy_model_id", - model_name="dummy_model", - base_model=BaseModelType.StableDiffusion1, - model_type=ModelType.Dummy, - ) - - dummy_input = build_dummy_input(torch_device) - - with torch.no_grad(), model_info as model: - model.to(torch_device, dtype=torch.float32) - output = model(dummy_input) - - # Validate output... - -``` - -## Test Coverage - -To review test coverage, append `--cov` to your pytest command: -```bash -pytest tests/ --cov -``` - -Test outcomes and coverage will be reported in the terminal. In addition, a more detailed report is created in both XML and HTML format in the `./coverage` folder. The HTML output is particularly helpful in identifying untested statements where coverage should be improved. The HTML report can be viewed by opening `./coverage/html/index.html`. - -??? info "HTML coverage report output" - - ![html-overview](../assets/contributing/html-overview.png) - - ![html-detail](../assets/contributing/html-detail.png) diff --git a/docs/contributing/contribution_guides/development.md b/docs/contributing/contribution_guides/development.md deleted file mode 100644 index 092d2acae30..00000000000 --- a/docs/contributing/contribution_guides/development.md +++ /dev/null @@ -1,49 +0,0 @@ -# Development - -## **What do I need to know to help?** - -If you are looking to help to with a code contribution, InvokeAI uses several different technologies under the hood: Python (Pydantic, FastAPI, diffusers) and Typescript (React, Redux Toolkit, ChakraUI, Mantine, Konva). Familiarity with StableDiffusion and image generation concepts is helpful, but not essential. - - -## **Get Started** - -To get started, take a look at our [new contributors checklist](newContributorChecklist.md) - -Once you're setup, for more information, you can review the documentation specific to your area of interest: - -* #### [InvokeAI Architecure](../ARCHITECTURE.md) -* #### [Frontend Documentation](https://github.com/invoke-ai/InvokeAI/tree/main/invokeai/frontend/web) -* #### [Node Documentation](../INVOCATIONS.md) -* #### [Local Development](../LOCAL_DEVELOPMENT.md) - - - -If you don't feel ready to make a code contribution yet, no problem! You can also help out in other ways, such as [documentation](documentation.md), [translation](translation.md) or helping support other users and triage issues as they're reported in GitHub. - -There are two paths to making a development contribution: - -1. Choosing an open issue to address. Open issues can be found in the [Issues](https://github.com/invoke-ai/InvokeAI/issues?q=is%3Aissue+is%3Aopen) section of the InvokeAI repository. These are tagged by the issue type (bug, enhancement, etc.) along with the “good first issues” tag denoting if they are suitable for first time contributors. - 1. Additional items can be found on our [roadmap](https://github.com/orgs/invoke-ai/projects/7). The roadmap is organized in terms of priority, and contains features of varying size and complexity. If there is an inflight item you’d like to help with, reach out to the contributor assigned to the item to see how you can help. -2. Opening a new issue or feature to add. **Please make sure you have searched through existing issues before creating new ones.** - -*Regardless of what you choose, please post in the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord before you start development in order to confirm that the issue or feature is aligned with the current direction of the project. We value our contributors time and effort and want to ensure that no one’s time is being misspent.* - -## Best Practices: -* Keep your pull requests small. Smaller pull requests are more likely to be accepted and merged -* Comments! Commenting your code helps 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, **@psychedelicious** is the best person to reach out to. - -For backend related work, please reach out to **@blessedcoolant**, **@lstein**, **@StAlKeR7779** or **@psychedelicious**. - - -## **What does the Code of Conduct mean for me?** - -Our [Code of Conduct](../../CODE_OF_CONDUCT.md) means that you are responsible for treating everyone on the project with respect and courtesy regardless of their identity. If you are the victim of any inappropriate behavior or comments as described in our Code of Conduct, we are here for you and will do the best to ensure that the abuser is reprimanded appropriately, per our code. - diff --git a/docs/contributing/contribution_guides/documentation.md b/docs/contributing/contribution_guides/documentation.md deleted file mode 100644 index 1b5b93f9ba9..00000000000 --- a/docs/contributing/contribution_guides/documentation.md +++ /dev/null @@ -1,13 +0,0 @@ -# Documentation - -Documentation is an important part of any open source project. It provides a clear and concise way to communicate how the software works, how to use it, and how to troubleshoot issues. Without proper documentation, it can be difficult for users to understand the purpose and functionality of the project. - -## Contributing - -All documentation is maintained in the InvokeAI GitHub repository. If you come across documentation that is out of date or incorrect, please submit a pull request with the necessary changes. - -When updating or creating documentation, please keep in mind InvokeAI is a tool for everyone, not just those who have familiarity with generative art. - -## Help & Questions - -Please ping @imic or @hipsterusername in the [Discord](https://discord.com/channels/1020123559063990373/1049495067846524939) if you have any questions. \ No newline at end of file diff --git a/docs/contributing/contribution_guides/newContributorChecklist.md b/docs/contributing/contribution_guides/newContributorChecklist.md deleted file mode 100644 index 90725f99ab1..00000000000 --- a/docs/contributing/contribution_guides/newContributorChecklist.md +++ /dev/null @@ -1,68 +0,0 @@ -# New Contributor Guide - -If you're a new contributor to InvokeAI or Open Source Projects, this is the guide for you. - -## New Contributor Checklist -- [x] Set up your local development environment & fork of InvokAI by following [the steps outlined here](../../installation/020_INSTALL_MANUAL.md#developer-install) -- [x] Set up your local tooling with [this guide](InvokeAI/contributing/LOCAL_DEVELOPMENT/#developing-invokeai-in-vscode). Feel free to skip this step if you already have tooling you're comfortable with. -- [x] Familiarize yourself with [Git](https://www.atlassian.com/git) & our project structure by reading through the [development documentation](development.md) -- [x] Join the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord -- [x] 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! -- [x] Make your first Pull Request with the guide below -- [x] 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](../LOCAL_DEVELOPMENT.md). - -1. Find a [good first issue](https://github.com/invoke-ai/InvokeAI/contribute) that you are interested in addressing or a feature that you would like to add. Then, reach out to our team in the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord to ensure you are setup for success. -2. Fork the [InvokeAI](https://github.com/invoke-ai/InvokeAI) repository to your GitHub profile. This means that you will have a copy of the repository under **your-GitHub-username/InvokeAI**. -3. Clone the repository to your local machine using: -```bash -git clone https://github.com/your-GitHub-username/InvokeAI.git -``` -If you're unfamiliar with using Git through the commandline, [GitHub Desktop](https://desktop.github.com) is a easy-to-use alternative with a UI. You can do all the same steps listed here, but through the interface. -4. Create a new branch for your fix using: -```bash -git checkout -b branch-name-here -``` -5. Make the appropriate changes for the issue you are trying to address or the feature that you want to add. -6. Add the file contents of the changed files to the "snapshot" git uses to manage the state of the project, also known as the index: -```bash -git add -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! - -If you’d like to learn more about contributing to Open Source projects, here is a [Getting Started Guide](https://opensource.com/article/19/7/create-pull-request-github). - - -## 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/contributing/contribution_guides/translation.md b/docs/contributing/contribution_guides/translation.md deleted file mode 100644 index 669e4033467..00000000000 --- a/docs/contributing/contribution_guides/translation.md +++ /dev/null @@ -1,19 +0,0 @@ -# Translation - -InvokeAI uses [Weblate](https://weblate.org/) for translation. Weblate is a FOSS project providing a scalable translation service. Weblate automates the tedious parts of managing translation of a growing project, and the service is generously provided at no cost to FOSS projects like InvokeAI. - -## Contributing - -If you'd like to contribute by adding or updating a translation, please visit our [Weblate project](https://hosted.weblate.org/engage/invokeai/). You'll need to sign in with your GitHub account (a number of other accounts are supported, including Google). - -Once signed in, select a language and then the Web UI component. From here you can Browse and Translate strings from English to your chosen language. Zen mode offers a simpler translation experience. - -Your changes will be attributed to you in the automated PR process; you don't need to do anything else. - -## Help & Questions - -Please check Weblate's [documentation](https://docs.weblate.org/en/latest/index.html) or ping @Harvestor on [Discord](https://discord.com/channels/1020123559063990373/1049495067846524939) if you have any questions. - -## Thanks - -Thanks to the InvokeAI community for their efforts to translate the project! \ No newline at end of file diff --git a/docs/contributing/contribution_guides/tutorials.md b/docs/contributing/contribution_guides/tutorials.md deleted file mode 100644 index 0d550e7023f..00000000000 --- a/docs/contributing/contribution_guides/tutorials.md +++ /dev/null @@ -1,11 +0,0 @@ -# Tutorials - -Tutorials help new & existing users expand their abilty to use InvokeAI to the full extent of our features and services. - -Currently, we have a set of tutorials available on our [YouTube channel](https://www.youtube.com/@invokeai), but as InvokeAI continues to evolve with new updates, we want to ensure that we are giving our users the resources they need to succeed. - -Tutorials can be in the form of videos or article walkthroughs on a subject of your choice. We recommend focusing tutorials on the key image generation methods, or on a specific component within one of the image generation methods. - -## Contributing - -Please reach out to @imic or @hipsterusername on [Discord](https://discord.gg/ZmtBAhwWhy) to help create tutorials for InvokeAI. \ No newline at end of file diff --git a/docs/contributing/frontend/OVERVIEW.md b/docs/contributing/frontend/OVERVIEW.md deleted file mode 100644 index 486e75ea9f9..00000000000 --- a/docs/contributing/frontend/OVERVIEW.md +++ /dev/null @@ -1,133 +0,0 @@ -# Invoke UI - -Invoke's UI is made possible by many contributors and open-source libraries. Thank you! - -## Dev environment - -### Setup - -1. Install [node] and [pnpm]. -1. Run `pnpm i` to install all packages. - -#### Run in dev mode - -1. From `invokeai/frontend/web/`, run `pnpm dev`. -1. From repo root, run `python scripts/invokeai-web.py`. -1. Point your browser to the dev server address, e.g. - -### Package scripts - -- `dev`: run the frontend in dev mode, enabling hot reloading -- `build`: run all checks (madge, eslint, prettier, tsc) and then build the frontend -- `typegen`: generate types from the OpenAPI schema (see [Type generation]) -- `lint: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 (failures here are just suggestions, not hard fails) -- `lint`: run all checks concurrently -- `fix`: run `eslint` and `prettier`, fixing fixable issues - -### Type generation - -We use [openapi-typescript] to generate types from the app's OpenAPI schema. - -The generated types are committed to the repo in [schema.ts]. - -```sh -# from the repo root, start the server -python scripts/invokeai-web.py -# from invokeai/frontend/web/, run the script -pnpm typegen -``` - -### Localization - -We use [i18next] for localization, but translation to languages other than English happens on our [Weblate] project. - -Only the English source strings should be changed on this repo. - -### VSCode - -#### Example debugger config - -```jsonc -{ - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Invoke UI", - "url": "http://localhost:5173", - "webRoot": "${workspaceFolder}/invokeai/frontend/web" - } - ] -} -``` - -#### Remote dev - -We've noticed an intermittent timeout issue with the VSCode remote dev port forwarding. - -We suggest disabling the editor's port forwarding feature and doing it manually via SSH: - -```sh -ssh -L 9090:localhost:9090 -L 5173:localhost:5173 user@host -``` - -## Contributing Guidelines - -Thanks for your interest in contributing to the Invoke Web UI! - -Please follow these guidelines when contributing. - -### Check in before investing your time - -Please check in before you invest your time on anything besides a trivial fix, in case it conflicts with ongoing work or isn't aligned with the vision for the app. - -If a feature request or issue doesn't already exist for the thing you want to work on, please create one. - -Ping `@psychedelicious` on [discord] in the `#frontend-dev` channel or in the feature request / issue you want to work on - we're happy 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` - -### Submitting a PR - -- Ensure your branch is tidy. Use an interactive rebase to clean up the commit history and reword the commit messages if they are not descriptive. -- Run `pnpm lint`. Some issues are auto-fixable with `pnpm fix`. -- Fill out the PR form when creating the PR. - - It doesn't need to be super detailed, but a screenshot or video is nice if you changed something visually. - - If a section isn't relevant, delete it. There are no UI tests at this time. - -## Other docs - -- [Workflows - Design and Implementation] -- [State Management] - -[node]: https://nodejs.org/en/download/ -[pnpm]: https://github.com/pnpm/pnpm -[discord]: https://discord.gg/ZmtBAhwWhy -[i18next]: https://github.com/i18next/react-i18next -[Weblate]: https://hosted.weblate.org/engage/invokeai/ -[openapi-typescript]: https://github.com/drwpow/openapi-typescript -[Type generation]: #type-generation -[schema.ts]: 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.md -[State Management]: ./STATE_MGMT.md diff --git a/docs/contributing/frontend/STATE_MGMT.md b/docs/contributing/frontend/STATE_MGMT.md deleted file mode 100644 index 443446c01c6..00000000000 --- a/docs/contributing/frontend/STATE_MGMT.md +++ /dev/null @@ -1,38 +0,0 @@ -# State Management - -The app makes heavy use of Redux Toolkit, its Query library, and `nanostores`. - -## Redux - -TODO - -## `nanostores` - -[nanostores] is a tiny state management library. It provides both imperative and declarative APIs. - -### Example - -```ts -export const $myStringOption = atom(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/contributing/frontend/WORKFLOWS.md b/docs/contributing/frontend/WORKFLOWS.md deleted file mode 100644 index 533419e0702..00000000000 --- a/docs/contributing/frontend/WORKFLOWS.md +++ /dev/null @@ -1,314 +0,0 @@ -# Workflows - Design and Implementation - -> 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/features/CONFIGURATION.md b/docs/features/CONFIGURATION.md deleted file mode 100644 index d6bfe44901c..00000000000 --- a/docs/features/CONFIGURATION.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -title: Configuration ---- - -# :material-tune-variant: InvokeAI Configuration - -## Intro - -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`. - -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 - -# Put user settings here - see https://invoke-ai.github.io/InvokeAI/features/CONFIGURATION/: -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. - -### 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 - -### All Settings - -Following the table are additional explanations for certain settings. - - -::: invokeai.app.services.config.config_default.InvokeAIAppConfig - options: - heading_level: 4 - members: false - show_docstring_description: false - group_by_category: true - show_category_heading: false - - -#### 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. - -#### 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. - -#### 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: - -``` - 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. - -``` - 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. - -[basic guide to yaml files]: https://circleci.com/blog/what-is-yaml-a-beginner-s-guide/ -[Model Marketplace API Keys]: #model-marketplace-api-keys diff --git a/docs/features/CONTROLNET.md b/docs/features/CONTROLNET.md deleted file mode 100644 index 718b12b0f8b..00000000000 --- a/docs/features/CONTROLNET.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -title: Control Adapters ---- - -# :material-loupe: Control Adapters - -## ControlNet - -ControlNet is a powerful set of features developed by the open-source -community (notably, Stanford researcher -[**@ilyasviel**](https://github.com/lllyasviel)) that allows you to -apply a secondary neural network model to your image generation -process in Invoke. - -With ControlNet, you can get more control over the output of your -image generation, providing you with a way to direct the network -towards generating images that better fit your desired style or -outcome. - -ControlNet works by analyzing an input image, pre-processing that -image to identify relevant information that can be interpreted by each -specific ControlNet model, and then inserting that control information -into the generation process. This can be used to adjust the style, -composition, or other aspects of the image to better achieve a -specific result. - -#### Installation - -InvokeAI provides access to a series of ControlNet models that provide -different effects or styles in your generated images. - -To install ControlNet Models: - -1. The easiest way to install them is -to use the InvokeAI model installer application. Use the -`invoke.sh`/`invoke.bat` launcher to select item [4] and then navigate -to the CONTROLNETS section. Select the models you wish to install and -press "APPLY CHANGES". You may also enter additional HuggingFace -repo_ids in the "Additional models" textbox. -2. Using the "Add Model" function of the model manager, enter the HuggingFace Repo ID of the ControlNet. The ID is in the format "author/repoName" - - -_Be aware that some ControlNet models require additional code -functionality in order to work properly, so just installing a -third-party ControlNet model may not have the desired effect._ Please -read and follow the documentation for installing a third party model -not currently included among InvokeAI's default list. - -Currently InvokeAI **only** supports 🤗 Diffusers-format ControlNet models. These are -folders that contain the files `config.json` and/or -`diffusion_pytorch_model.safetensors` and -`diffusion_pytorch_model.fp16.safetensors`. The name of the folder is -the name of the model. - -🤗 Diffusers-format ControlNet models are available at HuggingFace -(http://huggingface.co) and accessed via their repo IDs (identifiers -in the format "author/modelname"). - -#### ControlNet Models -The models currently supported include: - -**Canny**: - -When the Canny model is used in ControlNet, Invoke will attempt to generate images that match the edges detected. - -Canny edge detection works by detecting the edges in an image by looking for abrupt changes in intensity. It is known for its ability to detect edges accurately while reducing noise and false edges, and the preprocessor can identify more information by decreasing the thresholds. - -**M-LSD**: - -M-LSD is another edge detection algorithm used in ControlNet. It stands for Multi-Scale Line Segment Detector. - -It detects straight line segments in an image by analyzing the local structure of the image at multiple scales. It can be useful for architectural imagery, or anything where straight-line structural information is needed for the resulting output. - -**Lineart**: - -The Lineart model in ControlNet generates line drawings from an input image. The resulting pre-processed image is a simplified version of the original, with only the outlines of objects visible.The Lineart model in ControlNet is known for its ability to accurately capture the contours of the objects in an input sketch. - -**Lineart Anime**: - -A variant of the Lineart model that generates line drawings with a distinct style inspired by anime and manga art styles. - -**Depth**: -A model that generates depth maps of images, allowing you to create more realistic 3D models or to simulate depth effects in post-processing. - -**Normal Map (BAE):** -A model that generates normal maps from input images, allowing for more realistic lighting effects in 3D rendering. - -**Image Segmentation**: -A model that divides input images into segments or regions, each of which corresponds to a different object or part of the image. (More details coming soon) - -**QR Code Monster**: -A model that helps generate creative QR codes that still scan. Can also be used to create images with text, logos or shapes within them. - -**Openpose**: -The OpenPose control model allows for the identification of the general pose of a character by pre-processing an existing image with a clear human structure. With advanced options, Openpose can also detect the face or hands in the image. - -*Note:* The DWPose Processor has replaced the OpenPose processor in Invoke. Workflows and generations that relied on the OpenPose Processor will need to be updated to use the DWPose Processor instead. - -**Mediapipe Face**: - -The MediaPipe Face identification processor is able to clearly identify facial features in order to capture vivid expressions of human faces. - -**Tile**: - -The Tile model fills out details in the image to match the image, rather than the prompt. The Tile Model is a versatile tool that offers a range of functionalities. Its primary capabilities can be boiled down to two main behaviors: - -- It can reinterpret specific details within an image and create fresh, new elements. -- It has the ability to disregard global instructions if there's a discrepancy between them and the local context or specific parts of the image. In such cases, it uses the local context to guide the process. - -The Tile Model can be a powerful tool in your arsenal for enhancing image quality and details. If there are undesirable elements in your images, such as blurriness caused by resizing, this model can effectively eliminate these issues, resulting in cleaner, crisper images. Moreover, it can generate and add refined details to your images, improving their overall quality and appeal. - -**Pix2Pix (experimental)** - -With Pix2Pix, you can input an image into the controlnet, and then "instruct" the model to change it using your prompt. For example, you can say "Make it winter" to add more wintry elements to a scene. - -Each of these models can be adjusted and combined with other ControlNet models to achieve different results, giving you even more control over your image generation process. - - -### Using ControlNet - -To use ControlNet, you can simply select the desired model and adjust both the ControlNet and Pre-processor settings to achieve the desired result. You can also use multiple ControlNet models at the same time, allowing you to achieve even more complex effects or styles in your generated images. - - -Each ControlNet has two settings that are applied to the ControlNet. - -Weight - Strength of the Controlnet model applied to the generation for the section, defined by start/end. - -Start/End - 0 represents the start of the generation, 1 represents the end. The Start/end setting controls what steps during the generation process have the ControlNet applied. - -Additionally, each ControlNet section can be expanded in order to manipulate settings for the image pre-processor that adjusts your uploaded image before using it in when you Invoke. - -## T2I-Adapter -[T2I-Adapter](https://github.com/TencentARC/T2I-Adapter) is a tool similar to ControlNet that allows for control over the generation process by providing control information during the generation process. T2I-Adapter models tend to be smaller and more efficient than ControlNets. - -##### Installation -To install T2I-Adapter Models: - -1. The easiest way to install models is -to use the InvokeAI model installer application. Use the -`invoke.sh`/`invoke.bat` launcher to select item [5] and then navigate -to the T2I-Adapters section. Select the models you wish to install and -press "APPLY CHANGES". You may also enter additional HuggingFace -repo_ids in the "Additional models" textbox. -2. Using the "Add Model" function of the model manager, enter the HuggingFace Repo ID of the T2I-Adapter. The ID is in the format "author/repoName" - -#### Usage -Each T2I Adapter has two settings that are applied. - -Weight - Strength of the model applied to the generation for the section, defined by start/end. - -Start/End - 0 represents the start of the generation, 1 represents the end. The Start/end setting controls what steps during the generation process have the ControlNet applied. - -Additionally, each section can be expanded with the "Show Advanced" button in order to manipulate settings for the image pre-processor that adjusts your uploaded image before using it in during the generation process. - - -## IP-Adapter - -[IP-Adapter](https://ip-adapter.github.io) is a tooling that allows for image prompt capabilities with text-to-image diffusion models. IP-Adapter works by analyzing the given image prompt to extract features, then passing those features to the UNet along with any other conditioning provided. - -![IP-Adapter + T2I](https://github.com/tencent-ailab/IP-Adapter/raw/main/assets/demo/ip_adpter_plus_multi.jpg) - -![IP-Adapter + IMG2IMG](https://raw.githubusercontent.com/tencent-ailab/IP-Adapter/main/assets/demo/image-to-image.jpg) - -#### Installation -There are several ways to install IP-Adapter models with an existing InvokeAI installation: - -1. Through the command line interface launched from the invoke.sh / invoke.bat scripts, option [4] to download models. -2. Through the Model Manager UI with models from the *Tools* section of [models.invoke.ai](https://models.invoke.ai). To do this, copy the repo ID from the desired model page, and paste it in the Add Model field of the model manager. **Note** Both the IP-Adapter and the Image Encoder must be installed for IP-Adapter to work. For example, the [SD 1.5 IP-Adapter](https://models.invoke.ai/InvokeAI/ip_adapter_plus_sd15) and [SD1.5 Image Encoder](https://models.invoke.ai/InvokeAI/ip_adapter_sd_image_encoder) must be installed to use IP-Adapter with SD1.5 based models. -3. **Advanced -- Not recommended ** Manually downloading the IP-Adapter and Image Encoder files - Image Encoder folders shouid be placed in the `models\any\clip_vision` folders. IP Adapter Model folders should be placed in the relevant `ip-adapter` folder of relevant base model folder of Invoke root directory. For example, for the SDXL IP-Adapter, files should be added to the `model/sdxl/ip_adapter/` folder. - -#### Using IP-Adapter - -IP-Adapter can be used by navigating to the *Control Adapters* options and enabling IP-Adapter. - -IP-Adapter requires an image to be used as the Image Prompt. It can also be used in conjunction with text prompts, Image-to-Image, Inpainting, Outpainting, ControlNets and LoRAs. - - -Each IP-Adapter has two settings that are applied to the IP-Adapter: - -* Weight - Strength of the IP-Adapter model applied to the generation for the section, defined by start/end -* Start/End - 0 represents the start of the generation, 1 represents the end. The Start/end setting controls what steps during the generation process have the IP-Adapter applied. diff --git a/docs/features/DATABASE.md b/docs/features/DATABASE.md deleted file mode 100644 index 85829bef868..00000000000 --- a/docs/features/DATABASE.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Database ---- - -# Invoke's SQLite Database - -Invoke uses a SQLite database to store image, workflow, model, and execution data. - -We take great care to ensure your data is safe, by utilizing transactions and a database migration system. - -Even so, when testing an prerelease version of the app, we strongly suggest either backing up your database or using an in-memory database. This ensures any prelease hiccups or databases schema changes will not cause problems for your data. - -## Database Backup - -Backing up your database is very simple. Invoke's data is stored in an `$INVOKEAI_ROOT` directory - where your `invoke.sh`/`invoke.bat` and `invokeai.yaml` files live. - -To back up your database, copy the `invokeai.db` file from `$INVOKEAI_ROOT/databases/invokeai.db` to somewhere safe. - -If anything comes up during prelease testing, you can simply copy your backup back into `$INVOKEAI_ROOT/databases/`. - -## In-Memory Database - -SQLite can run on an in-memory database. Your existing database is untouched when this mode is enabled, but your existing data won't be accessible. - -This is very useful for testing, as there is no chance of a database change modifying your "physical" database. - -To run Invoke with a memory database, edit your `invokeai.yaml` file, and add `use_memory_db: true` to the `Paths:` stanza: - -```yaml -InvokeAI: - Development: - use_memory_db: true -``` - -Delete this line (or set it to `false`) to use your main database. diff --git a/docs/features/GALLERY.md b/docs/features/GALLERY.md deleted file mode 100644 index cc84dbf704e..00000000000 --- a/docs/features/GALLERY.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: InvokeAI Gallery Panel ---- - -# :material-web: InvokeAI Gallery Panel - -## Quick guided walkthrough of the Gallery Panel's features - -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. - -![image](../assets/gallery/gallery.png) - -### Board Display and Settings - -At the very top of the Gallery Panel are the boards disclosure and settings buttons. - -![image](../assets/gallery/top_controls.png) - -The disclosure button shows the name of the currently selected board and allows you to show and hide the board thumbnails (shown in the image below). - -![image](../assets/gallery/board_thumbnails.png) - -The settings button opens a list of options. - -![image](../assets/gallery/board_settings.png) - -- ***Image Size*** this slider lets you control the size of the image previews (images of three different sizes). -- ***Auto-Switch to New Images*** if you turn this on, whenever a new image is generated, it will automatically be loaded into the current image panel on the Text to Image tab and into the result panel on the [Image to Image](IMG2IMG.md) tab. This will happen invisibly if you are on any other tab when the image is generated. -- ***Auto-Assign Board on Click*** whenever an image is generated or saved, it always gets put in a board. The board it gets put into is marked with AUTO (image of board marked). Turning on Auto-Assign Board on Click will make whichever board you last selected be the destination when you click Invoke. That means you can click Invoke, select a different board, and then click Invoke again and the two images will be put in two different boards. (bold)It's the board selected when Invoke is clicked that's used, not the board that's selected when the image is finished generating.(bold) Turning this off, enables the Auto-Add Board drop down which lets you set one specific board to always put generated images into. This also enables and disables the Auto-add to this Board menu item described below. -- ***Always Show Image Size Badge*** this toggles whether to show image sizes for each image preview (show two images, one with sizes shown, one without) - -Below these two buttons, you'll see the Search Boards text entry area. You use this to search for specific boards by the name of the board. -Next to it is the Add Board (+) button which lets you add new boards. Boards can be renamed by clicking on the name of the board under its thumbnail and typing in the new name. - -### Board Thumbnail Menu - -Each board has a context menu (ctrl+click / right-click). - -![image](../assets/gallery/thumbnail_menu.png) - -- ***Auto-add to this Board*** if you've disabled Auto-Assign Board on Click in the board settings, you can use this option to set this board to be where new images are put. -- ***Download Board*** this will add all the images in the board into a zip file and provide a link to it in a notification (image of notification) -- ***Delete Board*** this will delete the board -> [!CAUTION] -> This will delete all the images in the board and the board itself. - -### Board Contents - -Every board is organized by two tabs, Images and Assets. - -![image](../assets/gallery/board_tabs.png) - -Images are the Invoke-generated images that are placed into the board. Assets are images that you upload into Invoke to be used as an [Image Prompt](https://support.invoke.ai/support/solutions/articles/151000159340-using-the-image-prompt-adapter-ip-adapter-) or in the [Image to Image](IMG2IMG.md) tab. - -### Image Thumbnail Menu - -Every image generated by Invoke has its generation information stored as text inside the image file itself. This can be read directly by selecting the image and clicking on the Info button ![image](../assets/gallery/info_button.png) in any of the image result panels. - -Each image also has a context menu (ctrl+click / right-click). - -![image](../assets/gallery/image_menu.png) - - The options are (items marked with an * will not work with images that lack generation information): -- ***Open in New Tab*** this will open the image alone in a new browser tab, separate from the Invoke interface. -- ***Download Image*** this will trigger your browser to download the image. -- ***Load Workflow **** this will load any workflow settings into the Workflow tab and automatically open it. -- ***Remix Image **** this will load all of the image's generation information, (bold)excluding its Seed, into the left hand control panel -- ***Use Prompt **** this will load only the image's text prompts into the left-hand control panel -- ***Use Seed **** this will load only the image's Seed into the left-hand control panel -- ***Use All **** this will load all of the image's generation information into the left-hand control panel -- ***Send to Image to Image*** this will put the image into the left-hand panel in the Image to Image tab ana automatically open it -- ***Send to Unified Canvas*** This will (bold)replace whatever is already present(bold) in the Unified Canvas tab with the image and automatically open the tab -- ***Change Board*** this will oipen a small window that will let you move the image to a different board. This is the same as dragging the image to that board's thumbnail. -- ***Star Image*** this will add the image to the board's list of starred images that are always kept at the top of the gallery. This is the same as clicking on the star on the top right-hand side of the image that appears when you hover over the image with the mouse -- ***Delete Image*** this will delete the image from the board -> [!CAUTION] -> This will delete the image entirely from Invoke. - -## Summary - -This walkthrough only covers the Gallery interface and Boards. Actually generating images is handled by [Prompts](PROMPTS.md), the [Image to Image](IMG2IMG.md) tab, and the [Unified Canvas](UNIFIED_CANVAS.md). - -## 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/features/IMG2IMG.md b/docs/features/IMG2IMG.md deleted file mode 100644 index 046a25fdca0..00000000000 --- a/docs/features/IMG2IMG.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Image-to-Image ---- - -# :material-image-multiple: Image-to-Image - -InvokeAI provides an "img2img" feature that lets you seed your -creations with an initial 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. - -For a walkthrough of using Image-to-Image in the Web UI, see [InvokeAI -Web Server](./WEB.md#image-to-image). - -The main difference between `img2img` and `prompt2img` is the starting point. -While `prompt2img` always starts with pure gaussian noise and progressively -refines it over the requested number of steps, `img2img` skips some of these -earlier steps (how many it skips is indirectly controlled by the `--strength` -parameter), and uses instead your initial image mixed with gaussian noise as the -starting image. - -**Let's start** by thinking about vanilla `prompt2img`, just generating an image -from a prompt. If the step count is 10, then the "latent space" (Stable -Diffusion's internal representation of the image) for the prompt "fire" with -seed `1592514025` develops something like this: - -!!! example "" - -
- ![latent steps](../assets/img2img/000019.steps.png){ width=720 } -
- -Put simply: starting from a frame of fuzz/static, SD finds details in each frame -that it thinks look like "fire" and brings them a little bit more into focus, -gradually scrubbing out the fuzz until a clear image remains. - -**When you use `img2img`** some of the earlier steps are cut, and instead an -initial image of your choice is used. But because of how the maths behind Stable -Diffusion works, this image needs to be mixed with just the right amount of -noise (fuzz/static) for where it is being inserted. This is where the strength -parameter comes in. Depending on the set strength, your image will be inserted -into the sequence at the appropriate point, with just the right amount of noise. - -### A concrete example - -!!! example "I want SD to draw a fire based on this hand-drawn image" - - ![drawing of a fireplace](../assets/img2img/fire-drawing.png){ align=left } - - Let's only do 10 steps, to make it easier to see what's happening. If strength - is `0.7`, this is what the internal steps the algorithm has to take will look - like: - -
- ![gravity32](../assets/img2img/000032.steps.gravity.png) -
- - With strength `0.4`, the steps look more like this: - -
- ![gravity30](../assets/img2img/000030.steps.gravity.png) -
- -Notice how much more fuzzy the starting image is for strength `0.7` compared to -`0.4`, and notice also how much longer the sequence is with `0.7`: - -| | strength = 0.7 | strength = 0.4 | -| --------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | -| initial image that SD sees | ![step-0](../assets/img2img/000032.step-0.png) | ![step-0](../assets/img2img/000030.step-0.png) | -| steps argument to `invoke>` | `-S10` | `-S10` | -| steps actually taken | `7` | `4` | -| latent space at each step | ![gravity32](../assets/img2img/000032.steps.gravity.png) | ![gravity30](../assets/img2img/000030.steps.gravity.png) | -| output | ![000032.1592514025](../assets/img2img/000032.1592514025.png) | ![000030.1592514025](../assets/img2img/000030.1592514025.png) | - -Both of the outputs look kind of like what I was thinking of. With the strength -higher, my input becomes more vague, _and_ Stable Diffusion has more steps to -refine its output. But it's not really making what I want, which is a picture of -cheery open fire. With the strength lower, my input is more clear, _but_ Stable -Diffusion has less chance to refine itself, so the result ends up inheriting all -the problems of my bad drawing. - -If you want to try this out yourself, all of these are using a seed of -`1592514025` with a width/height of `384`, step count `10`, the -`k_lms` sampler, and the single-word prompt `"fire"`. - -### Compensating for the reduced step count - -After putting this guide together I was curious to see how the difference would -be if I increased the step count to compensate, so that SD could have the same -amount of steps to develop the image regardless of the strength. So I ran the -generation again using the same seed, but this time adapting the step count to -give each generation 20 steps. - -Here's strength `0.4` (note step count `50`, which is `20 ÷ 0.4` to make sure SD -does `20` steps from my image): - -
-![000035.1592514025](../assets/img2img/000035.1592514025.png) -
- -and here is strength `0.7` (note step count `30`, which is roughly `20 ÷ 0.7` to -make sure SD does `20` steps from my image): - -
-![000046.1592514025](../assets/img2img/000046.1592514025.png) -
- -In both cases the image is nice and clean and "finished", but because at -strength `0.7` Stable Diffusion has been give so much more freedom to improve on -my badly-drawn flames, they've come out looking much better. You can really see -the difference when looking at the latent steps. There's more noise on the first -image with strength `0.7`: - -
-![gravity46](../assets/img2img/000046.steps.gravity.png) -
- -than there is for strength `0.4`: - -
-![gravity35](../assets/img2img/000035.steps.gravity.png) -
- -and that extra noise gives the algorithm more choices when it is evaluating how -to denoise any particular pixel in the image. - -Unfortunately, it seems that `img2img` is very sensitive to the step count. -Here's strength `0.7` with a step count of `29` (SD did 19 steps from my image): - -
-![gravity45](../assets/img2img/000045.1592514025.png) -
- -By comparing the latents we can sort of see that something got interpreted -differently enough on the third or fourth step to lead to a rather different -interpretation of the flames. - -
-![gravity46](../assets/img2img/000046.steps.gravity.png) -
- -
-![gravity45](../assets/img2img/000045.steps.gravity.png) -
- -This is the result of a difference in the de-noising "schedule" - basically the -noise has to be cleaned by a certain degree each step or the model won't -"converge" on the image properly (see -[stable diffusion blog](https://huggingface.co/blog/stable_diffusion) for more -about that). A different step count means a different schedule, which means -things get interpreted slightly differently at every step. diff --git a/docs/features/LOGGING.md b/docs/features/LOGGING.md deleted file mode 100644 index bda968140b3..00000000000 --- a/docs/features/LOGGING.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -title: Controlling Logging ---- - -# :material-image-off: Controlling Logging - -## Controlling How InvokeAI Logs Status Messages - -InvokeAI logs status messages using a configurable logging system. You -can log to the terminal window, to a designated file on the local -machine, to the syslog facility on a Linux or Mac, or to a properly -configured web server. You can configure several logs at the same -time, and control the level of message logged and the logging format -(to a limited extent). - -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: - -```bash -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: - -```bash -### 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 -``` diff --git a/docs/features/LORAS.md b/docs/features/LORAS.md deleted file mode 100644 index 5dcfc8bf21b..00000000000 --- a/docs/features/LORAS.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: LoRAs & LCM-LoRAs ---- - -# :material-library-shelves: LoRAs & LCM-LoRAs - -With the advances in research, many new capabilities are available to customize the knowledge and understanding of novel concepts not originally contained in the base model. - -## LoRAs - -Low-Rank Adaptation (LoRA) files are models that customize the output of Stable Diffusion -image generation. Larger than embeddings, but much smaller than full -models, they augment SD with improved understanding of subjects and -artistic styles. - -Unlike TI files, LoRAs do not introduce novel vocabulary into the -model's known tokens. Instead, LoRAs augment the model's weights that -are applied to generate imagery. LoRAs may be supplied with a -"trigger" word that they have been explicitly trained on, or may -simply apply their effect without being triggered. - -LoRAs are typically stored in .safetensors files, which are the most -secure way to store and transmit these types of weights. - -To use these when generating, open the LoRA menu item in the options -panel, select the LoRAs you want to apply and ensure that they have -the appropriate weight recommended by the model provider. Typically, -most LoRAs perform best at a weight of .75-1. - - -## LCM-LoRAs -Latent Consistency Models (LCMs) allowed a reduced number of steps to be used to generate images with Stable Diffusion. These are created by distilling base models, creating models that only require a small number of steps to generate images. However, LCMs require that any fine-tune of a base model be distilled to be used as an LCM. - -LCM-LoRAs are models that provide the benefit of LCMs but are able to be used as LoRAs and applied to any fine tune of a base model. LCM-LoRAs are created by training a small number of adapters, rather than distilling the entire fine-tuned base model. The resulting LoRA can be used the same way as a standard LoRA, but with a greatly reduced step count. This enables SDXL images to be generated up to 10x faster than without the use of LCM-LoRAs. - - -**Using LCM-LoRAs** - -LCM-LoRAs are natively supported in InvokeAI throughout the application. To get started, install any diffusers format LCM-LoRAs using the model manager and select it in the LoRA field. - -There are a number parameter differences when using LCM-LoRAs and standard generation: - -- When using LCM-LoRAs, the LoRA strength should be lower than if using a standard LoRA, with 0.35 recommended as a starting point. -- The LCM scheduler should be used for generation -- CFG-Scale should be reduced to ~1 -- Steps should be reduced in the range of 4-8 - -Standard LoRAs can also be used alongside LCM-LoRAs, but will also require a lower strength, with 0.45 being recommended as a starting point. - -More information can be found here: https://huggingface.co/blog/lcm_lora#fast-inference-with-sdxl-lcm-loras diff --git a/docs/features/MODEL_MERGING.md b/docs/features/MODEL_MERGING.md deleted file mode 100644 index e384662ef5d..00000000000 --- a/docs/features/MODEL_MERGING.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Model Merging ---- - -InvokeAI provides the ability to merge two or three diffusers-type models into a new merged model. The -resulting model will combine characteristics of the original, and can -be used to teach an old model new tricks. - -## How to Merge Models - -Model Merging can be be done by navigating to the Model Manager and clicking the "Merge Models" tab. From there, you can select the models and settings you want to use to merge th models. - -## Settings - -* Model Selection: there are three multiple choice fields that - display all the diffusers-style models that InvokeAI knows about. - If you do not see the model you are looking for, then it is probably - a legacy checkpoint model and needs to be converted using the - "Convert" option in the Web-based Model Manager tab. - - You must select at least two models to merge. The third can be left - at "None" if you desire. - -* Alpha: This is the ratio to use when combining models. It ranges - from 0 to 1. The higher the value, the more weight is given to the - 2d and (optionally) 3d models. So if you have two models named "A" - and "B", an alpha value of 0.25 will give you a merged model that is - 25% A and 75% B. - -* Interpolation Method: This is the method used to combine - weights. The options are "weighted_sum" (the default), "sigmoid", - "inv_sigmoid" and "add_difference". Each produces slightly different - results. When three models are in use, only "add_difference" is - available. - -* Save Location: The location you want the merged model to be saved in. Default is in the InvokeAI root folder - -* Name for merged model: This is the name for the new model. Please - use InvokeAI conventions - only alphanumeric letters and the - characters ".+-". - -* Ignore Mismatches / Force: Not all models are compatible with each other. The merge - script will check for compatibility and refuse to merge ones that - are incompatible. Set this checkbox to try merging anyway. - - - -You may run the merge script by starting the invoke launcher -(`invoke.sh` or `invoke.bat`) and choosing the option (4) for _merge -models_. This will launch a text-based interactive user interface that -prompts you to select the models to merge, how to merge them, and the -merged model name. - -Alternatively you may activate InvokeAI's virtual environment from the -command line, and call the script via `merge_models --gui` to open up -a version that has a nice graphical front end. To get the commandline- -only version, omit `--gui`. - -The user interface for the text-based interactive script is -straightforward. It shows you a series of setting fields. Use control-N (^N) -to move to the next field, and control-P (^P) to move to the previous -one. You can also use TAB and shift-TAB to move forward and -backward. Once you are in a multiple choice field, use the up and down -cursor arrows to move to your desired selection, and press or - to select it. Change text fields by typing in them, and adjust -scrollbars using the left and right arrow keys. - -Once you are happy with your settings, press the OK button. Note that -there may be two pages of settings, depending on the height of your -screen, and the OK button may be on the second page. Advance past the -last field of the first page to get to the second page, and reverse -this to get back. - -If the merge runs successfully, it will create a new diffusers model -under the selected name and register it with InvokeAI. - - diff --git a/docs/features/OTHER.md b/docs/features/OTHER.md deleted file mode 100644 index 00bfd05e146..00000000000 --- a/docs/features/OTHER.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Others ---- - -# :fontawesome-regular-share-from-square: Others - -## **Google Colab** - -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg){ align="right" }](https://colab.research.google.com/github/lstein/stable-diffusion/blob/main/notebooks/Stable_Diffusion_AI_Notebook.ipynb) - -Open and follow instructions to use an isolated environment running Dream. - -Output Example: - -![Colab Notebook](../assets/colab_notebook.png) - ---- - -## **Invisible Watermark** - -In keeping with the principles for responsible AI generation, and to -help AI researchers avoid synthetic images contaminating their -training sets, InvokeAI adds an invisible watermark to each of the -final images it generates. The watermark consists of the text -"InvokeAI" and can be viewed using the -[invisible-watermarks](https://github.com/ShieldMnt/invisible-watermark) -tool. - -Watermarking is controlled using the `invisible-watermark` setting in -`invokeai.yaml`. To turn it off, add the following line under the `Features` -category. - -``` -invisible_watermark: false -``` - - -## **Weighted Prompts** - -You may weight different sections of the prompt to tell the sampler to attach different levels of -priority to them, by adding `:` to the end of the section you wish to up- or downweight. For -example consider this prompt: - -```bash -(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. - diff --git a/docs/features/POSTPROCESS.md b/docs/features/POSTPROCESS.md deleted file mode 100644 index 33a2d194124..00000000000 --- a/docs/features/POSTPROCESS.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Postprocessing ---- - -# :material-image-edit: Postprocessing - -This sections details the ability to improve faces and upscale images. - -## Face Fixing - -As of InvokeAI 3.0, the easiest way to improve faces created during image generation is through the Inpainting functionality of the Unified Canvas. Simply add the image containing the faces that you would like to improve to the canvas, mask the face to be improved and run the invocation. For best results, make sure to use an inpainting specific model; these are usually identified by the "-inpainting" term in the model name. - -## Upscaling - -Open the upscaling dialog by clicking on the "expand" icon located -above the image display area in the Web UI: - -
-![upscale1](../assets/features/upscale-dialog.png) -
- -The default upscaling option is Real-ESRGAN x2 Plus, which will scale your image by a factor of two. This means upscaling a 512x512 image will result in a new 1024x1024 image. - -Other options are the x4 upscalers, which will scale your image by a factor of 4. - - -!!! note - - Real-ESRGAN is 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 after that - process is complete. While the image generation is taking place, you will still be able to preview - the base images. - -## How to disable - -If, for some reason, you do not wish to load the ESRGAN libraries, -you can disable them on the invoke.py command line with the `--no_esrgan` options. diff --git a/docs/features/PROMPTS.md b/docs/features/PROMPTS.md deleted file mode 100644 index 5eff1aa2a5f..00000000000 --- a/docs/features/PROMPTS.md +++ /dev/null @@ -1,274 +0,0 @@ ---- -title: Prompting-Features ---- - -# :octicons-command-palette-24: Prompting-Features - -## **Prompt Syntax Features** - -The InvokeAI prompting language has the following features: - -### Attention weighting - -Append a word or phrase with `-` or `+`, or a weight between `0` and `2` -(`1`=default), to decrease or increase "attention" (= a mix of per-token CFG -weighting multiplier and, for `-`, a weighted blend with the prompt without the -term). - -The following syntax is recognised: - -- single words without parentheses: `a tall thin man picking apricots+` -- single or multiple words with parentheses: - `a tall thin man picking (apricots)+` `a tall thin man picking (apricots)-` - `a tall thin man (picking apricots)+` `a tall thin man (picking apricots)-` -- more effect with more symbols `a tall thin man (picking apricots)++` -- nesting `a tall thin man (picking apricots+)++` (`apricots` effectively gets - `+++`) -- all of the above with explicit numbers `a tall thin man picking (apricots)1.1` - `a tall thin man (picking (apricots)1.3)1.1`. (`+` is equivalent to 1.1, `++` - is pow(1.1,2), `+++` is pow(1.1,3), etc; `-` means 0.9, `--` means pow(0.9,2), - etc.) - -You can use this to increase or decrease the amount of something. Starting from -this prompt of `a man picking apricots from a tree`, let's see what happens if -we increase and decrease how much attention we want Stable Diffusion to pay to -the word `apricots`: - -
- -![an AI generated image of a man picking apricots from a tree](../assets/prompt_syntax/apricots-0.png) - -
- -Using `-` to reduce apricot-ness: - -| `a man picking apricots- from a tree` | `a man picking apricots-- from a tree` | `a man picking apricots--- from a tree` | -| ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| ![an AI generated image of a man picking apricots from a tree, with smaller apricots](../assets/prompt_syntax/apricots--1.png) | ![an AI generated image of a man picking apricots from a tree, with even smaller and fewer apricots](../assets/prompt_syntax/apricots--2.png) | ![an AI generated image of a man picking apricots from a tree, with very few very small apricots](../assets/prompt_syntax/apricots--3.png) | - -Using `+` to increase apricot-ness: - -| `a man picking apricots+ from a tree` | `a man picking apricots++ from a tree` | `a man picking apricots+++ from a tree` | `a man picking apricots++++ from a tree` | `a man picking apricots+++++ from a tree` | -| ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ![an AI generated image of a man picking apricots from a tree, with larger, more vibrant apricots](../assets/prompt_syntax/apricots-1.png) | ![an AI generated image of a man picking apricots from a tree with even larger, even more vibrant apricots](../assets/prompt_syntax/apricots-2.png) | ![an AI generated image of a man picking apricots from a tree, but the man has been replaced by a pile of apricots](../assets/prompt_syntax/apricots-3.png) | ![an AI generated image of a man picking apricots from a tree, but the man has been replaced by a mound of giant melting-looking apricots](../assets/prompt_syntax/apricots-4.png) | ![an AI generated image of a man picking apricots from a tree, but the man and the leaves and parts of the ground have all been replaced by giant melting-looking apricots](../assets/prompt_syntax/apricots-5.png) | - -You can also change the balance between different parts of a prompt. For -example, below is a `mountain man`: - -
- -![an AI generated image of a mountain man](../assets/prompt_syntax/mountain-man.png) - -
- -And here he is with more mountain: - -| `mountain+ man` | `mountain++ man` | `mountain+++ man` | -| ---------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- | -| ![](../assets/prompt_syntax/mountain1-man.png) | ![](../assets/prompt_syntax/mountain2-man.png) | ![](../assets/prompt_syntax/mountain3-man.png) | - -Or, alternatively, with more man: - -| `mountain man+` | `mountain man++` | `mountain man+++` | `mountain man++++` | -| ---------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- | -| ![](../assets/prompt_syntax/mountain-man1.png) | ![](../assets/prompt_syntax/mountain-man2.png) | ![](../assets/prompt_syntax/mountain-man3.png) | ![](../assets/prompt_syntax/mountain-man4.png) | - -### Prompt Blending - -- `("a tall thin man picking apricots", "a tall thin man picking pears").blend(1,1)` -- The existing prompt blending using `:` will continue to be supported - - `("a tall thin man picking apricots", "a tall thin man picking pears").blend(1,1)` - is equivalent to - `a tall thin man picking apricots:1 a tall thin man picking pears:1` in the - old syntax. -- Attention weights can be nested inside blends. -- Non-normalized blends are supported by passing `no_normalize` as an additional - argument to the blend weights, eg - `("a tall thin man picking apricots", "a tall thin man picking pears").blend(1,-1,no_normalize)`. - very fun to explore local maxima in the feature space, but also easy to - produce garbage output. - -See the section below on "Prompt Blending" for more information about how this -works. - -### Prompt Conjunction -Join multiple clauses together to create a conjoined prompt. Each clause will be passed to CLIP separately. - -For example, the prompt: - -```bash -"A mystical valley surround by towering granite cliffs, watercolor, warm" -``` - -Can be used with .and(): -```bash -("A mystical valley", "surround by towering granite cliffs", "watercolor", "warm").and() -``` - -Each will give you different results - try them out and see what you prefer! - - -### Escaping parentheses and speech marks - -If the model you are using has parentheses () or speech marks "" as part of its -syntax, you will need to "escape" these using a backslash, so that`(my_keyword)` -becomes `\(my_keyword\)`. Otherwise, the prompt parser will attempt to interpret -the parentheses as part of the prompt syntax and it will get confused. - ---- - -## **Prompt Blending** - -You may blend together prompts to explore the AI's -latent semantic space and generate interesting (and often surprising!) -variations. The syntax is: - -```bash -("prompt #1", "prompt #2").blend(0.25, 0.75) -``` - -This will tell the sampler to blend 25% of the concept of prompt #1 with 75% -of the concept of prompt #2. It is recommended to keep the sum of the weights to around 1.0, but interesting things might happen if you go outside of this range. - -Because you are exploring the "mind" of the AI, the AI's way of mixing two -concepts may not match yours, leading to surprising effects. To illustrate, here -are three images generated using various combinations of blend weights. As -usual, unless you fix the seed, the prompts will give you different results each -time you run them. - -Let's examine how this affects image generation results: - - -```bash -"blue sphere, red cube, hybrid" -``` - -This example doesn't use blending at all and represents the default way of mixing -concepts. - -
- -![blue-sphere-red-cube-hyprid](../assets/prompt-blending/blue-sphere-red-cube-hybrid.png) - -
- -It's interesting to see how the AI expressed the concept of "cube" within the sphere. If you look closely, there is depth there, so the enclosing frame is actually a cube. - -
- -```bash -("blue sphere", "red cube").blend(0.25, 0.75) -``` - -![blue-sphere-25-red-cube-75](../assets/prompt-blending/blue-sphere-0.25-red-cube-0.75-hybrid.png) - -
- -Now that's interesting. We get an image with a resemblance of a red cube, with a hint of blue shadows which represents a melding of concepts within the AI's "latent space" of semantic representations. - -
- -```bash -("blue sphere", "red cube").blend(0.75, 0.25) -``` - -![blue-sphere-75-red-cube-25](../assets/prompt-blending/blue-sphere-0.75-red-cube-0.25-hybrid.png) - -
- -Definitely more blue-spherey. - -
- -```bash -("blue sphere", "red cube").blend(0.5, 0.5) -``` -
- -
-![blue-sphere-5-red-cube-5-hybrid](../assets/prompt-blending/blue-sphere-0.5-red-cube-0.5-hybrid.png) -
- - -Whoa...! I see blue and red, and if I squint, spheres and cubes. - - - -## Dynamic Prompts - -Dynamic Prompts are a powerful feature designed to produce a variety of prompts based on user-defined options. Using a special syntax, you can construct a prompt with multiple possibilities, and the system will automatically generate a series of permutations based on your settings. This is extremely beneficial for ideation, exploring various scenarios, or testing different concepts swiftly and efficiently. - -### Structure of a Dynamic Prompt - -A Dynamic Prompt comprises of regular text, supplemented with alternatives enclosed within curly braces {} and separated by a vertical bar |. For example: {option1|option2|option3}. The system will then select one of the options to include in the final prompt. This flexible system allows for options to be placed throughout the text as needed. - -Furthermore, Dynamic Prompts can designate multiple selections from a single group of options. This feature is triggered by prefixing the options with a numerical value followed by $$. For example, in {2$$option1|option2|option3}, the system will select two distinct options from the set. -### Creating Dynamic Prompts - -To create a Dynamic Prompt, follow these steps: - - Draft your sentence or phrase, identifying words or phrases with multiple possible options. - Encapsulate the different options within curly braces {}. - Within the braces, separate each option using a vertical bar |. - If you want to include multiple options from a single group, prefix with the desired number and $$. - -For instance: A {house|apartment|lodge|cottage} in {summer|winter|autumn|spring} designed in {style1|style2|style3}. -### How Dynamic Prompts Work - -Once a Dynamic Prompt is configured, the system generates an array of combinations using the options provided. Each group of options in curly braces is treated independently, with the system selecting one option from each group. For a prefixed set (e.g., 2$$), the system will select two distinct options. - -For example, the following prompts could be generated from the above Dynamic Prompt: - - A house in summer designed in style1, style2 - A lodge in autumn designed in style3, style1 - A cottage in winter designed in style2, style3 - And many more! - -When the `Combinatorial` setting is on, Invoke will disable the "Images" selection, and generate every combination up until the setting for Max Prompts is reached. -When the `Combinatorial` setting is off, Invoke will randomly generate combinations up until the setting for Images has been reached. - - - -### Tips and Tricks for Using Dynamic Prompts - -Below are some useful strategies for creating Dynamic Prompts: - - Utilize Dynamic Prompts to generate a wide spectrum of prompts, perfect for brainstorming and exploring diverse ideas. - Ensure that the options within a group are contextually relevant to the part of the sentence where they are used. For instance, group building types together, and seasons together. - Apply the 2$$ prefix when you want to incorporate more than one option from a single group. This becomes quite handy when mixing and matching different elements. - Experiment with different quantities for the prefix. For example, 3$$ will select three distinct options. - Be aware of coherence in your prompts. Although the system can generate all possible combinations, not all may semantically make sense. Therefore, carefully choose the options for each group. - Always review and fine-tune the generated prompts as needed. While Dynamic Prompts can help you generate a multitude of combinations, the final polishing and refining remain in your hands. - - -## SDXL Prompting - -Prompting with SDXL is slightly different than prompting with SD1.5 or SD2.1 models - SDXL expects a prompt _and_ a style. - - -### Prompting -
- -![SDXL prompt boxes in InvokeAI](../assets/prompt_syntax/sdxl-prompt.png) - -
- -In the prompt box, enter a positive or negative prompt as you normally would. - -For the style box you can enter a style that you want the image to be generated in. You can use styles from this example list, or any other style you wish: anime, photographic, digital art, comic book, fantasy art, analog film, neon punk, isometric, low poly, origami, line art, cinematic, 3d model, pixel art, etc. - - -### Concatenated Prompts - - -InvokeAI also has the option to concatenate the prompt and style inputs, by pressing the "link" button in the Positive Prompt box. - -This concatenates the prompt & style inputs, and passes the joined prompt and style to the SDXL model. -![SDXL concatenated prompt boxes in InvokeAI](../assets/prompt_syntax/sdxl-prompt-concatenated.png) - - - - - - - diff --git a/docs/features/TEXTUAL_INVERSIONS.md b/docs/features/TEXTUAL_INVERSIONS.md deleted file mode 100644 index a3ede80d1fd..00000000000 --- a/docs/features/TEXTUAL_INVERSIONS.md +++ /dev/null @@ -1,55 +0,0 @@ -## Using Textual Inversion Files - -Textual inversion (TI) files are small models that customize the output of -Stable Diffusion image generation. They can augment SD with specialized subjects -and artistic styles. They are also known as "embeds" in the machine learning -world. - -Each TI file introduces one or more vocabulary terms to the SD model. These are -known in InvokeAI as "triggers." Triggers are denoted using angle brackets -as in "<trigger-phrase>". The two most common type of -TI files that you'll encounter are `.pt` and `.bin` files, which are produced by -different TI training packages. InvokeAI supports both formats, but its -[built-in TI training system](TRAINING.md) produces `.pt`. - -[Hugging Face](https://huggingface.co/sd-concepts-library) has -amassed a large library of >800 community-contributed TI files covering a -broad range of subjects and styles. You can also install your own or others' TI files -by placing them in the designated directory for the compatible model type - -### An Example - -Here are a few examples to illustrate how it works. All these images -were generated using the legacy command-line client and the Stable -Diffusion 1.5 model: - -| Japanese gardener | Japanese gardener <ghibli-face> | Japanese gardener <hoi4-leaders> | Japanese gardener <cartoona-animals> | -| :--------------------------------: | :-----------------------------------: | :------------------------------------: | :----------------------------------------: | -| ![](../assets/concepts/image1.png) | ![](../assets/concepts/image2.png) | ![](../assets/concepts/image3.png) | ![](../assets/concepts/image4.png) | - -You can also combine styles and concepts: - -
- | A portrait of <alf> in <cartoona-animal> style | - | :--------------------------------------------------------: | - | ![](../assets/concepts/image5.png) | -
- - -## Installing your Own TI Files - -You may install any number of `.pt` and `.bin` files simply by copying them into -the `embedding` directory of the corresponding InvokeAI models directory (usually `invokeai` -in your home directory). For example, you can simply move a Stable Diffusion 1.5 embedding file to -the `sd-1/embedding` folder. Be careful not to overwrite one file with another. -For example, TI files generated by the Hugging Face toolkit share the named -`learned_embedding.bin`. You can rename these, or use subdirectories to keep them distinct. - -At startup time, InvokeAI will scan the various `embedding` directories and load any TI -files it finds there for compatible models. At startup you will see a message similar to this one: - -```bash ->> Current embedding manager terms: , -``` -To use these when generating, simply type the `<` key in your prompt to open the Textual Inversion WebUI and -select the embedding you'd like to use. This UI has type-ahead support, so you can easily find supported embeddings. \ No newline at end of file diff --git a/docs/features/TRAINING.md b/docs/features/TRAINING.md deleted file mode 100644 index 47f8557889e..00000000000 --- a/docs/features/TRAINING.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Training ---- - -# :material-file-document: Training - -Invoke Training has moved to its own repository, with a dedicated UI for accessing common scripts like Textual Inversion and LoRA training. - -You can find more by visiting the repo at https://github.com/invoke-ai/invoke-training diff --git a/docs/features/UNIFIED_CANVAS.md b/docs/features/UNIFIED_CANVAS.md deleted file mode 100644 index 476d2009be2..00000000000 --- a/docs/features/UNIFIED_CANVAS.md +++ /dev/null @@ -1,283 +0,0 @@ ---- -title: Unified Canvas ---- - -The Unified Canvas is a tool designed to streamline and simplify the process of -composing an image using Stable Diffusion. It offers artists all of the -available Stable Diffusion generation modes (Text To Image, Image To Image, -Inpainting, and Outpainting) as a single unified workflow. The flexibility of -the tool allows you to tweak and edit image generations, extend images beyond -their initial size, and to create new content in a freeform way both inside and -outside of existing images. - -This document explains the basics of using the Unified Canvas, introducing you -to its features and tools one by one. It also describes some of the more -advanced tools available to power users of the Canvas. - -## Basics - -The Unified Canvas consists of two layers: the **Base Layer** and the **Mask -Layer**. You can swap from one layer to the other by selecting the layer you -want in the drop-down menu on the top left corner of the Unified Canvas, or by -pressing the (Q) hotkey. - -### Base Layer - -The **Base Layer** is the image content currently managed by the Canvas, and can -be exported at any time to the gallery by using the **Save to Gallery** option. -When the Base Layer is selected, the Brush (B) and Eraser (E) tools will -directly manipulate the base layer. Any images uploaded to the Canvas, or sent -to the Unified Canvas from the gallery, will clear out all existing content and -set the Base layer to the new image. - -### Staging Area - -When you generate images, they will display in the Canvas's **Staging Area**, -alongside the Staging Area toolbar buttons. While the Staging Area is active, -you cannot interact with the Canvas itself. - -
- -![staging area](../assets/canvas/staging_area.png) - -
- -Accepting generations will commit the new generation to the **Base Layer**. You -can review all generated images using the Prev/Next arrows, save any individual -generations to your gallery (without committing to the Base layer) or discard -generations. While you can Undo a discard in an individual Canvas session, any -generations that are not saved will be lost when the Canvas resets. - -### Mask Layer - -The **Mask Layer** consists of any masked sections that have been created to -inform Inpainting generations. You can paint a new mask, or edit an existing -mask, using the Brush tool and the Eraser with the Mask layer set as your Active -layer. Any masked areas will only affect generation inside of the current -bounding box. - -### Bounding Box - -When generating a new image, Invoke will process and apply new images within the -area denoted by the **Bounding Box**. The Width & Height settings of the -Bounding Box, as well as its location within the Unified Canvas and pixels or -empty space that it encloses, determine how new invocations are generated - see -[Inpainting & Outpainting](#inpainting-and-outpainting) below. The Bounding Box -can be moved and resized using the Move (V) tool. It can also be resized using -the Bounding Box options in the Options Panel. By using these controls you can -generate larger or smaller images, control which sections of the image are being -processed, as well as control Bounding Box tools like the Bounding Box -fill/erase. - -### Inpainting & Outpainting - -"Inpainting" means asking the AI to refine part of an image while leaving the -rest alone. For example, updating a portrait of your grandmother to have her -wear a biker's jacket. - -| masked original | inpaint result | -| :-------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | -| ![granny with a mask applied](../assets/canvas/mask_granny.png) | ![just like magic, granny with a biker's jacket](../assets/canvas/biker_jacket_granny.png) | - -"Outpainting" means asking the AI to expand the original image beyond its -original borders, making a bigger image that's still based on the original. For -example, extending the above image of your Grandmother in a biker's jacket to -include her wearing jeans (and while we're at it, a motorcycle!) - -
- -![more magic - granny with a tattooed arm, denim pants, and an obscured motorcycle](../assets/canvas/biker_granny.png) - -
- -When you are using the Unified Canvas, Invoke decides automatically whether to -do Inpainting, Outpainting, ImageToImage, or TextToImage by looking inside the -area enclosed by the Bounding Box. It chooses the appropriate type of generation -based on whether the Bounding Box contains empty (transparent) areas on the Base -layer, or whether it contains colored areas from previous generations (or from -painted brushstrokes) on the Base layer, and/or whether the Mask layer contains -any brushstrokes. See [Generation Methods](#generation-methods) below for more -information. - -## Getting Started - -To get started with the Unified Canvas, you will want to generate a new base -layer using Txt2Img or importing an initial image. We'll refer to either of -these methods as the "initial image" in the below guide. - -From there, you can consider the following techniques to augment your image: - -- **New Images**: Move the bounding box to an empty area of the Canvas, type in - your prompt, and Invoke, to generate a new image using the Text to Image - function. -- **Image Correction**: Use the color picker and brush tool to paint corrections - on the image, switch to the Mask layer, and brush a mask over your painted - area to use **Inpainting**. You can also use the **ImageToImage** generation - method to invoke new interpretations of the image. -- **Image Expansion**: Move the bounding box to include a portion of your - initial image, and a portion of transparent/empty pixels, then Invoke using a - prompt that describes what you'd like to see in that area. This will Outpaint - the image. You'll typically find more coherent results if you keep about - 50-60% of the original image in the bounding box. Make sure that the Image To - Image Strength slider is set to a high value - you may need to set it higher - than you are used to. -- **New Content on Existing Images**: If you want to add new details or objects - into your image, use the brush tool to paint a sketch of what you'd like to - see on the image, switch to the Mask layer, and brush a mask over your painted - area to use **Inpainting**. If the masked area is small, consider using a - smaller bounding box to take advantage of Invoke's automatic Scaling features, - which can help to produce better details. -- **And more**: There are a number of creative ways to use the Canvas, and the - above are just starting points. We're excited to see what you come up with! - -## Generation Methods - -The Canvas can use all generation methods available (Txt2Img, Img2Img, -Inpainting, and Outpainting), and these will be automatically selected and used -based on the current selection area within the Bounding Box. - -### Text to Image - -If the Bounding Box is placed over an area of Canvas with an **empty Base -Layer**, invoking a new image will use **TextToImage**. This generates an -entirely new image based on your prompt. - -### Image to Image - -If the Bounding Box is placed over an area of Canvas with an **existing Base -Layer area with no transparent pixels or masks**, invoking a new image will use -**ImageToImage**. This uses the image within the bounding box and your prompt to -interpret a new image. The image will be closer to your original image at lower -Image to Image strengths. - -### Inpainting - -If the Bounding Box is placed over an area of Canvas with an **existing Base -Layer and any pixels selected using the Mask layer**, invoking a new image will -use **Inpainting**. Inpainting uses the existing colors/forms in the masked area -in order to generate a new image for the masked area only. The unmasked portion -of the image will remain the same. Image to Image strength applies to the -inpainted area. - -If you desire something completely different from the original image in your new -generation (i.e., if you want Invoke to ignore existing colors/forms), consider -toggling the Inpaint Replace setting on, and use high values for both Inpaint -Replace and Image To Image Strength. - -!!! note - - By default, the **Scale Before Processing** option — which - inpaints more coherent details by generating at a larger resolution and then - scaling — is only activated when the Bounding Box is relatively small. - To get the best inpainting results you should therefore resize your Bounding - Box to the smallest area that contains your mask and enough surrounding detail - to help Stable Diffusion understand the context of what you want it to draw. - You should also update your prompt so that it describes _just_ the area within - the Bounding Box. - -### Outpainting - -If the Bounding Box is placed over an area of Canvas partially filled by an -existing Base Layer area and partially by transparent pixels or masks, invoking -a new image will use **Outpainting**, as well as **Inpainting** any masked -areas. - ---- - -## Advanced Features - -Features with non-obvious behavior are detailed below, in order to provide -clarity on the intent and common use cases we expect for utilizing them. - -### Toolbar - -#### Mask Options - -- **Enable Mask** - This flag can be used to Enable or Disable the currently - painted mask. If you have painted a mask, but you don't want it affect the - next invocation, but you _also_ don't want to delete it, then you can set this - option to Disable. When you want the mask back, set this back to Enable. -- **Preserve Masked Area** - When enabled, Preserve Masked Area inverts the - effect of the Mask on the Inpainting process. Pixels in masked areas will be - kept unchanged, and unmasked areas will be regenerated. - -#### Creative Tools - -- **Brush - Base/Mask Modes** - The Brush tool switches automatically between - different modes of operation for the Base and Mask layers respectively. - - On the Base layer, the brush will directly paint on the Canvas using the - color selected on the Brush Options menu. - - On the Mask layer, the brush will create a new mask. If you're finding the - mask difficult to see over the existing content of the Unified Canvas, you - can change the color it is drawn with using the color selector on the Mask - Options dropdown. -- **Erase Bounding Box** - On the Base layer, erases all pixels within the - Bounding Box. -- **Fill Bounding Box** - On the Base layer, fills all pixels within the - Bounding Box with the currently selected color. - -#### Canvas Tools - -- **Move Tool** - Allows for manipulation of the Canvas view (by dragging on the - Canvas, outside the bounding box), the Bounding Box (by dragging the edges of - the box), or the Width/Height of the Bounding Box (by dragging one of the 9 - directional handles). -- **Reset View** - Click to re-orients the view to the center of the Bounding - Box. -- **Merge Visible** - If your browser is having performance problems drawing the - image in the Unified Canvas, click this to consolidate all of the information - currently being rendered by your browser into a merged copy of the image. This - lowers the resource requirements and should improve performance. - -### Compositing / Seam Correction - -When doing Inpainting or Outpainting, Invoke needs to merge the pixels generated -by Stable Diffusion into your existing image. This is achieved through compositing - the area around the the boundary between your image and the new generation is -automatically blended to produce a seamless output. In a fully automatic -process, a mask is generated to cover the boundary, and then the area of the boundary is -Inpainted. - -Although the default options should work well most of the time, sometimes it can -help to alter the parameters that control the Compositing. A larger blur and -a blur setting have been noted as producing -consistently strong results . Strength of 0.7 is best for reducing hard seams. - -- **Mode** - What part of the image will have the the Compositing applied to it. - - **Mask edge** will apply Compositing to the edge of the masked area - - **Mask** will apply Compositing to the entire masked area - - **Unmasked** will apply Compositing to the entire image -- **Steps** - Number of generation steps that will occur during the Coherence Pass, similar to Denoising Steps. Higher step counts will generally have better results. -- **Strength** - How much noise is added for the Coherence Pass, similar to Denoising Strength. A strength of 0 will result in an unchanged image, while a strength of 1 will result in an image with a completely new area as defined by the Mode setting. -- **Blur** - Adjusts the pixel radius of the the mask. A larger blur radius will cause the mask to extend past the visibly masked area, while too small of a blur radius will result in a mask that is smaller than the visibly masked area. -- **Blur Method** - The method of blur applied to the masked area. - - -### Infill & Scaling - -- **Scale Before Processing & W/H**: When generating images with a bounding box - smaller than the optimized W/H of the model (e.g., 512x512 for SD1.5), this - feature first generates at a larger size with the same aspect ratio, and then - scales that image down to fill the selected area. This is particularly useful - when inpainting very small details. Scaling is optional but is enabled by - default. -- **Inpaint Replace**: When Inpainting, the default method is to utilize the - existing RGB values of the Base layer to inform the generation process. If - Inpaint Replace is enabled, noise is generated and blended with the existing - pixels (completely replacing the original RGB values at an Inpaint Replace - value of 1). This can help generate more variation from the pixels on the Base - layers. - - When using Inpaint Replace you should use a higher Image To Image Strength - value, especially at higher Inpaint Replace values -- **Infill Method**: Invoke currently supports two methods for producing RGB - values for use in the Outpainting process: Patchmatch and Tile. We believe - that Patchmatch is the superior method, however we provide support for Tile in - case Patchmatch cannot be installed or is unavailable on your computer. -- **Tile Size**: The Tile method for Outpainting sources small portions of the - original image and randomly place these into the areas being Outpainted. This - value sets the size of those tiles. - -## Hot Keys - -The Unified Canvas is a tool that excels when you use hotkeys. You can view the -full list of keyboard shortcuts, updated with all new features, by clicking the -Keyboard Shortcuts icon at the top right of the InvokeAI WebUI. diff --git a/docs/features/UTILITIES.md b/docs/features/UTILITIES.md deleted file mode 100644 index 2d62fe3a79f..00000000000 --- a/docs/features/UTILITIES.md +++ /dev/null @@ -1,336 +0,0 @@ ---- -title: Command-line Utilities ---- - -# :material-file-document: Utilities - -# Command-line Utilities - -InvokeAI comes with several scripts that are accessible via the -command line. To access these commands, start the "developer's -console" from the launcher (`invoke.bat` menu item [7]). Users who are -familiar with Python can alternatively activate InvokeAI's virtual -environment (typically, but not necessarily `invokeai/.venv`). - -In the developer's console, type the script's name to run it. To get a -synopsis of what a utility does and the command-line arguments it -accepts, pass it the `-h` argument, e.g. - -```bash -invokeai-merge -h -``` -## **invokeai-web** - -This script launches the web server and is effectively identical to -selecting option [1] in the launcher. An advantage of launching the -server from the command line is that you can override any setting -configuration option in `invokeai.yaml` using like-named command-line -arguments. For example, to temporarily change the size of the RAM -cache to 7 GB, you can launch as follows: - -```bash -invokeai-web --ram 7 -``` - -## **invokeai-merge** - -This is the model merge script, the same as launcher option [3]. Call -it with the `--gui` command-line argument to start the interactive -console-based GUI. Alternatively, you can run it non-interactively -using command-line arguments as illustrated in the example below which -merges models named `stable-diffusion-1.5` and `inkdiffusion` into a new model named -`my_new_model`: - -```bash -invokeai-merge --force --base-model sd-1 --models stable-diffusion-1.5 inkdiffusion --merged_model_name my_new_model -``` - -## **invokeai-ti** - -This is the textual inversion training script that is run by launcher -option [2]. Call it with `--gui` to run the interactive console-based -front end. It can also be run non-interactively. It has about a -zillion arguments, but a typical training session can be launched -with: - -```bash -invokeai-ti --model stable-diffusion-1.5 \ - --placeholder_token 'jello' \ - --learnable_property object \ - --num_train_epochs 50 \ - --train_data_dir /path/to/training/images \ - --output_dir /path/to/trained/model -``` - -(Note that \\ is the Linux/Mac long-line continuation character. Use ^ -in Windows). - -## **invokeai-install** - -This is the console-based model install script that is run by launcher -option [4]. If called without arguments, it will launch the -interactive console-based interface. It can also be used -non-interactively to list, add and remove models as shown by these -examples: - -* This will download and install three models from CivitAI, HuggingFace, -and local disk: - -```bash -invokeai-install --add https://civitai.com/api/download/models/161302 ^ - gsdf/Counterfeit-V3.0 ^ - D:\Models\merge_model_two.safetensors -``` -(Note that ^ is the Windows long-line continuation character. Use \\ on -Linux/Mac). - -* This will list installed models of type `main`: - -```bash -invokeai-model-install --list-models main -``` - -* This will delete the models named `voxel-ish` and `realisticVision`: - -```bash -invokeai-model-install --delete voxel-ish realisticVision -``` - -## **invokeai-configure** - -This is the console-based configure script that ran when InvokeAI was -first installed. You can run it again at any time to change the -configuration, repair a broken install. - -Called without any arguments, `invokeai-configure` enters interactive -mode with two screens. The first screen is a form that provides access -to most of InvokeAI's configuration options. The second screen lets -you download, add, and delete models interactively. When you exit the -second screen, the script will add any missing "support models" -needed for core functionality, and any selected "sd weights" which are -the model checkpoint/diffusers files. - -This behavior can be changed via a series of command-line -arguments. Here are some of the useful ones: - -* `invokeai-configure --skip-sd-weights --skip-support-models` -This will run just the configuration part of the utility, skipping -downloading of support models and stable diffusion weights. - -* `invokeai-configure --yes` -This will run the configure script non-interactively. It will set the -configuration options to their default values, install/repair support -models, and download the "recommended" set of SD models. - -* `invokeai-configure --yes --default_only` -This will run the configure script non-interactively. In contrast to -the previous command, it will only download the default SD model, -Stable Diffusion v1.5 - -* `invokeai-configure --yes --default_only --skip-sd-weights` -This is similar to the previous command, but will not download any -SD models at all. It is usually used to repair a broken install. - -By default, `invokeai-configure` runs on the currently active InvokeAI -root folder. To run it against a different root, pass it the `--root -
` argument. - -Lastly, you can use `invokeai-configure` to create a working root -directory entirely from scratch. Assuming you wish to make a root directory -named `InvokeAI-New`, run this command: - -```bash -invokeai-configure --root InvokeAI-New --yes --default_only -``` -This will create a minimally functional root directory. You can now -launch the web server against it with `invokeai-web --root InvokeAI-New`. - -## **invokeai-update** - -This is the interactive console-based script that is run by launcher -menu item [8] to update to a new version of InvokeAI. It takes no -command-line arguments. - -## **invokeai-metadata** - -This is a script which takes a list of InvokeAI-generated images and -outputs their metadata in the same JSON format that you get from the -`` button in the Web GUI. For example: - -```bash -$ invokeai-metadata ffe2a115-b492-493c-afff-7679aa034b50.png -ffe2a115-b492-493c-afff-7679aa034b50.png: -{ - "app_version": "3.1.0", - "cfg_scale": 8.0, - "clip_skip": 0, - "controlnets": [], - "generation_mode": "sdxl_txt2img", - "height": 1024, - "loras": [], - "model": { - "base_model": "sdxl", - "model_name": "stable-diffusion-xl-base-1.0", - "model_type": "main" - }, - "negative_prompt": "", - "negative_style_prompt": "", - "positive_prompt": "military grade sushi dinner for shock troopers", - "positive_style_prompt": "", - "rand_device": "cpu", - "refiner_cfg_scale": 7.5, - "refiner_model": { - "base_model": "sdxl-refiner", - "model_name": "sd_xl_refiner_1.0", - "model_type": "main" - }, - "refiner_negative_aesthetic_score": 2.5, - "refiner_positive_aesthetic_score": 6.0, - "refiner_scheduler": "euler", - "refiner_start": 0.8, - "refiner_steps": 20, - "scheduler": "euler", - "seed": 387129902, - "steps": 25, - "width": 1024 -} -``` - -You may list multiple files on the command line. - -## **invokeai-import-images** - -InvokeAI uses a database to store information about images it -generated, and just copying the image files from one InvokeAI root -directory to another does not automatically import those images into -the destination's gallery. This script allows you to bulk import -images generated by one instance of InvokeAI into a gallery maintained -by another. It also works on images generated by older versions of -InvokeAI, going way back to version 1. - -This script has an interactive mode only. The following example shows -it in action: - -```bash -$ invokeai-import-images -=============================================================================== -This script will import images generated by earlier versions of -InvokeAI into the currently installed root directory: - /home/XXXX/invokeai-main -If this is not what you want to do, type ctrl-C now to cancel. -=============================================================================== -= Configuration & Settings -Found invokeai.yaml file at /home/XXXX/invokeai-main/invokeai.yaml: - Database : /home/XXXX/invokeai-main/databases/invokeai.db - Outputs : /home/XXXX/invokeai-main/outputs/images - -Use these paths for import (yes) or choose different ones (no) [Yn]: -Inputs: Specify absolute path containing InvokeAI .png images to import: /home/XXXX/invokeai-2.3/outputs/images/ -Include files from subfolders recursively [yN]? - -Options for board selection for imported images: -1) Select an existing board name. (found 4) -2) Specify a board name to create/add to. -3) Create/add to board named 'IMPORT'. -4) Create/add to board named 'IMPORT' with the current datetime string appended (.e.g IMPORT_20230919T203519Z). -5) Create/add to board named 'IMPORT' with a the original file app_version appended (.e.g IMPORT_2.2.5). -Specify desired board option: 3 - -=============================================================================== -= Import Settings Confirmation - -Database File Path : /home/XXXX/invokeai-main/databases/invokeai.db -Outputs/Images Directory : /home/XXXX/invokeai-main/outputs/images -Import Image Source Directory : /home/XXXX/invokeai-2.3/outputs/images/ - Recurse Source SubDirectories : No -Count of .png file(s) found : 5785 -Board name option specified : IMPORT -Database backup will be taken at : /home/XXXX/invokeai-main/databases/backup - -Notes about the import process: -- Source image files will not be modified, only copied to the outputs directory. -- If the same file name already exists in the destination, the file will be skipped. -- If the same file name already has a record in the database, the file will be skipped. -- Invoke AI metadata tags will be updated/written into the imported copy only. -- On the imported copy, only Invoke AI known tags (latest and legacy) will be retained (dream, sd-metadata, invokeai, invokeai_metadata) -- A property 'imported_app_version' will be added to metadata that can be viewed in the UI's metadata viewer. -- The new 3.x InvokeAI outputs folder structure is flat so recursively found source imges will all be placed into the single outputs/images folder. - -Do you wish to continue with the import [Yn] ? - -Making DB Backup at /home/lstein/invokeai-main/databases/backup/backup-20230919T203519Z-invokeai.db...Done! - -=============================================================================== -Importing /home/XXXX/invokeai-2.3/outputs/images/17d09907-297d-4db3-a18a-60b337feac66.png -... (5785 more lines) ... -=============================================================================== -= Import Complete - Elpased Time: 0.28 second(s) - -Source File(s) : 5785 -Total Imported : 5783 -Skipped b/c file already exists on disk : 1 -Skipped b/c file already exists in db : 0 -Errors during import : 1 -``` -## **invokeai-db-maintenance** - -This script helps maintain the integrity of your InvokeAI database by -finding and fixing three problems that can arise over time: - -1. An image was manually deleted from the outputs directory, leaving a - dangling image record in the InvokeAI database. This will cause a - black image to appear in the gallery. This is an "orphaned database - image record." The script can fix this by running a "clean" - operation on the database, removing the orphaned entries. - -2. An image is present in the outputs directory but there is no - corresponding entry in the database. This can happen when the image - is added manually to the outputs directory, or if a crash occurred - after the image was generated but before the database was - completely updated. The symptom is that the image is present in the - outputs folder but doesn't appear in the InvokeAI gallery. This is - called an "orphaned image file." The script can fix this problem by - running an "archive" operation in which orphaned files are moved - into a directory named `outputs/images-archive`. If you wish, you - can then run `invokeai-image-import` to reimport these images back - into the database. - -3. The thumbnail for an image is missing, again causing a black - gallery thumbnail. This is fixed by running the "thumbnaiils" - operation, which simply regenerates and re-registers the missing - thumbnail. - -You can find and fix all three of these problems in a single go by -executing this command: - -```bash -invokeai-db-maintenance --operation all -``` - -Or you can run just the clean and thumbnail operations like this: - -```bash -invokeai-db-maintenance -operation clean, thumbnail -``` - -If called without any arguments, the script will ask you which -operations you wish to perform. - -## **invokeai-migrate3** - -This script will migrate settings and models (but not images!) from an -InvokeAI v2.3 root folder to an InvokeAI 3.X folder. Call it with the -source and destination root folders like this: - -```bash -invokeai-migrate3 --from ~/invokeai-2.3 --to invokeai-3.1.1 -``` - -Both directories must previously have been properly created and -initialized by `invokeai-configure`. If you wish to migrate the images -contained in the older root as well, you can use the -`invokeai-image-migrate` script described earlier. - ---- - -Copyright (c) 2023, Lincoln Stein and the InvokeAI Development Team diff --git a/docs/features/WATERMARK+NSFW.md b/docs/features/WATERMARK+NSFW.md deleted file mode 100644 index c837d66b57b..00000000000 --- a/docs/features/WATERMARK+NSFW.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Watermarking, NSFW Image Checking ---- - -# :material-image-off: Invisible Watermark and the NSFW Checker - -## Watermarking - -InvokeAI does not apply watermarking to images by default. However, -many computer scientists working in the field of generative AI worry -that a flood of computer-generated imagery will contaminate the image -data sets needed to train future generations of generative models. - -InvokeAI offers an optional watermarking mode that writes a small bit -of text, **InvokeAI**, into each image that it generates using an -"invisible" watermarking library that spreads the information -throughout the image in a way that is not perceptible to the human -eye. If you are planning to share your generated images on -internet-accessible services, we encourage you to activate the -invisible watermark mode in order to help preserve the digital image -environment. - -The downside of watermarking is that it increases the size of the -image moderately, and has been reported by some individuals to degrade -image quality. Your mileage may vary. - -To read the watermark in an image, activate the InvokeAI virtual -environment (called the "developer's console" in the launcher) and run -the command: - -``` -invisible-watermark -a decode -t bytes -m dwtDct -l 64 /path/to/image.png -``` - -## The NSFW ("Safety") Checker - -Stable Diffusion 1.5-based image generation models will produce sexual -imagery if deliberately prompted, and will occasionally produce such -images when this is not intended. Such images are colloquially known -as "Not Safe for Work" (NSFW). This behavior is due to the nature of -the training set that Stable Diffusion was trained on, which culled -millions of "aesthetic" images from the Internet. - -You may not wish to be exposed to these images, and in some -jurisdictions it may be illegal to publicly distribute such imagery, -including mounting a publicly-available server that provides -unfiltered images to the public. Furthermore, the [Stable Diffusion -weights -License](https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE-SD1+SD2.txt), -and the [Stable Diffusion XL -License][https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE-SDXL.txt] -both forbid the models from being used to "exploit any of the -vulnerabilities of a specific group of persons." - -For these reasons Stable Diffusion offers a "safety checker," a -machine learning model trained to recognize potentially disturbing -imagery. When a potentially NSFW image is detected, the checker will -blur the image and paste a warning icon on top. The checker can be -turned on and off in the Web interface under Settings. - -## Caveats - -There are a number of caveats that you need to be aware of. - -### Accuracy - -The checker is [not perfect](https://arxiv.org/abs/2210.04610).It will -occasionally flag innocuous images (false positives), and will -frequently miss violent and gory imagery (false negatives). It rarely -fails to flag sexual imagery, but this has been known to happen. For -these reasons, the InvokeAI team prefers to refer to the software as a -"NSFW Checker" rather than "safety checker." - -### Memory Usage and Performance - -The NSFW checker consumes an additional 1.2G of GPU VRAM on top of the -3.4G of VRAM used by Stable Diffusion v1.5 (this is with -half-precision arithmetic). This means that the checker will not run -successfully on GPU cards with less than 6GB VRAM, and will reduce the -size of the images that you can produce. - -The checker also introduces a slight performance penalty. Images will -take ~1 second longer to generate when the checker is -activated. Generally this is not noticeable. - -### Intermediate Images in the Web UI - -The checker only operates on the final image produced by the Stable -Diffusion algorithm. If you are using the Web UI and have enabled the -display of intermediate images, you will briefly be exposed to a -low-resolution (mosaicized) version of the final image before it is -flagged by the checker and replaced by a fully blurred version. You -are encouraged to turn **off** intermediate image rendering when you -are using the checker. Future versions of InvokeAI will apply -additional blurring to intermediate images when the checker is active. - diff --git a/docs/features/WEB.md b/docs/features/WEB.md deleted file mode 100644 index 15a489b932b..00000000000 --- a/docs/features/WEB.md +++ /dev/null @@ -1,325 +0,0 @@ ---- -title: InvokeAI Web Server ---- - -# :material-web: InvokeAI Web Server - -## Quick guided walkthrough of the WebUI's features - -While most of the WebUI's features are intuitive, here is a guided walkthrough -through its various components. - -### Launching the WebUI - -To run the InvokeAI web server, start the `invoke.sh`/`invoke.bat` -script and select option (1). Alternatively, with the InvokeAI -environment active, run `invokeai-web`: - -```bash -invokeai-web -``` - -You can then connect to the server by pointing your web browser at -http://localhost:9090. To reach the server from a different machine on your LAN, -you may launch the web server with the `--host` argument and either the IP -address of the host you are running it on, or the wildcard `0.0.0.0`. For -example: - -```bash -invoke.sh --host 0.0.0.0 -``` - -or - -```bash -invokeai-web --host 0.0.0.0 -``` - -### The InvokeAI Web Interface - -![Invoke Web Server - Major Components](../assets/invoke-web-server-1.png){:width="640px"} - -The screenshot above shows the Text to Image tab of the WebUI. There are three -main sections: - -1. A **control panel** on the left, which contains various settings - for text to image generation. The most important part is the text - field (currently showing `fantasy painting, horned demon`) for - entering the positive text prompt, another text field right below it for an - optional negative text prompt (concepts to exclude), and a _Invoke_ button - to begin the image rendering process. - -2. The **current image** section in the middle, which shows a large - format version of the image you are currently working on. A series - of buttons at the top lets you modify and manipulate the image in - various ways. - -3. A **gallery** section on the right that contains a history of the images you - have generated. These images are read and written to the directory specified - in the `INVOKEAIROOT/invokeai.yaml` initialization file, usually a directory - named `outputs` in `INVOKEAIROOT`. - -In addition to these three elements, there are a series of icons for changing -global settings, reporting bugs, and changing the theme on the upper right. - -There are also a series of icons to the left of the control panel (see -highlighted area in the screenshot below) which select among a series of tabs -for performing different types of operations. - -
-![Invoke Web Server - Control Panel](../assets/invoke-web-server-2.png){:width="512px"} -
- -From top to bottom, these are: - -1. Text to Image - generate images from text -2. Image to Image - from an uploaded starting image (drawing or photograph) - generate a new one, modified by the text prompt -3. Unified Canvas - Interactively combine multiple images, extend them - with outpainting,and modify interior portions of the image with - inpainting, erase portions of a starting image and have the AI fill in - the erased region from a text prompt. -4. Node Editor - (experimental) this panel allows you to create - pipelines of common operations and combine them into workflows. -5. Model Manager - this panel allows you to import and configure new - models using URLs, local paths, or HuggingFace diffusers repo_ids. - -## Walkthrough - -The following walkthrough will exercise most (but not all) of the WebUI's -feature set. - -### Text to Image - -1. Launch the WebUI using launcher option [1] and connect to it with - your browser by accessing `http://localhost:9090`. If the browser - and server are running on different machines on your LAN, add the - option `--host 0.0.0.0` to the `invoke.sh` launch command line and connect to - the machine hosting the web server using its IP address or domain - name. - -2. If all goes well, the WebUI should come up and you'll see a green dot - meaning `connected` on the upper right. - -![Invoke Web Server - Control Panel](../assets/invoke-control-panel-1.png){ align=right width=300px } - -#### Basics - -1. Generate an image by typing _bluebird_ into the large prompt field - on the upper left and then clicking on the Invoke button or pressing - the return button. - After a short wait, you'll see a large image of a bluebird in the - image panel, and a new thumbnail in the gallery on the right. - - If you need more room on the screen, you can turn the gallery off - by typing the **g** hotkey. You can turn it back on later by clicking the - image icon that appears in the gallery's place. The list of hotkeys can - be found by clicking on the keyboard icon above the image gallery. - -2. Generate a bunch of bluebird images by increasing the number of - requested images by adjusting the Images counter just below the Invoke - button. As each is generated, it will be added to the gallery. You can - switch the active image by clicking on the gallery thumbnails. - - If you'd like to watch the image generation progress, click the hourglass - icon above the main image area. As generation progresses, you'll see - increasingly detailed versions of the ultimate image. - -3. Try playing with different settings, including changing the main - model, the image width and height, the Scheduler, the Steps and - the CFG scale. - - The _Model_ changes the main model. Thousands of custom models are - now available, which generate a variety of image styles and - subjects. While InvokeAI comes with a few starter models, it is - easy to import new models into the application. See [Installing - Models](../installation/050_INSTALLING_MODELS.md) for more details. - - Image _Width_ and _Height_ do what you'd expect. However, be aware that - larger images consume more VRAM memory and take longer to generate. - - The _Scheduler_ controls how the AI selects the image to display. Some - samplers are more "creative" than others and will produce a wider range of - variations (see next section). Some samplers run faster than others. - - _Steps_ controls how many noising/denoising/sampling steps the AI will take. - The higher this value, the more refined the image will be, but the longer - the image will take to generate. A typical strategy is to generate images - with a low number of steps in order to select one to work on further, and - then regenerate it using a higher number of steps. - - The _CFG Scale_ controls how hard the AI tries to match the generated image - to the input prompt. You can go as high or low as you like, but generally - values greater than 20 won't improve things much, and values lower than 5 - will produce unexpected images. There are complex interactions between - _Steps_, _CFG Scale_ and the _Scheduler_, so experiment to find out what works - for you. - - The _Seed_ controls the series of values returned by InvokeAI's - random number generator. Each unique seed value will generate a different - image. To regenerate a previous image, simply use the original image's - seed value. A slider to the right of the _Seed_ field will change the - seed each time an image is generated. - -![Invoke Web Server - Control Panel 2](../assets/control-panel-2.png){ align=right width=400px } - -4. To regenerate a previously-generated image, select the image you - want and click the asterisk ("*") button at the top of the - image. This loads the text prompt and other original settings into - the control panel. If you then press _Invoke_ it will regenerate - the image exactly. You can also selectively modify the prompt or - other settings to tweak the image. - - Alternatively, you may click on the "sprouting plant icon" to load - just the image's seed, and leave other settings unchanged or the - quote icon to load just the positive and negative prompts. - -5. To regenerate a Stable Diffusion image that was generated by another SD - package, you need to know its text prompt and its _Seed_. Copy-paste the - prompt into the prompt box, unset the _Randomize Seed_ control in the - control panel, and copy-paste the desired _Seed_ into its text field. When - you Invoke, you will get something similar to the original image. It will - not be exact unless you also set the correct values for the original - sampler, CFG, steps and dimensions, but it will (usually) be close. - -6. To save an image, right click on it to bring up a menu that will - let you download the image, save it to a named image gallery, and - copy it to the clipboard, among other things. - -#### Upscaling - -![Invoke Web Server - Upscaling](../assets/upscaling.png){ align=right width=400px } - -"Upscaling" is the process of increasing the size of an image while - retaining the sharpness. InvokeAI uses an external library called - "ESRGAN" to do this. To invoke upscaling, simply select an image - and press the "expanding arrows" button above it. You can select - between 2X and 4X upscaling, and adjust the upscaling strength, - which has much the same meaning as in facial reconstruction. Try - running this on one of your previously-generated images. - -### Image to Image - -InvokeAI lets you take an existing image and use it as the basis for a new -creation. You can use any sort of image, including a photograph, a scanned -sketch, or a digital drawing, as long as it is in PNG or JPEG format. - -For this tutorial, we'll use the file named -[Lincoln-and-Parrot-512.png](../assets/Lincoln-and-Parrot-512.png). - -1. Click on the _Image to Image_ tab icon, which is the second icon - from the top on the left-hand side of the screen. This will bring - you to a screen similar to the one shown here: - - ![Invoke Web Server - Image to Image Tab](../assets/invoke-web-server-6.png){ width="640px" } - -2. Drag-and-drop the Lincoln-and-Parrot image into the Image panel, or click - the blank area to get an upload dialog. The image will load into an area - marked _Initial Image_. (The WebUI will also load the most - recently-generated image from the gallery into a section on the left, but - this image will be replaced in the next step.) - -3. Go to the prompt box and type _old sea captain with raven on shoulder_ and - press Invoke. A derived image will appear to the right of the original one: - - ![Invoke Web Server - Image to Image example](../assets/invoke-web-server-7.png){:width="640px"} - -4. Experiment with the different settings. The most influential one in Image to - Image is _Denoising Strength_ located about midway down the control - panel. By default it is set to 0.75, but can range from 0.0 to 0.99. The - higher the value, the more of the original image the AI will replace. A - value of 0 will leave the initial image completely unchanged, while 0.99 - will replace it completely. However, the _Scheduler_ and _CFG Scale_ also - influence the final result. You can also generate variations in the same way - as described in Text to Image. - -5. What if we only want to change certain part(s) of the image and - leave the rest intact? This is called Inpainting, and you can do - it in the [Unified Canvas](UNIFIED_CANVAS.md). The Unified Canvas - also allows you to extend borders of the image and fill in the - blank areas, a process called outpainting. - -6. Would you like to modify a previously-generated image using the Image to - Image facility? Easy! While in the Image to Image panel, drag and drop any - image in the gallery into the Initial Image area, and it will be ready for - use. You can do the same thing with the main image display. Click on the - _Send to_ icon to get a menu of - commands and choose "Send to Image to Image". - - ![Send To Icon](../assets/send-to-icon.png) - -### Textual Inversion, LoRA and ControlNet - -InvokeAI supports several different types of model files that -extending the capabilities of the main model by adding artistic -styles, special effects, or subjects. By mixing and matching textual -inversion, LoRA and ControlNet models, you can achieve many -interesting and beautiful effects. - -We will give an example using a LoRA model named "Ink Scenery". This -LoRA, which can be downloaded from Civitai (civitai.com), is -specialized to paint landscapes that look like they were made with -dripping india ink. To install this LoRA, we first download it and -put it into the `autoimport/lora` folder located inside the -`invokeai` root directory. After restarting the web server, the -LoRA will now become available for use. - -To see this LoRA at work, we'll first generate an image without it -using the standard `stable-diffusion-v1-5` model. Choose this -model and enter the prompt "mountains, ink". Here is a typical -generated image, a mountain range rendered in ink and watercolor -wash: - -![Ink Scenery without LoRA](../assets/lora-example-0.png){ width=512px } - -Now let's install and activate the Ink Scenery LoRA. Go to -https://civitai.com/models/78605/ink-scenery-or and download the LoRA -model file to `invokeai/autoimport/lora` and restart the web -server. (Alternatively, you can use [InvokeAI's Web Model -Manager](../installation/050_INSTALLING_MODELS.md) to download and -install the LoRA directly by typing its URL into the _Import -Models_->_Location_ field). - -Scroll down the control panel until you get to the LoRA accordion -section, and open it: - -![LoRA Section](../assets/lora-example-1.png){ width=512px } - -Click the popup menu and select "Ink scenery". (If it isn't there, then -the model wasn't installed to the right place, or perhaps you forgot -to restart the web server.) The LoRA section will change to look like this: - -![LoRA Section Loaded](../assets/lora-example-2.png){ width=512px } - -Note that there is now a slider control for _Ink scenery_. The slider -controls how much influence the LoRA model will have on the generated -image. - -Run the "mountains, ink" prompt again and observe the change in style: - -![Ink Scenery](../assets/lora-example-3.png){ width=512px } - -Try adjusting the weight slider for larger and smaller weights and -generate the image after each adjustment. The higher the weight, the -more influence the LoRA will have. - -To remove the LoRA completely, just click on its trash can icon. - -Multiple LoRAs can be added simultaneously and combined with textual -inversions and ControlNet models. Please see [Textual Inversions and -LoRAs](CONCEPTS.md) and [Using ControlNet](CONTROLNET.md) for details. - -## Summary - -This walkthrough just skims the surface of the many things InvokeAI -can do. Please see [Features](index.md) for more detailed reference -guides. - -## 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/features/WEBUIHOTKEYS.md b/docs/features/WEBUIHOTKEYS.md deleted file mode 100644 index 4aa3d385fe7..00000000000 --- a/docs/features/WEBUIHOTKEYS.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: WebUI Hotkey List ---- - -# :material-keyboard: **WebUI Hotkey List** - -## App Hotkeys - -| Setting | Hotkey | -| --------------- | ------------------ | -| ++ctrl+enter++ | Invoke | -| ++shift+x++ | Cancel | -| ++alt+a++ | Focus Prompt | -| ++o++ | Toggle Options | -| ++shift+o++ | Pin Options | -| ++z++ | Toggle Viewer | -| ++g++ | Toggle Gallery | -| ++f++ | Maximize Workspace | -| ++1++ - ++5++ | Change Tabs | -| ++"`"++ | Toggle Console | - -## General Hotkeys - -| Setting | Hotkey | -| -------------- | ---------------------- | -| ++p++ | Set Prompt | -| ++s++ | Set Seed | -| ++a++ | Set Parameters | -| ++shift+r++ | Restore Faces | -| ++shift+u++ | Upscale | -| ++i++ | Show Info | -| ++shift+i++ | Send To Image To Image | -| ++del++ | Delete Image | -| ++esc++ | Close Panels | - -## Gallery Hotkeys - -| Setting | Hotkey | -| ----------------------| --------------------------- | -| ++arrow-left++ | Previous Image | -| ++arrow-right++ | Next Image | -| ++shift+g++ | Toggle Gallery Pin | -| ++shift+arrow-up++ | Increase Gallery Image Size | -| ++shift+arrow-down++ | Decrease Gallery Image Size | - -## Unified Canvas Hotkeys - -| Setting | Hotkey | -| --------------------------------- | ---------------------- | -| ++b++ | Select Brush | -| ++e++ | Select Eraser | -| ++bracket-left++ | Decrease Brush Size | -| ++bracket-right++ | Increase Brush Size | -| ++shift+bracket-left++ | Decrease Brush Opacity | -| ++shift+bracket-right++ | Increase Brush Opacity | -| ++v++ | Move Tool | -| ++shift+f++ | Fill Bounding Box | -| ++del++ / ++backspace++ | Erase Bounding Box | -| ++c++ | Select Color Picker | -| ++n++ | Toggle Snap | -| ++"Hold Space"++ | Quick Toggle Move | -| ++q++ | Toggle Layer | -| ++shift+c++ | Clear Mask | -| ++h++ | Hide Mask | -| ++shift+h++ | Show/Hide Bounding Box | -| ++shift+m++ | Merge Visible | -| ++shift+s++ | Save To Gallery | -| ++ctrl+c++ | Copy To Clipboard | -| ++shift+d++ | Download Image | -| ++ctrl+z++ | Undo | -| ++ctrl+y++ / ++ctrl+shift+z++ | Redo | -| ++r++ | Reset View | -| ++arrow-left++ | Previous Staging Image | -| ++arrow-right++ | Next Staging Image | -| ++enter++ | Accept Staging Image | \ No newline at end of file diff --git a/docs/features/index.md b/docs/features/index.md deleted file mode 100644 index 7d0c0c25e5d..00000000000 --- a/docs/features/index.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Overview ---- - -Here you can find the documentation for InvokeAI's various features. - -## The [Getting Started Guide](../help/gettingStartedWithAI) -A getting started guide for those new to AI image generation. - -## The Basics -### * The [Web User Interface](WEB.md) -Guide to the Web interface. Also see the [WebUI Hotkeys Reference Guide](WEBUIHOTKEYS.md) - -### * The [Unified Canvas](UNIFIED_CANVAS.md) -Build complex scenes by combine and modifying multiple images in a stepwise -fashion. This feature combines img2img, inpainting and outpainting in -a single convenient digital artist-optimized user interface. - -## Image Generation -### * [Prompt Engineering](PROMPTS.md) -Get the images you want with the InvokeAI prompt engineering language. - -### * The [LoRA, LyCORIS, LCM-LoRA Models](CONCEPTS.md) -Add custom subjects and styles using a variety of fine-tuned models. - -### * [ControlNet](CONTROLNET.md) -Learn how to install and use ControlNet models for fine control over -image output. - -### * [Image-to-Image Guide](IMG2IMG.md) -Use a seed image to build new creations. - -## Model Management - -### * [Model Installation](../installation/050_INSTALLING_MODELS.md) -Learn how to import third-party models and switch among them. This -guide also covers optimizing models to load quickly. - -### * [Merging Models](MODEL_MERGING.md) -Teach an old model new tricks. Merge 2-3 models together to create a -new model that combines characteristics of the originals. - -### * [Textual Inversion](TEXTUAL_INVERSIONS.md) -Personalize models by adding your own style or subjects. - -## Other Features - -### * [The NSFW Checker](WATERMARK+NSFW.md) -Prevent InvokeAI from displaying unwanted racy images. - -### * [Controlling Logging](LOGGING.md) -Control how InvokeAI logs status messages. - -### * [Command-line Utilities](UTILITIES.md) -A list of the command-line utilities available with InvokeAI. - - diff --git a/docs/help/FAQ.md b/docs/help/FAQ.md deleted file mode 100644 index 25880f7cd2b..00000000000 --- a/docs/help/FAQ.md +++ /dev/null @@ -1,244 +0,0 @@ -# FAQ - -!!! info "How to Reinstall" - - Many issues can be resolved by re-installing the application. You won't lose any data by re-installing. We suggest downloading the [latest release](https://github.com/invoke-ai/InvokeAI/releases/latest) and using it to re-install the application. Consult the [installer guide](../installation/010_INSTALL_AUTOMATED.md) for more information. - - When you run the installer, you'll have an option to select the version to install. If you aren't ready to upgrade, you choose the current version to fix a broken install. - -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 - -You can download the latest installers [here](https://github.com/invoke-ai/InvokeAI/releases). - -Note that any releases marked as _pre-release_ are in a beta state. You may experience some issues, but we appreciate your help testing those! For stable/reliable installations, please install the [latest release]. - -## 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 to v4 - -If you find some models are missing after updating to v4, 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. -- Check the `ram` setting in `invokeai.yaml`. This setting tells Invoke how much of your system RAM can be used to cache models. Having this too high or too low can slow things down. That said, it's generally safest to not set this at all and instead let Invoke manage it. -- Check the `vram` setting in `invokeai.yaml`. This setting tells Invoke how much of your GPU VRAM can be used to cache models. Counter-intuitively, if this setting is too high, Invoke will need to do a lot of shuffling of models as it juggles the VRAM cache and the currently-loaded model. The default value of 0.25 is generally works well for GPUs without 16GB or more VRAM. Even on a 24GB card, the default works well. -- Check that your generations are happening on your GPU (if you have one). InvokeAI will log what is being used for generation upon startup. If your GPU isn't used, re-install to ensure the correct versions of torch get installed. -- If you are on Windows, you may have exceeded your GPU's VRAM capacity and are using slower [shared GPU memory](#shared-gpu-memory-windows). There's a guide to opt out of this behaviour in the linked FAQ entry. - -## Shared GPU Memory (Windows) - -!!! tip "Nvidia GPUs with driver 536.40" - - This only applies to current Nvidia cards with driver 536.40 or later, released in June 2023. - -When the GPU doesn't have enough VRAM for a task, Windows is able to allocate some of its CPU RAM to the GPU. This is much slower than VRAM, but it does allow the system to generate when it otherwise might no have enough VRAM. - -When shared GPU memory is used, generation slows down dramatically - but at least it doesn't crash. - -If you'd like to opt out of this behavior and instead get an error when you exceed your GPU's VRAM, follow [this guide from Nvidia](https://nvidia.custhelp.com/app/answers/detail/a_id/5490). - -Here's how to get the python path required in the linked guide: - -- Run `invoke.bat`. -- Select option 2 for developer console. -- At least one python path will be printed. Copy the path that includes your invoke installation directory (typically the first). - -## Installer cannot find python (Windows) - -Ensure that you checked **Add python.exe to PATH** when installing Python. This can be found at the bottom of the Python Installer window. If you already have Python installed, you can re-run the python installer, choose the Modify option and check the box. - -## Triton error on startup - -This can be safely ignored. InvokeAI doesn't use Triton, but if you are on Linux and wish to dismiss the error, you can install Triton. - -## Updated to 3.4.0 and xformers can’t load C++/CUDA - -An issue occurred with your PyTorch update. Follow these steps to fix : - -1. Launch your invoke.bat / invoke.sh and select the option to open the developer console -2. Run:`pip install ".[xformers]" --upgrade --force-reinstall --extra-index-url https://download.pytorch.org/whl/cu121` - - If you run into an error with `typing_extensions`, re-open the developer console and run: `pip install -U typing-extensions` - -Note that v3.4.0 is an old, unsupported version. Please upgrade to the [latest release]. - -## Install failed and says `pip` is out of date - -An out of date `pip` typically won't cause an installation to fail. The cause of the error can likely be found above the message that says `pip` is out of date. - -If you saw that warning but the install went well, don't worry about it (but you can update `pip` afterwards if you'd like). - -## 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. - -## OSErrors on Windows while installing dependencies - -During a zip file installation or an update, installation stops with an error like this: - -![broken-dependency-screenshot](../assets/troubleshooting/broken-dependency.png){:width="800px"} - -To resolve this, re-install the application as described above. - -## HuggingFace install failed due to invalid access token - -Some HuggingFace models require you to authenticate using an [access token]. - -Invoke doesn't manage this token for you, but it's easy to set it up: - -- Follow the instructions in the link above to create an access token. Copy it. -- Run the launcher script. -- Select option 2 (developer console). -- Paste the following command: - - ```sh - python -c "import huggingface_hub; huggingface_hub.login()" - ``` - -- Paste your access token when prompted and press Enter. You won't see anything when you paste it. -- Type `n` if prompted about git credentials. - -If you get an error, try the command again - maybe the token didn't paste correctly. - -Once your token is set, start Invoke and try downloading the model again. The installer will automatically use the access token. - -If the install still fails, you may not have access to the model. - -## Stable Diffusion XL generation fails after trying to load UNet - -InvokeAI is working in other respects, but when trying to generate -images with Stable Diffusion XL you get a "Server Error". The text log -in the launch window contains this log line above several more lines of -error messages: - -`INFO --> Loading model:D:\LONG\PATH\TO\MODEL, type sdxl:main:unet` - -This failure mode occurs when there is a network glitch during -downloading the very large SDXL model. - -To address this, first go to the Model Manager and delete the -Stable-Diffusion-XL-base-1.X model. Then, click the HuggingFace tab, -paste the Repo ID stabilityai/stable-diffusion-xl-base-1.0 and install -the model. - -## Package dependency conflicts during installation or update - -If you have previously installed InvokeAI or another Stable Diffusion -package, the installer may occasionally pick up outdated libraries and -either the installer or `invoke` will fail with complaints about -library conflicts. - -To resolve this, re-install the application as described above. - -## 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. - -## `ModuleNotFoundError: No module named 'controlnet_aux'` - -`controlnet_aux` is a dependency of Invoke and appears to have been packaged or distributed strangely. Sometimes, it doesn't install correctly. This is outside our control. - -If you encounter this error, the solution is to remove the package from the `pip` cache and re-run the Invoke installer so a fresh, working version of `controlnet_aux` can be downloaded and installed: - -- Run the Invoke launcher -- Choose the developer console option -- Run this command: `pip cache remove controlnet_aux` -- Close the terminal window -- Download and run the [installer](https://github.com/invoke-ai/InvokeAI/releases/latest), selecting your current install location - -## Out of Memory Issues - -The models are large, VRAM is expensive, and you may find yourself -faced with Out of Memory errors when generating images. Here are some -tips to reduce the problem: - -!!! info "Optimizing for GPU VRAM" - - === "4GB VRAM GPU" - - This should be adequate for 512x512 pixel images using Stable Diffusion 1.5 - and derived models, provided that you do not use the NSFW checker. It won't be loaded unless you go into the UI settings and turn it on. - - If you are on a CUDA-enabled GPU, we will automatically use xformers or torch-sdp to reduce VRAM requirements, though you can explicitly configure this. See the [configuration docs]. - - === "6GB VRAM GPU" - - This is a border case. Using the SD 1.5 series you should be able to - generate images up to 640x640 with the NSFW checker enabled, and up to - 1024x1024 with it disabled. - - If you run into persistent memory issues there are a series of - environment variables that you can set before launching InvokeAI that - alter how the PyTorch machine learning library manages memory. See - for - a list of these tweaks. - - === "12GB VRAM GPU" - - This should be sufficient to generate larger images up to about 1280x1280. - -## 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 -``` - -!!! warning "Speed vs Memory Tradeoff" - - Your generations may be slower overall when setting this environment variable. - -!!! info "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: -- Details: - -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]: ../installation/050_INSTALLING_MODELS.md -[system requirements]: ../installation/INSTALL_REQUIREMENTS.md -[latest release]: https://github.com/invoke-ai/InvokeAI/releases/latest -[create an issue]: https://github.com/invoke-ai/InvokeAI/issues -[discord]: https://discord.gg/ZmtBAhwWhy -[configuration docs]: ../features/CONFIGURATION.md -[access token]: https://huggingface.co/docs/hub/security-tokens#how-to-manage-user-access-tokens diff --git a/docs/help/SAMPLER_CONVERGENCE.md b/docs/help/SAMPLER_CONVERGENCE.md deleted file mode 100644 index 61fdca7b9cc..00000000000 --- a/docs/help/SAMPLER_CONVERGENCE.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Sampler Convergence ---- - -# :material-palette-advanced: *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's a TL;DR in 3 tables. - -!!! note "Remember" - - - 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 less steps (but are slower). - - `K_DPM_2_A` and `K_EULER_A` incorporate a lot of creativity/variability. - -
- -| Sampler | (3 sample avg) it/s (M1 Max 64GB, 512x512) | -|---|---| -| `DDIM` | 1.89 | -| `PLMS` | 1.86 | -| `K_EULER` | 1.86 | -| `K_LMS` | 1.91 | -| `K_HEUN` | 0.95 (slower) | -| `K_DPM_2` | 0.95 (slower) | -| `K_DPM_2_A` | 0.95 (slower) | -| `K_EULER_A` | 1.86 | - -
- -!!! tip "suggestions" - - For most use cases, `K_LMS`, `K_HEUN` and `K_DPM_2` are the best choices (the latter 2 run 0.5x as quick, but tend to converge 2x as quick as `K_LMS`). At very low steps (≤ `-s8`), `K_HEUN` and `K_DPM_2` are not recommended. Use `K_LMS` instead. - - For variability, use `K_EULER_A` (runs 2x as quick as `K_DPM_2_A`). - ---- - -### *Sampler results* - -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` - -![191636411-083c8282-6ed1-4f78-9273-ee87c0a0f1b6-min (1)](https://user-images.githubusercontent.com/50542132/191868725-7f7af991-e254-4c1f-83e7-bed8c9b2d34f.png) - -### *Sampler convergence* - -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. And 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. - -### *Batch generation speedup* - -This realization is very useful because it means you don't need to create a batch of 100 images (`-n100`) at `-s100` 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 images to polish some details. -The latter technique is 3-8x as quick. - -!!! example - - At 60s per 100 steps. - - A) 60s * 100 images = 6000s (100 images at `-s100`, manually picking 3 favorites) - - 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) - - The result is __1 hour and 40 minutes__ for Variant A, vs __13 minutes__ for Variant B. - -### *Topic convergance* - -Now, these results seem interesting, but do they hold for other topics? How about nature? Food? People? Animals? Let's try! - -Nature. `"valley landscape wallpaper, d&d art, fantasy, painted, 4k, high detail, sharp focus, washed colors, elaborate excellent painted illustration" -W512 -H512 -C7.5 -S1458228930` - -![191736091-dda76929-00d1-4590-bef4-7314ea4ea419-min (1)](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 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` - -![191639011-f81d9d38-0a15-45f0-9442-a5e8d5c25f1f-min (1)](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` - -![191771922-6029a4f5-f707-4684-9011-c6f96e25fe56-min (1)](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. - -![Screenshot 2022-09-23 at 02 05 48-min (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. - -![191988191-c586b75a-2d7f-4351-b705-83cc1149881a-min (1)](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. I prefer an earlier version of the face, more rounded), but it will probably be the most coherent arms/hands/face attributes-wise. You can always merge different images with a photo editing tool and pass it through `img2img` to smoothen the composition. - -### *Sampler generation times* - -Once we understand the concept of sampler convergence, we must look into the performance of each sampler in terms of steps (iterations) per second, as not all samplers run at the same speed. - -
- -On my M1 Max with 64GB of RAM, for a 512x512 image - -| Sampler | (3 sample average) it/s | -| :--- | :--- | -| `DDIM` | 1.89 | -| `PLMS` | 1.86 | -| `K_EULER` | 1.86 | -| `K_LMS` | 1.91 | -| `K_HEUN` | 0.95 (slower) | -| `K_DPM_2` | 0.95 (slower) | -| `K_DPM_2_A` | 0.95 (slower) | -| `K_EULER_A` | 1.86 | - -
- -Combining our results with the steps per second of each sampler, three choices come out on top: `K_LMS`, `K_HEUN` and `K_DPM_2` (where the latter two run 0.5x as quick but tend to converge 2x as quick as `K_LMS`). For creativity and a lot of variation between iterations, `K_EULER_A` can be a good choice (which runs 2x as quick as `K_DPM_2_A`). - -Additionally, image generation at very low steps (≤ `-s8`) is not recommended for `K_HEUN` and `K_DPM_2`. Use `K_LMS` instead. - -![K-compare](https://user-images.githubusercontent.com/50542132/192046823-2714cb29-bbf3-4eb1-9213-e27a0963905c.png){ width=600} - -### *Three key points* - -Finally, it is relevant to mention that, in general, there are 3 important moments in the process of image formation as steps increase: - -* The (earliest) point at which an image becomes a good indicator of the final result (useful for batch generation at low step values, to then improve the quality/coherence of the chosen images via running the same prompt and seed for more steps). - -* The (earliest) point at which an image becomes coherent, even if different from the result if steps are increased (useful for batch generation at low step values, where quality/coherence is improved via techniques other than increasing the steps -e.g. via inpainting). - -* The point at which an image fully converges. - -Hence, 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` in the case of the red-haired girl, but `K_LMS` and `-s20`-taking one tenth the time- may do as well if your workflow includes adding small details, such as the missing shoulder strap, via `img2img`). diff --git a/docs/help/diffusion.md b/docs/help/diffusion.md deleted file mode 100644 index 7182a51d67f..00000000000 --- a/docs/help/diffusion.md +++ /dev/null @@ -1,27 +0,0 @@ -Taking the time to understand the diffusion process will help you to understand how to more effectively use InvokeAI. - -There are two main ways Stable Diffusion works - with images, and latents. - -Image space represents images in pixel form that you look at. Latent space represents compressed inputs. It’s in latent space that Stable Diffusion processes images. A VAE (Variational Auto Encoder) is responsible for compressing and encoding inputs into latent space, as well as decoding outputs back into image space. - -To fully understand the diffusion process, we need to understand a few more terms: UNet, CLIP, and conditioning. - -A U-Net is a model trained on a large number of latent images with with known amounts of random noise added. This means that 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, with the U-Net removing noise in such a way that the result is similar to images in the U-Net’s training set, while CLIP guides the U-Net towards creating images that are most similar to the prompt. - - -When you generate an image using text-to-image, multiple steps occur in latent space: -1. Random noise is generated at the chosen height and width. The noise’s characteristics are dictated by seed. This noise tensor is passed into latent space. We’ll call this noise A. -2. 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. 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. The VAE decodes the final latent image from latent space into image space. - -Image-to-image is a similar process, with only step 1 being different: -1. 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 Denoising Strength of 0 means there are 0 steps and no noise added, resulting in an unchanged image, while a Denoising Strength of 1 results in the image being completely replaced with noise and a full set of denoising steps are performance. The process is then the same as steps 2-4 in the text-to-image process. - -Furthermore, 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 (eg. DPM++ 2M Karras) schedules the subtraction of noise from the latent image across the sampler steps chosen (step 3 above). Less noise is usually subtracted at higher sampler steps. diff --git a/docs/help/gettingStartedWithAI.md b/docs/help/gettingStartedWithAI.md deleted file mode 100644 index 617bd604010..00000000000 --- a/docs/help/gettingStartedWithAI.md +++ /dev/null @@ -1,97 +0,0 @@ -# Getting Started with AI Image Generation - -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 InvokeAI. Please note, this is not an exhaustive guide and may be out of date due to the rapidly changing nature of the space. - -## Using InvokeAI - -### **Prompt Crafting** - -- Prompts are the basis of using InvokeAI, providing the models directions on what to generate. As a general rule of thumb, the more detailed your prompt is, the better your result will be. - - *To get started, 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. - - 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 examples prompts you can try on your own: - - A detailed oil painting of a tranquil forest at sunset with vibrant+ colors and soft, golden light filtering through the trees - - friends sharing beers in a busy city, realistic colored pencil sketch, twilight, masterpiece, bright, lively - -### Generation Workflows - -- Invoke offers a number of different workflows for interacting with models to produce images. Each is extremely powerful on its own, but together provide you an unparalleled way of producing high quality creative outputs that align with your vision. - - **Text to Image:** The text to image tab focuses on the key workflow of using a prompt to generate a new image. It includes other features that help control the generation process as well. - - **Image to Image:** With image to image, you provide an image as a reference (called the “initial image”), which provides more guidance around color and structure to the AI as it generates a new image. This is provided alongside the same features as Text to Image. - - **Unified Canvas:** The Unified Canvas is an advanced AI-first image editing tool that is easy to use, but hard to master. Drag an image onto the canvas from your gallery in order to regenerate certain elements, edit content or colors (known as inpainting), or extend the image with an exceptional degree of consistency and clarity (called outpainting). - -### Improving Image Quality - -- Fine tuning your prompt - the more specific you are, the closer the image will turn out to what is in your head! Adding more details in the Positive Prompt or Negative Prompt can help add / remove pieces of your image to improve it - You can also use advanced techniques like upweighting and downweighting to control the influence of certain words. [Learn more here](https://invoke-ai.github.io/InvokeAI/features/PROMPTS/#prompt-syntax-features). - - **Tip: If you’re seeing poor results, try adding the things you don’t like about the image to your negative prompt may help. E.g. distorted, low quality, unrealistic, etc.** -- Explore different models - Other models can produce different results due to the data 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! -- Increasing Steps - The number of steps used controls how much time the model is given to produce an image, and depends on the “Scheduler” used. The schedule controls how each step is processed by the model. More steps tends to mean better results, but will take longer - We recommend at least 30 steps for most -- Tweak and Iterate - Remember, 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. -- Explore Advanced Settings - InvokeAI has a full suite of tools available to allow you complete control over your image creation process - Check out our [docs if you want to learn more](https://invoke-ai.github.io/InvokeAI/features/). - - -## Terms & Concepts - -If you're interested in learning more, check out [this presentation](https://docs.google.com/presentation/d/1IO78i8oEXFTZ5peuHHYkVF-Y3e2M6iM5tCnc-YBfcCM/edit?usp=sharing) from one of our maintainers (@lstein). - -### Stable Diffusion - -Stable Diffusion is deep learning, text-to-image model that is the foundation of the capabilities found in InvokeAI. Since the release of Stable Diffusion, there have been many subsequent models created based on Stable Diffusion that are designed to generate specific types of images. - -### Prompts - -Prompts provide the models directions on what to generate. As a general rule of thumb, the more detailed your prompt is, the better your result will be. - -### Models - -Models are the magic that power InvokeAI. These files represent the output of training a machine on understanding massive amounts of images - providing them with the capability to generate new images using just a text description of what you’d like to see. (Like Stable Diffusion!) - -Invoke offers a simple way to download several different models upon installation, but many more can be discovered online, including at https://models.invoke.ai - -Each model can produce a unique style of output, based on the images it was trained on - Try out different models to see which best fits your creative vision! - -- *Models that contain “inpainting” in the name are designed for use with the inpainting feature of the Unified Canvas* - -### Scheduler - -Schedulers guide the process of removing noise (de-noising) from data. They determine: - -1. The number of steps to take to remove the noise. -2. Whether the steps are random (stochastic) or predictable (deterministic). -3. The specific method (algorithm) used for de-noising. - -Experimenting with different schedulers is recommended as each will produce different outputs! - -### Steps - -The number of de-noising steps each generation through. - -Schedulers can be intricate and there's often a balance to strike between how quickly they can de-noise data and how well they can do it. It's typically advised to experiment with different schedulers to see which one gives the best results. There has been a lot written on the internet about different schedulers, as well as exploring what the right level of "steps" are for each. You can save generation time by reducing the number of steps used, but you'll want to make sure that you are satisfied with the quality of images produced! - -### Low-Rank Adaptations / LoRAs - -Low-Rank Adaptations (LoRAs) are like a smaller, more focused version of models, intended to focus on training a better understanding of how a specific character, style, or concept looks. - -### Textual Inversion Embeddings - -Textual Inversion Embeddings, like LoRAs, assist with more easily prompting for certain characters, styles, or concepts. However, embeddings are trained to update the relationship between a specific word (known as the “trigger”) and the intended output. - -### ControlNet - -ControlNets are neural network models that are able to extract key features from an existing image and use these features to guide the output of the image generation model. - -### VAE - -Variational auto-encoder (VAE) is a encode/decode model that translates the "latents" image produced during the image generation procees to the large pixel images that we see. - diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico deleted file mode 100644 index 16a72bebcbb..00000000000 Binary files a/docs/img/favicon.ico and /dev/null differ diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index d55d6a14f8f..00000000000 --- a/docs/index.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -title: Home ---- - - - - - - - - - -
- - -[![project logo](https://github.com/invoke-ai/InvokeAI/assets/31807370/6e3728c7-e90e-4711-905c-3b55844ff5be)](https://github.com/invoke-ai/InvokeAI) - -[![discord badge]][discord link] - -[![latest release badge]][latest release link] -[![github stars badge]][github stars link] -[![github forks badge]][github forks link] - - - -[![github open issues badge]][github open issues link] -[![github open prs badge]][github open prs link] - -[ci checks on dev badge]: - https://flat.badgen.net/github/checks/invoke-ai/InvokeAI/development?label=CI%20status%20on%20dev&cache=900&icon=github -[ci checks on dev link]: - https://github.com/invoke-ai/InvokeAI/actions?query=branch%3Adevelopment -[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/workflows/test-invoke-conda.yml -[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=lstein%2Fstable-diffusion -[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 release badge]: - https://flat.badgen.net/github/release/invoke-ai/InvokeAI/development?icon=github -[latest release link]: https://github.com/invoke-ai/InvokeAI/releases - -
- -InvokeAI is an -implementation of Stable Diffusion, the open source text-to-image and -image-to-image generator. It provides a streamlined process with various new -features and options to aid the image generation process. It runs on Windows, -Mac and Linux machines, and runs on GPU cards with as little as 4 GB of RAM. - -
- -## :octicons-link-24: Quick Links - - - - -## :octicons-gift-24: InvokeAI Features - -### Installation - - [Automated Installer](installation/010_INSTALL_AUTOMATED.md) - - [Manual Installation](installation/020_INSTALL_MANUAL.md) - - [Docker Installation](installation/040_INSTALL_DOCKER.md) - -### The InvokeAI Web Interface -- [WebUI overview](features/WEB.md) -- [WebUI hotkey reference guide](features/WEBUIHOTKEYS.md) -- [WebUI Unified Canvas for Img2Img, inpainting and outpainting](features/UNIFIED_CANVAS.md) - - - -### Image Management -- [Image2Image](features/IMG2IMG.md) -- [Adding custom styles and subjects](features/CONCEPTS.md) -- [Upscaling and Face Reconstruction](features/POSTPROCESS.md) -- [Other Features](features/OTHER.md) - - -### Model Management -- [Installing](installation/050_INSTALLING_MODELS.md) -- [Model Merging](features/MODEL_MERGING.md) -- [ControlNet Models](features/CONTROLNET.md) -- [Style/Subject Concepts and Embeddings](features/CONCEPTS.md) -- [Watermarking and the Not Safe for Work (NSFW) Checker](features/WATERMARK+NSFW.md) - -### Prompt Engineering -- [Prompt Syntax](features/PROMPTS.md) - -### InvokeAI Configuration -- [Guide to InvokeAI Runtime Settings](features/CONFIGURATION.md) -- [Database Maintenance and other Command Line Utilities](features/UTILITIES.md) - -## :material-target: Troubleshooting - -Please check out our **[:material-frequently-asked-questions: -FAQ](help/FAQ/)** to -get solutions for common installation problems and other issues. - -## :octicons-repo-push-24: 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. - -[Please take a look at our Contribution documentation to learn more about contributing to InvokeAI. -](contributing/CONTRIBUTING.md) - -## :octicons-person-24: Contributors - -This software is a combined effort of various people from across the world. -[Check out the list of all these amazing people](other/CONTRIBUTORS.md). We -thank them for their time, hard work and effort. - -## :octicons-question-24: 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 portions of the software are Copyright (c) 2022-23 -by [The InvokeAI Team](https://github.com/invoke-ai). - diff --git a/docs/installation/010_INSTALL_AUTOMATED.md b/docs/installation/010_INSTALL_AUTOMATED.md deleted file mode 100644 index 3c6c90afdcf..00000000000 --- a/docs/installation/010_INSTALL_AUTOMATED.md +++ /dev/null @@ -1,107 +0,0 @@ -# Automatic Install & Updates - -**The same packaged installer file can be used for both new installs and updates.** -Using the installer for updates will leave everything you've added since installation, and just update the core libraries used to run Invoke. -Simply use the same path you installed to originally. - -Both release and pre-release versions can be installed using the installer. It also supports install through a wheel if needed. - -Be sure to review the [installation requirements] and ensure your system has everything it needs to install Invoke. - -## Getting the Latest Installer - -Download the `InvokeAI-installer-vX.Y.Z.zip` file from the [latest release] page. It is at the bottom of the page, under **Assets**. - -After unzipping the installer, you should have a `InvokeAI-Installer` folder with some files inside, including `install.bat` and `install.sh`. - -## Running the Installer - -!!! tip - - Windows users should first double-click the `WinLongPathsEnabled.reg` file to prevent a failed installation due to long file paths. - -Double-click the install script: - -=== "Windows" - - ```sh - install.bat - ``` - -=== "Linux/macOS" - - ```sh - install.sh - ``` - -!!! info "Running the Installer from the commandline" - - You can also run the install script from cmd/powershell (Windows) or terminal (Linux/macOS). - -!!! warning "Untrusted Publisher (Windows)" - - You may get a popup saying the file comes from an `Untrusted Publisher`. Click `More Info` and `Run Anyway` to get past this. - -The installation process is simple, with a few prompts: - -- Select the version to install. Unless you have a specific reason to install a specific version, select the default (the latest version). -- Select location for the install. Be sure you have enough space in this folder for the base application, as described in the [installation requirements]. -- Select a GPU device. - -!!! info "Slow Installation" - - The installer needs to download several GB of data and install it all. It may appear to get stuck at 99.9% when installing `pytorch` or during a step labeled "Installing collected packages". - - If it is stuck for over 10 minutes, something has probably gone wrong and you should close the window and restart. - -## Running the Application - -Find the install location you selected earlier. Double-click the launcher script to run the app: - -=== "Windows" - - ```sh - invoke.bat - ``` - -=== "Linux/macOS" - - ```sh - invoke.sh - ``` - -Choose the first option to run the UI. After a series of startup messages, you'll see something like this: - -``` -Uvicorn running on http://127.0.0.1:9090 (Press CTRL+C to quit) -``` - -Copy the URL into your browser and you should see the UI. - -## First-time Setup - -You will need to [install some models] before you can generate. - -Check the [configuration docs] for details on configuring the application. - -## Updating - -Updating is exactly the same as installing - download the latest installer, choose the latest version and off you go. - -!!! info "Dependency Resolution Issues" - - We've found that pip's dependency resolution can cause issues when upgrading packages. One very common problem was pip "downgrading" torch from CUDA to CPU, but things broke in other novel ways. - - The installer doesn't have this kind of problem, so we use it for updating as well. - -## Installation Issues - -If you have installation issues, please review the [FAQ]. You can also [create an issue] or ask for help on [discord]. - -[installation requirements]: INSTALL_REQUIREMENTS.md -[FAQ]: ../help/FAQ.md -[install some models]: 050_INSTALLING_MODELS.md -[configuration docs]: ../features/CONFIGURATION.md -[latest release]: https://github.com/invoke-ai/InvokeAI/releases/latest -[create an issue]: https://github.com/invoke-ai/InvokeAI/issues -[discord]: https://discord.gg/ZmtBAhwWhy diff --git a/docs/installation/020_INSTALL_MANUAL.md b/docs/installation/020_INSTALL_MANUAL.md deleted file mode 100644 index 059834eb453..00000000000 --- a/docs/installation/020_INSTALL_MANUAL.md +++ /dev/null @@ -1,119 +0,0 @@ -# Manual Install - -!!! warning "This is for Advanced Users" - - **Python experience is mandatory.** - -## Introduction - -InvokeAI is distributed as a python package on PyPI, installable with `pip`. There are a few things that are handled by the installer and launcher that you'll need to manage manually, described in this guide. - -### Requirements - -Before you start, go through the [installation requirements](./INSTALL_REQUIREMENTS.md). - -### Installation Walkthrough - -1. Create a directory to contain your InvokeAI library, configuration - files, and models. This is known as the "runtime" or "root" - directory, and often lives in your home directory under the name `invokeai`. - - We will refer to this directory as `INVOKEAI_ROOT`. For convenience, create an environment variable pointing to the directory. - - === "Linux/macOS" - - ```bash - export INVOKEAI_ROOT=~/invokeai - mkdir $INVOKEAI_ROOT - ``` - - === "Windows (PowerShell)" - - ```bash - Set-Variable -Name INVOKEAI_ROOT -Value $Home/invokeai - mkdir $INVOKEAI_ROOT - ``` - -1. Enter the root (invokeai) directory and create a virtual Python environment within it named `.venv`. - - !!! warning "Virtual Environment Location" - - While you may create the virtual environment anywhere in the file system, we recommend that you create it within the root directory as shown here. This allows the application to automatically detect its data directories. - - If you choose a different location for the venv, then you _must_ set the `INVOKEAI_ROOT` environment variable or specify the root directory using the `--root` CLI arg. - - ```terminal - cd $INVOKEAI_ROOT - python3 -m venv .venv --prompt InvokeAI - ``` - -1. Activate the new environment: - - === "Linux/macOS" - - ```bash - source .venv/bin/activate - ``` - - === "Windows" - - ```ps - .venv\Scripts\activate - ``` - - !!! info "Permissions Error (Windows)" - - If you get a permissions error at this point, run this command and try again - - `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` - - The command-line prompt should change to to show `(InvokeAI)` at the beginning of the prompt. - - The following steps should be run while inside the `INVOKEAI_ROOT` directory. - -1. Make sure that pip is installed in your virtual environment and up to date: - - ```bash - python3 -m pip install --upgrade pip - ``` - -1. Install the InvokeAI Package. The base command is `pip install InvokeAI --use-pep517`, but you may need to change this depending on your system and the desired features. - - - You may need to provide an [extra index URL](https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-extra-index-url). Select your platform configuration using [this tool on the PyTorch website](https://pytorch.org/get-started/locally/). Copy the `--extra-index-url` string from this and append it to your install command. - - !!! example "Install with an extra index URL" - - ```bash - pip install InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121 - ``` - - - If you have a CUDA GPU and want to install with `xformers`, you need to add an option to the package name. Note that `xformers` is not necessary. PyTorch includes an implementation of the SDP attention algorithm with the same performance. - - !!! example "Install with `xformers`" - - ```bash - pip install "InvokeAI[xformers]" --use-pep517 - ``` - -1. Deactivate and reactivate your runtime directory so that the invokeai-specific commands become available in the environment: - - === "Linux/macOS" - - ```bash - deactivate && source .venv/bin/activate - ``` - - === "Windows" - - ```ps - deactivate - .venv\Scripts\activate - ``` - -1. Run the application: - - Run `invokeai-web` to start the UI. You must activate the virtual environment before running the app. - - !!! warning - - If the virtual environment is _not_ inside the root directory, then you _must_ specify the path to the root directory with `--root \path\to\invokeai` or the `INVOKEAI_ROOT` environment variable. diff --git a/docs/installation/040_INSTALL_DOCKER.md b/docs/installation/040_INSTALL_DOCKER.md deleted file mode 100644 index 3814b72e80e..00000000000 --- a/docs/installation/040_INSTALL_DOCKER.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: Installing with Docker ---- - -# :fontawesome-brands-docker: Docker - -!!! warning "macOS and AMD GPU Users" - - We highly recommend to Install InvokeAI locally using [these instructions](INSTALLATION.md), - because Docker containers can not access the GPU on macOS. - -!!! warning "AMD GPU Users" - - Container support for AMD GPUs has been reported to work by the community, but has not received - extensive testing. Please make sure to set the `GPU_DRIVER=rocm` environment variable (see below), and - use the `build.sh` script to build the image for this to take effect at build time. - -!!! tip "Linux and Windows Users" - - For optimal performance, configure your Docker daemon to access your machine's GPU. - Docker Desktop on Windows [includes GPU support](https://www.docker.com/blog/wsl-2-gpu-support-for-docker-desktop-on-nvidia-gpus/). - Linux users should install and configure the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) - -## Why containers? - -They provide a flexible, reliable way to build and deploy InvokeAI. -See [Processes](https://12factor.net/processes) under the Twelve-Factor App -methodology for details on why running applications in such a stateless fashion is important. - -The container is configured for CUDA by default, but can be built to support AMD GPUs -by setting the `GPU_DRIVER=rocm` environment variable at Docker image build time. - -Developers on Apple silicon (M1/M2/M3): You -[can't access your GPU cores from Docker containers](https://github.com/pytorch/pytorch/issues/81224) -and performance is reduced compared with running it directly on macOS but for -development purposes it's fine. Once you're done with development tasks on your -laptop you can build for the target platform and architecture and deploy to -another environment with NVIDIA GPUs on-premises or in the cloud. - -## TL;DR - -This assumes properly configured Docker on Linux or Windows/WSL2. Read on for detailed customization options. - - ```bash - # docker compose commands should be run from the `docker` directory - cd docker - docker compose up - ``` - -## Installation in a Linux container (desktop) - -### 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. - -#### Get a Huggingface-Token - -Besides the Docker Agent you will need an Account on -[huggingface.co](https://huggingface.co/join). - -After you succesfully registered your account, go to -[huggingface.co/settings/tokens](https://huggingface.co/settings/tokens), create -a token and copy it, since you will need in for the next step. - -### Setup - -Set up your environmnent 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](../features/CONFIGURATION.md) for further detail. - -At a minimum, 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 - -- Q: I am running on Windows under WSL2, and am seeing a "no such file or directory" error. -- A: Your `docker-entrypoint.sh` file likely has Windows (CRLF) as opposed to Unix (LF) line endings, - and you may have cloned this repository before the issue was fixed. To solve this, please change - 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, please see the [Docker Desktop documentation](https://docs.docker.com/desktop/troubleshoot/topics/#avoid-unexpected-syntax-errors-use-unix-style-line-endings-for-files-in-containers) diff --git a/docs/installation/050_INSTALLING_MODELS.md b/docs/installation/050_INSTALLING_MODELS.md deleted file mode 100644 index 360a5043d39..00000000000 --- a/docs/installation/050_INSTALLING_MODELS.md +++ /dev/null @@ -1,52 +0,0 @@ -# Installing Models - -## 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 [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. Our backend will convert models to `diffusers` format before running them. This is a transparent process. - -## 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 - -You can install other models using the Model Manager. You'll find tabs for the following install methods: - -- **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]. -- **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. - -!!! tip "Autoimport" - - The dedicated autoimport folder is removed as of v4.0.0. You can do the same thing on the **Scan Folder** tab - paste the folder you'd like to import from and then click `Install All`. - -### 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. - -!!! 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]: ../../features/CONFIGURATION#model-marketplace-api-keys diff --git a/docs/installation/060_INSTALL_PATCHMATCH.md b/docs/installation/060_INSTALL_PATCHMATCH.md deleted file mode 100644 index a9646f8b607..00000000000 --- a/docs/installation/060_INSTALL_PATCHMATCH.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: Installing PyPatchMatch ---- - -# :material-image-size-select-large: Installing PyPatchMatch - -pypatchmatch is a Python module for inpainting images. It is not needed to run -InvokeAI, but it greatly improves the quality of inpainting and outpainting and -is recommended. - -Unfortunately, it is a C++ optimized module and installation can be somewhat -challenging. This guide leads you through the steps. - -## Windows - -You're in luck! On Windows platforms PyPatchMatch will install automatically on -Windows systems with no extra intervention. - -## Macintosh - -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. - -## Linux - -Prior to installing PyPatchMatch, you need to take the following steps: - -### Debian Based Distros - -1. Install the `build-essential` tools: - - ```sh - sudo apt update - 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. - -4. Install pypatchmatch: - - ```sh - pip install pypatchmatch - ``` - -5. 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.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.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 ... - ``` - -### Arch Based Distros - -1. Install the `base-devel` package: - - ```sh - sudo pacman -Syu - sudo pacman -S --needed base-devel - ``` - -2. Install `opencv` and `blas`: - - ```sh - sudo pacman -S opencv blas - ``` - - or for CUDA support - - ```sh - sudo pacman -S opencv-cuda blas - ``` - -3. Fix the naming of the `opencv` package configuration file: - - ```sh - cd /usr/lib/pkgconfig/ - ln -sf opencv4.pc opencv.pc - ``` - -[**Next, Follow Steps 4-6 from the Debian Section above**](#linux) - -If you see no errors you're ready to go! diff --git a/docs/installation/INSTALLATION.md b/docs/installation/INSTALLATION.md deleted file mode 100644 index 267376f197c..00000000000 --- a/docs/installation/INSTALLATION.md +++ /dev/null @@ -1,48 +0,0 @@ -# Installation and Updating Overview - -Before installing, review the [installation requirements] to ensure your system is set up properly. - -See the [FAQ] for frequently-encountered installation issues. - -If you need more help, join our [discord] or [create an issue]. - -

Automatic Install & Updates

- -✅ The automatic install is the best way to run InvokeAI. Check out the [installation guide] to get started. - -⬆️ The same installer is also the best way to update InvokeAI - Simply rerun it for the same folder you installed to. - -The installation process simply manages installation for the core libraries & application dependencies that run Invoke. -Any models, images, or other assets in the Invoke root folder won't be affected by the installation process. - -

Manual Install

- -If you are familiar with python and want more control over the packages that are installed, you can [install InvokeAI manually via PyPI]. - -Updates are managed by reinstalling the latest version through PyPi. - -

Developer Install

- -If you want to contribute to InvokeAI, consult the [developer install guide]. - -

Docker Install

- -This method is recommended for those familiar with running Docker containers. - -We offer a method for creating Docker containers containing InvokeAI and its dependencies. This method is recommended for individuals with experience with Docker containers and understand the pluses and minuses of a container-based install. - -See the [docker installation guide]. - -

Other Installation Guides

- -- [PyPatchMatch](060_INSTALL_PATCHMATCH.md) -- [Installing Models](050_INSTALLING_MODELS.md) - -[install InvokeAI manually via PyPI]: 020_INSTALL_MANUAL.md -[developer install guide]: INSTALL_DEVELOPMENT.md -[docker installation guide]: 040_INSTALL_DOCKER.md -[installation guide]: 010_INSTALL_AUTOMATED.md -[FAQ]: ../help/FAQ.md -[discord]: discord.gg/invoke-ai -[create an issue]: https://github.com/invoke-ai/InvokeAI/issues -[installation requirements]: INSTALL_REQUIREMENTS.md diff --git a/docs/installation/INSTALL_DEVELOPMENT.md b/docs/installation/INSTALL_DEVELOPMENT.md deleted file mode 100644 index ead6b3bc8d6..00000000000 --- a/docs/installation/INSTALL_DEVELOPMENT.md +++ /dev/null @@ -1,37 +0,0 @@ -# Developer Install - -!!! warning - - InvokeAI uses a SQLite database. By running on `main`, 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 persistent backend storage, 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. - - If this is untenable, you should run the application via the official installer or a manual install of the - python package from PyPI. These releases will not break your database. - -If you have an interest in how InvokeAI works, or you would like to add features or bugfixes, you are encouraged to install the source code for InvokeAI. - -!!! info "Why do I need the frontend toolchain?" - - The repo doesn't contain a build of the frontend. You'll be responsible for rebuilding it (or running it in dev mode) to use the app, as described in the [frontend dev toolchain] docs. - -

Installation

- -1. [Fork and clone] the [InvokeAI repo]. -1. Follow the [manual installation] docs to create a new virtual environment for the development install. - - Create a new folder outside the repo root for the installation and create the venv inside that folder. - - When installing the InvokeAI package, add `-e` to the command so you get an [editable install]. -1. Install the [frontend dev toolchain] and do a production build of the UI as described. -1. You can now run the app as described in the [manual installation] docs. - -As described in the [frontend dev toolchain] docs, you can run the UI using a dev server. If you do this, you won't need to continually rebuild the frontend. Instead, you run the dev server and use the app with the server URL it provides. - -[Fork and clone]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo -[InvokeAI repo]: https://github.com/invoke-ai/InvokeAI -[frontend dev toolchain]: ../contributing/frontend/OVERVIEW.md -[manual installation]: ./020_INSTALL_MANUAL.md -[editable install]: https://pip.pypa.io/en/latest/cli/pip_install/#cmdoption-e diff --git a/docs/installation/INSTALL_REQUIREMENTS.md b/docs/installation/INSTALL_REQUIREMENTS.md deleted file mode 100644 index 2279e7efb8a..00000000000 --- a/docs/installation/INSTALL_REQUIREMENTS.md +++ /dev/null @@ -1,181 +0,0 @@ -# Requirements - -## GPU - -!!! warning "Problematic Nvidia GPUs" - - We do not recommend these GPUs. They cannot operate with half precision, but have insufficient VRAM to generate 512x512 images at full precision. - - - NVIDIA 10xx series cards such as the 1080 TI - - GTX 1650 series cards - - GTX 1660 series cards - -Invoke runs best with a dedicated GPU, but will fall back to running on CPU, albeit much slower. You'll need a beefier GPU for SDXL. - -!!! example "Stable Diffusion 1.5" - - === "Nvidia" - - ``` - Any GPU with at least 4GB VRAM. - ``` - - === "AMD" - - ``` - Any GPU with at least 4GB VRAM. Linux only. - ``` - - === "Mac" - - ``` - Any Apple Silicon Mac with at least 8GB memory. - ``` - -!!! example "Stable Diffusion XL" - - === "Nvidia" - - ``` - Any GPU with at least 8GB VRAM. - ``` - - === "AMD" - - ``` - Any GPU with at least 16GB VRAM. Linux only. - ``` - - === "Mac" - - ``` - Any Apple Silicon Mac with at least 16GB memory. - ``` - -## RAM - -At least 12GB of RAM. - -## Disk - -SSDs will, of course, offer the best performance. - -The base application disk usage depends on the torch backend. - -!!! example "Disk" - - === "Nvidia (CUDA)" - - ``` - ~6.5GB - ``` - - === "AMD (ROCm)" - - ``` - ~12GB - ``` - - === "Mac (MPS)" - - ``` - ~3.5GB - ``` - -You'll need to set aside some space for images, depending on how much you generate. A couple GB is enough to get started. - -You'll need a good chunk of space for models. Even if you only install the most popular models and the usual support models (ControlNet, IP Adapter ,etc), you will quickly hit 50GB of models. - -!!! info "`tmpfs` on Linux" - - If your temporary directory is mounted as a `tmpfs`, ensure it has sufficient space. - -## Python - -Invoke requires python 3.10 or 3.11. If you don't already have one of these versions installed, we suggest installing 3.11, as it will be supported for longer. - -Check that your system has an up-to-date Python installed by running `python --version` in the terminal (Linux, macOS) or cmd/powershell (Windows). - -

Installing Python (Windows)

- -- Install python 3.11 with [an official installer]. -- 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. -- You may need to install [Microsoft Visual C++ Redistributable]. - -

Installing Python (macOS)

- -- Install python 3.11 with [an official installer]. -- 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.10/Install\ Certificates.command` -- If you haven't already, you will need to install the XCode CLI Tools by running `xcode-select --install` in a terminal. - -

Installing Python (Linux)

- -- Follow the [linux install instructions], being sure to install python 3.11. -- 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 . -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 - -!!! info "Linux Only" - - AMD GPUs are supported on Linux only, due to ROCm (the AMD equivalent of CUDA) support being Linux only. - -!!! warning "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. - -[ROCm docker container]: https://github.com/ROCm/ROCm-docker -[ROCm Documentation]: https://rocm.docs.amd.com/projects/install-on-linux/en/latest/tutorial/quick-start.html -[cuDNN support matrix]: https://docs.nvidia.com/deeplearning/cudnn/support-matrix/index.html -[Nvidia Container Runtime]: https://developer.nvidia.com/container-runtime -[linux install instructions]: https://docs.python-guide.org/starting/install3/linux/ -[Microsoft Visual C++ Redistributable]: https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170 -[an official installer]: https://www.python.org/downloads/release/python-3118/ -[CUDA Toolkit Downloads]: https://developer.nvidia.com/cuda-downloads diff --git a/installer/lib/__init__.py b/docs/invoke-config.json similarity index 100% rename from installer/lib/__init__.py rename to docs/invoke-config.json diff --git a/docs/javascripts/init_kapa_widget.js b/docs/javascripts/init_kapa_widget.js deleted file mode 100644 index 06885c464cd..00000000000 --- a/docs/javascripts/init_kapa_widget.js +++ /dev/null @@ -1,10 +0,0 @@ -document.addEventListener("DOMContentLoaded", function () { - var script = document.createElement("script"); - script.src = "https://widget.kapa.ai/kapa-widget.bundle.js"; - script.setAttribute("data-website-id", "b5973bb1-476b-451e-8cf4-98de86745a10"); - script.setAttribute("data-project-name", "Invoke.AI"); - script.setAttribute("data-project-color", "#11213C"); - script.setAttribute("data-project-logo", "https://avatars.githubusercontent.com/u/113954515?s=280&v=4"); - script.async = true; - document.head.appendChild(script); -}); diff --git a/docs/javascripts/tablesort.js b/docs/javascripts/tablesort.js deleted file mode 100644 index ceb1f94acdb..00000000000 --- a/docs/javascripts/tablesort.js +++ /dev/null @@ -1,7 +0,0 @@ -document$.subscribe(function() { - var tables = document.querySelectorAll("article table:not([class])") - tables.forEach(function(table) { - new Tablesort(table) - }) - }) - \ No newline at end of file diff --git a/docs/nodes/INVOCATION_API.md b/docs/nodes/INVOCATION_API.md deleted file mode 100644 index 40dfca8d7db..00000000000 --- a/docs/nodes/INVOCATION_API.md +++ /dev/null @@ -1,63 +0,0 @@ -# Invocation API - -Each invocation's `invoke` method is provided a single arg - the Invocation -Context. - -This object provides access to various methods, used to interact with the -application. Loading and saving images, logging messages, etc. - -!!! warning "" - - This API may shift slightly until the release of v4.0.0 as we work through a few final updates to the Model Manager. - -```py -class MyInvocation(BaseInvocation): - ... - def invoke(self, context: InvocationContext) -> ImageOutput: - image_pil = context.images.get_pil(image_name) - # Do something to the image - image_dto = context.images.save(image_pil) - # Log a message - context.logger.info(f"Did something cool, image saved!") - ... -``` - -The full API is documented below. - -## Invocation 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. - - -::: invokeai.app.services.shared.invocation_context.InvocationContext - options: - members: false - -::: invokeai.app.services.shared.invocation_context.ImagesInterface - -::: invokeai.app.services.shared.invocation_context.TensorsInterface - -::: invokeai.app.services.shared.invocation_context.ConditioningInterface - -::: invokeai.app.services.shared.invocation_context.ModelsInterface - -::: invokeai.app.services.shared.invocation_context.LoggerInterface - -::: invokeai.app.services.shared.invocation_context.ConfigInterface - -::: invokeai.app.services.shared.invocation_context.UtilInterface - -::: invokeai.app.services.shared.invocation_context.BoardsInterface - diff --git a/docs/nodes/NODES.md b/docs/nodes/NODES.md deleted file mode 100644 index b7f7aa82ad4..00000000000 --- a/docs/nodes/NODES.md +++ /dev/null @@ -1,97 +0,0 @@ -# Using the Workflow Editor - -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. - -If you're not familiar with Diffusion, take a look at our [Diffusion Overview.](../help/diffusion.md) Understanding how diffusion works will enable you to more easily use the Workflow Editor and build workflows to suit your needs. - -## Features - -### Workflow Library -The Workflow Library enables you to save workflows to the Invoke database, allowing you to easily creating, modify and share workflows as needed. - -A curated set of workflows are provided by default - these are designed to help explain important nodes' usage in the Workflow Editor. - -![workflow_library](../assets/nodes/workflow_library.png) - -### Linear View -The Workflow Editor allows you to create a UI for your workflow, to make it easier to iterate on your generations. - -To add an input to the Linear UI, right click on the **input label** and select "Add to Linear View". - -The Linear UI View will also be part of the saved workflow, allowing you share workflows and enable other to use them, regardless of complexity. - -![linearview](../assets/nodes/linearview.png) - -### Renaming Fields and Nodes -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 the Linear View and the node. - -### Managing Nodes - -* Ctrl+C to copy a node -* Ctrl+V to paste a node -* Backspace/Delete to delete a node -* Shift+Click to drag and select multiple nodes - -### Node Caching - -Nodes have a "Use Cache" option in their footer. This allows for performance improvements by using the previously cached values during the workflow processing. - - -## Important 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 that the screenshots below aren't examples of complete functioning node graphs (see Examples). - -### Noise - -An initial noise tensor is necessary for the latent diffusion process. As a result, the Denoising node requires a noise node input. - -![groupsnoise](../assets/nodes/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. - -![groupsconditioning](../assets/nodes/groupsconditioning.png) - -### Image to Latents & VAE - -The ImageToLatents node takes in a pixel image and a VAE and outputs a latents. The LatentsToImage node does the opposite, taking in a latents and a VAE and outpus a pixel image. - -![groupsimgvae](../assets/nodes/groupsimgvae.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. - -![groupsrandseed](../assets/nodes/groupsnoise.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. - -![groupscontrol](../assets/nodes/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. - -![groupslora](../assets/nodes/groupslora.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. - -![groupsallscale](../assets/nodes/groupsallscale.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. - -![groupsiterate](../assets/nodes/groupsiterate.png) - -### Batch / Multiple Image Generation + Random Seeds - -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. - -![groupsmultigenseeding](../assets/nodes/groupsmultigenseeding.png) - diff --git a/docs/nodes/NODES_MIGRATION_V3_V4.md b/docs/nodes/NODES_MIGRATION_V3_V4.md deleted file mode 100644 index 3ba08545817..00000000000 --- a/docs/nodes/NODES_MIGRATION_V3_V4.md +++ /dev/null @@ -1,148 +0,0 @@ -# Invoke v4.0.0 Nodes API Migration guide - -Invoke v4.0.0 is versioned as such due to breaking changes to the API utilized -by nodes, both core and custom. - -## Motivation - -Prior to v4.0.0, the `invokeai` python package has not be set up to be utilized -as a library. That is to say, it didn't have any explicitly public API, and node -authors had to work with the unstable internal application API. - -v4.0.0 introduces a stable public API for nodes. - -## Changes - -There are two node-author-facing changes: - -1. Import Paths -1. Invocation Context API - -### Import Paths - -All public objects are now exported from `invokeai.invocation_api`: - -```py -# Old -from invokeai.app.invocations.baseinvocation import ( - BaseInvocation, - InputField, - InvocationContext, - invocation, -) -from invokeai.app.invocations.primitives import ImageField - -# New -from invokeai.invocation_api import ( - BaseInvocation, - ImageField, - InputField, - InvocationContext, - invocation, -) -``` - -It's possible that we've missed some classes you need in your node. Please let -us know if that's the case. - -### Invocation Context API - -Most nodes utilize the Invocation Context, an object that is passed to the -`invoke` that provides access to data and services a node may need. - -Until now, that object and the services it exposed were internal. Exposing them -to nodes means that changes to our internal implementation could break nodes. -The methods on the services are also often fairly complicated and allowed nodes -to footgun. - -In v4.0.0, this object has been refactored to be much simpler. - -See [INVOCATION_API](./INVOCATION_API.md) for full details of the API. - -!!! warning "" - - This API may shift slightly until the release of v4.0.0 as we work through a few final updates to the Model Manager. - -#### Improved Service Methods - -The biggest offender was the image save method: - -```py -# Old -image_dto = context.services.images.create( - image=image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, -) - -# New -image_dto = context.images.save(image=image) -``` - -Other methods are simplified, or enhanced with additional functionality: - -```py -# Old -image = context.services.images.get_pil_image(image_name) - -# New -image = context.images.get_pil(image_name) -image_cmyk = context.images.get_pil(image_name, "CMYK") -``` - -We also had some typing issues around tensors: - -```py -# Old -# `latents` typed as `torch.Tensor`, but could be `ConditioningFieldData` -latents = context.services.latents.get(self.latents.latents_name) -# `data` typed as `torch.Tenssor,` but could be `ConditioningFieldData` -context.services.latents.save(latents_name, data) - -# New - separate methods for tensors and conditioning data w/ correct typing -# Also, the service generates the names -tensor_name = context.tensors.save(tensor) -tensor = context.tensors.load(tensor_name) -# For conditioning -cond_name = context.conditioning.save(cond_data) -cond_data = context.conditioning.load(cond_name) -``` - -#### Output Construction - -Core Outputs have builder functions right on them - no need to manually -construct these objects, or use an extra utility: - -```py -# Old -image_output = ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, -) -latents_output = build_latents_output(latents_name=name, latents=latents, seed=None) -noise_output = NoiseOutput( - noise=LatentsField(latents_name=latents_name, seed=seed), - width=latents.size()[3] * 8, - height=latents.size()[2] * 8, -) -cond_output = ConditioningOutput( - conditioning=ConditioningField( - conditioning_name=conditioning_name, - ), -) - -# New -image_output = ImageOutput.build(image_dto) -latents_output = LatentsOutput.build(latents_name=name, latents=noise, seed=self.seed) -noise_output = NoiseOutput.build(latents_name=name, latents=noise, seed=self.seed) -cond_output = ConditioningOutput.build(conditioning_name) -``` - -You can still create the objects using constructors if you want, but we suggest -using the builder methods. diff --git a/docs/nodes/comfyToInvoke.md b/docs/nodes/comfyToInvoke.md deleted file mode 100644 index 2d894dc74c9..00000000000 --- a/docs/nodes/comfyToInvoke.md +++ /dev/null @@ -1,80 +0,0 @@ -# ComfyUI to InvokeAI - -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! - -Some things to note: - -- 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 have over your workflows. -- InvokeAI's backend and ComfyUI's backend are very different which means Comfy workflows are not able to be imported into InvokeAI. However, we have created a [list of popular workflows](exampleWorkflows.md) for you to get started with Nodes in InvokeAI! - -## Node Equivalents: - -| Comfy UI Category | ComfyUI Node | Invoke Equivalent | -|:---------------------------------- |:---------------------------------- | :----------------------------------| -| Sampling |KSampler |Denoise Latents| -| Sampling |Ksampler Advanced|Denoise Latents | -| Loaders |Load Checkpoint | Main Model Loader _or_ SDXL Main Model Loader| -| Loaders |Load VAE | VAE Loader | -| Loaders |Load Lora | LoRA Loader _or_ SDXL Lora Loader| -| Loaders |Load ControlNet Model | ControlNet| -| Loaders |Load ControlNet Model (diff) | ControlNet| -| Loaders |Load Style Model | Reference Only ControlNet will be coming in a future version of InvokeAI| -| Loaders |unCLIPCheckpointLoader | N/A | -| Loaders |GLIGENLoader | N/A | -| Loaders |Hypernetwork Loader | N/A | -| Loaders |Load Upscale Model | Occurs within "Upscale (RealESRGAN)"| -|Conditioning |CLIP Text Encode (Prompt) | Compel (Prompt) or SDXL Compel (Prompt) | -|Conditioning |CLIP Set Last Layer | CLIP Skip| -|Conditioning |Conditioning (Average) | Use the .blend() feature of prompts | -|Conditioning |Conditioning (Combine) | N/A | -|Conditioning |Conditioning (Concat) | See the Prompt Tools Community Node| -|Conditioning |Conditioning (Set Area) | N/A | -|Conditioning |Conditioning (Set Mask) | Mask Edge | -|Conditioning |CLIP Vision Encode | N/A | -|Conditioning |unCLIPConditioning | N/A | -|Conditioning |Apply ControlNet | ControlNet | -|Conditioning |Apply ControlNet (Advanced) | ControlNet | -|Latent |VAE Decode | Latents to Image| -|Latent |VAE Encode | Image to Latents | -|Latent |Empty Latent Image | Noise | -|Latent |Upscale Latent |Resize Latents | -|Latent |Upscale Latent By |Scale Latents | -|Latent |Latent Composite | Blend Latents | -|Latent |LatentCompositeMasked | N/A | -|Image |Save Image | Image | -|Image |Preview Image |Current | -|Image |Load Image | Image| -|Image |Empty Image| Blank Image | -|Image |Invert Image | Invert Lerp Image | -|Image |Batch Images | Link "Image" nodes into an "Image Collection" node | -|Image |Pad Image for Outpainting | Outpainting is easily accomplished in the Unified Canvas | -|Image |ImageCompositeMasked | Paste Image | -|Image | Upscale Image | Resize Image | -|Image | Upscale Image By | Upscale Image | -|Image | Upscale Image (using Model) | Upscale Image | -|Image | ImageBlur | Blur Image | -|Image | ImageQuantize | N/A | -|Image | ImageSharpen | N/A | -|Image | Canny | Canny Processor | -|Mask |Load Image (as Mask) | Image | -|Mask |Convert Mask to Image | Image| -|Mask |Convert Image to Mask | Image | -|Mask |SolidMask | N/A | -|Mask |InvertMask |Invert Lerp Image | -|Mask |CropMask | Crop Image | -|Mask |MaskComposite | Combine Mask | -|Mask |FeatherMask | Blur Image | -|Advanced | Load CLIP | Main Model Loader _or_ SDXL Main Model Loader| -|Advanced | UNETLoader | Main Model Loader _or_ SDXL Main Model Loader| -|Advanced | DualCLIPLoader | Main Model Loader _or_ SDXL Main Model Loader| -|Advanced | Load Checkpoint | Main Model Loader _or_ SDXL Main Model Loader | -|Advanced | ConditioningZeroOut | N/A | -|Advanced | ConditioningSetTimestepRange | N/A | -|Advanced | CLIPTextEncodeSDXLRefiner | Compel (Prompt) or SDXL Compel (Prompt) | -|Advanced | CLIPTextEncodeSDXL |Compel (Prompt) or SDXL Compel (Prompt) | -|Advanced | ModelMergeSimple | Model Merging is available in the Model Manager | -|Advanced | ModelMergeBlocks | Model Merging is available in the Model Manager| -|Advanced | CheckpointSave | Model saving is available in the Model Manager| -|Advanced | CLIPMergeSimple | N/A | - - diff --git a/docs/nodes/communityNodes.md b/docs/nodes/communityNodes.md deleted file mode 100644 index 296fbb7ee61..00000000000 --- a/docs/nodes/communityNodes.md +++ /dev/null @@ -1,605 +0,0 @@ -# Community Nodes - -These are nodes that have been developed by the community, for the community. If you're not sure what a node is, you can learn more about nodes [here](overview.md). - -If you'd like to submit a node for the community, please refer to the [node creation overview](contributingNodes.md). - -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 the `.json` node graph file and load it into Invoke AI via the **Load Workflow** button in the Workflow Editor. - -- Community Nodes - + [Adapters-Linked](#adapters-linked-nodes) - + [Autostereogram](#autostereogram-nodes) - + [Average Images](#average-images) - + [Clean Image Artifacts After Cut](#clean-image-artifacts-after-cut) - + [Close Color Mask](#close-color-mask) - + [Clothing Mask](#clothing-mask) - + [Contrast Limited Adaptive Histogram Equalization](#contrast-limited-adaptive-histogram-equalization) - + [Depth Map from Wavefront OBJ](#depth-map-from-wavefront-obj) - + [Film Grain](#film-grain) - + [Generative Grammar-Based Prompt Nodes](#generative-grammar-based-prompt-nodes) - + [GPT2RandomPromptMaker](#gpt2randompromptmaker) - + [Grid to Gif](#grid-to-gif) - + [Halftone](#halftone) - + [Hand Refiner with MeshGraphormer](#hand-refiner-with-meshgraphormer) - + [Image and Mask Composition Pack](#image-and-mask-composition-pack) - + [Image Dominant Color](#image-dominant-color) - + [Image to Character Art Image Nodes](#image-to-character-art-image-nodes) - + [Image Picker](#image-picker) - + [Image Resize Plus](#image-resize-plus) - + [Latent Upscale](#latent-upscale) - + [Load Video Frame](#load-video-frame) - + [Make 3D](#make-3d) - + [Mask Operations](#mask-operations) - + [Match Histogram](#match-histogram) - + [Metadata-Linked](#metadata-linked-nodes) - + [Negative Image](#negative-image) - + [Nightmare Promptgen](#nightmare-promptgen) - + [Oobabooga](#oobabooga) - + [Prompt Tools](#prompt-tools) - + [Remote Image](#remote-image) - + [BriaAI Background Remove](#briaai-remove-background) - + [Remove Background](#remove-background) - + [Retroize](#retroize) - + [Size Stepper Nodes](#size-stepper-nodes) - + [Simple Skin Detection](#simple-skin-detection) - + [Text font to Image](#text-font-to-image) - + [Thresholding](#thresholding) - + [Unsharp Mask](#unsharp-mask) - + [XY Image to Grid and Images to Grids nodes](#xy-image-to-grid-and-images-to-grids-nodes) -- [Example Node Template](#example-node-template) -- [Disclaimer](#disclaimer) -- [Help](#help) - - --------------------------------- -### 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 - --------------------------------- -### 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: -
- --------------------------------- -### 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:** -
- --------------------------------- -### 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 - --------------------------------- -### 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 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) - --------------------------------- -### 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 nolonger 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 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** - - - --------------------------------- -### 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/nodes/contributingNodes.md b/docs/nodes/contributingNodes.md deleted file mode 100644 index 7a30c8aeb0f..00000000000 --- a/docs/nodes/contributingNodes.md +++ /dev/null @@ -1,27 +0,0 @@ -# Contributing Nodes - -To learn about the specifics of creating a new node, please visit our [Node creation documentation](../contributing/INVOCATIONS.md). - -Once you’ve created a node and confirmed that it behaves as expected locally, follow these steps: - -- Make sure the node is contained in a new Python (.py) file. Preferably, the node is in a repo with a README detailing the nodes usage & examples to help others more easily use your node. Including the tag "invokeai-node" in your repository's README can also help other users find it more easily. -- Submit a pull request with a link to your node(s) repo in GitHub against the `main` branch to add the node to the [Community Nodes](communityNodes.md) list - - Make sure you are following the template below and have provided all relevant details about the node and what it does. Example output images and workflows are very helpful for other users looking to use your node. -- 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 - -```markdown --------------------------------- -### Super Cool Node Template - -**Description:** This node allows you to do super cool things with InvokeAI. - -**Node Link:** https://github.com/invoke-ai/InvokeAI/fake_node.py - -**Example Node Graph:** https://github.com/invoke-ai/InvokeAI/fake_node_graph.json - -**Output Examples** - -![InvokeAI](https://invoke-ai.github.io/InvokeAI/assets/invoke_ai_banner.png) -``` diff --git a/docs/nodes/defaultNodes.md b/docs/nodes/defaultNodes.md deleted file mode 100644 index b78c9af9010..00000000000 --- a/docs/nodes/defaultNodes.md +++ /dev/null @@ -1,109 +0,0 @@ -# List of Default Nodes - -The table below contains a list of the default nodes shipped with InvokeAI and -their descriptions. - -| Node | Function | -| :------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------- | -| Add Integers | Adds two numbers | -| Boolean Primitive Collection | A collection of boolean primitive values | -| Boolean Primitive | A boolean primitive value | -| Canny Processor | Canny edge detection for ControlNet | -| CenterPadCrop | Pad or crop an image's sides from the center by specified pixels. Positive values are outside of the image. | -| CLIP Skip | Skip layers in clip text_encoder model. | -| Collect | Collects values into a collection | -| Color Correct | Shifts the colors of a target image to match the reference image, optionally using a mask to only color-correct certain regions of the target image. | -| Color Primitive | A color primitive value | -| Compel Prompt | Parse prompt using compel package to conditioning. | -| Conditioning Primitive Collection | A collection of conditioning tensor primitive values | -| Conditioning Primitive | A conditioning tensor primitive value | -| Content Shuffle Processor | Applies content shuffle processing to image | -| ControlNet | Collects ControlNet info to pass to other nodes | -| Create Denoise Mask | Converts a greyscale or transparency image into a mask for denoising. | -| Create Gradient Mask | Creates a mask for Gradient ("soft", "differential") inpainting that gradually expands during denoising. Improves edge coherence. | -| Denoise Latents | Denoises noisy latents to decodable images | -| Divide Integers | Divides two numbers | -| Dynamic Prompt | Parses a prompt using adieyal/dynamicprompts' random or combinatorial generator | -| [FaceMask](./detailedNodes/faceTools.md#facemask) | Generates masks for faces in an image to use with Inpainting | -| [FaceIdentifier](./detailedNodes/faceTools.md#faceidentifier) | Identifies and labels faces in an image | -| [FaceOff](./detailedNodes/faceTools.md#faceoff) | Creates a new image that is a scaled bounding box with a mask on the face for Inpainting | -| Float Math | Perform basic math operations on two floats | -| Float Primitive Collection | A collection of float primitive values | -| Float Primitive | A float primitive value | -| Float Range | Creates a range | -| HED (softedge) Processor | Applies HED edge detection to image | -| Blur Image | Blurs an image | -| Extract Image Channel | Gets a channel from an image. | -| Image Primitive Collection | A collection of image primitive values | -| Integer Math | Perform basic math operations on two integers | -| Convert Image Mode | Converts an image to a different mode. | -| Crop Image | Crops an image to a specified box. The box can be outside of the image. | -| Ideal Size | Calculates an ideal image size for latents for a first pass of a multi-pass upscaling to avoid duplication and other artifacts | -| Image Hue Adjustment | Adjusts the Hue of an image. | -| Inverse Lerp Image | Inverse linear interpolation of all pixels of an image | -| Image Primitive | An image primitive value | -| Lerp Image | Linear interpolation of all pixels of an image | -| Offset Image Channel | Add to or subtract from an image color channel by a uniform value. | -| Multiply Image Channel | Multiply or Invert an image color channel by a scalar value. | -| Multiply Images | Multiplies two images together using `PIL.ImageChops.multiply()`. | -| Blur NSFW Image | Add blur to NSFW-flagged images | -| Paste Image | Pastes an image into another image. | -| ImageProcessor | Base class for invocations that preprocess images for ControlNet | -| Resize Image | Resizes an image to specific dimensions | -| Round Float | Rounds a float to a specified number of decimal places | -| Float to Integer | Converts a float to an integer. Optionally rounds to an even multiple of a input number. | -| Scale Image | Scales an image by a factor | -| Image to Latents | Encodes an image into latents. | -| Add Invisible Watermark | Add an invisible watermark to an image | -| Solid Color Infill | Infills transparent areas of an image with a solid color | -| PatchMatch Infill | Infills transparent areas of an image using the PatchMatch algorithm | -| Tile Infill | Infills transparent areas of an image with tiles of the image | -| Integer Primitive Collection | A collection of integer primitive values | -| Integer Primitive | An integer primitive value | -| Iterate | Iterates over a list of items | -| Latents Primitive Collection | A collection of latents tensor primitive values | -| Latents Primitive | A latents tensor primitive value | -| Latents to Image | Generates an image from latents. | -| Leres (Depth) Processor | Applies leres processing to image | -| Lineart Anime Processor | Applies line art anime processing to image | -| Lineart Processor | Applies line art processing to image | -| LoRA Loader | Apply selected lora to unet and text_encoder. | -| Main Model Loader | Loads a main model, outputting its submodels. | -| Combine Mask | Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`. | -| Mask Edge | Applies an edge mask to an image | -| Mask from Alpha | Extracts the alpha channel of an image as a mask. | -| Mediapipe Face Processor | Applies mediapipe face processing to image | -| Midas (Depth) Processor | Applies Midas depth processing to image | -| MLSD Processor | Applies MLSD processing to image | -| Multiply Integers | Multiplies two numbers | -| Noise | Generates latent noise. | -| Normal BAE Processor | Applies NormalBae processing to image | -| ONNX Latents to Image | Generates an image from latents. | -| ONNX Prompt (Raw) | A node to process inputs and produce outputs. May use dependency injection in **init** to receive providers. | -| ONNX Text to Latents | Generates latents from conditionings. | -| ONNX Model Loader | Loads a main model, outputting its submodels. | -| OpenCV Inpaint | Simple inpaint using opencv. | -| DW Openpose Processor | Applies Openpose processing to image | -| PIDI Processor | Applies PIDI processing to image | -| Prompts from File | Loads prompts from a text file | -| Random Integer | Outputs a single random integer. | -| Random Range | Creates a collection of random numbers | -| Integer Range | Creates a range of numbers from start to stop with step | -| Integer Range of Size | Creates a range from start to start + size with step | -| Resize Latents | Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8. | -| SDXL Compel Prompt | Parse prompt using compel package to conditioning. | -| SDXL LoRA Loader | Apply selected lora to unet and text_encoder. | -| SDXL Main Model Loader | Loads an sdxl base model, outputting its submodels. | -| SDXL Refiner Compel Prompt | Parse prompt using compel package to conditioning. | -| SDXL Refiner Model Loader | Loads an sdxl refiner model, outputting its submodels. | -| Scale Latents | Scales latents by a given factor. | -| Segment Anything Processor | Applies segment anything processing to image | -| Show Image | Displays a provided image, and passes it forward in the pipeline. | -| Step Param Easing | Experimental per-step parameter easing for denoising steps | -| String Primitive Collection | A collection of string primitive values | -| String Primitive | A string primitive value | -| Subtract Integers | Subtracts two numbers | -| Tile Resample Processor | Tile resampler processor | -| Upscale (RealESRGAN) | Upscales an image using RealESRGAN. | -| VAE Loader | Loads a VAE model, outputting a VaeLoaderOutput | -| Zoe (Depth) Processor | Applies Zoe depth processing to image | diff --git a/docs/nodes/detailedNodes/faceTools.md b/docs/nodes/detailedNodes/faceTools.md deleted file mode 100644 index 632212d3c33..00000000000 --- a/docs/nodes/detailedNodes/faceTools.md +++ /dev/null @@ -1,154 +0,0 @@ -# Face Nodes - -## FaceOff - -FaceOff mimics a user finding a face in an image and resizing the bounding box -around the head in Canvas. - -Enter a face ID (found with FaceIdentifier) to choose which face to mask. - -Just as you would add more context inside the bounding box by making it larger -in Canvas, the node gives you a padding input (in pixels) which will -simultaneously add more context, and increase the resolution of the bounding box -so the face remains the same size inside it. - -The "Minimum Confidence" input defaults to 0.5 (50%), and represents a pass/fail -threshold a detected face must reach for it to be processed. Lowering this value -may help if detection is failing. If the detected masks are imperfect and stray -too far outside/inside of faces, the node gives you X & Y offsets to shrink/grow -the masks by a multiplier. - -FaceOff will output the face in a bounded image, taking the face off of the -original image for input into any node that accepts image inputs. The node also -outputs a face mask with the dimensions of the bounded image. The X & Y outputs -are for connecting to the X & Y inputs of the Paste Image node, which will place -the bounded image back on the original image using these coordinates. - -###### Inputs/Outputs - -| Input | Description | -| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Image | Image for face detection | -| Face ID | The face ID to process, numbered from 0. Multiple faces not supported. Find a face's ID with FaceIdentifier node. | -| Minimum Confidence | Minimum confidence for face detection (lower if detection is failing) | -| X Offset | X-axis offset of the mask | -| Y Offset | Y-axis offset of the mask | -| Padding | All-axis padding around the mask in pixels | -| Chunk | Chunk (or divide) the image into sections to greatly improve face detection success. Defaults to off, but will activate if no faces are detected normally. Activate to chunk by default. | - -| Output | Description | -| ------------- | ------------------------------------------------ | -| Bounded Image | Original image bound, cropped, and resized | -| Width | The width of the bounded image in pixels | -| Height | The height of the bounded image in pixels | -| Mask | The output mask | -| X | The x coordinate of the bounding box's left side | -| Y | The y coordinate of the bounding box's top side | - -## FaceMask - -FaceMask mimics a user drawing masks on faces in an image in Canvas. - -The "Face IDs" input allows the user to select specific faces to be masked. -Leave empty to detect and mask all faces, or a comma-separated list for a -specific combination of faces (ex: `1,2,4`). A single integer will detect and -mask that specific face. Find face IDs with the FaceIdentifier node. - -The "Minimum Confidence" input defaults to 0.5 (50%), and represents a pass/fail -threshold a detected face must reach for it to be processed. Lowering this value -may help if detection is failing. - -If the detected masks are imperfect and stray too far outside/inside of faces, -the node gives you X & Y offsets to shrink/grow the masks by a multiplier. All -masks shrink/grow together by the X & Y offset values. - -By default, masks are created to change faces. When masks are inverted, they -change surrounding areas, protecting faces. - -###### Inputs/Outputs - -| Input | Description | -| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Image | Image for face detection | -| Face IDs | Comma-separated list of face ids to mask eg '0,2,7'. Numbered from 0. Leave empty to mask all. Find face IDs with FaceIdentifier node. | -| Minimum Confidence | Minimum confidence for face detection (lower if detection is failing) | -| X Offset | X-axis offset of the mask | -| Y Offset | Y-axis offset of the mask | -| Chunk | Chunk (or divide) the image into sections to greatly improve face detection success. Defaults to off, but will activate if no faces are detected normally. Activate to chunk by default. | -| Invert Mask | Toggle to invert the face mask | - -| Output | Description | -| ------ | --------------------------------- | -| Image | The original image | -| Width | The width of the image in pixels | -| Height | The height of the image in pixels | -| Mask | The output face mask | - -## FaceIdentifier - -FaceIdentifier outputs an image with detected face IDs printed in white numbers -onto each face. - -Face IDs can then be used in FaceMask and FaceOff to selectively mask all, a -specific combination, or single faces. - -The FaceIdentifier output image is generated for user reference, and isn't meant -to be passed on to other image-processing nodes. - -The "Minimum Confidence" input defaults to 0.5 (50%), and represents a pass/fail -threshold a detected face must reach for it to be processed. Lowering this value -may help if detection is failing. If an image is changed in the slightest, run -it through FaceIdentifier again to get updated FaceIDs. - -###### Inputs/Outputs - -| Input | Description | -| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Image | Image for face detection | -| Minimum Confidence | Minimum confidence for face detection (lower if detection is failing) | -| Chunk | Chunk (or divide) the image into sections to greatly improve face detection success. Defaults to off, but will activate if no faces are detected normally. Activate to chunk by default. | - -| Output | Description | -| ------ | ------------------------------------------------------------------------------------------------ | -| Image | The original image with small face ID numbers printed in white onto each face for user reference | -| Width | The width of the original image in pixels | -| Height | The height of the original image in pixels | - -## Tips - -- If not all target faces are being detected, activate Chunk to bypass full - image face detection and greatly improve detection success. -- Final results will vary between full-image detection and chunking for faces - that are detectable by both due to the nature of the process. Try either to - your taste. -- Be sure Minimum Confidence is set the same when using FaceIdentifier with - FaceOff/FaceMask. -- For FaceOff, use the color correction node before faceplace to correct edges - being noticeable in the final image (see example screenshot). -- Non-inpainting models may struggle to paint/generate correctly around faces. -- If your face won't change the way you want it to no matter what you change, - consider that the change you're trying to make is too much at that resolution. - For example, if an image is only 512x768 total, the face might only be 128x128 - or 256x256, much smaller than the 512x512 your SD1.5 model was probably - trained on. Try increasing the resolution of the image by upscaling or - resizing, add padding to increase the bounding box's resolution, or use an - image where the face takes up more pixels. -- If the resulting face seems out of place pasted back on the original image - (ie. too large, not proportional), add more padding on the FaceOff node to - give inpainting more context. Context and good prompting are important to - keeping things proportional. -- If you find the mask is too big/small and going too far outside/inside the - area you want to affect, adjust the x & y offsets to shrink/grow the mask area -- Use a higher denoise start value to resemble aspects of the original face or - surroundings. Denoise start = 0 & denoise end = 1 will make something new, - while denoise start = 0.50 & denoise end = 1 will be 50% old and 50% new. -- mediapipe isn't good at detecting faces with lots of face paint, hair covering - the face, etc. Anything that obstructs the face will likely result in no faces - being detected. -- If you find your face isn't being detected, try lowering the minimum - confidence value from 0.5. This could result in false positives, however - (random areas being detected as faces and masked). -- After altering an image and wanting to process a different face in the newly - altered image, run the altered image through FaceIdentifier again to see the - new Face IDs. MediaPipe will most likely detect faces in a different order - after an image has been changed in the slightest. diff --git a/docs/nodes/exampleWorkflows.md b/docs/nodes/exampleWorkflows.md deleted file mode 100644 index b52493f365f..00000000000 --- a/docs/nodes/exampleWorkflows.md +++ /dev/null @@ -1,18 +0,0 @@ -# Example Workflows - -We've curated some example workflows for you to get started with Workflows in InvokeAI! These can also be found in the Workflow Library, located in the Workflow Editor of Invoke. - -To use them, right click on your desired workflow, follow the link to GitHub and click the "⬇" button to download the raw file. You can then use the "Load Workflow" functionality in InvokeAI to load the workflow and start generating images! - -If you're interested in finding more workflows, checkout the [#share-your-workflows](https://discord.com/channels/1020123559063990373/1130291608097661000) channel in the InvokeAI Discord. - -* [SD1.5 / SD2 Text to Image](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/Text_to_Image.json) -* [SDXL Text to Image](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/SDXL_Text_to_Image.json) -* [SDXL Text to Image with Refiner](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/SDXL_w_Refiner_Text_to_Image.json) -* [Multi ControlNet (Canny & Depth)](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/Multi_ControlNet_Canny_and_Depth.json) -* [Tiled Upscaling with ControlNet](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/ESRGAN_img2img_upscale_w_Canny_ControlNet.json) -* [Prompt From File](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/Prompt_from_File.json) -* [Face Detailer with IP-Adapter & ControlNet](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/Face_Detailer_with_IP-Adapter_and_Canny.json) -* [FaceMask](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/FaceMask.json) -* [FaceOff with 2x Face Scaling](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/FaceOff_FaceScale2x.json) -* [QR Code Monster](https://github.com/invoke-ai/InvokeAI/blob/main/docs/workflows/QR_Code_Monster.json) diff --git a/docs/nodes/overview.md b/docs/nodes/overview.md deleted file mode 100644 index 71e316c8561..00000000000 --- a/docs/nodes/overview.md +++ /dev/null @@ -1,26 +0,0 @@ -# Nodes - -## What are Nodes? -An Node is simply a single operation that takes in inputs and returns -out outputs. Multiple nodes can be linked together to create more -complex functionality. All InvokeAI features are added through nodes. - -### Anatomy of a Node - -Individual nodes are made up of the following: - -- Inputs: Edge points on the left side of the node window where you connect outputs from other nodes. -- Outputs: Edge points on the right side of the node window where you connect to inputs on other nodes. -- Options: Various options which are either manually configured, or overridden by connecting an output from another node to the input. - - -With nodes, you can can easily extend the image generation capabilities of InvokeAI, and allow you build workflows that suit your needs. - -You can read more about nodes and the node editor [here](../nodes/NODES.md). - -To get started with nodes, take a look at some of our examples for [common workflows](../nodes/exampleWorkflows.md) - -## Downloading New Nodes -To download a new node, visit our list of [Community Nodes](../nodes/communityNodes.md). These are nodes that have been created by the community, for the community. - - diff --git a/docs/other/CONTRIBUTORS.md b/docs/other/CONTRIBUTORS.md deleted file mode 100644 index 448eddbcf01..00000000000 --- a/docs/other/CONTRIBUTORS.md +++ /dev/null @@ -1,380 +0,0 @@ ---- -title: Contributors ---- - -# :octicons-person-24: Contributors - -The list of all the amazing people who have contributed to the various features that you get to -experience in this fork. - -We thank them for all of their time and hard work. - -## **Original Author** - -- [Lincoln D. Stein](mailto:lincoln.stein@gmail.com) - -## **Current Core Team** - -* @lstein (Lincoln Stein) - Co-maintainer -* @blessedcoolant - Co-maintainer -* @hipsterusername (Kent Keirsey) - Co-maintainer, CEO, Positive Vibes -* @psychedelicious (Spencer Mabrito) - Web Team Leader -* @chainchompa (Jennifer Player) - Web Development & Chain-Chomping -* @josh is toast (Josh Corbett) - Web Development -* @cheerio (Mary Rogers) - Lead Engineer & Web App Development -* @ebr (Eugene Brodsky) - Cloud/DevOps/Sofware engineer; your friendly neighbourhood cluster-autoscaler -* @sunija - Standalone version -* @genomancer (Gregg Helt) - Controlnet support -* @brandon (Brandon Rising) - Platform, Infrastructure, Backend Systems -* @ryanjdick (Ryan Dick) - Machine Learning & Training -* @JPPhoto - Core image generation nodes -* @dunkeroni - Image generation backend -* @SkunkWorxDark - Image generation backend -* @keturn (Kevin Turner) - Diffusers -* @millu (Millun Atluri) - Community Wizard, Documentation, Node-wrangler, -* @glimmerleaf (Devon Hopkins) - Community Wizard -* @gogurt enjoyer - Discord moderator and end user support -* @whosawhatsis - Discord moderator and end user support -* @dwinrger - Discord moderator and end user support -* @526christian - Discord moderator and end user support -* @harvester62 - Discord moderator and end user support - - -## **Honored Team Alumni** - -* @StAlKeR7779 (Sergey Borisov) - Torch stack, ONNX, model management, optimization -* @damian0815 - Attention Systems and Compel Maintainer -* @netsvetaev (Artur) - Localization support -* @Kyle0654 (Kyle Schouviller) - Node Architect and General Backend Wizard -* @tildebyte - Installation and configuration -* @mauwii (Matthias Wilde) - Installation, release, continuous integration - - -## **Full List of Contributors by Commit Name** - -- 이승석 -- AbdBarho -- ablattmann -- AdamOStark -- Adam Rice -- Airton Silva -- Aldo Hoeben -- Alexander Eichhorn -- Alexandre D. Roberge -- Alexandre Macabies -- Alfie John -- Andreas Rozek -- Andre LaBranche -- Andy Bearman -- Andy Luhrs -- Andy Pilate -- Anonymous -- Anthony Monthe -- Any-Winter-4079 -- apolinario -- Ar7ific1al -- ArDiouscuros -- Armando C. Santisbon -- Arnold Cordewiner -- Arthur Holstvoogd -- artmen1516 -- Artur -- Arturo Mendivil -- Ben Alkov -- Benjamin Warner -- Bernard Maltais -- blessedcoolant -- blhook -- BlueAmulet -- Bouncyknighter -- Brandon -- Brandon Rising -- Brent Ozar -- Brian Racer -- bsilvereagle -- c67e708d -- camenduru -- CapableWeb -- Carson Katri -- chainchompa -- Chloe -- Chris Dawson -- Chris Hayes -- Chris Jones -- chromaticist -- Claus F. Strasburger -- cmdr2 -- cody -- Conor Reid -- Cora Johnson-Roberson -- coreco -- cosmii02 -- cpacker -- Cragin Godley -- creachec -- CrypticWit -- d8ahazard -- damian -- damian0815 -- Damian at mba -- Damian Stewart -- Daniel Manzke -- Danny Beer -- Dan Sully -- Darren Ringer -- David Burnett -- David Ford -- David Regla -- David Sisco -- David Wager -- Daya Adianto -- db3000 -- DekitaRPG -- Denis Olshin -- Dennis -- dependabot[bot] -- Dmitry Parnas -- Dobrynia100 -- Dominic Letz -- DrGunnarMallon -- Drun555 -- dunkeroni -- Edward Johan -- elliotsayes -- Elrik -- ElrikUnderlake -- Eric Khun -- Eric Wolf -- Eugene -- Eugene Brodsky -- ExperimentalCyborg -- Fabian Bahl -- Fabio 'MrWHO' Torchetti -- Fattire -- fattire -- Felipe Nogueira -- Félix Sanz -- figgefigge -- Gabriel Mackievicz Telles -- gabrielrotbart -- gallegonovato -- Gérald LONLAS -- Gille -- GitHub Actions Bot -- glibesyck -- gogurtenjoyer -- Gohsuke Shimada -- greatwolf -- greentext2 -- Gregg Helt -- H4rk -- Håvard Gulldahl -- henry -- Henry van Megen -- hipsterusername -- hj -- Hosted Weblate -- Iman Karim -- ismail ihsan bülbül -- ItzAttila -- Ivan Efimov -- jakehl -- Jakub Kolčář -- JamDon2 -- James Reynolds -- Jan Skurovec -- Jari Vetoniemi -- Jason Toffaletti -- Jaulustus -- Jeff Mahoney -- Jennifer Player -- jeremy -- Jeremy Clark -- JigenD -- Jim Hays -- Johan Roxendal -- Johnathon Selstad -- Jonathan -- Jordan Hewitt -- Joseph Dries III -- Josh Corbett -- JPPhoto -- jspraul -- junzi -- Justin Wong -- Juuso V -- Kaspar Emanuel -- Katsuyuki-Karasawa -- Keerigan45 -- Kent Keirsey -- Kevin Brack -- Kevin Coakley -- Kevin Gibbons -- Kevin Schaul -- Kevin Turner -- Kieran Klaassen -- krummrey -- Kyle -- Kyle Lacy -- Kyle Schouviller -- Lawrence Norton -- LemonDouble -- Leo Pasanen -- Lincoln Stein -- LoganPederson -- Lynne Whitehorn -- majick -- Marco Labarile -- Marta Nahorniuk -- Martin Kristiansen -- Mary Hipp -- maryhipp -- Mary Hipp Rogers -- mastercaster -- mastercaster9000 -- Matthias Wild -- mauwii -- michaelk71 -- mickr777 -- Mihai -- Mihail Dumitrescu -- Mikhail Tishin -- Millun Atluri -- Minjune Song -- Mitchell Allain -- mitien -- mofuzz -- Muhammad Usama -- Name -- _nderscore -- Neil Wang -- nekowaiz -- nemuruibai -- Netzer R -- Nicholas Koh -- Nicholas Körfer -- nicolai256 -- Niek van der Maas -- noodlebox -- Nuno Coração -- ofirkris -- Olivier Louvignes -- owenvincent -- pand4z31 -- Patrick Esser -- Patrick Tien -- Patrick von Platen -- Paul Curry -- Paul Sajna -- pejotr -- Peter Baylies -- Peter Lin -- plucked -- prixt -- psychedelicious -- psychedelicious@windows -- Rainer Bernhardt -- Riccardo Giovanetti -- Rich Jones -- rmagur1203 -- Rob Baines -- Robert Bolender -- Robin Rombach -- Rohan Barar -- Rohinish -- rpagliuca -- rromb -- Rupesh Sreeraman -- Ryan -- Ryan Cao -- Ryan Dick -- Saifeddine -- Saifeddine ALOUI -- Sam -- SammCheese -- Sam McLeod -- Sammy -- sammyf -- Samuel Husso -- Saurav Maheshkar -- Scott Lahteine -- Sean McLellan -- Sebastian Aigner -- Sergey Borisov -- Sergey Krashevich -- Shapor Naghibzadeh -- Shawn Zhong -- Simona Liliac -- Simon Vans-Colina -- skunkworxdark -- slashtechno -- SoheilRezaei -- Song, Pengcheng -- spezialspezial -- ssantos -- StAlKeR7779 -- Stefan Tobler -- Stephan Koglin-Fischer -- SteveCaruso -- Steve Martinelli -- Steven Frank -- Surisen -- System X - Files -- Taylor Kems -- techicode -- techybrain-dev -- tesseractcat -- thealanle -- Thomas -- tildebyte -- Tim Cabbage -- Tom -- Tom Elovi Spruce -- Tom Gouville -- tomosuto -- Travco -- Travis Palmer -- tyler -- unknown -- user1 -- vedant-3010 -- Vedant Madane -- veprogames -- wa.code -- wfng92 -- whjms -- whosawhatsis -- Will -- William Becher -- William Chong -- Wilson E. Alvarez -- woweenie -- Wubbbi -- xra -- Yeung Yiu Hung -- ymgenesis -- Yorzaren -- Yosuke Shinya -- yun saki -- ZachNagengast -- Zadagu -- zeptofine -- Zerdoumi -- Васянатор -- 冯不游 -- 唐澤 克幸 - -## **Original CompVis (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) - ---- - -_If you have contributed and don't see your name on the list of contributors, please let one of the -collaborators know about the omission, or feel free to make a pull request._ diff --git a/docs/other/README-CompViz.md b/docs/other/README-CompViz.md deleted file mode 100644 index cb3a207b2e0..00000000000 --- a/docs/other/README-CompViz.md +++ /dev/null @@ -1,255 +0,0 @@ ---- -title: CompViz-Readme ---- - -# _README from [CompViz/stable-diffusion](https://github.com/CompVis/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 -conda activate ldm -``` - -Note that the first line may be abbreviated `conda env create`, since conda will -look for `environment.yml` by default. - -You can also update an existing -[latent diffusion](https://github.com/CompVis/latent-diffusion) environment by -running - -```bash -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/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/assets/invoke-web-server-1.png b/docs/src/content/docs/assets/invoke-web-server-1.png similarity index 100% rename from docs/assets/invoke-web-server-1.png rename to docs/src/content/docs/assets/invoke-web-server-1.png 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/assets/invoke_ai_banner.png b/docs/src/content/docs/assets/splash-banner.png similarity index 100% rename from docs/assets/invoke_ai_banner.png rename to docs/src/content/docs/assets/splash-banner.png 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..1c79fbf82aa --- /dev/null +++ b/docs/src/content/docs/configuration/invokeai-yaml.mdx @@ -0,0 +1,266 @@ +--- +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. + +#### Multi-GPU Generation + +On a machine with more than one GPU, InvokeAI can run several generation sessions at the same time — one per GPU — instead of processing the queue one job at a time. Jobs are distributed fairly across users, so a single user's large batch cannot monopolize every GPU while others wait. + +This is controlled by the `generation_devices` setting: + +```yaml +generation_devices: auto # default value +``` + +| Value | Behavior | +| -------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `auto` | Use every available CUDA GPU, running one generation session per GPU concurrently. This is the default. | +| `[cuda:0,cuda:1]` | Use the specific devices listed, one session per device. Useful for reserving a GPU for other work. | +| `[cuda:0]` | Use a single specific device. Generation runs serially, as it did before multi-GPU support. | +| `[]` | Use the first detected device. Generation runs serially, as it did before multi-GPU support. | + +Each entry in the list must be one of `cpu`, `cuda`, `mps`, or `cuda:N`, where `N` is a zero-based device number (`cuda:0` is the first GPU, `cuda:1` the second, and so on). + +```yaml +# Use the first and third GPUs, leaving the second free for other tasks +generation_devices: [cuda:0, cuda:2] +``` + +Notes: + +- On a system without a CUDA GPU, `auto` resolves to the single best available device (`mps` on Apple Silicon, otherwise `cpu`), so generation runs serially. +- Each active GPU gets its own model cache, and model weights are duplicated in system RAM for every device. Running many GPUs in parallel therefore increases RAM usage — ensure you have ample system memory before enabling a large device list. +- Duplicate entries are ignored; `[cuda:0, cuda:0]` is treated as `[cuda:0]`. +- You can restrict which physical GPUs InvokeAI sees with the `CUDA_VISIBLE_DEVICES` environment variable. When set, `auto` only enumerates the visible subset, and `cuda:N` indices refer to positions within that subset. + +During parallel generation, the progress display shows one progress bar per active session, stacked vertically, each disappearing as its session completes. + +#### Text Encoder Offload to Idle GPUs + +When more than one GPU is configured for generation but not all of them are busy, InvokeAI can run a session's text/prompt encoder on a currently-idle GPU instead of the GPU running its denoise pipeline. This avoids evicting the denoise model from VRAM just to make room for the encoder, and lets the cached encoder be reused across generations — making repeated generations noticeably smoother. + +This is controlled by the `offload_text_encoders_to_idle_gpus` setting: + +```yaml +offload_text_encoders_to_idle_gpus: true # default value +``` + +| Value | Behavior | +| ------- | ---------------------------------------------------------------------------------------------------------------- | +| `true` | Run text encoders on an idle GPU when one is available. This is the default. | +| `false` | Always run text encoders on the same GPU as the rest of the pipeline (the behavior before this feature existed). | + +Notes: + +- This has no effect unless at least two `generation_devices` are configured. On a single device — or when every GPU is already busy with its own session — encoders run on the session's own GPU, exactly as if the setting were `false`. +- It is purely a placement optimization and does not change generated images. +- A borrowed GPU is used exclusively for the encoder while it runs, so it never interferes with a generation session running on that same GPU. + +#### 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/assets/contributing/resize_invocation.png b/docs/src/content/docs/development/Architecture/assets/resize_invocation.png similarity index 100% rename from docs/assets/contributing/resize_invocation.png rename to docs/src/content/docs/development/Architecture/assets/resize_invocation.png diff --git a/docs/assets/contributing/resize_node_editor.png b/docs/src/content/docs/development/Architecture/assets/resize_node_editor.png similarity index 100% rename from docs/assets/contributing/resize_node_editor.png rename to docs/src/content/docs/development/Architecture/assets/resize_node_editor.png 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/assets/contributing/html-detail.png b/docs/src/content/docs/development/Guides/assets/html-detail.png similarity index 100% rename from docs/assets/contributing/html-detail.png rename to docs/src/content/docs/development/Guides/assets/html-detail.png diff --git a/docs/assets/contributing/html-overview.png b/docs/src/content/docs/development/Guides/assets/html-overview.png similarity index 100% rename from docs/assets/contributing/html-overview.png rename to docs/src/content/docs/development/Guides/assets/html-overview.png 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..abc905f6e6a --- /dev/null +++ b/docs/src/content/docs/development/Guides/creating-nodes.mdx @@ -0,0 +1,75 @@ +--- +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. + + +### Supporting multi-GPU text-encoder offload + +On a machine with more than one GPU, InvokeAI can run several generation sessions at once — one per GPU. When fewer sessions are running than there are GPUs, the spare GPUs sit idle. To put that capacity to use, InvokeAI can run a session's **prompt/text encoder** on a currently-idle GPU instead of on the GPU running the denoise pipeline. This avoids evicting the denoise model from VRAM just to make room for the encoder, and lets the cached encoder be reused across generations. + +This is controlled globally by the `offload_text_encoders_to_idle_gpus` config setting (enabled by default) and opted into **per node** via the `@invocation` decorator: + +```python +from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation + + +@invocation( + "my_text_encoder", + title="Prompt - My Model", + category="conditioning", + version="1.0.0", + idle_gpu_offloadable=True, # opt in to idle-GPU offload +) +class MyTextEncoderInvocation(BaseInvocation): + ... +``` + +When the feature is enabled and an idle GPU is available, the **entire node** is temporarily re-pinned to a borrowed idle GPU: any model it loads goes onto that GPU and runs there. If no idle GPU is free (e.g. every GPU is busy with its own session), the node simply runs on its own GPU, unchanged. The borrow holds the idle GPU exclusively for the duration of the node, so it can never run concurrently against a native session on that same GPU. + +Because the whole node is moved to another device, only mark a node `idle_gpu_offloadable=True` if **all** of the following hold: + +- **It is encoder-only.** Its sole GPU work is loading one or more encoder models and running their forward pass. It must not load or run the denoise/transformer or VAE, or do any other work tied to the session's own GPU. +- **It stores its result on the CPU before returning.** Move output tensors to the CPU (`tensor.detach().to("cpu")`) and save them as conditioning/tensors. The denoiser picks them up and moves them onto its own GPU later — this is what makes the cross-GPU handoff safe and device-agnostic. +- **It places inputs on the loaded model's device, not a fixed device.** Resolve the device from the model you just loaded (e.g. `get_effective_device(model)` from `invokeai.backend.model_manager.load.model_cache.utils`, or `TorchDevice.choose_torch_device()`), rather than hard-coding `cuda:0`. The built-in `flux_text_encoder` and `compel` nodes are good references. + +:::caution[Only mark encoder-only nodes] +If a node that also runs the denoiser, VAE, or other session-GPU work is marked `idle_gpu_offloadable=True`, that work will be re-pinned to the wrong GPU and can misplace tensors or raise device-mismatch errors. When in doubt, leave it unset (the default is `False`) — the node will still work correctly, just without the offload optimization. +::: + +### 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..f376e2af4bd --- /dev/null +++ b/docs/src/content/docs/development/Guides/recall-api.mdx @@ -0,0 +1,572 @@ +--- +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. + +#### Query parameters + +The POST endpoint accepts two optional boolean query parameters that control +how reference images are merged into the frontend state: + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `strict` | `false` | When `true`, parameters **not** included in the request body are reset to their defaults (cleared on the frontend). When `false`, only the parameters you send are updated and everything else is left as-is. | +| `append` | `false` | When `true`, recalled reference images (`ip_adapters` and `reference_images`) are **appended** to the frontend's existing reference-image list instead of replacing it. When `false` (or omitted), the recalled reference images **replace** the existing list. | + +`strict` and `append` are mutually exclusive — `strict` clears omitted +parameters while `append` preserves and extends the existing list, so the two +cannot be combined. Sending `?strict=true&append=true` returns +**400 Bad Request**: + +```json +{ + "detail": "The 'strict' and 'append' query parameters are mutually exclusive" +} +``` + +`append` only affects the reference-image collections (`ip_adapters` and +`reference_images`). All other parameters (prompts, model, LoRAs, control +layers, etc.) are updated the same way regardless of the flag. + +### 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" + } + ] + }' +``` + +### Appending reference images (append mode) + +By default, recalled reference images **replace** whatever the frontend +already has. Pass `?append=true` to **add** the recalled `ip_adapters` and +`reference_images` to the existing list instead: + +```bash +# Add a reference image without clearing the ones already on the frontend +curl -X POST 'http://localhost:9090/api/v1/recall/default?append=true' \ + -H "Content-Type: application/json" \ + -d '{ + "reference_images": [ + {"image_name": "extra_reference.png"} + ] + }' +``` + +Combining `append=true` with `strict=true` is invalid and returns +**400 Bad Request** (see [Query parameters](#query-parameters)). + +### 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. Merge the recalled reference images — IP Adapter / FLUX Redux entries and + model-free reference images both feed the same reference-image list, using + the config flavor that matches the currently-selected main model. By + default this **replaces** the existing list; with `append=true` it is + **added** to whatever is already there (see + [Query parameters](#query-parameters)). + +## Error Handling + +- **400 Bad Request** — invalid parameters or parameter values, or the + mutually exclusive `strict=true&append=true` combination (see + [Query parameters](#query-parameters)). +- **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 2 reference image(s) (IP adapters + model-free), replacing existing list +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/Guides/workflow-api.mdx b/docs/src/content/docs/development/Guides/workflow-api.mdx new file mode 100644 index 00000000000..effe66a352a --- /dev/null +++ b/docs/src/content/docs/development/Guides/workflow-api.mdx @@ -0,0 +1,106 @@ +--- +title: Workflow Execution API +--- + +## Overview + +InvokeAI's HTTP API can be used programmatically from external clients, but one distinction is easy to miss: + +- A saved **workflow** is not executed directly. +- The queue accepts an executable **graph**. +- If you fetch a saved workflow from `/api/v1/workflows/`, you still need to enqueue a graph with `POST /api/v1/queue/default/enqueue_batch`. + +This is a common source of confusion when a client can successfully read workflow records but cannot execute them. + +If you are using multi-user mode, include your `Authorization: Bearer ` header on these requests. + +## Minimal `enqueue_batch` Example + +The smallest useful "hello world" is a simple graph with one math node. This avoids any image-model setup and lets you verify that your client can enqueue work and poll for completion. + +```python +import requests +import time + +BASE_URL = "http://localhost:9090" + +graph = { + "id": "hello-graph", + "nodes": { + "add-node": { + "id": "add-node", + "type": "add", + "a": 2, + "b": 3, + "is_intermediate": False, + "use_cache": True, + } + }, + "edges": [], +} + +payload = { + "batch": { + "graph": graph, + "runs": 1, + "origin": "external-client", + } +} + +enqueue_response = requests.post( + f"{BASE_URL}/api/v1/queue/default/enqueue_batch", + json=payload, +) +enqueue_response.raise_for_status() + +item_id = enqueue_response.json()["item_ids"][0] + +while True: + item_response = requests.get(f"{BASE_URL}/api/v1/queue/default/i/{item_id}") + item_response.raise_for_status() + item = item_response.json() + + status = item["status"] + if status == "completed": + print(item["session"]["results"]) + break + if status == "failed": + raise RuntimeError(item["error_message"]) + if status == "canceled": + raise RuntimeError("Queue item was canceled") + + time.sleep(0.5) +``` + +For the graph above, the completed queue item will contain the output in `session.results`. + +## Getting an Image Output + +For image-generation graphs, the completed queue item also includes `session.results`, but the output object will typically contain an image reference instead of a plain integer value. + +For example, an image output may look like this: + +```json +{ + "type": "image_output", + "image": { + "image_name": "abc123.png" + } +} +``` + +Once you have the `image_name`, you can download the generated file from: + +```text +GET /api/v1/images/i/{image_name}/full +``` + +If you only need the file preview, you can also use: + +```text +GET /api/v1/images/i/{image_name}/thumbnail +``` + +## Polling vs. WebSockets + +The example above uses polling because it is the easiest way to get started from an external client. If you need lower-latency updates, you can also use the socket events emitted during queue execution. 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..36083388daf --- /dev/null +++ b/docs/src/content/docs/features/Multi-User Mode/api-guide.mdx @@ -0,0 +1,1236 @@ +--- +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() +``` + +## Programmatic Graph Execution + +If you are building an external client against the HTTP API, see the dedicated [Workflow Execution guide](/development/guides/workflow-api/). + +In multi-user mode, remember to include your `Authorization: Bearer ` header on queue and workflow requests. + +## 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/assets/nodes/groupsallscale.png b/docs/src/content/docs/features/Workflows/assets/groupsallscale.png similarity index 100% rename from docs/assets/nodes/groupsallscale.png rename to docs/src/content/docs/features/Workflows/assets/groupsallscale.png diff --git a/docs/assets/nodes/groupsconditioning.png b/docs/src/content/docs/features/Workflows/assets/groupsconditioning.png similarity index 100% rename from docs/assets/nodes/groupsconditioning.png rename to docs/src/content/docs/features/Workflows/assets/groupsconditioning.png diff --git a/docs/assets/nodes/groupscontrol.png b/docs/src/content/docs/features/Workflows/assets/groupscontrol.png similarity index 100% rename from docs/assets/nodes/groupscontrol.png rename to docs/src/content/docs/features/Workflows/assets/groupscontrol.png diff --git a/docs/assets/nodes/groupsimgvae.png b/docs/src/content/docs/features/Workflows/assets/groupsimgvae.png similarity index 100% rename from docs/assets/nodes/groupsimgvae.png rename to docs/src/content/docs/features/Workflows/assets/groupsimgvae.png diff --git a/docs/assets/nodes/groupsiterate.png b/docs/src/content/docs/features/Workflows/assets/groupsiterate.png similarity index 100% rename from docs/assets/nodes/groupsiterate.png rename to docs/src/content/docs/features/Workflows/assets/groupsiterate.png diff --git a/docs/assets/nodes/groupslora.png b/docs/src/content/docs/features/Workflows/assets/groupslora.png similarity index 100% rename from docs/assets/nodes/groupslora.png rename to docs/src/content/docs/features/Workflows/assets/groupslora.png diff --git a/docs/assets/nodes/groupsmultigenseeding.png b/docs/src/content/docs/features/Workflows/assets/groupsmultigenseeding.png similarity index 100% rename from docs/assets/nodes/groupsmultigenseeding.png rename to docs/src/content/docs/features/Workflows/assets/groupsmultigenseeding.png diff --git a/docs/assets/nodes/groupsnoise.png b/docs/src/content/docs/features/Workflows/assets/groupsnoise.png similarity index 100% rename from docs/assets/nodes/groupsnoise.png rename to docs/src/content/docs/features/Workflows/assets/groupsnoise.png diff --git a/docs/assets/nodes/linearview.png b/docs/src/content/docs/features/Workflows/assets/linearview.png similarity index 100% rename from docs/assets/nodes/linearview.png rename to docs/src/content/docs/features/Workflows/assets/linearview.png diff --git a/docs/assets/nodes/nodescontrol.png b/docs/src/content/docs/features/Workflows/assets/nodescontrol.png similarity index 100% rename from docs/assets/nodes/nodescontrol.png rename to docs/src/content/docs/features/Workflows/assets/nodescontrol.png diff --git a/docs/assets/nodes/nodesi2i.png b/docs/src/content/docs/features/Workflows/assets/nodesi2i.png similarity index 100% rename from docs/assets/nodes/nodesi2i.png rename to docs/src/content/docs/features/Workflows/assets/nodesi2i.png diff --git a/docs/assets/nodes/nodest2i.png b/docs/src/content/docs/features/Workflows/assets/nodest2i.png similarity index 100% rename from docs/assets/nodes/nodest2i.png rename to docs/src/content/docs/features/Workflows/assets/nodest2i.png diff --git a/docs/assets/nodes/workflow_library.png b/docs/src/content/docs/features/Workflows/assets/workflow_library.png similarity index 100% rename from docs/assets/nodes/workflow_library.png rename to docs/src/content/docs/features/Workflows/assets/workflow_library.png 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/assets/gallery/board_settings.png b/docs/src/content/docs/features/assets/board_settings.png similarity index 100% rename from docs/assets/gallery/board_settings.png rename to docs/src/content/docs/features/assets/board_settings.png diff --git a/docs/assets/gallery/board_tabs.png b/docs/src/content/docs/features/assets/board_tabs.png similarity index 100% rename from docs/assets/gallery/board_tabs.png rename to docs/src/content/docs/features/assets/board_tabs.png diff --git a/docs/assets/gallery/board_thumbnails.png b/docs/src/content/docs/features/assets/board_thumbnails.png similarity index 100% rename from docs/assets/gallery/board_thumbnails.png rename to docs/src/content/docs/features/assets/board_thumbnails.png diff --git a/docs/assets/gallery/gallery.png b/docs/src/content/docs/features/assets/gallery.png similarity index 100% rename from docs/assets/gallery/gallery.png rename to docs/src/content/docs/features/assets/gallery.png diff --git a/docs/assets/gallery/image_menu.png b/docs/src/content/docs/features/assets/image_menu.png similarity index 100% rename from docs/assets/gallery/image_menu.png rename to docs/src/content/docs/features/assets/image_menu.png diff --git a/docs/assets/gallery/info_button.png b/docs/src/content/docs/features/assets/info_button.png similarity index 100% rename from docs/assets/gallery/info_button.png rename to docs/src/content/docs/features/assets/info_button.png diff --git a/docs/assets/gallery/thumbnail_menu.png b/docs/src/content/docs/features/assets/thumbnail_menu.png similarity index 100% rename from docs/assets/gallery/thumbnail_menu.png rename to docs/src/content/docs/features/assets/thumbnail_menu.png diff --git a/docs/assets/gallery/top_controls.png b/docs/src/content/docs/features/assets/top_controls.png similarity index 100% rename from docs/assets/gallery/top_controls.png rename to docs/src/content/docs/features/assets/top_controls.png 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..cc7b45d5fe6 --- /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=rocm7.1 + ``` + + + 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..1987a90abce --- /dev/null +++ b/docs/src/generated/settings.json @@ -0,0 +1,843 @@ +{ + "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": "Devices to use for parallel generation. `auto` (the default) uses every available GPU, running one generation session per GPU concurrently and distributing jobs fairly across users. Provide an explicit list (e.g. `[cuda:0, cuda:1]`) to use specific devices, or a single-device list (e.g. `[cuda:0]`) to run serially. On systems without a GPU, `auto` resolves to the single `cpu`/`mps` device.
Valid values: `auto`, or a list whose entries are each `cpu`, `cuda`, `mps`, or `cuda:N` (where N is a device number)", + "env_var": "INVOKEAI_GENERATION_DEVICES", + "literal_values": [], + "name": "generation_devices", + "required": false, + "type": "typing.Union[typing.Literal['auto'], list[str]]", + "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/docs/workflows/ESRGAN_img2img_upscale_w_Canny_ControlNet.json b/docs/workflows/ESRGAN_img2img_upscale_w_Canny_ControlNet.json deleted file mode 100644 index abde0cab76b..00000000000 --- a/docs/workflows/ESRGAN_img2img_upscale_w_Canny_ControlNet.json +++ /dev/null @@ -1,1364 +0,0 @@ -{ - "id": "6bfa0b3a-7090-4cd9-ad2d-a4b8662b6e71", - "name": "ESRGAN Upscaling with Canny ControlNet", - "author": "InvokeAI", - "description": "Sample workflow for using Upscaling with ControlNet with SD1.5", - "version": "1.0.1", - "contact": "invoke@invoke.ai", - "tags": "upscale, controlnet, default", - "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" - } - ], - "meta": { - "category": "default", - "version": "2.0.0" - }, - "nodes": [ - { - "id": "e8bf67fe-67de-4227-87eb-79e86afdfc74", - "type": "invocation", - "data": { - "id": "e8bf67fe-67de-4227-87eb-79e86afdfc74", - "type": "compel", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "5f762fae-d791-42d9-8ab5-2b830c33ff20", - "name": "prompt", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "clip": { - "id": "8ac95f40-317d-4513-bbba-b99effd3b438", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "46c65b2b-c0b5-40c2-b183-74e9451c6d56", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 256, - "position": { - "x": 1250, - "y": 1500 - } - }, - { - "id": "d8ace142-c05f-4f1d-8982-88dc7473958d", - "type": "invocation", - "data": { - "id": "d8ace142-c05f-4f1d-8982-88dc7473958d", - "type": "main_model_loader", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "model": { - "id": "b35ae88a-f2d2-43f6-958c-8c624391250f", - "name": "model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MainModelField" - }, - "value": { - "model_name": "stable-diffusion-v1-5", - "base_model": "sd-1", - "model_type": "main" - } - } - }, - "outputs": { - "unet": { - "id": "02f243cb-c6e2-42c5-8be9-ef0519d54383", - "name": "unet", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "clip": { - "id": "7762ed13-5b28-40f4-85f1-710942ceb92a", - "name": "clip", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "vae": { - "id": "69566153-1918-417d-a3bb-32e9e857ef6b", - "name": "vae", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - } - } - }, - "width": 320, - "height": 227, - "position": { - "x": 700, - "y": 1375 - } - }, - { - "id": "771bdf6a-0813-4099-a5d8-921a138754d4", - "type": "invocation", - "data": { - "id": "771bdf6a-0813-4099-a5d8-921a138754d4", - "type": "image", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "image": { - "id": "0f6d68a2-38bd-4f65-a112-0a256c7a2678", - "name": "image", - "fieldKind": "input", - "label": "Image To Upscale", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - } - }, - "outputs": { - "image": { - "id": "76f6f9b6-755b-4373-93fa-6a779998d2c8", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "6858e46b-707c-444f-beda-9b5f4aecfdf8", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "421bdc6e-ecd1-4935-9665-d38ab8314f79", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 225, - "position": { - "x": 375, - "y": 1900 - } - }, - { - "id": "f7564dd2-9539-47f2-ac13-190804461f4e", - "type": "invocation", - "data": { - "id": "f7564dd2-9539-47f2-ac13-190804461f4e", - "type": "esrgan", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.3.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "8fa0c7eb-5bd3-4575-98e7-72285c532504", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "3c949799-a504-41c9-b342-cff4b8146c48", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "model_name": { - "id": "77cb4750-53d6-4c2c-bb5c-145981acbf17", - "name": "model_name", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "RealESRGAN_x4plus.pth" - }, - "tile_size": { - "id": "7787b3ad-46ee-4248-995f-bc740e1f988b", - "name": "tile_size", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 400 - } - }, - "outputs": { - "image": { - "id": "37e6308e-e926-4e07-b0db-4e8601f495d0", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "c194d84a-fac7-4856-b646-d08477a5ad2b", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "b2a6206c-a9c8-4271-a055-0b93a7f7d505", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 340, - "position": { - "x": 775, - "y": 1900 - } - }, - { - "id": "1d887701-df21-4966-ae6e-a7d82307d7bd", - "type": "invocation", - "data": { - "id": "1d887701-df21-4966-ae6e-a7d82307d7bd", - "type": "canny_image_processor", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "52c877c8-25d9-4949-8518-f536fcdd152d", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "e0af11fe-4f95-4193-a599-cf40b6a963f5", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "low_threshold": { - "id": "ab775f7b-f556-4298-a9d6-2274f3a6c77c", - "name": "low_threshold", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 100 - }, - "high_threshold": { - "id": "9e58b615-06e4-417f-b0d8-63f1574cd174", - "name": "high_threshold", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 200 - } - }, - "outputs": { - "image": { - "id": "61feb8bf-95c9-4634-87e2-887fc43edbdf", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "9e203e41-73f7-4cfa-bdca-5040e5e60c55", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "ec7d99dc-0d82-4495-a759-6423808bff1c", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 340, - "position": { - "x": 1200, - "y": 1900 - } - }, - { - "id": "ca1d020c-89a8-4958-880a-016d28775cfa", - "type": "invocation", - "data": { - "id": "ca1d020c-89a8-4958-880a-016d28775cfa", - "type": "controlnet", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.1.0", - "nodePack": "invokeai", - "inputs": { - "image": { - "id": "2973c126-e301-4595-a7dc-d6e1729ccdbf", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "control_model": { - "id": "4bb4d987-8491-4839-b41b-6e2f546fe2d0", - "name": "control_model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ControlNetModelField" - }, - "value": { - "model_name": "canny", - "base_model": "sd-1" - } - }, - "control_weight": { - "id": "a3cf387a-b58f-4058-858f-6a918efac609", - "name": "control_weight", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 1 - }, - "begin_step_percent": { - "id": "e0614f69-8a58-408b-9238-d3a44a4db4e0", - "name": "begin_step_percent", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "end_step_percent": { - "id": "ac683539-b6ed-4166-9294-2040e3ede206", - "name": "end_step_percent", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1 - }, - "control_mode": { - "id": "f00b21de-cbd7-4901-8efc-e7134a2dc4c8", - "name": "control_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "balanced" - }, - "resize_mode": { - "id": "cafb60ee-3959-4d57-a06c-13b83be6ea4f", - "name": "resize_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "just_resize" - } - }, - "outputs": { - "control": { - "id": "dfb88dd1-12bf-4034-9268-e726f894c131", - "name": "control", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ControlField" - } - } - } - }, - "width": 320, - "height": 511, - "position": { - "x": 1650, - "y": 1900 - } - }, - { - "id": "f50624ce-82bf-41d0-bdf7-8aab11a80d48", - "type": "invocation", - "data": { - "id": "f50624ce-82bf-41d0-bdf7-8aab11a80d48", - "type": "noise", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.1", - "nodePack": "invokeai", - "inputs": { - "seed": { - "id": "f76b0e01-b601-423f-9b5f-ab7a1f10fe82", - "name": "seed", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "width": { - "id": "eec326d6-710c-45de-a25c-95704c80d7e2", - "name": "width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "height": { - "id": "2794a27d-5337-43ca-95d9-41b673642c94", - "name": "height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "use_cpu": { - "id": "ae7654e3-979e-44a1-8968-7e3199e91e66", - "name": "use_cpu", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": true - } - }, - "outputs": { - "noise": { - "id": "8b6dc166-4ead-4124-8ac9-529814b0cbb9", - "name": "noise", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "e3fe3940-a277-4838-a448-5f81f2a7d99d", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "48ecd6ef-c216-40d5-9d1b-d37bd00c82e7", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 1650, - "y": 1775 - } - }, - { - "id": "c3737554-8d87-48ff-a6f8-e71d2867f434", - "type": "invocation", - "data": { - "id": "c3737554-8d87-48ff-a6f8-e71d2867f434", - "type": "denoise_latents", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.5.0", - "nodePack": "invokeai", - "inputs": { - "positive_conditioning": { - "id": "e127084b-72f5-4fe4-892b-84f34f88bce9", - "name": "positive_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "negative_conditioning": { - "id": "72cde4ee-55de-4d3e-9057-74e741c04e20", - "name": "negative_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "noise": { - "id": "747f7023-1c19-465b-bec8-1d9695dd3505", - "name": "noise", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "steps": { - "id": "80860292-633c-46f2-83d0-60d0029b65d2", - "name": "steps", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 10 - }, - "cfg_scale": { - "id": "ebc71e6f-9148-4f12-b455-5e1f179d1c3a", - "name": "cfg_scale", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 7.5 - }, - "denoising_start": { - "id": "ced44b8f-3bad-4c34-8113-13bc0faed28a", - "name": "denoising_start", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "denoising_end": { - "id": "79bf4b77-3502-4f72-ba8b-269c4c3c5c72", - "name": "denoising_end", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1 - }, - "scheduler": { - "id": "ed56e2b8-f477-41a2-b9f5-f15f4933ae65", - "name": "scheduler", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SchedulerField" - }, - "value": "euler" - }, - "unet": { - "id": "146b790c-b08e-437c-a2e1-e393c2c1c41a", - "name": "unet", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "control": { - "id": "75ed3df1-d261-4b8e-a89b-341c4d7161fb", - "name": "control", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "ControlField" - } - }, - "ip_adapter": { - "id": "eab9a61d-9b64-44d3-8d90-4686f5887cb0", - "name": "ip_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "IPAdapterField" - } - }, - "t2i_adapter": { - "id": "2dc8d637-58fd-4069-ad33-85c32d958b7b", - "name": "t2i_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "T2IAdapterField" - } - }, - "cfg_rescale_multiplier": { - "id": "391e5010-e402-4380-bb46-e7edaede3512", - "name": "cfg_rescale_multiplier", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "latents": { - "id": "6767e40a-97c6-4487-b3c9-cad1c150bf9f", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "denoise_mask": { - "id": "6251efda-d97d-4ff1-94b5-8cc6b458c184", - "name": "denoise_mask", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "DenoiseMaskField" - } - } - }, - "outputs": { - "latents": { - "id": "4e7986a4-dff2-4448-b16b-1af477b81f8b", - "name": "latents", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "dad525dd-d2f8-4f07-8c8d-51f2a3c5456e", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "af03a089-4739-40c6-8b48-25d458d63c2f", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 705, - "position": { - "x": 2128.740065979906, - "y": 1232.6219060454753 - } - }, - { - "id": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0", - "type": "invocation", - "data": { - "id": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0", - "type": "l2i", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": false, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "9f7a1a9f-7861-4f09-874b-831af89b7474", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "latents": { - "id": "a5b42432-8ee7-48cd-b61c-b97be6e490a2", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "vae": { - "id": "890de106-e6c3-4c2c-8d67-b368def64894", - "name": "vae", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - }, - "tiled": { - "id": "b8e5a2ca-5fbc-49bd-ad4c-ea0e109d46e3", - "name": "tiled", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - }, - "fp32": { - "id": "fdaf6264-4593-4bd2-ac71-8a0acff261af", - "name": "fp32", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - } - }, - "outputs": { - "image": { - "id": "94c5877d-6c78-4662-a836-8a84fc75d0a0", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "2a854e42-1616-42f5-b9ef-7b73c40afc1d", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "dd649053-1433-4f31-90b3-8bb103efc5b1", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 267, - "position": { - "x": 2559.4751127537957, - "y": 1246.6000376741406 - } - }, - { - "id": "5ca498a4-c8c8-4580-a396-0c984317205d", - "type": "invocation", - "data": { - "id": "5ca498a4-c8c8-4580-a396-0c984317205d", - "type": "i2l", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "image": { - "id": "9e6c4010-0f79-4587-9062-29d9a8f96b3b", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "vae": { - "id": "b9ed2ec4-e8e3-4d69-8a42-27f2d983bcd6", - "name": "vae", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - }, - "tiled": { - "id": "bb48d10b-2440-4c46-b835-646ae5ebc013", - "name": "tiled", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - }, - "fp32": { - "id": "1048612c-c0f4-4abf-a684-0045e7d158f8", - "name": "fp32", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - } - }, - "outputs": { - "latents": { - "id": "55301367-0578-4dee-8060-031ae13c7bf8", - "name": "latents", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "2eb65690-1f20-4070-afbd-1e771b9f8ca9", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "d5bf64c7-c30f-43b8-9bc2-95e7718c1bdc", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 1650, - "y": 1675 - } - }, - { - "id": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b", - "type": "invocation", - "data": { - "id": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b", - "type": "compel", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "5f762fae-d791-42d9-8ab5-2b830c33ff20", - "name": "prompt", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "clip": { - "id": "8ac95f40-317d-4513-bbba-b99effd3b438", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "46c65b2b-c0b5-40c2-b183-74e9451c6d56", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 256, - "position": { - "x": 1250, - "y": 1200 - } - }, - { - "id": "eb8f6f8a-c7b1-4914-806e-045ee2717a35", - "type": "invocation", - "data": { - "id": "eb8f6f8a-c7b1-4914-806e-045ee2717a35", - "type": "rand_int", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": false, - "version": "1.0.0", - "inputs": { - "low": { - "id": "2118026f-1c64-41fa-ab6b-7532410f60ae", - "name": "low", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "high": { - "id": "c12f312a-fdfd-4aca-9aa6-4c99bc70bd63", - "name": "high", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 2147483647 - } - }, - "outputs": { - "value": { - "id": "75552bad-6212-4ae7-96a7-68e666acea4c", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 1650, - "y": 1600 - } - } - ], - "edges": [ - { - "id": "5ca498a4-c8c8-4580-a396-0c984317205d-f50624ce-82bf-41d0-bdf7-8aab11a80d48-collapsed", - "source": "5ca498a4-c8c8-4580-a396-0c984317205d", - "target": "f50624ce-82bf-41d0-bdf7-8aab11a80d48", - "type": "collapsed" - }, - { - "id": "eb8f6f8a-c7b1-4914-806e-045ee2717a35-f50624ce-82bf-41d0-bdf7-8aab11a80d48-collapsed", - "source": "eb8f6f8a-c7b1-4914-806e-045ee2717a35", - "target": "f50624ce-82bf-41d0-bdf7-8aab11a80d48", - "type": "collapsed" - }, - { - "id": "reactflow__edge-771bdf6a-0813-4099-a5d8-921a138754d4image-f7564dd2-9539-47f2-ac13-190804461f4eimage", - "source": "771bdf6a-0813-4099-a5d8-921a138754d4", - "target": "f7564dd2-9539-47f2-ac13-190804461f4e", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-f7564dd2-9539-47f2-ac13-190804461f4eimage-1d887701-df21-4966-ae6e-a7d82307d7bdimage", - "source": "f7564dd2-9539-47f2-ac13-190804461f4e", - "target": "1d887701-df21-4966-ae6e-a7d82307d7bd", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-5ca498a4-c8c8-4580-a396-0c984317205dwidth-f50624ce-82bf-41d0-bdf7-8aab11a80d48width", - "source": "5ca498a4-c8c8-4580-a396-0c984317205d", - "target": "f50624ce-82bf-41d0-bdf7-8aab11a80d48", - "type": "default", - "sourceHandle": "width", - "targetHandle": "width" - }, - { - "id": "reactflow__edge-5ca498a4-c8c8-4580-a396-0c984317205dheight-f50624ce-82bf-41d0-bdf7-8aab11a80d48height", - "source": "5ca498a4-c8c8-4580-a396-0c984317205d", - "target": "f50624ce-82bf-41d0-bdf7-8aab11a80d48", - "type": "default", - "sourceHandle": "height", - "targetHandle": "height" - }, - { - "id": "reactflow__edge-f50624ce-82bf-41d0-bdf7-8aab11a80d48noise-c3737554-8d87-48ff-a6f8-e71d2867f434noise", - "source": "f50624ce-82bf-41d0-bdf7-8aab11a80d48", - "target": "c3737554-8d87-48ff-a6f8-e71d2867f434", - "type": "default", - "sourceHandle": "noise", - "targetHandle": "noise" - }, - { - "id": "reactflow__edge-5ca498a4-c8c8-4580-a396-0c984317205dlatents-c3737554-8d87-48ff-a6f8-e71d2867f434latents", - "source": "5ca498a4-c8c8-4580-a396-0c984317205d", - "target": "c3737554-8d87-48ff-a6f8-e71d2867f434", - "type": "default", - "sourceHandle": "latents", - "targetHandle": "latents" - }, - { - "id": "reactflow__edge-e8bf67fe-67de-4227-87eb-79e86afdfc74conditioning-c3737554-8d87-48ff-a6f8-e71d2867f434negative_conditioning", - "source": "e8bf67fe-67de-4227-87eb-79e86afdfc74", - "target": "c3737554-8d87-48ff-a6f8-e71d2867f434", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "negative_conditioning" - }, - { - "id": "reactflow__edge-63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16bconditioning-c3737554-8d87-48ff-a6f8-e71d2867f434positive_conditioning", - "source": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b", - "target": "c3737554-8d87-48ff-a6f8-e71d2867f434", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "positive_conditioning" - }, - { - "id": "reactflow__edge-d8ace142-c05f-4f1d-8982-88dc7473958dclip-63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16bclip", - "source": "d8ace142-c05f-4f1d-8982-88dc7473958d", - "target": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-d8ace142-c05f-4f1d-8982-88dc7473958dclip-e8bf67fe-67de-4227-87eb-79e86afdfc74clip", - "source": "d8ace142-c05f-4f1d-8982-88dc7473958d", - "target": "e8bf67fe-67de-4227-87eb-79e86afdfc74", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-1d887701-df21-4966-ae6e-a7d82307d7bdimage-ca1d020c-89a8-4958-880a-016d28775cfaimage", - "source": "1d887701-df21-4966-ae6e-a7d82307d7bd", - "target": "ca1d020c-89a8-4958-880a-016d28775cfa", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-ca1d020c-89a8-4958-880a-016d28775cfacontrol-c3737554-8d87-48ff-a6f8-e71d2867f434control", - "source": "ca1d020c-89a8-4958-880a-016d28775cfa", - "target": "c3737554-8d87-48ff-a6f8-e71d2867f434", - "type": "default", - "sourceHandle": "control", - "targetHandle": "control" - }, - { - "id": "reactflow__edge-c3737554-8d87-48ff-a6f8-e71d2867f434latents-3ed9b2ef-f4ec-40a7-94db-92e63b583ec0latents", - "source": "c3737554-8d87-48ff-a6f8-e71d2867f434", - "target": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0", - "type": "default", - "sourceHandle": "latents", - "targetHandle": "latents" - }, - { - "id": "reactflow__edge-d8ace142-c05f-4f1d-8982-88dc7473958dvae-3ed9b2ef-f4ec-40a7-94db-92e63b583ec0vae", - "source": "d8ace142-c05f-4f1d-8982-88dc7473958d", - "target": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0", - "type": "default", - "sourceHandle": "vae", - "targetHandle": "vae" - }, - { - "id": "reactflow__edge-f7564dd2-9539-47f2-ac13-190804461f4eimage-5ca498a4-c8c8-4580-a396-0c984317205dimage", - "source": "f7564dd2-9539-47f2-ac13-190804461f4e", - "target": "5ca498a4-c8c8-4580-a396-0c984317205d", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-d8ace142-c05f-4f1d-8982-88dc7473958dunet-c3737554-8d87-48ff-a6f8-e71d2867f434unet", - "source": "d8ace142-c05f-4f1d-8982-88dc7473958d", - "target": "c3737554-8d87-48ff-a6f8-e71d2867f434", - "type": "default", - "sourceHandle": "unet", - "targetHandle": "unet" - }, - { - "id": "reactflow__edge-d8ace142-c05f-4f1d-8982-88dc7473958dvae-5ca498a4-c8c8-4580-a396-0c984317205dvae", - "source": "d8ace142-c05f-4f1d-8982-88dc7473958d", - "target": "5ca498a4-c8c8-4580-a396-0c984317205d", - "type": "default", - "sourceHandle": "vae", - "targetHandle": "vae" - }, - { - "id": "reactflow__edge-eb8f6f8a-c7b1-4914-806e-045ee2717a35value-f50624ce-82bf-41d0-bdf7-8aab11a80d48seed", - "source": "eb8f6f8a-c7b1-4914-806e-045ee2717a35", - "target": "f50624ce-82bf-41d0-bdf7-8aab11a80d48", - "type": "default", - "sourceHandle": "value", - "targetHandle": "seed" - } - ] -} \ No newline at end of file diff --git a/docs/workflows/FaceMask.json b/docs/workflows/FaceMask.json deleted file mode 100644 index 54c1f800b67..00000000000 --- a/docs/workflows/FaceMask.json +++ /dev/null @@ -1,1081 +0,0 @@ -{ - "name": "FaceMask", - "author": "YMGenesis", - "description": "Place an image with recognizable face(s) in Image Primitive, and write what sort of new face you want in the top prompt text box. See Notes for more info.", - "version": "1.0", - "contact": "YMGenesis on InvokeAI Discord", - "tags": "facemask, facetools", - "notes": "If you want to choose one face out of many, run the original image through FaceIdentifier and view its output to get the FaceID you want to change. Then, enter it into the \"Face Ids\" field on FaceMask. If changing many faces, but not all, enter the IDs you wish to change in a comma separated list (ex: 1,3,5 or 1, 3, 5). \n\nTo resemble the original face more when doing small touchups, change Denoise Start on Denoise Latents to a number closer to 1 (ex: 0.5-0.9). To create something fairly new, Denoise Start should be around 0.2-0.5. When using a regular model, anything around 0.2 and below might not create a face, but a new \"image\" inside the face area instead. Use an inpaint model in that case.\n\nAdjust X&Y Offsets on FaceMask to adjust the shape of the mask along those axes. Note: X&Y Offset changes will apply to all face masks in the mask image.", - "exposedFields": [], - "meta": { - "version": "1.0.0" - }, - "nodes": [ - { - "id": "c9897be0-7f59-4388-816d-86cb72cc4036", - "type": "invocation", - "data": { - "id": "c9897be0-7f59-4388-816d-86cb72cc4036", - "type": "main_model_loader", - "inputs": { - "model": { - "id": "e3c5384f-3b73-45c4-bdd0-9f394819f33d", - "name": "model", - "type": "MainModelField", - "fieldKind": "input", - "label": "", - "value": { - "model_name": "stable-diffusion-v1-5", - "base_model": "sd-1", - "model_type": "main" - } - } - }, - "outputs": { - "unet": { - "id": "e578c176-1de1-4e36-bfc0-771f60d615da", - "name": "unet", - "type": "UNetField", - "fieldKind": "output" - }, - "clip": { - "id": "54a97800-72ac-4951-94e5-2711ac139a07", - "name": "clip", - "type": "ClipField", - "fieldKind": "output" - }, - "vae": { - "id": "814a8df5-7882-48a2-8ac1-8ff82d7c1b07", - "name": "vae", - "type": "VaeField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 226, - "position": { - "x": 4103.832039728059, - "y": 1987.4435345183065 - } - }, - { - "id": "fb7e72d9-51cb-432a-b511-c6c608d07413", - "type": "invocation", - "data": { - "id": "fb7e72d9-51cb-432a-b511-c6c608d07413", - "type": "compel", - "inputs": { - "prompt": { - "id": "a4f25874-bc29-4900-abef-47701c805132", - "name": "prompt", - "type": "string", - "fieldKind": "input", - "label": "", - "value": "" - }, - "clip": { - "id": "6464b46f-fc74-4917-88b0-3fd458fc11f0", - "name": "clip", - "type": "ClipField", - "fieldKind": "input", - "label": "" - } - }, - "outputs": { - "conditioning": { - "id": "65bea1c6-e3b6-4120-a546-d93ff5dd7765", - "name": "conditioning", - "type": "ConditioningField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 261, - "position": { - "x": 4988.591898842789, - "y": 1700.8901379603535 - } - }, - { - "id": "7c4e5071-5b76-4d42-b340-68b52c5ded7a", - "type": "invocation", - "data": { - "id": "7c4e5071-5b76-4d42-b340-68b52c5ded7a", - "type": "compel", - "inputs": { - "prompt": { - "id": "fcb313f2-74ab-443d-a8a8-eb5f2a9b5f96", - "name": "prompt", - "type": "string", - "fieldKind": "input", - "label": "", - "value": "" - }, - "clip": { - "id": "a278e13b-823a-4dda-96ca-44cf56f994dd", - "name": "clip", - "type": "ClipField", - "fieldKind": "input", - "label": "" - } - }, - "outputs": { - "conditioning": { - "id": "3c1f0f11-b8b0-444a-97ad-1cf80acf4bf7", - "name": "conditioning", - "type": "ConditioningField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 261, - "position": { - "x": 4986.228031951785, - "y": 1987.19695578231 - } - }, - { - "id": "098898c8-7a20-4d78-9363-296d42e3d8da", - "type": "invocation", - "data": { - "id": "098898c8-7a20-4d78-9363-296d42e3d8da", - "type": "noise", - "inputs": { - "seed": { - "id": "9358ec7b-e575-40d9-af22-2d4786ba1aa7", - "name": "seed", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "width": { - "id": "8422b4ba-9d8e-41ac-bf08-1ea826859b46", - "name": "width", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 512 - }, - "height": { - "id": "2467c79c-302b-4800-9efd-5bca58103322", - "name": "height", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 512 - }, - "use_cpu": { - "id": "911c827e-a6af-4168-9f6d-cecc732938ad", - "name": "use_cpu", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": true - } - }, - "outputs": { - "noise": { - "id": "9db55798-5c98-40f6-9015-56bfa8618f12", - "name": "noise", - "type": "LatentsField", - "fieldKind": "output" - }, - "width": { - "id": "bc091210-946e-410e-8c70-e20982dd1ee7", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "4df04a89-43a2-441a-8f15-089350b36ea7", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": false, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 32, - "position": { - "x": 5398.389401611981, - "y": 2019.4053462371755 - } - }, - { - "id": "27dd9fc3-8c6e-4602-8754-e9ca2f478d68", - "type": "invocation", - "data": { - "id": "27dd9fc3-8c6e-4602-8754-e9ca2f478d68", - "type": "rand_int", - "inputs": { - "low": { - "id": "86e06b6e-7c84-40b0-9df2-12f966c3db4d", - "name": "low", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "high": { - "id": "5ee9adcc-d48c-4b9a-951a-6892d234acbc", - "name": "high", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 2147483647 - } - }, - "outputs": { - "value": { - "id": "b6c69841-ba37-43f0-8904-0026b1caf8ff", - "name": "value", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": false, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": false, - "version": "1.0.0" - }, - "width": 320, - "height": 32, - "position": { - "x": 5386.304039775159, - "y": 1979.791644235275 - } - }, - { - "id": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "type": "invocation", - "data": { - "id": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "type": "create_denoise_mask", - "inputs": { - "vae": { - "id": "c7991df7-9f68-4b42-96ce-d795a8e2f714", - "name": "vae", - "type": "VaeField", - "fieldKind": "input", - "label": "" - }, - "image": { - "id": "3345a725-f5d5-4f47-9942-b1dfffbe5906", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "mask": { - "id": "41fb1d83-1ca3-4299-b039-fb3b7c90f04e", - "name": "mask", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "tiled": { - "id": "32d8cf55-9910-4e09-8486-1c556a580a2d", - "name": "tiled", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - }, - "fp32": { - "id": "e26410cc-54f3-44f2-a81c-a22a8dad6f24", - "name": "fp32", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - } - }, - "outputs": { - "denoise_mask": { - "id": "9e1b78c0-5ac6-4632-91e9-831ce328237f", - "name": "denoise_mask", - "type": "DenoiseMaskField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": false, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 32, - "position": { - "x": 5009.179957658444, - "y": 2346.7322639404283 - } - }, - { - "id": "3fac3aa6-910a-4a90-a8b6-5b7e1611efba", - "type": "invocation", - "data": { - "id": "3fac3aa6-910a-4a90-a8b6-5b7e1611efba", - "type": "image", - "inputs": { - "image": { - "id": "6efba7ef-b986-4488-84ca-80f23f939ba8", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - } - }, - "outputs": { - "image": { - "id": "2512df7a-9981-4186-93ed-aa5405dc3057", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "68d2a9c6-43ff-49aa-989c-db6f5452134f", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "2cb4e505-7e9e-40bd-b402-1e7470167d30", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 225, - "position": { - "x": 4107.933245141945, - "y": 2255.443448115275 - } - }, - { - "id": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "type": "invocation", - "data": { - "id": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "type": "i2l", - "inputs": { - "image": { - "id": "4aef9c20-51be-47e4-bdc4-d449694d75e1", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "vae": { - "id": "9507adad-df7c-447b-8aad-4d8d6f638420", - "name": "vae", - "type": "VaeField", - "fieldKind": "input", - "label": "" - }, - "tiled": { - "id": "2a100779-503a-4fb4-a2b5-507eb0f8328f", - "name": "tiled", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - }, - "fp32": { - "id": "3dbf8dbc-4ad4-4451-89f4-99a59ec87453", - "name": "fp32", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - } - }, - "outputs": { - "latents": { - "id": "215003b1-64dd-4c97-b5a4-7593d41ffd0e", - "name": "latents", - "type": "LatentsField", - "fieldKind": "output" - }, - "width": { - "id": "dc9b2940-79e0-49b2-906c-05417a691175", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "605de93b-c1c7-409f-b059-24918a292bfc", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": false, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 32, - "position": { - "x": 5006.155303630073, - "y": 2277.2727128782517 - } - }, - { - "id": "d14b4d95-bf74-4ec5-827b-4c9e797c7ae9", - "type": "invocation", - "data": { - "id": "d14b4d95-bf74-4ec5-827b-4c9e797c7ae9", - "type": "denoise_latents", - "inputs": { - "noise": { - "id": "175b4d0a-3017-46e2-933f-c02f1cfb29b2", - "name": "noise", - "type": "LatentsField", - "fieldKind": "input", - "label": "" - }, - "steps": { - "id": "dd174b3e-3f6c-46cb-a703-3c6f3b3c72f1", - "name": "steps", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 10 - }, - "cfg_scale": { - "id": "0ea30aa7-8747-4c93-87e8-3c84e0dfd187", - "name": "cfg_scale", - "type": "FloatPolymorphic", - "fieldKind": "input", - "label": "", - "value": 7.5 - }, - "denoising_start": { - "id": "a6392edb-8895-41ed-918b-0ba8d2ac72ac", - "name": "denoising_start", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "denoising_end": { - "id": "1d1807cc-a24d-426e-9de5-a7e61d45c006", - "name": "denoising_end", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 1 - }, - "scheduler": { - "id": "947a5212-8923-4d5d-934c-dbc5879b9d07", - "name": "scheduler", - "type": "Scheduler", - "fieldKind": "input", - "label": "", - "value": "euler" - }, - "control": { - "id": "585378b9-2686-4573-b762-3dc2d6179193", - "name": "control", - "type": "ControlPolymorphic", - "fieldKind": "input", - "label": "" - }, - "ip_adapter": { - "id": "191f4687-fdcc-45da-859f-71fd5091a8bd", - "name": "ip_adapter", - "type": "IPAdapterPolymorphic", - "fieldKind": "input", - "label": "" - }, - "t2i_adapter": { - "id": "39fc54ae-2141-4cab-9c01-c6c415f964cd", - "name": "t2i_adapter", - "type": "T2IAdapterPolymorphic", - "fieldKind": "input", - "label": "" - }, - "latents": { - "id": "7f1a388e-8355-496c-a45d-fce5b8685a63", - "name": "latents", - "type": "LatentsField", - "fieldKind": "input", - "label": "" - }, - "denoise_mask": { - "id": "de922941-b2d8-4c57-92a7-201f9ddaf262", - "name": "denoise_mask", - "type": "DenoiseMaskField", - "fieldKind": "input", - "label": "" - }, - "positive_conditioning": { - "id": "2b42b4e8-4795-4fcc-bef1-c08cb8e25e0a", - "name": "positive_conditioning", - "type": "ConditioningField", - "fieldKind": "input", - "label": "" - }, - "negative_conditioning": { - "id": "ff29b7d7-1bff-4aa9-b5c0-f8786a55023a", - "name": "negative_conditioning", - "type": "ConditioningField", - "fieldKind": "input", - "label": "" - }, - "unet": { - "id": "0155f1cb-152b-4097-9395-afcc745c697b", - "name": "unet", - "type": "UNetField", - "fieldKind": "input", - "label": "" - } - }, - "outputs": { - "latents": { - "id": "03cfb327-02a1-4fbe-b7ce-b07fd501d2b8", - "name": "latents", - "type": "LatentsField", - "fieldKind": "output" - }, - "width": { - "id": "3ffb87d7-b5a6-4b35-bdf1-2bb9b718d815", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "2c1f0588-943a-4fd1-b75b-48d04c944296", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.3.0" - }, - "width": 320, - "height": 646, - "position": { - "x": 5512.059705982663, - "y": 2103.8364934988267 - } - }, - { - "id": "eb725b0b-1fa6-4f79-aedb-52c19afcfad9", - "type": "invocation", - "data": { - "id": "eb725b0b-1fa6-4f79-aedb-52c19afcfad9", - "type": "face_mask_detection", - "inputs": { - "metadata": { - "id": "a56bf310-b5c3-4440-8ba6-79f5e434a9e6", - "name": "metadata", - "type": "MetadataField", - "fieldKind": "input", - "label": "" - }, - "image": { - "id": "11ece2fd-57ee-4504-b1f5-8d0c9332d785", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "face_ids": { - "id": "c5eaafc3-70f4-4bcd-9df7-0d4cd26e1734", - "name": "face_ids", - "type": "string", - "fieldKind": "input", - "label": "", - "value": "" - }, - "minimum_confidence": { - "id": "ab85fdbd-d61e-4584-8595-c1cea1ffb288", - "name": "minimum_confidence", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 0.5 - }, - "x_offset": { - "id": "e381e04d-b54d-4457-9b6e-b3b554a8e343", - "name": "x_offset", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "y_offset": { - "id": "98e771c8-df97-4a57-a5d4-9601dac68338", - "name": "y_offset", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "chunk": { - "id": "ae4af045-99f9-4d84-81d2-438dc3d13b8d", - "name": "chunk", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - }, - "invert_mask": { - "id": "a6a0fe16-da45-46aa-9f85-9469fde40d71", - "name": "invert_mask", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - } - }, - "outputs": { - "image": { - "id": "7de8643a-c6b1-4260-b843-728c8d0fc6d4", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "fb1450b6-de42-465a-98d7-1dc93ceb20d7", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "cea5abb6-1584-440a-9ee2-1e4c926235e7", - "name": "height", - "type": "integer", - "fieldKind": "output" - }, - "mask": { - "id": "272e4224-1736-42de-895d-096309259ac7", - "name": "mask", - "type": "ImageField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.2" - }, - "width": 320, - "height": 585, - "position": { - "x": 4559.385043470649, - "y": 2082.7157021692556 - } - }, - { - "id": "e4681270-ea7e-4063-9116-880408854eee", - "type": "invocation", - "data": { - "id": "e4681270-ea7e-4063-9116-880408854eee", - "type": "l2i", - "inputs": { - "metadata": { - "id": "1632b6ac-605d-42a7-853c-65539a6f664e", - "name": "metadata", - "type": "MetadataField", - "fieldKind": "input", - "label": "" - }, - "latents": { - "id": "9986c874-6d4b-47fc-895a-88933ef2b473", - "name": "latents", - "type": "LatentsField", - "fieldKind": "input", - "label": "" - }, - "vae": { - "id": "d5842416-e575-4e35-a5d0-fd1ce4401b52", - "name": "vae", - "type": "VaeField", - "fieldKind": "input", - "label": "" - }, - "tiled": { - "id": "6a84e45e-5f54-4952-9285-9dedc6d056d5", - "name": "tiled", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - }, - "fp32": { - "id": "2fa37674-8685-4ec4-87d9-d4683131d79c", - "name": "fp32", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - } - }, - "outputs": { - "image": { - "id": "b1856446-1b03-4825-aae0-0859e27c3c8c", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "c95d92f6-2416-4fc8-a542-998c7c3fac73", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "7033a118-3b20-4c25-8450-e15d7fc8657c", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 267, - "position": { - "x": 5941.909912847396, - "y": 2111.4771842290065 - } - }, - { - "id": "7bc3c331-4658-46fd-8736-fe3043fcd9d1", - "type": "invocation", - "data": { - "id": "7bc3c331-4658-46fd-8736-fe3043fcd9d1", - "type": "color_correct", - "inputs": { - "metadata": { - "id": "203d08d1-586f-47c9-95a1-2afa1db23751", - "name": "metadata", - "type": "MetadataField", - "fieldKind": "input", - "label": "" - }, - "image": { - "id": "50bc42c6-c3b8-44c4-89fe-ef2edd9b67f4", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "reference": { - "id": "2eef99df-b1f8-441c-a316-466a46812df0", - "name": "reference", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "mask": { - "id": "6cfde62b-e6fc-4f13-91dc-e679b26ec04b", - "name": "mask", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "mask_blur_radius": { - "id": "51125aec-bbcb-4ae0-b618-a9bc632a5a86", - "name": "mask_blur_radius", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 8 - } - }, - "outputs": { - "image": { - "id": "4c82dcb2-1c55-4b6d-a42c-061478881393", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "b7d23fea-e361-468d-96e7-9b243592e904", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "d056d182-d261-4037-bb5e-caec9bda9ca6", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 396, - "position": { - "x": 6399.947577155154, - "y": 2127.7477011465667 - } - } - ], - "edges": [ - { - "source": "27dd9fc3-8c6e-4602-8754-e9ca2f478d68", - "target": "098898c8-7a20-4d78-9363-296d42e3d8da", - "id": "27dd9fc3-8c6e-4602-8754-e9ca2f478d68-098898c8-7a20-4d78-9363-296d42e3d8da-collapsed", - "type": "collapsed" - }, - { - "source": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "target": "098898c8-7a20-4d78-9363-296d42e3d8da", - "id": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d-098898c8-7a20-4d78-9363-296d42e3d8da-collapsed", - "type": "collapsed" - }, - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "clip", - "target": "fb7e72d9-51cb-432a-b511-c6c608d07413", - "targetHandle": "clip", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036clip-fb7e72d9-51cb-432a-b511-c6c608d07413clip", - "type": "default" - }, - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "clip", - "target": "7c4e5071-5b76-4d42-b340-68b52c5ded7a", - "targetHandle": "clip", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036clip-7c4e5071-5b76-4d42-b340-68b52c5ded7aclip", - "type": "default" - }, - { - "source": "27dd9fc3-8c6e-4602-8754-e9ca2f478d68", - "sourceHandle": "value", - "target": "098898c8-7a20-4d78-9363-296d42e3d8da", - "targetHandle": "seed", - "id": "reactflow__edge-27dd9fc3-8c6e-4602-8754-e9ca2f478d68value-098898c8-7a20-4d78-9363-296d42e3d8daseed", - "type": "default" - }, - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "vae", - "target": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "targetHandle": "vae", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036vae-bcbdc4ea-1fad-40d4-8632-70f84116f4b6vae", - "type": "default" - }, - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "vae", - "target": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "targetHandle": "vae", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036vae-a6d08bcb-0b52-4dd8-9247-8b6480238c6dvae", - "type": "default" - }, - { - "source": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "sourceHandle": "width", - "target": "098898c8-7a20-4d78-9363-296d42e3d8da", - "targetHandle": "width", - "id": "reactflow__edge-a6d08bcb-0b52-4dd8-9247-8b6480238c6dwidth-098898c8-7a20-4d78-9363-296d42e3d8dawidth", - "type": "default" - }, - { - "source": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "sourceHandle": "height", - "target": "098898c8-7a20-4d78-9363-296d42e3d8da", - "targetHandle": "height", - "id": "reactflow__edge-a6d08bcb-0b52-4dd8-9247-8b6480238c6dheight-098898c8-7a20-4d78-9363-296d42e3d8daheight", - "type": "default" - }, - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "unet", - "target": "d14b4d95-bf74-4ec5-827b-4c9e797c7ae9", - "targetHandle": "unet", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036unet-d14b4d95-bf74-4ec5-827b-4c9e797c7ae9unet", - "type": "default" - }, - { - "source": "7c4e5071-5b76-4d42-b340-68b52c5ded7a", - "sourceHandle": "conditioning", - "target": "d14b4d95-bf74-4ec5-827b-4c9e797c7ae9", - "targetHandle": "negative_conditioning", - "id": "reactflow__edge-7c4e5071-5b76-4d42-b340-68b52c5ded7aconditioning-d14b4d95-bf74-4ec5-827b-4c9e797c7ae9negative_conditioning", - "type": "default" - }, - { - "source": "fb7e72d9-51cb-432a-b511-c6c608d07413", - "sourceHandle": "conditioning", - "target": "d14b4d95-bf74-4ec5-827b-4c9e797c7ae9", - "targetHandle": "positive_conditioning", - "id": "reactflow__edge-fb7e72d9-51cb-432a-b511-c6c608d07413conditioning-d14b4d95-bf74-4ec5-827b-4c9e797c7ae9positive_conditioning", - "type": "default" - }, - { - "source": "098898c8-7a20-4d78-9363-296d42e3d8da", - "sourceHandle": "noise", - "target": "d14b4d95-bf74-4ec5-827b-4c9e797c7ae9", - "targetHandle": "noise", - "id": "reactflow__edge-098898c8-7a20-4d78-9363-296d42e3d8danoise-d14b4d95-bf74-4ec5-827b-4c9e797c7ae9noise", - "type": "default" - }, - { - "source": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "sourceHandle": "denoise_mask", - "target": "d14b4d95-bf74-4ec5-827b-4c9e797c7ae9", - "targetHandle": "denoise_mask", - "id": "reactflow__edge-bcbdc4ea-1fad-40d4-8632-70f84116f4b6denoise_mask-d14b4d95-bf74-4ec5-827b-4c9e797c7ae9denoise_mask", - "type": "default" - }, - { - "source": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "sourceHandle": "latents", - "target": "d14b4d95-bf74-4ec5-827b-4c9e797c7ae9", - "targetHandle": "latents", - "id": "reactflow__edge-a6d08bcb-0b52-4dd8-9247-8b6480238c6dlatents-d14b4d95-bf74-4ec5-827b-4c9e797c7ae9latents", - "type": "default" - }, - { - "source": "3fac3aa6-910a-4a90-a8b6-5b7e1611efba", - "sourceHandle": "image", - "target": "eb725b0b-1fa6-4f79-aedb-52c19afcfad9", - "targetHandle": "image", - "id": "reactflow__edge-3fac3aa6-910a-4a90-a8b6-5b7e1611efbaimage-eb725b0b-1fa6-4f79-aedb-52c19afcfad9image", - "type": "default" - }, - { - "source": "eb725b0b-1fa6-4f79-aedb-52c19afcfad9", - "sourceHandle": "image", - "target": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "targetHandle": "image", - "id": "reactflow__edge-eb725b0b-1fa6-4f79-aedb-52c19afcfad9image-bcbdc4ea-1fad-40d4-8632-70f84116f4b6image", - "type": "default" - }, - { - "source": "eb725b0b-1fa6-4f79-aedb-52c19afcfad9", - "sourceHandle": "mask", - "target": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "targetHandle": "mask", - "id": "reactflow__edge-eb725b0b-1fa6-4f79-aedb-52c19afcfad9mask-bcbdc4ea-1fad-40d4-8632-70f84116f4b6mask", - "type": "default" - }, - { - "source": "eb725b0b-1fa6-4f79-aedb-52c19afcfad9", - "sourceHandle": "image", - "target": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "targetHandle": "image", - "id": "reactflow__edge-eb725b0b-1fa6-4f79-aedb-52c19afcfad9image-a6d08bcb-0b52-4dd8-9247-8b6480238c6dimage", - "type": "default" - }, - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "vae", - "target": "e4681270-ea7e-4063-9116-880408854eee", - "targetHandle": "vae", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036vae-e4681270-ea7e-4063-9116-880408854eeevae", - "type": "default" - }, - { - "source": "d14b4d95-bf74-4ec5-827b-4c9e797c7ae9", - "sourceHandle": "latents", - "target": "e4681270-ea7e-4063-9116-880408854eee", - "targetHandle": "latents", - "id": "reactflow__edge-d14b4d95-bf74-4ec5-827b-4c9e797c7ae9latents-e4681270-ea7e-4063-9116-880408854eeelatents", - "type": "default" - }, - { - "source": "3fac3aa6-910a-4a90-a8b6-5b7e1611efba", - "sourceHandle": "image", - "target": "7bc3c331-4658-46fd-8736-fe3043fcd9d1", - "targetHandle": "reference", - "id": "reactflow__edge-3fac3aa6-910a-4a90-a8b6-5b7e1611efbaimage-7bc3c331-4658-46fd-8736-fe3043fcd9d1reference", - "type": "default" - }, - { - "source": "e4681270-ea7e-4063-9116-880408854eee", - "sourceHandle": "image", - "target": "7bc3c331-4658-46fd-8736-fe3043fcd9d1", - "targetHandle": "image", - "id": "reactflow__edge-e4681270-ea7e-4063-9116-880408854eeeimage-7bc3c331-4658-46fd-8736-fe3043fcd9d1image", - "type": "default" - }, - { - "source": "eb725b0b-1fa6-4f79-aedb-52c19afcfad9", - "sourceHandle": "mask", - "target": "7bc3c331-4658-46fd-8736-fe3043fcd9d1", - "targetHandle": "mask", - "id": "reactflow__edge-eb725b0b-1fa6-4f79-aedb-52c19afcfad9mask-7bc3c331-4658-46fd-8736-fe3043fcd9d1mask", - "type": "default" - } - ] -} \ No newline at end of file diff --git a/docs/workflows/FaceOff_FaceScale2x.json b/docs/workflows/FaceOff_FaceScale2x.json deleted file mode 100644 index d1707e5e732..00000000000 --- a/docs/workflows/FaceOff_FaceScale2x.json +++ /dev/null @@ -1,1451 +0,0 @@ -{ - "name": "FaceOff_FaceScale2x", - "author": "YMGenesis", - "description": "21 September 2023\n\nPlace an image with recognizable face(s) in Image Primitive, and write what sort of new face you want in the top prompt text box. The face (and mask) will be scaled by 2 factor to allow for more details when generating. It'll then be downscaled by half before being pasted back onto the original image. See Notes for more info.", - "version": "1.0", - "contact": "YMGenesis on InvokeAI Discord", - "tags": "faceoff, facetools", - "notes": "If you want to choose one face out of many, run the original image through FaceIdentifier and view its output to get the FaceID you want to change. Then, enter it into the \"Face Id\" field on FaceOff.\n\nTo resemble the original face more when doing small touchups, change Denoise Start on Denoise Latents to a number closer to 1 (ex: 0.5-0.9). To create something fairly new, Denoise Start should be around 0.2-0.5. When using a regular model, anything around 0.2 and below might not create a face, but a new \"image\" inside the face area instead. Use an inpaint model in that case.\n\nAdjust X&Y Offsets on FaceOff to adjust the shape of the mask along those axes. Adjust Padding to zoom the bounding box out (positive integers) and in (negative integers). Zooming out will give more context to the diffusion process, resulting in a face more likely to exist in that image.", - "exposedFields": [], - "meta": { - "version": "1.0.0" - }, - "nodes": [ - { - "id": "c9897be0-7f59-4388-816d-86cb72cc4036", - "type": "invocation", - "data": { - "id": "c9897be0-7f59-4388-816d-86cb72cc4036", - "type": "main_model_loader", - "inputs": { - "model": { - "id": "e3c5384f-3b73-45c4-bdd0-9f394819f33d", - "name": "model", - "type": "MainModelField", - "fieldKind": "input", - "label": "", - "value": { - "model_name": "stable-diffusion-v1-5", - "base_model": "sd-1", - "model_type": "main" - } - } - }, - "outputs": { - "unet": { - "id": "e578c176-1de1-4e36-bfc0-771f60d615da", - "name": "unet", - "type": "UNetField", - "fieldKind": "output" - }, - "clip": { - "id": "54a97800-72ac-4951-94e5-2711ac139a07", - "name": "clip", - "type": "ClipField", - "fieldKind": "output" - }, - "vae": { - "id": "814a8df5-7882-48a2-8ac1-8ff82d7c1b07", - "name": "vae", - "type": "VaeField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 226, - "position": { - "x": 4625, - "y": 1275 - } - }, - { - "id": "fb7e72d9-51cb-432a-b511-c6c608d07413", - "type": "invocation", - "data": { - "id": "fb7e72d9-51cb-432a-b511-c6c608d07413", - "type": "compel", - "inputs": { - "prompt": { - "id": "a4f25874-bc29-4900-abef-47701c805132", - "name": "prompt", - "type": "string", - "fieldKind": "input", - "label": "", - "value": "" - }, - "clip": { - "id": "6464b46f-fc74-4917-88b0-3fd458fc11f0", - "name": "clip", - "type": "ClipField", - "fieldKind": "input", - "label": "" - } - }, - "outputs": { - "conditioning": { - "id": "65bea1c6-e3b6-4120-a546-d93ff5dd7765", - "name": "conditioning", - "type": "ConditioningField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 259, - "position": { - "x": 5025, - "y": 1275 - } - }, - { - "id": "7c4e5071-5b76-4d42-b340-68b52c5ded7a", - "type": "invocation", - "data": { - "id": "7c4e5071-5b76-4d42-b340-68b52c5ded7a", - "type": "compel", - "inputs": { - "prompt": { - "id": "fcb313f2-74ab-443d-a8a8-eb5f2a9b5f96", - "name": "prompt", - "type": "string", - "fieldKind": "input", - "label": "", - "value": "" - }, - "clip": { - "id": "a278e13b-823a-4dda-96ca-44cf56f994dd", - "name": "clip", - "type": "ClipField", - "fieldKind": "input", - "label": "" - } - }, - "outputs": { - "conditioning": { - "id": "3c1f0f11-b8b0-444a-97ad-1cf80acf4bf7", - "name": "conditioning", - "type": "ConditioningField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 259, - "position": { - "x": 5025, - "y": 1550 - } - }, - { - "id": "098898c8-7a20-4d78-9363-296d42e3d8da", - "type": "invocation", - "data": { - "id": "098898c8-7a20-4d78-9363-296d42e3d8da", - "type": "noise", - "inputs": { - "seed": { - "id": "9358ec7b-e575-40d9-af22-2d4786ba1aa7", - "name": "seed", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "width": { - "id": "8422b4ba-9d8e-41ac-bf08-1ea826859b46", - "name": "width", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 512 - }, - "height": { - "id": "2467c79c-302b-4800-9efd-5bca58103322", - "name": "height", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 512 - }, - "use_cpu": { - "id": "911c827e-a6af-4168-9f6d-cecc732938ad", - "name": "use_cpu", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": true - } - }, - "outputs": { - "noise": { - "id": "9db55798-5c98-40f6-9015-56bfa8618f12", - "name": "noise", - "type": "LatentsField", - "fieldKind": "output" - }, - "width": { - "id": "bc091210-946e-410e-8c70-e20982dd1ee7", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "4df04a89-43a2-441a-8f15-089350b36ea7", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 389, - "position": { - "x": 5425, - "y": 1525 - } - }, - { - "id": "27dd9fc3-8c6e-4602-8754-e9ca2f478d68", - "type": "invocation", - "data": { - "id": "27dd9fc3-8c6e-4602-8754-e9ca2f478d68", - "type": "rand_int", - "inputs": { - "low": { - "id": "86e06b6e-7c84-40b0-9df2-12f966c3db4d", - "name": "low", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "high": { - "id": "5ee9adcc-d48c-4b9a-951a-6892d234acbc", - "name": "high", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 2147483647 - } - }, - "outputs": { - "value": { - "id": "b6c69841-ba37-43f0-8904-0026b1caf8ff", - "name": "value", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": false, - "version": "1.0.0" - }, - "width": 320, - "height": 218, - "position": { - "x": 5425, - "y": 1275 - } - }, - { - "id": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "type": "invocation", - "data": { - "id": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "type": "create_denoise_mask", - "inputs": { - "vae": { - "id": "c7991df7-9f68-4b42-96ce-d795a8e2f714", - "name": "vae", - "type": "VaeField", - "fieldKind": "input", - "label": "" - }, - "image": { - "id": "3345a725-f5d5-4f47-9942-b1dfffbe5906", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "mask": { - "id": "41fb1d83-1ca3-4299-b039-fb3b7c90f04e", - "name": "mask", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "tiled": { - "id": "32d8cf55-9910-4e09-8486-1c556a580a2d", - "name": "tiled", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - }, - "fp32": { - "id": "e26410cc-54f3-44f2-a81c-a22a8dad6f24", - "name": "fp32", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - } - }, - "outputs": { - "denoise_mask": { - "id": "9e1b78c0-5ac6-4632-91e9-831ce328237f", - "name": "denoise_mask", - "type": "DenoiseMaskField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 318, - "position": { - "x": 5025, - "y": 2175 - } - }, - { - "id": "3fac3aa6-910a-4a90-a8b6-5b7e1611efba", - "type": "invocation", - "data": { - "id": "3fac3aa6-910a-4a90-a8b6-5b7e1611efba", - "type": "image", - "inputs": { - "image": { - "id": "6efba7ef-b986-4488-84ca-80f23f939ba8", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - } - }, - "outputs": { - "image": { - "id": "2512df7a-9981-4186-93ed-aa5405dc3057", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "68d2a9c6-43ff-49aa-989c-db6f5452134f", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "2cb4e505-7e9e-40bd-b402-1e7470167d30", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 225, - "position": { - "x": 4625, - "y": 1525 - } - }, - { - "id": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "type": "invocation", - "data": { - "id": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "type": "i2l", - "inputs": { - "image": { - "id": "4aef9c20-51be-47e4-bdc4-d449694d75e1", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "vae": { - "id": "9507adad-df7c-447b-8aad-4d8d6f638420", - "name": "vae", - "type": "VaeField", - "fieldKind": "input", - "label": "" - }, - "tiled": { - "id": "2a100779-503a-4fb4-a2b5-507eb0f8328f", - "name": "tiled", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - }, - "fp32": { - "id": "3dbf8dbc-4ad4-4451-89f4-99a59ec87453", - "name": "fp32", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - } - }, - "outputs": { - "latents": { - "id": "215003b1-64dd-4c97-b5a4-7593d41ffd0e", - "name": "latents", - "type": "LatentsField", - "fieldKind": "output" - }, - "width": { - "id": "dc9b2940-79e0-49b2-906c-05417a691175", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "605de93b-c1c7-409f-b059-24918a292bfc", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 325, - "position": { - "x": 5025, - "y": 1825 - } - }, - { - "id": "01a35dfd-b4bd-4901-8088-49972eac7582", - "type": "invocation", - "data": { - "id": "01a35dfd-b4bd-4901-8088-49972eac7582", - "type": "l2i", - "inputs": { - "metadata": { - "id": "ce479dbf-d12f-43e7-9047-ec0e6bd838a7", - "name": "metadata", - "type": "MetadataField", - "fieldKind": "input", - "label": "" - }, - "latents": { - "id": "0d3f0abc-5c60-495e-af10-8136b25532d0", - "name": "latents", - "type": "LatentsField", - "fieldKind": "input", - "label": "" - }, - "vae": { - "id": "c0563226-c977-456a-8cf5-2873ab56a9f4", - "name": "vae", - "type": "VaeField", - "fieldKind": "input", - "label": "" - }, - "tiled": { - "id": "f909d67c-f789-4daa-8528-50113bba78ea", - "name": "tiled", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - }, - "fp32": { - "id": "d7c26ac3-df0f-47ab-aa91-0c5bfbe1ea38", - "name": "fp32", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - } - }, - "outputs": { - "image": { - "id": "380a7af7-bba5-434b-9996-47b8194d69d4", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "a0f2a9cf-77f6-42a5-801d-30f635d1de26", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "890ec07c-9bc0-46f6-b962-8a6fd149d562", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 267, - "position": { - "x": 5810.2137275110845, - "y": 1270.641572039504 - } - }, - { - "id": "43c00342-9ca3-498a-8635-c4c716e32d5f", - "type": "invocation", - "data": { - "id": "43c00342-9ca3-498a-8635-c4c716e32d5f", - "type": "img_scale", - "inputs": { - "metadata": { - "id": "d35fa75e-439b-4fda-9064-4ed73f378de6", - "name": "metadata", - "type": "MetadataField", - "fieldKind": "input", - "label": "" - }, - "image": { - "id": "e8c46885-2ec1-4fb9-8d34-07534e62ec97", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "scale_factor": { - "id": "94b04846-9f70-44ac-ae82-99af13632255", - "name": "scale_factor", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 2 - }, - "resample_mode": { - "id": "2fbcd8bb-6668-4661-868a-1452f0e73e6d", - "name": "resample_mode", - "type": "enum", - "fieldKind": "input", - "label": "", - "value": "bicubic" - } - }, - "outputs": { - "image": { - "id": "96600931-0da3-4c68-842f-d06f2d93f383", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "ff158681-580c-4a51-a2c8-c4bd2058a011", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "2a332d0e-4442-4a6e-8cf9-5e2e42f713c6", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 339, - "position": { - "x": 5808.235568293352, - "y": 1551.540180957498 - } - }, - { - "id": "0c71919b-a030-44fb-8c09-1baf37088d20", - "type": "invocation", - "data": { - "id": "0c71919b-a030-44fb-8c09-1baf37088d20", - "type": "color_correct", - "inputs": { - "metadata": { - "id": "89536a0d-1c62-4975-ada9-33f359837481", - "name": "metadata", - "type": "MetadataField", - "fieldKind": "input", - "label": "" - }, - "image": { - "id": "10d7c1da-e8e1-4226-ad9f-1bde9816c118", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "reference": { - "id": "9c7354ed-33dc-462a-bc57-717082eb3f45", - "name": "reference", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "mask": { - "id": "03aee643-6873-421c-8caf-1e05e85f6a9d", - "name": "mask", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "mask_blur_radius": { - "id": "2d2dffa6-f719-4ec9-a201-c07a3a914c77", - "name": "mask_blur_radius", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 8 - } - }, - "outputs": { - "image": { - "id": "b652c45e-e797-47d2-9175-aa77ede6a8f5", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "385015ed-2c6b-4b72-adda-ba5639841352", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "fe6632e0-560c-4c56-9b52-6e5e5d7eb642", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 396, - "position": { - "x": 5806.25740907562, - "y": 1903.6525217138576 - } - }, - { - "id": "4e11665c-b932-493d-ab4f-019bed730b47", - "type": "invocation", - "data": { - "id": "4e11665c-b932-493d-ab4f-019bed730b47", - "type": "img_paste", - "inputs": { - "metadata": { - "id": "6f365d9d-bf84-4053-b936-8035a4c7c991", - "name": "metadata", - "type": "MetadataField", - "fieldKind": "input", - "label": "" - }, - "base_image": { - "id": "fb323f35-d1fd-4d20-b145-9a3db7a57a28", - "name": "base_image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "image": { - "id": "ea76a063-ec51-4b2b-bfa8-8df9c64ecc2e", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "mask": { - "id": "9b3f194a-d7f1-45f3-989a-6b2096dd3fd6", - "name": "mask", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "x": { - "id": "d74b8e26-bc59-4a27-bb00-f769b81e8ef0", - "name": "x", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "y": { - "id": "6ada744a-8f67-49c6-a8b6-df03b3397d73", - "name": "y", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "crop": { - "id": "dec1e8e1-2fb0-43e8-9ddf-0eb478e5e567", - "name": "crop", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - } - }, - "outputs": { - "image": { - "id": "4a01e54d-9a7b-458d-aa87-640ce8c5cd62", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "be903c7c-b867-4938-9a0e-de8a6676ae89", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "7dbb23a1-bedc-418c-8c27-09cf24e0e777", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.1" - }, - "width": 320, - "height": 504, - "position": { - "x": 5810.103371182836, - "y": 2315.9071582023707 - } - }, - { - "id": "9a6a35cd-5c05-4df1-81bf-e40a1954c618", - "type": "invocation", - "data": { - "id": "9a6a35cd-5c05-4df1-81bf-e40a1954c618", - "type": "denoise_latents", - "inputs": { - "positive_conditioning": { - "id": "1039d676-b54f-4848-903d-a5e3eef84781", - "name": "positive_conditioning", - "type": "ConditioningField", - "fieldKind": "input", - "label": "" - }, - "negative_conditioning": { - "id": "52e4b032-e32f-48b1-ac02-b17f0dff78f6", - "name": "negative_conditioning", - "type": "ConditioningField", - "fieldKind": "input", - "label": "" - }, - "noise": { - "id": "7c6c35bb-576c-42fa-8191-169afff13d73", - "name": "noise", - "type": "LatentsField", - "fieldKind": "input", - "label": "" - }, - "steps": { - "id": "e0df62c7-edb8-4777-8ca1-c7a6fb30f6a2", - "name": "steps", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 10 - }, - "cfg_scale": { - "id": "e1a87d18-671d-4a6e-a528-7d4376500211", - "name": "cfg_scale", - "type": "FloatPolymorphic", - "fieldKind": "input", - "label": "", - "value": 7.5 - }, - "denoising_start": { - "id": "60ab8906-162c-4817-bd46-f5d7a93aa213", - "name": "denoising_start", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "denoising_end": { - "id": "c8f73ade-d5a4-4e69-8462-c0f9ee3dfb96", - "name": "denoising_end", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 1 - }, - "scheduler": { - "id": "e2872b56-4250-4a0d-86b3-37c60485fdc7", - "name": "scheduler", - "type": "Scheduler", - "fieldKind": "input", - "label": "", - "value": "euler" - }, - "unet": { - "id": "b1f93d41-b399-4774-a913-4a0c0c6d5f66", - "name": "unet", - "type": "UNetField", - "fieldKind": "input", - "label": "" - }, - "control": { - "id": "89315aa7-95d7-463b-86fd-a8064092503c", - "name": "control", - "type": "ControlPolymorphic", - "fieldKind": "input", - "label": "" - }, - "ip_adapter": { - "id": "370ded4f-ef23-4741-9e4c-1f3d1d429cfd", - "name": "ip_adapter", - "type": "IPAdapterPolymorphic", - "fieldKind": "input", - "label": "" - }, - "t2i_adapter": { - "id": "dac77b99-0cc9-46c5-9d17-66ddbabdb7f8", - "name": "t2i_adapter", - "type": "T2IAdapterPolymorphic", - "fieldKind": "input", - "label": "" - }, - "latents": { - "id": "229e7492-abfd-47cb-9218-67c1ba75c4b4", - "name": "latents", - "type": "LatentsField", - "fieldKind": "input", - "label": "" - }, - "denoise_mask": { - "id": "1248eb2e-d362-446c-8ffc-ddab52b7c20f", - "name": "denoise_mask", - "type": "DenoiseMaskField", - "fieldKind": "input", - "label": "" - } - }, - "outputs": { - "latents": { - "id": "de5c8012-df50-4fae-a4b7-ca09aaa181ca", - "name": "latents", - "type": "LatentsField", - "fieldKind": "output" - }, - "width": { - "id": "ba1e5e4f-3ba9-467b-b80d-3c85aa97afab", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "b0fa6029-ede9-45be-b1ec-b1ec469bfeb6", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.4.0" - }, - "width": 320, - "height": 646, - "position": { - "x": 5468.65700494231, - "y": 1926.1754472409636 - } - }, - { - "id": "98048fa7-dd08-49ec-92b8-76c9017e5444", - "type": "invocation", - "data": { - "id": "98048fa7-dd08-49ec-92b8-76c9017e5444", - "type": "img_scale", - "inputs": { - "metadata": { - "id": "02fa128c-3d3b-4083-aa73-fb7acbfb072a", - "name": "metadata", - "type": "MetadataField", - "fieldKind": "input", - "label": "" - }, - "image": { - "id": "8008b9db-bd61-4a89-b2df-de444961bc5b", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "scale_factor": { - "id": "45268d5f-b8f9-49ee-b770-a6876100878a", - "name": "scale_factor", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 2 - }, - "resample_mode": { - "id": "645b99b1-fd64-429b-b61d-caf739830efa", - "name": "resample_mode", - "type": "enum", - "fieldKind": "input", - "label": "", - "value": "bicubic" - } - }, - "outputs": { - "image": { - "id": "ff088e78-dd52-4605-a14d-d10d7c0015a5", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "b0bede16-55d6-4fee-8709-6bce92118431", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "88dc53cd-9d12-4f95-b23a-382b3db92db9", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 339, - "position": { - "x": 5026.491824134117, - "y": 2513.056664333261 - } - }, - { - "id": "894b9ca6-e7bc-428e-87e9-cce64095bce9", - "type": "invocation", - "data": { - "id": "894b9ca6-e7bc-428e-87e9-cce64095bce9", - "type": "img_scale", - "inputs": { - "metadata": { - "id": "ae97ac6c-6dc7-4c01-955a-ae98a608db4b", - "name": "metadata", - "type": "MetadataField", - "fieldKind": "input", - "label": "" - }, - "image": { - "id": "56d9afd5-b1c6-4b62-b332-a5585e863000", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "scale_factor": { - "id": "cdeeecb8-8f12-4d2b-b961-4d18cca858de", - "name": "scale_factor", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 2 - }, - "resample_mode": { - "id": "b120b6a1-dbdb-4989-80fb-af0a682e124c", - "name": "resample_mode", - "type": "enum", - "fieldKind": "input", - "label": "", - "value": "bicubic" - } - }, - "outputs": { - "image": { - "id": "7888a231-4f89-4ad7-a216-d5664a96eb7d", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "9030b8fd-4963-4bc7-86a2-5a964443fc41", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "4beb6975-4ec2-4c76-8916-81331a2d2230", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 339, - "position": { - "x": 4624.131880974906, - "y": 2614.5694940293906 - } - }, - { - "id": "efea8306-de20-418f-806c-31ae4e5eb6bf", - "type": "invocation", - "data": { - "id": "efea8306-de20-418f-806c-31ae4e5eb6bf", - "type": "face_off", - "inputs": { - "metadata": { - "id": "4e7eb36f-3742-49d9-91b4-092b955ef588", - "name": "metadata", - "type": "MetadataField", - "fieldKind": "input", - "label": "" - }, - "image": { - "id": "003c3ff3-d0d0-464e-85ed-fb4df75fd513", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "face_id": { - "id": "9281074a-be25-4e43-8b80-9504b2365999", - "name": "face_id", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "minimum_confidence": { - "id": "808efdd1-41d5-4c3c-9ea3-0a1854503087", - "name": "minimum_confidence", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 0.5 - }, - "x_offset": { - "id": "fd4fedf5-7fba-4687-864b-bfd018c296cf", - "name": "x_offset", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "y_offset": { - "id": "3d64697c-707e-404b-8261-9665c78f686d", - "name": "y_offset", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "padding": { - "id": "1f147de3-8fde-4bcc-8c68-a6617feed334", - "name": "padding", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "chunk": { - "id": "e60fae93-2056-4621-a6bf-d0248df6f538", - "name": "chunk", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - } - }, - "outputs": { - "image": { - "id": "1c6aa0cf-4bb8-4236-b92e-a862036c9522", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "8dac27bd-7d72-471b-a310-bb7dae686a0b", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "4bfb3364-fe37-4fb8-b8a0-75d1038ce726", - "name": "height", - "type": "integer", - "fieldKind": "output" - }, - "mask": { - "id": "221c6022-5314-4f8f-afb0-4b00ceea8ecd", - "name": "mask", - "type": "ImageField", - "fieldKind": "output" - }, - "x": { - "id": "657fa37b-6e9c-4c04-b5d2-e3e659fcb0f3", - "name": "x", - "type": "integer", - "fieldKind": "output" - }, - "y": { - "id": "bc5cf72e-0389-4b8c-b886-40d145b6b73a", - "name": "y", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.2" - }, - "width": 320, - "height": 656, - "position": { - "x": 4618.594817536934, - "y": 1836.6663791833205 - } - } - ], - "edges": [ - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "clip", - "target": "fb7e72d9-51cb-432a-b511-c6c608d07413", - "targetHandle": "clip", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036clip-fb7e72d9-51cb-432a-b511-c6c608d07413clip", - "type": "default" - }, - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "clip", - "target": "7c4e5071-5b76-4d42-b340-68b52c5ded7a", - "targetHandle": "clip", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036clip-7c4e5071-5b76-4d42-b340-68b52c5ded7aclip", - "type": "default" - }, - { - "source": "27dd9fc3-8c6e-4602-8754-e9ca2f478d68", - "sourceHandle": "value", - "target": "098898c8-7a20-4d78-9363-296d42e3d8da", - "targetHandle": "seed", - "id": "reactflow__edge-27dd9fc3-8c6e-4602-8754-e9ca2f478d68value-098898c8-7a20-4d78-9363-296d42e3d8daseed", - "type": "default" - }, - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "vae", - "target": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "targetHandle": "vae", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036vae-bcbdc4ea-1fad-40d4-8632-70f84116f4b6vae", - "type": "default" - }, - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "vae", - "target": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "targetHandle": "vae", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036vae-a6d08bcb-0b52-4dd8-9247-8b6480238c6dvae", - "type": "default" - }, - { - "source": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "sourceHandle": "width", - "target": "098898c8-7a20-4d78-9363-296d42e3d8da", - "targetHandle": "width", - "id": "reactflow__edge-a6d08bcb-0b52-4dd8-9247-8b6480238c6dwidth-098898c8-7a20-4d78-9363-296d42e3d8dawidth", - "type": "default" - }, - { - "source": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "sourceHandle": "height", - "target": "098898c8-7a20-4d78-9363-296d42e3d8da", - "targetHandle": "height", - "id": "reactflow__edge-a6d08bcb-0b52-4dd8-9247-8b6480238c6dheight-098898c8-7a20-4d78-9363-296d42e3d8daheight", - "type": "default" - }, - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "vae", - "target": "01a35dfd-b4bd-4901-8088-49972eac7582", - "targetHandle": "vae", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036vae-01a35dfd-b4bd-4901-8088-49972eac7582vae", - "type": "default" - }, - { - "source": "01a35dfd-b4bd-4901-8088-49972eac7582", - "sourceHandle": "image", - "target": "43c00342-9ca3-498a-8635-c4c716e32d5f", - "targetHandle": "image", - "id": "reactflow__edge-01a35dfd-b4bd-4901-8088-49972eac7582image-43c00342-9ca3-498a-8635-c4c716e32d5fimage", - "type": "default" - }, - { - "source": "43c00342-9ca3-498a-8635-c4c716e32d5f", - "sourceHandle": "image", - "target": "0c71919b-a030-44fb-8c09-1baf37088d20", - "targetHandle": "image", - "id": "reactflow__edge-43c00342-9ca3-498a-8635-c4c716e32d5fimage-0c71919b-a030-44fb-8c09-1baf37088d20image", - "type": "default" - }, - { - "source": "3fac3aa6-910a-4a90-a8b6-5b7e1611efba", - "sourceHandle": "image", - "target": "4e11665c-b932-493d-ab4f-019bed730b47", - "targetHandle": "base_image", - "id": "reactflow__edge-3fac3aa6-910a-4a90-a8b6-5b7e1611efbaimage-4e11665c-b932-493d-ab4f-019bed730b47base_image", - "type": "default" - }, - { - "source": "0c71919b-a030-44fb-8c09-1baf37088d20", - "sourceHandle": "image", - "target": "4e11665c-b932-493d-ab4f-019bed730b47", - "targetHandle": "image", - "id": "reactflow__edge-0c71919b-a030-44fb-8c09-1baf37088d20image-4e11665c-b932-493d-ab4f-019bed730b47image", - "type": "default" - }, - { - "source": "9a6a35cd-5c05-4df1-81bf-e40a1954c618", - "sourceHandle": "latents", - "target": "01a35dfd-b4bd-4901-8088-49972eac7582", - "targetHandle": "latents", - "id": "reactflow__edge-9a6a35cd-5c05-4df1-81bf-e40a1954c618latents-01a35dfd-b4bd-4901-8088-49972eac7582latents", - "type": "default" - }, - { - "source": "c9897be0-7f59-4388-816d-86cb72cc4036", - "sourceHandle": "unet", - "target": "9a6a35cd-5c05-4df1-81bf-e40a1954c618", - "targetHandle": "unet", - "id": "reactflow__edge-c9897be0-7f59-4388-816d-86cb72cc4036unet-9a6a35cd-5c05-4df1-81bf-e40a1954c618unet", - "type": "default" - }, - { - "source": "7c4e5071-5b76-4d42-b340-68b52c5ded7a", - "sourceHandle": "conditioning", - "target": "9a6a35cd-5c05-4df1-81bf-e40a1954c618", - "targetHandle": "negative_conditioning", - "id": "reactflow__edge-7c4e5071-5b76-4d42-b340-68b52c5ded7aconditioning-9a6a35cd-5c05-4df1-81bf-e40a1954c618negative_conditioning", - "type": "default" - }, - { - "source": "fb7e72d9-51cb-432a-b511-c6c608d07413", - "sourceHandle": "conditioning", - "target": "9a6a35cd-5c05-4df1-81bf-e40a1954c618", - "targetHandle": "positive_conditioning", - "id": "reactflow__edge-fb7e72d9-51cb-432a-b511-c6c608d07413conditioning-9a6a35cd-5c05-4df1-81bf-e40a1954c618positive_conditioning", - "type": "default" - }, - { - "source": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "sourceHandle": "denoise_mask", - "target": "9a6a35cd-5c05-4df1-81bf-e40a1954c618", - "targetHandle": "denoise_mask", - "id": "reactflow__edge-bcbdc4ea-1fad-40d4-8632-70f84116f4b6denoise_mask-9a6a35cd-5c05-4df1-81bf-e40a1954c618denoise_mask", - "type": "default" - }, - { - "source": "098898c8-7a20-4d78-9363-296d42e3d8da", - "sourceHandle": "noise", - "target": "9a6a35cd-5c05-4df1-81bf-e40a1954c618", - "targetHandle": "noise", - "id": "reactflow__edge-098898c8-7a20-4d78-9363-296d42e3d8danoise-9a6a35cd-5c05-4df1-81bf-e40a1954c618noise", - "type": "default" - }, - { - "source": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "sourceHandle": "latents", - "target": "9a6a35cd-5c05-4df1-81bf-e40a1954c618", - "targetHandle": "latents", - "id": "reactflow__edge-a6d08bcb-0b52-4dd8-9247-8b6480238c6dlatents-9a6a35cd-5c05-4df1-81bf-e40a1954c618latents", - "type": "default" - }, - { - "source": "98048fa7-dd08-49ec-92b8-76c9017e5444", - "sourceHandle": "image", - "target": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "targetHandle": "mask", - "id": "reactflow__edge-98048fa7-dd08-49ec-92b8-76c9017e5444image-bcbdc4ea-1fad-40d4-8632-70f84116f4b6mask", - "type": "default" - }, - { - "source": "894b9ca6-e7bc-428e-87e9-cce64095bce9", - "sourceHandle": "image", - "target": "a6d08bcb-0b52-4dd8-9247-8b6480238c6d", - "targetHandle": "image", - "id": "reactflow__edge-894b9ca6-e7bc-428e-87e9-cce64095bce9image-a6d08bcb-0b52-4dd8-9247-8b6480238c6dimage", - "type": "default" - }, - { - "source": "894b9ca6-e7bc-428e-87e9-cce64095bce9", - "sourceHandle": "image", - "target": "bcbdc4ea-1fad-40d4-8632-70f84116f4b6", - "targetHandle": "image", - "id": "reactflow__edge-894b9ca6-e7bc-428e-87e9-cce64095bce9image-bcbdc4ea-1fad-40d4-8632-70f84116f4b6image", - "type": "default" - }, - { - "source": "efea8306-de20-418f-806c-31ae4e5eb6bf", - "sourceHandle": "image", - "target": "894b9ca6-e7bc-428e-87e9-cce64095bce9", - "targetHandle": "image", - "id": "reactflow__edge-efea8306-de20-418f-806c-31ae4e5eb6bfimage-894b9ca6-e7bc-428e-87e9-cce64095bce9image", - "type": "default" - }, - { - "source": "efea8306-de20-418f-806c-31ae4e5eb6bf", - "sourceHandle": "image", - "target": "0c71919b-a030-44fb-8c09-1baf37088d20", - "targetHandle": "reference", - "id": "reactflow__edge-efea8306-de20-418f-806c-31ae4e5eb6bfimage-0c71919b-a030-44fb-8c09-1baf37088d20reference", - "type": "default" - }, - { - "source": "efea8306-de20-418f-806c-31ae4e5eb6bf", - "sourceHandle": "mask", - "target": "98048fa7-dd08-49ec-92b8-76c9017e5444", - "targetHandle": "image", - "id": "reactflow__edge-efea8306-de20-418f-806c-31ae4e5eb6bfmask-98048fa7-dd08-49ec-92b8-76c9017e5444image", - "type": "default" - }, - { - "source": "efea8306-de20-418f-806c-31ae4e5eb6bf", - "sourceHandle": "mask", - "target": "0c71919b-a030-44fb-8c09-1baf37088d20", - "targetHandle": "mask", - "id": "reactflow__edge-efea8306-de20-418f-806c-31ae4e5eb6bfmask-0c71919b-a030-44fb-8c09-1baf37088d20mask", - "type": "default" - }, - { - "source": "efea8306-de20-418f-806c-31ae4e5eb6bf", - "sourceHandle": "x", - "target": "4e11665c-b932-493d-ab4f-019bed730b47", - "targetHandle": "x", - "id": "reactflow__edge-efea8306-de20-418f-806c-31ae4e5eb6bfx-4e11665c-b932-493d-ab4f-019bed730b47x", - "type": "default" - }, - { - "source": "efea8306-de20-418f-806c-31ae4e5eb6bf", - "sourceHandle": "y", - "target": "4e11665c-b932-493d-ab4f-019bed730b47", - "targetHandle": "y", - "id": "reactflow__edge-efea8306-de20-418f-806c-31ae4e5eb6bfy-4e11665c-b932-493d-ab4f-019bed730b47y", - "type": "default" - }, - { - "source": "3fac3aa6-910a-4a90-a8b6-5b7e1611efba", - "sourceHandle": "image", - "target": "efea8306-de20-418f-806c-31ae4e5eb6bf", - "targetHandle": "image", - "id": "reactflow__edge-3fac3aa6-910a-4a90-a8b6-5b7e1611efbaimage-efea8306-de20-418f-806c-31ae4e5eb6bfimage", - "type": "default" - } - ] -} \ No newline at end of file diff --git a/docs/workflows/Face_Detailer_with_IP-Adapter_and_Canny.json b/docs/workflows/Face_Detailer_with_IP-Adapter_and_Canny.json deleted file mode 100644 index 6f392d6a7b5..00000000000 --- a/docs/workflows/Face_Detailer_with_IP-Adapter_and_Canny.json +++ /dev/null @@ -1,2930 +0,0 @@ -{ - "id": "972fc0e9-a13f-4658-86b9-aef100d124ba", - "name": "Face Detailer with IP-Adapter & Canny (See Note)", - "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. For best results, use this image as the blur mask: https://i.imgur.com/Gxi61zP.png", - "version": "1.0.0", - "contact": "invoke@invoke.ai", - "tags": "face detailer, IP-Adapter, Canny", - "notes": "", - "exposedFields": [ - { - "nodeId": "cdfa5ab0-b3e2-43ed-85bb-2ac4aa83bc05", - "fieldName": "value" - }, - { - "nodeId": "64712037-92e8-483f-9f6e-87588539c1b8", - "fieldName": "value" - }, - { - "nodeId": "77da4e4d-5778-4469-8449-ffed03d54bdb", - "fieldName": "radius" - }, - { - "nodeId": "f0de6c44-4515-4f79-bcc0-dee111bcfe31", - "fieldName": "value" - }, - { - "nodeId": "2c9bc2a6-6c03-4861-aad4-db884a7682f8", - "fieldName": "image" - }, - { - "nodeId": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955", - "fieldName": "image" - } - ], - "meta": { - "category": "default", - "version": "2.0.0" - }, - "nodes": [ - { - "id": "44f2c190-eb03-460d-8d11-a94d13b33f19", - "type": "invocation", - "data": { - "id": "44f2c190-eb03-460d-8d11-a94d13b33f19", - "type": "compel", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "916b229a-38e1-45a2-a433-cca97495b143", - "name": "prompt", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "clip": { - "id": "ae9aeb1a-4ebd-4bc3-b6e6-a8c9adca01f6", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "4d59bad1-99a9-43e2-bdb4-7a0f3dd5b787", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 256, - "position": { - "x": 2575, - "y": -250 - } - }, - { - "id": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955", - "type": "invocation", - "data": { - "id": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955", - "type": "img_resize", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "48ff6f70-380c-4e19-acf7-91063cfff8a8", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "2a348a03-07a3-4a97-93c8-46051045b32f", - "name": "image", - "fieldKind": "input", - "label": "Blur Mask", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "6b95969c-ca73-4b54-815d-7aae305a67bd", - "name": "width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "height": { - "id": "af83c526-c730-4a34-8f73-80f443fecc05", - "name": "height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "resample_mode": { - "id": "43f7d6b5-ebe0-43c4-bf5d-8fb7fdc40a3f", - "name": "resample_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "lanczos" - } - }, - "outputs": { - "image": { - "id": "e492b013-615d-4dfd-b0d8-7df7b5ba9a9d", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "05f829b3-c253-495a-b1ad-9906c0833e49", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "65d5d662-2527-4f3c-8a87-0a7d9d4486de", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 397, - "position": { - "x": 4675, - "y": 625 - } - }, - { - "id": "2c9bc2a6-6c03-4861-aad4-db884a7682f8", - "type": "invocation", - "data": { - "id": "2c9bc2a6-6c03-4861-aad4-db884a7682f8", - "type": "image", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "image": { - "id": "729c571b-d5a0-4b53-8f50-5e11eb744f66", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - }, - "value": { - "image_name": "42a524fd-ed77-46e2-b52f-40375633f357.png" - } - } - }, - "outputs": { - "image": { - "id": "3632a144-58d6-4447-bafc-e4f7d6ca96bf", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "30faefcc-81a1-445b-a3fe-0110ceb56772", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "d173d225-849a-4498-a75d-ba17210dbd3e", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 489, - "position": { - "x": 2050, - "y": -75 - } - }, - { - "id": "9ae34718-a17d-401d-9859-086896c29fca", - "type": "invocation", - "data": { - "id": "9ae34718-a17d-401d-9859-086896c29fca", - "type": "face_off", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "0144d549-b04a-49c2-993f-9ecb2695191a", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "9c580dec-a958-43a4-9aa0-7ab9491c56a0", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "face_id": { - "id": "1ee762e1-810f-46f7-a591-ecd28abff85b", - "name": "face_id", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "minimum_confidence": { - "id": "74d2bae8-7ac5-4f8f-afb7-1efc76ed72a0", - "name": "minimum_confidence", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0.5 - }, - "x_offset": { - "id": "f1c1489d-cbbb-45d2-ba94-e150fc5ce24d", - "name": "x_offset", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "y_offset": { - "id": "6621377a-7e29-4841-aa47-aa7d438662f9", - "name": "y_offset", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "padding": { - "id": "0414c830-1be8-48cd-b0c8-6de10b099029", - "name": "padding", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 64 - }, - "chunk": { - "id": "22eba30a-c68a-4e5f-8f97-315830959b04", - "name": "chunk", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - } - }, - "outputs": { - "image": { - "id": "7d8fa807-0e9c-4d6a-a85a-388bd0cb5166", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "048361ca-314c-44a4-8b6e-ca4917e3468f", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "7b721071-63e5-4d54-9607-9fa2192163a8", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "mask": { - "id": "a9c6194a-3dbd-4b18-af04-a85a9498f04e", - "name": "mask", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "x": { - "id": "d072582c-2db5-430b-9e50-b50277baa7d5", - "name": "x", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "y": { - "id": "40624813-e55d-42d7-bc14-dea48ccfd9c8", - "name": "y", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 657, - "position": { - "x": 2575, - "y": 200 - } - }, - { - "id": "50a8db6a-3796-4522-8547-53275efa4e7d", - "type": "invocation", - "data": { - "id": "50a8db6a-3796-4522-8547-53275efa4e7d", - "type": "img_resize", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "6bf91925-e22e-4bcb-90e4-f8ac32ddff36", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "65af3f4e-7a89-4df4-8ba9-377af1701d16", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "1f036fee-77f3-480f-b690-089b27616ab8", - "name": "width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "height": { - "id": "0c703b6e-e91a-4cd7-8b9e-b97865f76793", - "name": "height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "resample_mode": { - "id": "ed77cf71-94cb-4da4-b536-75cb7aad8e54", - "name": "resample_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "lanczos" - } - }, - "outputs": { - "image": { - "id": "0d9e437b-6a08-45e1-9a55-ef03ae28e616", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "7c56c704-b4e5-410f-bb62-cd441ddb454f", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "45e12e17-7f25-46bd-a804-4384ec6b98cc", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 3000, - "y": 0 - } - }, - { - "id": "de8b1a48-a2e4-42ca-90bb-66058bffd534", - "type": "invocation", - "data": { - "id": "de8b1a48-a2e4-42ca-90bb-66058bffd534", - "type": "i2l", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "image": { - "id": "6c4d2827-4995-49d4-94ce-0ba0541d8839", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "vae": { - "id": "9d6e3ab6-b6a4-45ac-ad75-0a96efba4c2f", - "name": "vae", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - }, - "tiled": { - "id": "9c258141-a75d-4ffd-bce5-f3fb3d90b720", - "name": "tiled", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - }, - "fp32": { - "id": "2235cc48-53c9-4e8a-a74a-ed41c61f2993", - "name": "fp32", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": true - } - }, - "outputs": { - "latents": { - "id": "8eb9293f-8f43-4c0c-b0fb-8c4db1200f87", - "name": "latents", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "ce493959-d308-423c-b0f5-db05912e0318", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "827bf290-94fb-455f-a970-f98ba8800eac", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 3100, - "y": -275 - } - }, - { - "id": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "type": "invocation", - "data": { - "id": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "type": "denoise_latents", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.5.0", - "nodePack": "invokeai", - "inputs": { - "positive_conditioning": { - "id": "673e4094-448b-4c59-ab05-d05b29b3e07f", - "name": "positive_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "negative_conditioning": { - "id": "3b3bac27-7e8a-4c4e-8b5b-c5231125302a", - "name": "negative_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "noise": { - "id": "d7c20d11-fbfb-4613-ba58-bf00307e53be", - "name": "noise", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "steps": { - "id": "e4af3bea-dae4-403f-8cec-6cb66fa888ce", - "name": "steps", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 80 - }, - "cfg_scale": { - "id": "9ec125bb-6819-4970-89fe-fe09e6b96885", - "name": "cfg_scale", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 3 - }, - "denoising_start": { - "id": "7190b40d-9467-4238-b180-0d19065258e2", - "name": "denoising_start", - "fieldKind": "input", - "label": "Original Image Percent", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0.2 - }, - "denoising_end": { - "id": "c033f2d4-b60a-4a25-8a88-7852b556657a", - "name": "denoising_end", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1 - }, - "scheduler": { - "id": "bb043b8a-a119-4b2a-bb88-8afb364bdcc1", - "name": "scheduler", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SchedulerField" - }, - "value": "dpmpp_2m_sde_k" - }, - "unet": { - "id": "16d7688a-fc80-405c-aa55-a8ba2a1efecb", - "name": "unet", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "control": { - "id": "d0225ede-7d03-405d-b5b4-97c07d9b602b", - "name": "control", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "ControlField" - } - }, - "ip_adapter": { - "id": "6fbe0a17-6b85-4d3e-835d-0e23d3040bf4", - "name": "ip_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IPAdapterField" - } - }, - "t2i_adapter": { - "id": "2e4e1e6e-0278-47e3-a464-1a51760a9411", - "name": "t2i_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "T2IAdapterField" - } - }, - "cfg_rescale_multiplier": { - "id": "c9703a2e-4178-493c-90b9-81325a83a5ec", - "name": "cfg_rescale_multiplier", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "latents": { - "id": "b2527e78-6f55-4274-aaa0-8fef1c928faa", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "denoise_mask": { - "id": "cceec5a4-d3e9-4cff-a1e1-b5b62cb12588", - "name": "denoise_mask", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "DenoiseMaskField" - } - } - }, - "outputs": { - "latents": { - "id": "6eca0515-4357-40be-ac8d-f84eb927dc31", - "name": "latents", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "14453d1c-6202-4377-811c-88ac9c024e77", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "0e0e04dc-4158-4461-b0ba-6c6af16e863c", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 705, - "position": { - "x": 4597.554345564559, - "y": -265.6421598623905 - } - }, - { - "id": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42", - "type": "invocation", - "data": { - "id": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42", - "type": "noise", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.1", - "nodePack": "invokeai", - "inputs": { - "seed": { - "id": "c6b5bc5e-ef09-4f9c-870e-1110a0f5017f", - "name": "seed", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 123451234 - }, - "width": { - "id": "7bdd24b6-4f14-4d0a-b8fc-9b24145b4ba9", - "name": "width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "height": { - "id": "dc15bf97-b8d5-49c6-999b-798b33679418", - "name": "height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "use_cpu": { - "id": "00626297-19dd-4989-9688-e8d527c9eacf", - "name": "use_cpu", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": true - } - }, - "outputs": { - "noise": { - "id": "2915f8ae-0f6e-4f26-8541-0ebf477586b6", - "name": "noise", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "26587461-a24a-434d-9ae5-8d8f36fea221", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "335d08fc-8bf1-4393-8902-2c579f327b51", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 4025, - "y": -175 - } - }, - { - "id": "2224ed72-2453-4252-bd89-3085240e0b6f", - "type": "invocation", - "data": { - "id": "2224ed72-2453-4252-bd89-3085240e0b6f", - "type": "l2i", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": false, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "2f077d6f-579e-4399-9986-3feabefa9ade", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "latents": { - "id": "568a1537-dc7c-44f5-8a87-3571b0528b85", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "vae": { - "id": "392c3757-ad3a-46af-8d76-9724ca30aad8", - "name": "vae", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - }, - "tiled": { - "id": "90f98601-c05d-453e-878b-18e23cc222b4", - "name": "tiled", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - }, - "fp32": { - "id": "6be5cad7-dd41-4f83-98e7-124a6ad1728d", - "name": "fp32", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": true - } - }, - "outputs": { - "image": { - "id": "6f90a4f5-42dd-472a-8f30-403bcbc16531", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "4d1c8a66-35fc-40e9-a7a9-d8604a247d33", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "e668cfb3-aedc-4032-94c9-b8add1fbaacf", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 267, - "position": { - "x": 4980.1395106966565, - "y": -255.9158921745602 - } - }, - { - "id": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323", - "type": "invocation", - "data": { - "id": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323", - "type": "lscale", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "latents": { - "id": "79e8f073-ddc3-416e-b818-6ef8ec73cc07", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "scale_factor": { - "id": "23f78d24-72df-4bde-8d3c-8593ce507205", - "name": "scale_factor", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1.5 - }, - "mode": { - "id": "4ab30c38-57d3-480d-8b34-918887e92340", - "name": "mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "bilinear" - }, - "antialias": { - "id": "22b39171-0003-44f0-9c04-d241581d2a39", - "name": "antialias", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": true - } - }, - "outputs": { - "latents": { - "id": "f6d71aef-6251-4d51-afa8-f692a72bfd1f", - "name": "latents", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "8db4cf33-5489-4887-a5f6-5e926d959c40", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "74e1ec7c-50b6-4e97-a7b8-6602e6d78c08", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 3075, - "y": -175 - } - }, - { - "id": "a7d14545-aa09-4b96-bfc5-40c009af9110", - "type": "invocation", - "data": { - "id": "a7d14545-aa09-4b96-bfc5-40c009af9110", - "type": "img_paste", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": false, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "159f5a3c-4b47-46dd-b8fe-450455ee3dcf", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "base_image": { - "id": "445cfacf-5042-49ae-a63e-c19f21f90b1d", - "name": "base_image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "image": { - "id": "c292948b-8efd-4f5c-909d-d902cfd1509a", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "mask": { - "id": "8b7bc7e9-35b5-45bc-9b8c-e15e92ab4d74", - "name": "mask", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "x": { - "id": "0694ba58-07bc-470b-80b5-9c7a99b64607", - "name": "x", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "y": { - "id": "40f8b20b-f804-4ec2-9622-285726f7665f", - "name": "y", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "crop": { - "id": "8a09a132-c07e-4cfb-8ec2-7093dc063f99", - "name": "crop", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - } - }, - "outputs": { - "image": { - "id": "4a96ec78-d4b6-435f-b5a3-6366cbfcfba7", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "c1ee69c7-6ca0-4604-abc9-b859f4178421", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "6c198681-5f16-4526-8e37-0bf000962acd", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 504, - "position": { - "x": 6000, - "y": -200 - } - }, - { - "id": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef", - "type": "invocation", - "data": { - "id": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef", - "type": "img_resize", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "afbbb112-60e4-44c8-bdfd-30f48d58b236", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "2a348a03-07a3-4a97-93c8-46051045b32f", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "6b95969c-ca73-4b54-815d-7aae305a67bd", - "name": "width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "height": { - "id": "af83c526-c730-4a34-8f73-80f443fecc05", - "name": "height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "resample_mode": { - "id": "43f7d6b5-ebe0-43c4-bf5d-8fb7fdc40a3f", - "name": "resample_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "lanczos" - } - }, - "outputs": { - "image": { - "id": "e492b013-615d-4dfd-b0d8-7df7b5ba9a9d", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "05f829b3-c253-495a-b1ad-9906c0833e49", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "65d5d662-2527-4f3c-8a87-0a7d9d4486de", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 397, - "position": { - "x": 5500, - "y": -225 - } - }, - { - "id": "cdfa5ab0-b3e2-43ed-85bb-2ac4aa83bc05", - "type": "invocation", - "data": { - "id": "cdfa5ab0-b3e2-43ed-85bb-2ac4aa83bc05", - "type": "float", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "value": { - "id": "d5d8063d-44f6-4e20-b557-2f4ce093c7ef", - "name": "value", - "fieldKind": "input", - "label": "Orignal Image Percentage", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0.4 - } - }, - "outputs": { - "value": { - "id": "562416a4-0d75-48aa-835e-5e2d221dfbb7", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 4025, - "y": -75 - } - }, - { - "id": "64712037-92e8-483f-9f6e-87588539c1b8", - "type": "invocation", - "data": { - "id": "64712037-92e8-483f-9f6e-87588539c1b8", - "type": "float", - "label": "CFG Main", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "value": { - "id": "750358d5-251d-4fe6-a673-2cde21995da2", - "name": "value", - "fieldKind": "input", - "label": "CFG Main", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 6 - } - }, - "outputs": { - "value": { - "id": "eea7f6d2-92e4-4581-b555-64a44fda2be9", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 4025, - "y": 75 - } - }, - { - "id": "c865f39f-f830-4ed7-88a5-e935cfe050a9", - "type": "invocation", - "data": { - "id": "c865f39f-f830-4ed7-88a5-e935cfe050a9", - "type": "rand_int", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": false, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "low": { - "id": "31e29709-9f19-45b0-a2de-fdee29a50393", - "name": "low", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "high": { - "id": "d47d875c-509d-4fa3-9112-e335d3144a17", - "name": "high", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 2147483647 - } - }, - "outputs": { - "value": { - "id": "15b8d1ea-d2ac-4b3a-9619-57bba9a6da75", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 4025, - "y": -275 - } - }, - { - "id": "76ea1e31-eabe-4080-935e-e74ce20e2805", - "type": "invocation", - "data": { - "id": "76ea1e31-eabe-4080-935e-e74ce20e2805", - "type": "main_model_loader", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "model": { - "id": "54e737f9-2547-4bd9-a607-733d02f0c990", - "name": "model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MainModelField" - }, - "value": { - "model_name": "epicrealism_naturalSinRC1VAE", - "base_model": "sd-1", - "model_type": "main" - } - } - }, - "outputs": { - "unet": { - "id": "3483ea21-f0b3-4422-894b-36c5d7701365", - "name": "unet", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "clip": { - "id": "dddd055f-5c1b-4e61-977b-6393da9006fa", - "name": "clip", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "vae": { - "id": "879893b4-3415-4879-8dff-aa1727ef5e63", - "name": "vae", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - } - } - }, - "width": 320, - "height": 227, - "position": { - "x": 2050, - "y": -525 - } - }, - { - "id": "f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65", - "type": "invocation", - "data": { - "id": "f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65", - "type": "compel", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "916b229a-38e1-45a2-a433-cca97495b143", - "name": "prompt", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "clip": { - "id": "ae9aeb1a-4ebd-4bc3-b6e6-a8c9adca01f6", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "4d59bad1-99a9-43e2-bdb4-7a0f3dd5b787", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 256, - "position": { - "x": 2550, - "y": -525 - } - }, - { - "id": "22b750db-b85e-486b-b278-ac983e329813", - "type": "invocation", - "data": { - "id": "22b750db-b85e-486b-b278-ac983e329813", - "type": "ip_adapter", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.1.0", - "nodePack": "invokeai", - "inputs": { - "image": { - "id": "088a126c-ab1d-4c7a-879a-c1eea09eac8e", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "ip_adapter_model": { - "id": "f2ac529f-f778-4a12-af12-0c7e449de17a", - "name": "ip_adapter_model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IPAdapterModelField" - }, - "value": { - "model_name": "ip_adapter_plus_face_sd15", - "base_model": "sd-1" - } - }, - "weight": { - "id": "ddb4a7cb-607d-47e8-b46b-cc1be27ebde0", - "name": "weight", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0.5 - }, - "begin_step_percent": { - "id": "1807371f-b56c-4777-baa2-de71e21f0b80", - "name": "begin_step_percent", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "end_step_percent": { - "id": "4ea8e4dc-9cd7-445e-9b32-8934c652381b", - "name": "end_step_percent", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0.8 - } - }, - "outputs": { - "ip_adapter": { - "id": "739b5fed-d813-4611-8f87-1dded25a7619", - "name": "ip_adapter", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IPAdapterField" - } - } - } - }, - "width": 320, - "height": 396, - "position": { - "x": 3575, - "y": -200 - } - }, - { - "id": "f60b6161-8f26-42f6-89ff-545e6011e501", - "type": "invocation", - "data": { - "id": "f60b6161-8f26-42f6-89ff-545e6011e501", - "type": "controlnet", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.1.0", - "nodePack": "invokeai", - "inputs": { - "image": { - "id": "96434c75-abd8-4b73-ab82-0b358e4735bf", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "control_model": { - "id": "21551ac2-ee50-4fe8-b06c-5be00680fb5c", - "name": "control_model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ControlNetModelField" - }, - "value": { - "model_name": "canny", - "base_model": "sd-1" - } - }, - "control_weight": { - "id": "1dacac0a-b985-4bdf-b4b5-b960f4cff6ed", - "name": "control_weight", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 0.5 - }, - "begin_step_percent": { - "id": "b2a3f128-7fc1-4c12-acc8-540f013c856b", - "name": "begin_step_percent", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "end_step_percent": { - "id": "0e701834-f7ba-4a6e-b9cb-6d4aff5dacd8", - "name": "end_step_percent", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0.5 - }, - "control_mode": { - "id": "f9a5f038-ae80-4b6e-8a48-362a2c858299", - "name": "control_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "balanced" - }, - "resize_mode": { - "id": "5369dd44-a708-4b66-8182-fea814d2a0ae", - "name": "resize_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "just_resize" - } - }, - "outputs": { - "control": { - "id": "f470a1af-7b68-4849-a144-02bc345fd810", - "name": "control", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ControlField" - } - } - } - }, - "width": 320, - "height": 511, - "position": { - "x": 3950, - "y": 150 - } - }, - { - "id": "8fe598c6-d447-44fa-a165-4975af77d080", - "type": "invocation", - "data": { - "id": "8fe598c6-d447-44fa-a165-4975af77d080", - "type": "canny_image_processor", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "855f4575-309a-4810-bd02-7c4a04e0efc8", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "7d2695fa-a617-431a-bc0e-69ac7c061651", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "low_threshold": { - "id": "ceb37a49-c989-4afa-abbf-49b6e52ef663", - "name": "low_threshold", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 100 - }, - "high_threshold": { - "id": "6ec118f8-ca6c-4be7-9951-6eee58afbc1b", - "name": "high_threshold", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 200 - } - }, - "outputs": { - "image": { - "id": "264bcaaf-d185-43ce-9e1a-b6f707140c0c", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "0d374d8e-d5c7-49be-9057-443e32e45048", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "fa892b68-6135-4598-a765-e79cd5c6e3f6", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 340, - "position": { - "x": 3519.4131037388597, - "y": 576.7946795840575 - } - }, - { - "id": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a", - "type": "invocation", - "data": { - "id": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a", - "type": "img_scale", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "46fc750e-7dda-4145-8f72-be88fb93f351", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "f7f41bb3-9a5a-4022-b671-0acc2819f7c3", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "scale_factor": { - "id": "1ae95574-f725-4cbc-bb78-4c9db240f78a", - "name": "scale_factor", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1.5 - }, - "resample_mode": { - "id": "0756e37d-3d01-4c2c-9364-58e8978b04a2", - "name": "resample_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "bicubic" - } - }, - "outputs": { - "image": { - "id": "2729e697-cacc-4874-94bf-3aee5c18b5f9", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "d9551981-cbd3-419a-bcb9-5b50600e8c18", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "3355c99e-cdc6-473b-a7c6-a9d1dcc941e5", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 340, - "position": { - "x": 3079.916484101321, - "y": 151.0148192064986 - } - }, - { - "id": "381d5b6a-f044-48b0-bc07-6138fbfa8dfc", - "type": "invocation", - "data": { - "id": "381d5b6a-f044-48b0-bc07-6138fbfa8dfc", - "type": "mask_combine", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "061ea794-2494-429e-bfdd-5fbd2c7eeb99", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "mask1": { - "id": "446c6c99-9cb5-4035-a452-ab586ef4ede9", - "name": "mask1", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "mask2": { - "id": "ebbe37b8-8bf3-4520-b6f5-631fe2d05d66", - "name": "mask2", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - } - }, - "outputs": { - "image": { - "id": "7c33cfae-ea9a-4572-91ca-40fc4112369f", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "7410c10c-3060-44a9-b399-43555c3e1156", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "3eb74c96-ae3e-4091-b027-703428244fdf", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 283, - "position": { - "x": 5450, - "y": 250 - } - }, - { - "id": "77da4e4d-5778-4469-8449-ffed03d54bdb", - "type": "invocation", - "data": { - "id": "77da4e4d-5778-4469-8449-ffed03d54bdb", - "type": "img_blur", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "755d4fa2-a520-4837-a3da-65e1f479e6e6", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "a4786d7d-9593-4f5f-830b-d94bb0e42bca", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "radius": { - "id": "e1c9afa0-4d41-4664-b560-e1e85f467267", - "name": "radius", - "fieldKind": "input", - "label": "Mask Blue", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 150 - }, - "blur_type": { - "id": "f6231886-0981-444b-bd26-24674f87e7cb", - "name": "blur_type", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "gaussian" - } - }, - "outputs": { - "image": { - "id": "20c7bcfc-269d-4119-8b45-6c59dd4005d7", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "37e228ef-cb59-4477-b18f-343609d7bb4e", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "2de6080e-8bc3-42cd-bb8a-afd72f082df4", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 340, - "position": { - "x": 5000, - "y": 300 - } - }, - { - "id": "f0de6c44-4515-4f79-bcc0-dee111bcfe31", - "type": "invocation", - "data": { - "id": "f0de6c44-4515-4f79-bcc0-dee111bcfe31", - "type": "float", - "label": "Face Detail Scale", - "isOpen": false, - "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", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "value": { - "id": "9b51a26f-af3c-4caa-940a-5183234b1ed7", - "name": "value", - "fieldKind": "input", - "label": "Face Detail Scale", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1.5 - } - }, - "outputs": { - "value": { - "id": "c7c87b77-c149-4e9c-8ed1-beb1ba013055", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 2550, - "y": 125 - } - }, - { - "id": "a6482723-4e0a-4e40-98c0-b20622bf5f16", - "type": "invocation", - "data": { - "id": "a6482723-4e0a-4e40-98c0-b20622bf5f16", - "type": "face_mask_detection", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "inputs": { - "metadata": { - "id": "44e1fca6-3f06-4698-9562-6743c1f7a739", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "65ba4653-8c35-403d-838b-ddac075e4118", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "face_ids": { - "id": "630e86e5-3041-47c4-8864-b8ee71ec7da5", - "name": "face_ids", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "minimum_confidence": { - "id": "8c4aef57-90c6-4904-9113-7189db1357c9", - "name": "minimum_confidence", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0.5 - }, - "x_offset": { - "id": "2d409495-4aee-41e2-9688-f37fb6add537", - "name": "x_offset", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "y_offset": { - "id": "de3c1e68-6962-4520-b672-989afff1d2a1", - "name": "y_offset", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "chunk": { - "id": "9a11971a-0759-4782-902a-8300070d8861", - "name": "chunk", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - }, - "invert_mask": { - "id": "2abecba7-35d0-4cc4-9732-769e97d34c75", - "name": "invert_mask", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - } - }, - "outputs": { - "image": { - "id": "af33585c-5451-4738-b1da-7ff83afdfbf8", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "52cec0bd-d2e5-4514-8ebf-1c0d8bd1b81d", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "05edbbc8-99df-42e0-9b9b-de7b555e44cc", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "mask": { - "id": "cf928567-3e05-42dd-9591-0d3d942034f8", - "name": "mask", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - } - } - }, - "width": 320, - "height": 585, - "position": { - "x": 4300, - "y": 600 - } - } - ], - "edges": [ - { - "id": "50a8db6a-3796-4522-8547-53275efa4e7d-de8b1a48-a2e4-42ca-90bb-66058bffd534-collapsed", - "source": "50a8db6a-3796-4522-8547-53275efa4e7d", - "target": "de8b1a48-a2e4-42ca-90bb-66058bffd534", - "type": "collapsed" - }, - { - "id": "f0de6c44-4515-4f79-bcc0-dee111bcfe31-2974e5b3-3d41-4b6f-9953-cd21e8f3a323-collapsed", - "source": "f0de6c44-4515-4f79-bcc0-dee111bcfe31", - "target": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323", - "type": "collapsed" - }, - { - "id": "de8b1a48-a2e4-42ca-90bb-66058bffd534-2974e5b3-3d41-4b6f-9953-cd21e8f3a323-collapsed", - "source": "de8b1a48-a2e4-42ca-90bb-66058bffd534", - "target": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323", - "type": "collapsed" - }, - { - "id": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323-35623411-ba3a-4eaa-91fd-1e0fda0a5b42-collapsed", - "source": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323", - "target": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42", - "type": "collapsed" - }, - { - "id": "c865f39f-f830-4ed7-88a5-e935cfe050a9-35623411-ba3a-4eaa-91fd-1e0fda0a5b42-collapsed", - "source": "c865f39f-f830-4ed7-88a5-e935cfe050a9", - "target": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42", - "type": "collapsed" - }, - { - "id": "reactflow__edge-2c9bc2a6-6c03-4861-aad4-db884a7682f8image-9ae34718-a17d-401d-9859-086896c29fcaimage", - "source": "2c9bc2a6-6c03-4861-aad4-db884a7682f8", - "target": "9ae34718-a17d-401d-9859-086896c29fca", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcaimage-50a8db6a-3796-4522-8547-53275efa4e7dimage", - "source": "9ae34718-a17d-401d-9859-086896c29fca", - "target": "50a8db6a-3796-4522-8547-53275efa4e7d", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-35623411-ba3a-4eaa-91fd-1e0fda0a5b42noise-bd06261d-a74a-4d1f-8374-745ed6194bc2noise", - "source": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42", - "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "type": "default", - "sourceHandle": "noise", - "targetHandle": "noise" - }, - { - "id": "reactflow__edge-de8b1a48-a2e4-42ca-90bb-66058bffd534latents-2974e5b3-3d41-4b6f-9953-cd21e8f3a323latents", - "source": "de8b1a48-a2e4-42ca-90bb-66058bffd534", - "target": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323", - "type": "default", - "sourceHandle": "latents", - "targetHandle": "latents" - }, - { - "id": "reactflow__edge-2974e5b3-3d41-4b6f-9953-cd21e8f3a323latents-bd06261d-a74a-4d1f-8374-745ed6194bc2latents", - "source": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323", - "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "type": "default", - "sourceHandle": "latents", - "targetHandle": "latents" - }, - { - "id": "reactflow__edge-2974e5b3-3d41-4b6f-9953-cd21e8f3a323width-35623411-ba3a-4eaa-91fd-1e0fda0a5b42width", - "source": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323", - "target": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42", - "type": "default", - "sourceHandle": "width", - "targetHandle": "width" - }, - { - "id": "reactflow__edge-2974e5b3-3d41-4b6f-9953-cd21e8f3a323height-35623411-ba3a-4eaa-91fd-1e0fda0a5b42height", - "source": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323", - "target": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42", - "type": "default", - "sourceHandle": "height", - "targetHandle": "height" - }, - { - "id": "reactflow__edge-2c9bc2a6-6c03-4861-aad4-db884a7682f8image-a7d14545-aa09-4b96-bfc5-40c009af9110base_image", - "source": "2c9bc2a6-6c03-4861-aad4-db884a7682f8", - "target": "a7d14545-aa09-4b96-bfc5-40c009af9110", - "type": "default", - "sourceHandle": "image", - "targetHandle": "base_image" - }, - { - "id": "reactflow__edge-2224ed72-2453-4252-bd89-3085240e0b6fimage-ff8c23dc-da7c-45b7-b5c9-d984b12f02efimage", - "source": "2224ed72-2453-4252-bd89-3085240e0b6f", - "target": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcawidth-ff8c23dc-da7c-45b7-b5c9-d984b12f02efwidth", - "source": "9ae34718-a17d-401d-9859-086896c29fca", - "target": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef", - "type": "default", - "sourceHandle": "width", - "targetHandle": "width" - }, - { - "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcaheight-ff8c23dc-da7c-45b7-b5c9-d984b12f02efheight", - "source": "9ae34718-a17d-401d-9859-086896c29fca", - "target": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef", - "type": "default", - "sourceHandle": "height", - "targetHandle": "height" - }, - { - "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcax-a7d14545-aa09-4b96-bfc5-40c009af9110x", - "source": "9ae34718-a17d-401d-9859-086896c29fca", - "target": "a7d14545-aa09-4b96-bfc5-40c009af9110", - "type": "default", - "sourceHandle": "x", - "targetHandle": "x" - }, - { - "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcay-a7d14545-aa09-4b96-bfc5-40c009af9110y", - "source": "9ae34718-a17d-401d-9859-086896c29fca", - "target": "a7d14545-aa09-4b96-bfc5-40c009af9110", - "type": "default", - "sourceHandle": "y", - "targetHandle": "y" - }, - { - "id": "reactflow__edge-50a8db6a-3796-4522-8547-53275efa4e7dimage-de8b1a48-a2e4-42ca-90bb-66058bffd534image", - "source": "50a8db6a-3796-4522-8547-53275efa4e7d", - "target": "de8b1a48-a2e4-42ca-90bb-66058bffd534", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-cdfa5ab0-b3e2-43ed-85bb-2ac4aa83bc05value-bd06261d-a74a-4d1f-8374-745ed6194bc2denoising_start", - "source": "cdfa5ab0-b3e2-43ed-85bb-2ac4aa83bc05", - "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "type": "default", - "sourceHandle": "value", - "targetHandle": "denoising_start" - }, - { - "id": "reactflow__edge-64712037-92e8-483f-9f6e-87588539c1b8value-bd06261d-a74a-4d1f-8374-745ed6194bc2cfg_scale", - "source": "64712037-92e8-483f-9f6e-87588539c1b8", - "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "type": "default", - "sourceHandle": "value", - "targetHandle": "cfg_scale" - }, - { - "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcawidth-c59e815c-1f3a-4e2b-b6b8-66f4b005e955width", - "source": "9ae34718-a17d-401d-9859-086896c29fca", - "target": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955", - "type": "default", - "sourceHandle": "width", - "targetHandle": "width" - }, - { - "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcaheight-c59e815c-1f3a-4e2b-b6b8-66f4b005e955height", - "source": "9ae34718-a17d-401d-9859-086896c29fca", - "target": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955", - "type": "default", - "sourceHandle": "height", - "targetHandle": "height" - }, - { - "id": "reactflow__edge-ff8c23dc-da7c-45b7-b5c9-d984b12f02efimage-a7d14545-aa09-4b96-bfc5-40c009af9110image", - "source": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef", - "target": "a7d14545-aa09-4b96-bfc5-40c009af9110", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-bd06261d-a74a-4d1f-8374-745ed6194bc2latents-2224ed72-2453-4252-bd89-3085240e0b6flatents", - "source": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "target": "2224ed72-2453-4252-bd89-3085240e0b6f", - "type": "default", - "sourceHandle": "latents", - "targetHandle": "latents" - }, - { - "id": "reactflow__edge-c865f39f-f830-4ed7-88a5-e935cfe050a9value-35623411-ba3a-4eaa-91fd-1e0fda0a5b42seed", - "source": "c865f39f-f830-4ed7-88a5-e935cfe050a9", - "target": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42", - "type": "default", - "sourceHandle": "value", - "targetHandle": "seed" - }, - { - "id": "reactflow__edge-76ea1e31-eabe-4080-935e-e74ce20e2805clip-f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65clip", - "source": "76ea1e31-eabe-4080-935e-e74ce20e2805", - "target": "f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-76ea1e31-eabe-4080-935e-e74ce20e2805vae-de8b1a48-a2e4-42ca-90bb-66058bffd534vae", - "source": "76ea1e31-eabe-4080-935e-e74ce20e2805", - "target": "de8b1a48-a2e4-42ca-90bb-66058bffd534", - "type": "default", - "sourceHandle": "vae", - "targetHandle": "vae" - }, - { - "id": "reactflow__edge-f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65conditioning-bd06261d-a74a-4d1f-8374-745ed6194bc2positive_conditioning", - "source": "f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65", - "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "positive_conditioning" - }, - { - "id": "reactflow__edge-44f2c190-eb03-460d-8d11-a94d13b33f19conditioning-bd06261d-a74a-4d1f-8374-745ed6194bc2negative_conditioning", - "source": "44f2c190-eb03-460d-8d11-a94d13b33f19", - "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "negative_conditioning" - }, - { - "id": "reactflow__edge-76ea1e31-eabe-4080-935e-e74ce20e2805unet-bd06261d-a74a-4d1f-8374-745ed6194bc2unet", - "source": "76ea1e31-eabe-4080-935e-e74ce20e2805", - "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "type": "default", - "sourceHandle": "unet", - "targetHandle": "unet" - }, - { - "id": "reactflow__edge-76ea1e31-eabe-4080-935e-e74ce20e2805vae-2224ed72-2453-4252-bd89-3085240e0b6fvae", - "source": "76ea1e31-eabe-4080-935e-e74ce20e2805", - "target": "2224ed72-2453-4252-bd89-3085240e0b6f", - "type": "default", - "sourceHandle": "vae", - "targetHandle": "vae" - }, - { - "id": "reactflow__edge-76ea1e31-eabe-4080-935e-e74ce20e2805clip-44f2c190-eb03-460d-8d11-a94d13b33f19clip", - "source": "76ea1e31-eabe-4080-935e-e74ce20e2805", - "target": "44f2c190-eb03-460d-8d11-a94d13b33f19", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-22b750db-b85e-486b-b278-ac983e329813ip_adapter-bd06261d-a74a-4d1f-8374-745ed6194bc2ip_adapter", - "source": "22b750db-b85e-486b-b278-ac983e329813", - "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "type": "default", - "sourceHandle": "ip_adapter", - "targetHandle": "ip_adapter" - }, - { - "id": "reactflow__edge-50a8db6a-3796-4522-8547-53275efa4e7dimage-4bd4ae80-567f-4366-b8c6-3bb06f4fb46aimage", - "source": "50a8db6a-3796-4522-8547-53275efa4e7d", - "target": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-4bd4ae80-567f-4366-b8c6-3bb06f4fb46aimage-22b750db-b85e-486b-b278-ac983e329813image", - "source": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a", - "target": "22b750db-b85e-486b-b278-ac983e329813", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-8fe598c6-d447-44fa-a165-4975af77d080image-f60b6161-8f26-42f6-89ff-545e6011e501image", - "source": "8fe598c6-d447-44fa-a165-4975af77d080", - "target": "f60b6161-8f26-42f6-89ff-545e6011e501", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-4bd4ae80-567f-4366-b8c6-3bb06f4fb46aimage-8fe598c6-d447-44fa-a165-4975af77d080image", - "source": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a", - "target": "8fe598c6-d447-44fa-a165-4975af77d080", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-f60b6161-8f26-42f6-89ff-545e6011e501control-bd06261d-a74a-4d1f-8374-745ed6194bc2control", - "source": "f60b6161-8f26-42f6-89ff-545e6011e501", - "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2", - "type": "default", - "sourceHandle": "control", - "targetHandle": "control" - }, - { - "id": "reactflow__edge-c59e815c-1f3a-4e2b-b6b8-66f4b005e955image-381d5b6a-f044-48b0-bc07-6138fbfa8dfcmask2", - "source": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955", - "target": "381d5b6a-f044-48b0-bc07-6138fbfa8dfc", - "type": "default", - "sourceHandle": "image", - "targetHandle": "mask2" - }, - { - "id": "reactflow__edge-381d5b6a-f044-48b0-bc07-6138fbfa8dfcimage-a7d14545-aa09-4b96-bfc5-40c009af9110mask", - "source": "381d5b6a-f044-48b0-bc07-6138fbfa8dfc", - "target": "a7d14545-aa09-4b96-bfc5-40c009af9110", - "type": "default", - "sourceHandle": "image", - "targetHandle": "mask" - }, - { - "id": "reactflow__edge-77da4e4d-5778-4469-8449-ffed03d54bdbimage-381d5b6a-f044-48b0-bc07-6138fbfa8dfcmask1", - "source": "77da4e4d-5778-4469-8449-ffed03d54bdb", - "target": "381d5b6a-f044-48b0-bc07-6138fbfa8dfc", - "type": "default", - "sourceHandle": "image", - "targetHandle": "mask1" - }, - { - "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcamask-77da4e4d-5778-4469-8449-ffed03d54bdbimage", - "source": "9ae34718-a17d-401d-9859-086896c29fca", - "target": "77da4e4d-5778-4469-8449-ffed03d54bdb", - "type": "default", - "sourceHandle": "mask", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-f0de6c44-4515-4f79-bcc0-dee111bcfe31value-2974e5b3-3d41-4b6f-9953-cd21e8f3a323scale_factor", - "source": "f0de6c44-4515-4f79-bcc0-dee111bcfe31", - "target": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323", - "type": "default", - "sourceHandle": "value", - "targetHandle": "scale_factor" - }, - { - "id": "reactflow__edge-f0de6c44-4515-4f79-bcc0-dee111bcfe31value-4bd4ae80-567f-4366-b8c6-3bb06f4fb46ascale_factor", - "source": "f0de6c44-4515-4f79-bcc0-dee111bcfe31", - "target": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a", - "type": "default", - "sourceHandle": "value", - "targetHandle": "scale_factor" - }, - { - "id": "reactflow__edge-2c9bc2a6-6c03-4861-aad4-db884a7682f8image-a6482723-4e0a-4e40-98c0-b20622bf5f16image", - "source": "2c9bc2a6-6c03-4861-aad4-db884a7682f8", - "target": "a6482723-4e0a-4e40-98c0-b20622bf5f16", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-a6482723-4e0a-4e40-98c0-b20622bf5f16image-c59e815c-1f3a-4e2b-b6b8-66f4b005e955image", - "source": "a6482723-4e0a-4e40-98c0-b20622bf5f16", - "target": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - } - ] -} \ No newline at end of file diff --git a/docs/workflows/Multi_ControlNet_Canny_and_Depth.json b/docs/workflows/Multi_ControlNet_Canny_and_Depth.json deleted file mode 100644 index 70dc2047725..00000000000 --- a/docs/workflows/Multi_ControlNet_Canny_and_Depth.json +++ /dev/null @@ -1,1480 +0,0 @@ -{ - "id": "1e385b84-86f8-452e-9697-9e5abed20518", - "name": "Multi ControlNet (Canny & Depth)", - "author": "InvokeAI", - "description": "A sample workflow using canny & depth ControlNets to guide the generation process. ", - "version": "1.0.0", - "contact": "invoke@invoke.ai", - "tags": "ControlNet, canny, depth", - "notes": "", - "exposedFields": [ - { - "nodeId": "54486974-835b-4d81-8f82-05f9f32ce9e9", - "fieldName": "model" - }, - { - "nodeId": "7ce68934-3419-42d4-ac70-82cfc9397306", - "fieldName": "prompt" - }, - { - "nodeId": "273e3f96-49ea-4dc5-9d5b-9660390f14e1", - "fieldName": "prompt" - }, - { - "nodeId": "c4b23e64-7986-40c4-9cad-46327b12e204", - "fieldName": "image" - }, - { - "nodeId": "8e860e51-5045-456e-bf04-9a62a2a5c49e", - "fieldName": "image" - } - ], - "meta": { - "category": "default", - "version": "2.0.0" - }, - "nodes": [ - { - "id": "8e860e51-5045-456e-bf04-9a62a2a5c49e", - "type": "invocation", - "data": { - "id": "8e860e51-5045-456e-bf04-9a62a2a5c49e", - "type": "image", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "image": { - "id": "189c8adf-68cc-4774-a729-49da89f6fdf1", - "name": "image", - "fieldKind": "input", - "label": "Depth Input Image", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - } - }, - "outputs": { - "image": { - "id": "1a31cacd-9d19-4f32-b558-c5e4aa39ce73", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "12f298fd-1d11-4cca-9426-01240f7ec7cf", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "c47dabcb-44e8-40c9-992d-81dca59f598e", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 225, - "position": { - "x": 3625, - "y": -75 - } - }, - { - "id": "a33199c2-8340-401e-b8a2-42ffa875fc1c", - "type": "invocation", - "data": { - "id": "a33199c2-8340-401e-b8a2-42ffa875fc1c", - "type": "controlnet", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.1.0", - "nodePack": "invokeai", - "inputs": { - "image": { - "id": "4e0a3172-d3c2-4005-a84c-fa12a404f8a0", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "control_model": { - "id": "8cb2d998-4086-430a-8b13-94cbc81e3ca3", - "name": "control_model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ControlNetModelField" - }, - "value": { - "model_name": "depth", - "base_model": "sd-1" - } - }, - "control_weight": { - "id": "5e32bd8a-9dc8-42d8-9bcc-c2b0460c0b0f", - "name": "control_weight", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 1 - }, - "begin_step_percent": { - "id": "c258a276-352a-416c-8358-152f11005c0c", - "name": "begin_step_percent", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "end_step_percent": { - "id": "43001125-0d70-4f87-8e79-da6603ad6c33", - "name": "end_step_percent", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1 - }, - "control_mode": { - "id": "d2f14561-9443-4374-9270-e2f05007944e", - "name": "control_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "balanced" - }, - "resize_mode": { - "id": "727ee7d3-8bf6-4c7d-8b8a-43546b3b59cd", - "name": "resize_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "just_resize" - } - }, - "outputs": { - "control": { - "id": "b034aa0f-4d0d-46e4-b5e3-e25a9588d087", - "name": "control", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ControlField" - } - } - } - }, - "width": 320, - "height": 511, - "position": { - "x": 4477.604342844504, - "y": -49.39005411272677 - } - }, - { - "id": "273e3f96-49ea-4dc5-9d5b-9660390f14e1", - "type": "invocation", - "data": { - "id": "273e3f96-49ea-4dc5-9d5b-9660390f14e1", - "type": "compel", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "7c2c4771-2161-4d77-aced-ff8c4b3f1c15", - "name": "prompt", - "fieldKind": "input", - "label": "Negative Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "clip": { - "id": "06d59e91-9cca-411d-bf05-86b099b3e8f7", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "858bc33c-134c-4bf6-8855-f943e1d26f14", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 256, - "position": { - "x": 4075, - "y": -825 - } - }, - { - "id": "54486974-835b-4d81-8f82-05f9f32ce9e9", - "type": "invocation", - "data": { - "id": "54486974-835b-4d81-8f82-05f9f32ce9e9", - "type": "main_model_loader", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "model": { - "id": "f4a915a5-593e-4b6d-9198-c78eb5cefaed", - "name": "model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MainModelField" - }, - "value": { - "model_name": "stable-diffusion-v1-5", - "base_model": "sd-1", - "model_type": "main" - } - } - }, - "outputs": { - "unet": { - "id": "ee24fb16-da38-4c66-9fbc-e8f296ed40d2", - "name": "unet", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "clip": { - "id": "f3fb0524-8803-41c1-86db-a61a13ee6a33", - "name": "clip", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "vae": { - "id": "5c4878a8-b40f-44ab-b146-1c1f42c860b3", - "name": "vae", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - } - } - }, - "width": 320, - "height": 227, - "position": { - "x": 3600, - "y": -1000 - } - }, - { - "id": "7ce68934-3419-42d4-ac70-82cfc9397306", - "type": "invocation", - "data": { - "id": "7ce68934-3419-42d4-ac70-82cfc9397306", - "type": "compel", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "7c2c4771-2161-4d77-aced-ff8c4b3f1c15", - "name": "prompt", - "fieldKind": "input", - "label": "Positive Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "clip": { - "id": "06d59e91-9cca-411d-bf05-86b099b3e8f7", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "858bc33c-134c-4bf6-8855-f943e1d26f14", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 256, - "position": { - "x": 4075, - "y": -1125 - } - }, - { - "id": "d204d184-f209-4fae-a0a1-d152800844e1", - "type": "invocation", - "data": { - "id": "d204d184-f209-4fae-a0a1-d152800844e1", - "type": "controlnet", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.1.0", - "nodePack": "invokeai", - "inputs": { - "image": { - "id": "4e0a3172-d3c2-4005-a84c-fa12a404f8a0", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "control_model": { - "id": "8cb2d998-4086-430a-8b13-94cbc81e3ca3", - "name": "control_model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ControlNetModelField" - }, - "value": { - "model_name": "canny", - "base_model": "sd-1" - } - }, - "control_weight": { - "id": "5e32bd8a-9dc8-42d8-9bcc-c2b0460c0b0f", - "name": "control_weight", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 1 - }, - "begin_step_percent": { - "id": "c258a276-352a-416c-8358-152f11005c0c", - "name": "begin_step_percent", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "end_step_percent": { - "id": "43001125-0d70-4f87-8e79-da6603ad6c33", - "name": "end_step_percent", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1 - }, - "control_mode": { - "id": "d2f14561-9443-4374-9270-e2f05007944e", - "name": "control_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "balanced" - }, - "resize_mode": { - "id": "727ee7d3-8bf6-4c7d-8b8a-43546b3b59cd", - "name": "resize_mode", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "EnumField" - }, - "value": "just_resize" - } - }, - "outputs": { - "control": { - "id": "b034aa0f-4d0d-46e4-b5e3-e25a9588d087", - "name": "control", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ControlField" - } - } - } - }, - "width": 320, - "height": 511, - "position": { - "x": 4479.68542130465, - "y": -618.4221638099414 - } - }, - { - "id": "c4b23e64-7986-40c4-9cad-46327b12e204", - "type": "invocation", - "data": { - "id": "c4b23e64-7986-40c4-9cad-46327b12e204", - "type": "image", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "image": { - "id": "189c8adf-68cc-4774-a729-49da89f6fdf1", - "name": "image", - "fieldKind": "input", - "label": "Canny Input Image", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - } - }, - "outputs": { - "image": { - "id": "1a31cacd-9d19-4f32-b558-c5e4aa39ce73", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "12f298fd-1d11-4cca-9426-01240f7ec7cf", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "c47dabcb-44e8-40c9-992d-81dca59f598e", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 225, - "position": { - "x": 3625, - "y": -425 - } - }, - { - "id": "ca4d5059-8bfb-447f-b415-da0faba5a143", - "type": "invocation", - "data": { - "id": "ca4d5059-8bfb-447f-b415-da0faba5a143", - "type": "collect", - "label": "ControlNet Collection", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "inputs": { - "item": { - "id": "b16ae602-8708-4b1b-8d4f-9e0808d429ab", - "name": "item", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "CollectionItemField" - } - } - }, - "outputs": { - "collection": { - "id": "d8987dd8-dec8-4d94-816a-3e356af29884", - "name": "collection", - "fieldKind": "output", - "type": { - "isCollection": true, - "isCollectionOrScalar": false, - "name": "CollectionField" - } - } - } - }, - "width": 320, - "height": 104, - "position": { - "x": 4875, - "y": -575 - } - }, - { - "id": "018b1214-c2af-43a7-9910-fb687c6726d7", - "type": "invocation", - "data": { - "id": "018b1214-c2af-43a7-9910-fb687c6726d7", - "type": "midas_depth_image_processor", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "77f91980-c696-4a18-a9ea-6e2fc329a747", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "50710a20-2af5-424d-9d17-aa08167829c6", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "a_mult": { - "id": "f3b26f9d-2498-415e-9c01-197a8d06c0a5", - "name": "a_mult", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 2 - }, - "bg_th": { - "id": "4b1eb3ae-9d4a-47d6-b0ed-da62501e007f", - "name": "bg_th", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0.1 - } - }, - "outputs": { - "image": { - "id": "b4ed637c-c4a0-4fdd-a24e-36d6412e4ccf", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "6bf9b609-d72c-4239-99bd-390a73cc3a9c", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "3e8aef09-cf44-4e3e-a490-d3c9e7b23119", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 340, - "position": { - "x": 4100, - "y": -75 - } - }, - { - "id": "c826ba5e-9676-4475-b260-07b85e88753c", - "type": "invocation", - "data": { - "id": "c826ba5e-9676-4475-b260-07b85e88753c", - "type": "canny_image_processor", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "08331ea6-99df-4e61-a919-204d9bfa8fb2", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "image": { - "id": "33a37284-06ac-459c-ba93-1655e4f69b2d", - "name": "image", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "low_threshold": { - "id": "21ec18a3-50c5-4ba1-9642-f921744d594f", - "name": "low_threshold", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 100 - }, - "high_threshold": { - "id": "ebeab271-a5ff-4c88-acfd-1d0271ab6ed4", - "name": "high_threshold", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 200 - } - }, - "outputs": { - "image": { - "id": "c0caadbf-883f-4cb4-a62d-626b9c81fc4e", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "df225843-8098-49c0-99d1-3b0b6600559f", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "e4abe0de-aa16-41f3-9cd7-968b49db5da3", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 340, - "position": { - "x": 4095.757337055795, - "y": -455.63440891935863 - } - }, - { - "id": "9db25398-c869-4a63-8815-c6559341ef12", - "type": "invocation", - "data": { - "id": "9db25398-c869-4a63-8815-c6559341ef12", - "type": "l2i", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": false, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "2f269793-72e5-4ff3-b76c-fab4f93e983f", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "latents": { - "id": "4aaedd3b-cc77-420c-806e-c7fa74ec4cdf", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "vae": { - "id": "432b066a-2462-4d18-83d9-64620b72df45", - "name": "vae", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - }, - "tiled": { - "id": "61f86e0f-7c46-40f8-b3f5-fe2f693595ca", - "name": "tiled", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - }, - "fp32": { - "id": "39b6c89a-37ef-4a7e-9509-daeca49d5092", - "name": "fp32", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - } - }, - "outputs": { - "image": { - "id": "6204e9b0-61dd-4250-b685-2092ba0e28e6", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "b4140649-8d5d-4d2d-bfa6-09e389ede5f9", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "f3a0c0c8-fc24-4646-8be1-ed8cdd140828", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 267, - "position": { - "x": 5675, - "y": -825 - } - }, - { - "id": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce", - "type": "invocation", - "data": { - "id": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce", - "type": "denoise_latents", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.5.0", - "nodePack": "invokeai", - "inputs": { - "positive_conditioning": { - "id": "869cd309-c238-444b-a1a0-5021f99785ba", - "name": "positive_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "negative_conditioning": { - "id": "343447b4-1e37-4e9e-8ac7-4d04864066af", - "name": "negative_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "noise": { - "id": "b556571e-0cf9-4e03-8cfc-5caad937d957", - "name": "noise", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "steps": { - "id": "a3b3d2de-9308-423e-b00d-c209c3e6e808", - "name": "steps", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 10 - }, - "cfg_scale": { - "id": "b13c50a4-ec7e-4579-b0ef-2fe5df2605ea", - "name": "cfg_scale", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 7.5 - }, - "denoising_start": { - "id": "57d5d755-f58f-4347-b991-f0bca4a0ab29", - "name": "denoising_start", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "denoising_end": { - "id": "323e78a6-880a-4d73-a62c-70faff965aa6", - "name": "denoising_end", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1 - }, - "scheduler": { - "id": "c25fdc17-a089-43ac-953e-067c45d5c76b", - "name": "scheduler", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SchedulerField" - }, - "value": "euler" - }, - "unet": { - "id": "6cde662b-e633-4569-b6b4-ec87c52c9c11", - "name": "unet", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "control": { - "id": "276a4df9-bb26-4505-a4d3-a94e18c7b541", - "name": "control", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "ControlField" - } - }, - "ip_adapter": { - "id": "48d40c51-b5e2-4457-a428-eef0696695e8", - "name": "ip_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "IPAdapterField" - } - }, - "t2i_adapter": { - "id": "75dd8af2-e7d7-48b4-a574-edd9f6e686ad", - "name": "t2i_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "T2IAdapterField" - } - }, - "cfg_rescale_multiplier": { - "id": "b90460cf-d0c9-4676-8909-2e8e22dc8ee5", - "name": "cfg_rescale_multiplier", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "latents": { - "id": "9223d67b-1dd7-4b34-a45f-ed0a725d9702", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "denoise_mask": { - "id": "4ee99177-6923-4b7f-8fe0-d721dd7cb05b", - "name": "denoise_mask", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "DenoiseMaskField" - } - } - }, - "outputs": { - "latents": { - "id": "7fb4e326-a974-43e8-9ee7-2e3ab235819d", - "name": "latents", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "6bb8acd0-8973-4195-a095-e376385dc705", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "795dea52-1c7d-4e64-99f7-2f60ec6e3ab9", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 705, - "position": { - "x": 5274.672987098195, - "y": -823.0752416664332 - } - }, - { - "id": "2e77a0a1-db6a-47a2-a8bf-1e003be6423b", - "type": "invocation", - "data": { - "id": "2e77a0a1-db6a-47a2-a8bf-1e003be6423b", - "type": "noise", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.1", - "inputs": { - "seed": { - "id": "96d7667a-9c56-4fb4-99db-868e2f08e874", - "name": "seed", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "width": { - "id": "1ce644ea-c9bf-48c5-9822-bdec0d2895c5", - "name": "width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "height": { - "id": "26d68b53-8a04-4db7-b0f8-57c9bddc0e49", - "name": "height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "use_cpu": { - "id": "cf8fb92e-2a8e-4cd5-baf5-4011e0ddfa22", - "name": "use_cpu", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": true - } - }, - "outputs": { - "noise": { - "id": "d9cb9305-6b3a-49a9-b27c-00fb3a58b85c", - "name": "noise", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "4ff28d00-ceee-42b8-90e7-f5e5a518376d", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "a6314b9c-346a-4aa6-9260-626ed46c060a", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 4875, - "y": -675 - } - }, - { - "id": "8b260b4d-3fd6-44d4-b1be-9f0e43c628ce", - "type": "invocation", - "data": { - "id": "8b260b4d-3fd6-44d4-b1be-9f0e43c628ce", - "type": "rand_int", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": false, - "version": "1.0.0", - "inputs": { - "low": { - "id": "a190ad12-a6bd-499b-a82a-100e09fe9aa4", - "name": "low", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "high": { - "id": "a085063f-b9ba-46f2-a21b-c46c321949aa", - "name": "high", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 2147483647 - } - }, - "outputs": { - "value": { - "id": "a15aff56-4874-47fe-be32-d66745ed2ab5", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 4875, - "y": -750 - } - } - ], - "edges": [ - { - "id": "8b260b4d-3fd6-44d4-b1be-9f0e43c628ce-2e77a0a1-db6a-47a2-a8bf-1e003be6423b-collapsed", - "source": "8b260b4d-3fd6-44d4-b1be-9f0e43c628ce", - "target": "2e77a0a1-db6a-47a2-a8bf-1e003be6423b", - "type": "collapsed" - }, - { - "id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9clip-7ce68934-3419-42d4-ac70-82cfc9397306clip", - "source": "54486974-835b-4d81-8f82-05f9f32ce9e9", - "target": "7ce68934-3419-42d4-ac70-82cfc9397306", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9clip-273e3f96-49ea-4dc5-9d5b-9660390f14e1clip", - "source": "54486974-835b-4d81-8f82-05f9f32ce9e9", - "target": "273e3f96-49ea-4dc5-9d5b-9660390f14e1", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-a33199c2-8340-401e-b8a2-42ffa875fc1ccontrol-ca4d5059-8bfb-447f-b415-da0faba5a143item", - "source": "a33199c2-8340-401e-b8a2-42ffa875fc1c", - "target": "ca4d5059-8bfb-447f-b415-da0faba5a143", - "type": "default", - "sourceHandle": "control", - "targetHandle": "item" - }, - { - "id": "reactflow__edge-d204d184-f209-4fae-a0a1-d152800844e1control-ca4d5059-8bfb-447f-b415-da0faba5a143item", - "source": "d204d184-f209-4fae-a0a1-d152800844e1", - "target": "ca4d5059-8bfb-447f-b415-da0faba5a143", - "type": "default", - "sourceHandle": "control", - "targetHandle": "item" - }, - { - "id": "reactflow__edge-8e860e51-5045-456e-bf04-9a62a2a5c49eimage-018b1214-c2af-43a7-9910-fb687c6726d7image", - "source": "8e860e51-5045-456e-bf04-9a62a2a5c49e", - "target": "018b1214-c2af-43a7-9910-fb687c6726d7", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-018b1214-c2af-43a7-9910-fb687c6726d7image-a33199c2-8340-401e-b8a2-42ffa875fc1cimage", - "source": "018b1214-c2af-43a7-9910-fb687c6726d7", - "target": "a33199c2-8340-401e-b8a2-42ffa875fc1c", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-c4b23e64-7986-40c4-9cad-46327b12e204image-c826ba5e-9676-4475-b260-07b85e88753cimage", - "source": "c4b23e64-7986-40c4-9cad-46327b12e204", - "target": "c826ba5e-9676-4475-b260-07b85e88753c", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-c826ba5e-9676-4475-b260-07b85e88753cimage-d204d184-f209-4fae-a0a1-d152800844e1image", - "source": "c826ba5e-9676-4475-b260-07b85e88753c", - "target": "d204d184-f209-4fae-a0a1-d152800844e1", - "type": "default", - "sourceHandle": "image", - "targetHandle": "image" - }, - { - "id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9vae-9db25398-c869-4a63-8815-c6559341ef12vae", - "source": "54486974-835b-4d81-8f82-05f9f32ce9e9", - "target": "9db25398-c869-4a63-8815-c6559341ef12", - "type": "default", - "sourceHandle": "vae", - "targetHandle": "vae" - }, - { - "id": "reactflow__edge-ac481b7f-08bf-4a9d-9e0c-3a82ea5243celatents-9db25398-c869-4a63-8815-c6559341ef12latents", - "source": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce", - "target": "9db25398-c869-4a63-8815-c6559341ef12", - "type": "default", - "sourceHandle": "latents", - "targetHandle": "latents" - }, - { - "id": "reactflow__edge-ca4d5059-8bfb-447f-b415-da0faba5a143collection-ac481b7f-08bf-4a9d-9e0c-3a82ea5243cecontrol", - "source": "ca4d5059-8bfb-447f-b415-da0faba5a143", - "target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce", - "type": "default", - "sourceHandle": "collection", - "targetHandle": "control" - }, - { - "id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9unet-ac481b7f-08bf-4a9d-9e0c-3a82ea5243ceunet", - "source": "54486974-835b-4d81-8f82-05f9f32ce9e9", - "target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce", - "type": "default", - "sourceHandle": "unet", - "targetHandle": "unet" - }, - { - "id": "reactflow__edge-273e3f96-49ea-4dc5-9d5b-9660390f14e1conditioning-ac481b7f-08bf-4a9d-9e0c-3a82ea5243cenegative_conditioning", - "source": "273e3f96-49ea-4dc5-9d5b-9660390f14e1", - "target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "negative_conditioning" - }, - { - "id": "reactflow__edge-7ce68934-3419-42d4-ac70-82cfc9397306conditioning-ac481b7f-08bf-4a9d-9e0c-3a82ea5243cepositive_conditioning", - "source": "7ce68934-3419-42d4-ac70-82cfc9397306", - "target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "positive_conditioning" - }, - { - "id": "reactflow__edge-2e77a0a1-db6a-47a2-a8bf-1e003be6423bnoise-ac481b7f-08bf-4a9d-9e0c-3a82ea5243cenoise", - "source": "2e77a0a1-db6a-47a2-a8bf-1e003be6423b", - "target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce", - "type": "default", - "sourceHandle": "noise", - "targetHandle": "noise" - }, - { - "id": "reactflow__edge-8b260b4d-3fd6-44d4-b1be-9f0e43c628cevalue-2e77a0a1-db6a-47a2-a8bf-1e003be6423bseed", - "source": "8b260b4d-3fd6-44d4-b1be-9f0e43c628ce", - "target": "2e77a0a1-db6a-47a2-a8bf-1e003be6423b", - "type": "default", - "sourceHandle": "value", - "targetHandle": "seed" - } - ] -} \ No newline at end of file diff --git a/docs/workflows/Prompt_from_File.json b/docs/workflows/Prompt_from_File.json deleted file mode 100644 index 08e76fd7932..00000000000 --- a/docs/workflows/Prompt_from_File.json +++ /dev/null @@ -1,975 +0,0 @@ -{ - "name": "Prompt from File", - "author": "InvokeAI", - "description": "Sample workflow using Prompt from File node", - "version": "0.1.0", - "contact": "invoke@invoke.ai", - "tags": "text2image, prompt from file, default", - "notes": "", - "exposedFields": [ - { - "nodeId": "d6353b7f-b447-4e17-8f2e-80a88c91d426", - "fieldName": "model" - }, - { - "nodeId": "1b7e0df8-8589-4915-a4ea-c0088f15d642", - "fieldName": "file_path" - } - ], - "meta": { - "category": "default", - "version": "2.0.0" - }, - "id": "d1609af5-eb0a-4f73-b573-c9af96a8d6bf", - "nodes": [ - { - "id": "c2eaf1ba-5708-4679-9e15-945b8b432692", - "type": "invocation", - "data": { - "id": "c2eaf1ba-5708-4679-9e15-945b8b432692", - "type": "compel", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "dcdf3f6d-9b96-4bcd-9b8d-f992fefe4f62", - "name": "prompt", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "clip": { - "id": "3f1981c9-d8a9-42eb-a739-4f120eb80745", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "46205e6c-c5e2-44cb-9c82-1cd20b95674a", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 925, - "y": -200 - } - }, - { - "id": "1b7e0df8-8589-4915-a4ea-c0088f15d642", - "type": "invocation", - "data": { - "id": "1b7e0df8-8589-4915-a4ea-c0088f15d642", - "type": "prompt_from_file", - "label": "Prompts from File", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.1", - "nodePack": "invokeai", - "inputs": { - "file_path": { - "id": "37e37684-4f30-4ec8-beae-b333e550f904", - "name": "file_path", - "fieldKind": "input", - "label": "Prompts File Path", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "pre_prompt": { - "id": "7de02feb-819a-4992-bad3-72a30920ddea", - "name": "pre_prompt", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "post_prompt": { - "id": "95f191d8-a282-428e-bd65-de8cb9b7513a", - "name": "post_prompt", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "start_line": { - "id": "efee9a48-05ab-4829-8429-becfa64a0782", - "name": "start_line", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1 - }, - "max_prompts": { - "id": "abebb428-3d3d-49fd-a482-4e96a16fff08", - "name": "max_prompts", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1 - } - }, - "outputs": { - "collection": { - "id": "77d5d7f1-9877-4ab1-9a8c-33e9ffa9abf3", - "name": "collection", - "fieldKind": "output", - "type": { - "isCollection": true, - "isCollectionOrScalar": false, - "name": "StringField" - } - } - } - }, - "width": 320, - "height": 580, - "position": { - "x": 475, - "y": -400 - } - }, - { - "id": "1b89067c-3f6b-42c8-991f-e3055789b251", - "type": "invocation", - "data": { - "id": "1b89067c-3f6b-42c8-991f-e3055789b251", - "type": "iterate", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.1.0", - "inputs": { - "collection": { - "id": "4c564bf8-5ed6-441e-ad2c-dda265d5785f", - "name": "collection", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": true, - "isCollectionOrScalar": false, - "name": "CollectionField" - } - } - }, - "outputs": { - "item": { - "id": "36340f9a-e7a5-4afa-b4b5-313f4e292380", - "name": "item", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "CollectionItemField" - } - }, - "index": { - "id": "1beca95a-2159-460f-97ff-c8bab7d89336", - "name": "index", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "total": { - "id": "ead597b8-108e-4eda-88a8-5c29fa2f8df9", - "name": "total", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 925, - "y": -400 - } - }, - { - "id": "d6353b7f-b447-4e17-8f2e-80a88c91d426", - "type": "invocation", - "data": { - "id": "d6353b7f-b447-4e17-8f2e-80a88c91d426", - "type": "main_model_loader", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "model": { - "id": "3f264259-3418-47d5-b90d-b6600e36ae46", - "name": "model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MainModelField" - }, - "value": { - "model_name": "stable-diffusion-v1-5", - "base_model": "sd-1", - "model_type": "main" - } - } - }, - "outputs": { - "unet": { - "id": "8e182ea2-9d0a-4c02-9407-27819288d4b5", - "name": "unet", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "clip": { - "id": "d67d9d30-058c-46d5-bded-3d09d6d1aa39", - "name": "clip", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "vae": { - "id": "89641601-0429-4448-98d5-190822d920d8", - "name": "vae", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - } - } - }, - "width": 320, - "height": 227, - "position": { - "x": 0, - "y": -375 - } - }, - { - "id": "fc9d0e35-a6de-4a19-84e1-c72497c823f6", - "type": "invocation", - "data": { - "id": "fc9d0e35-a6de-4a19-84e1-c72497c823f6", - "type": "compel", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "dcdf3f6d-9b96-4bcd-9b8d-f992fefe4f62", - "name": "prompt", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "clip": { - "id": "3f1981c9-d8a9-42eb-a739-4f120eb80745", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "46205e6c-c5e2-44cb-9c82-1cd20b95674a", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 925, - "y": -275 - } - }, - { - "id": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77", - "type": "invocation", - "data": { - "id": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77", - "type": "noise", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.1", - "nodePack": "invokeai", - "inputs": { - "seed": { - "id": "b722d84a-eeee-484f-bef2-0250c027cb67", - "name": "seed", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "width": { - "id": "d5f8ce11-0502-4bfc-9a30-5757dddf1f94", - "name": "width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "height": { - "id": "f187d5ff-38a5-4c3f-b780-fc5801ef34af", - "name": "height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "use_cpu": { - "id": "12f112b8-8b76-4816-b79e-662edc9f9aa5", - "name": "use_cpu", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": true - } - }, - "outputs": { - "noise": { - "id": "08576ad1-96d9-42d2-96ef-6f5c1961933f", - "name": "noise", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "f3e1f94a-258d-41ff-9789-bd999bd9f40d", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "6cefc357-4339-415e-a951-49b9c2be32f4", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 925, - "y": 25 - } - }, - { - "id": "dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5", - "type": "invocation", - "data": { - "id": "dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5", - "type": "rand_int", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": false, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "low": { - "id": "b9fc6cf1-469c-4037-9bf0-04836965826f", - "name": "low", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "high": { - "id": "06eac725-0f60-4ba2-b8cd-7ad9f757488c", - "name": "high", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 2147483647 - } - }, - "outputs": { - "value": { - "id": "df08c84e-7346-4e92-9042-9e5cb773aaff", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 925, - "y": -50 - } - }, - { - "id": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1", - "type": "invocation", - "data": { - "id": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1", - "type": "l2i", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "022e4b33-562b-438d-b7df-41c3fd931f40", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "latents": { - "id": "67cb6c77-a394-4a66-a6a9-a0a7dcca69ec", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "vae": { - "id": "7b3fd9ad-a4ef-4e04-89fa-3832a9902dbd", - "name": "vae", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - }, - "tiled": { - "id": "5ac5680d-3add-4115-8ec0-9ef5bb87493b", - "name": "tiled", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - }, - "fp32": { - "id": "db8297f5-55f8-452f-98cf-6572c2582152", - "name": "fp32", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - } - }, - "outputs": { - "image": { - "id": "d8778d0c-592a-4960-9280-4e77e00a7f33", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "c8b0a75a-f5de-4ff2-9227-f25bb2b97bec", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "83c05fbf-76b9-49ab-93c4-fa4b10e793e4", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 267, - "position": { - "x": 2037.861329274915, - "y": -329.8393457509562 - } - }, - { - "id": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e", - "type": "invocation", - "data": { - "id": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e", - "type": "denoise_latents", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.5.0", - "nodePack": "invokeai", - "inputs": { - "positive_conditioning": { - "id": "751fb35b-3f23-45ce-af1c-053e74251337", - "name": "positive_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "negative_conditioning": { - "id": "b9dc06b6-7481-4db1-a8c2-39d22a5eacff", - "name": "negative_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "noise": { - "id": "6e15e439-3390-48a4-8031-01e0e19f0e1d", - "name": "noise", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "steps": { - "id": "bfdfb3df-760b-4d51-b17b-0abb38b976c2", - "name": "steps", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 10 - }, - "cfg_scale": { - "id": "47770858-322e-41af-8494-d8b63ed735f3", - "name": "cfg_scale", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 7.5 - }, - "denoising_start": { - "id": "2ba78720-ee02-4130-a348-7bc3531f790b", - "name": "denoising_start", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "denoising_end": { - "id": "a874dffb-d433-4d1a-9f59-af4367bb05e4", - "name": "denoising_end", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1 - }, - "scheduler": { - "id": "36e021ad-b762-4fe4-ad4d-17f0291c40b2", - "name": "scheduler", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SchedulerField" - }, - "value": "euler" - }, - "unet": { - "id": "98d3282d-f9f6-4b5e-b9e8-58658f1cac78", - "name": "unet", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "control": { - "id": "f2ea3216-43d5-42b4-887f-36e8f7166d53", - "name": "control", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "ControlField" - } - }, - "ip_adapter": { - "id": "d0780610-a298-47c8-a54e-70e769e0dfe2", - "name": "ip_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "IPAdapterField" - } - }, - "t2i_adapter": { - "id": "fdb40970-185e-4ea8-8bb5-88f06f91f46a", - "name": "t2i_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "T2IAdapterField" - } - }, - "cfg_rescale_multiplier": { - "id": "3af2d8c5-de83-425c-a100-49cb0f1f4385", - "name": "cfg_rescale_multiplier", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "latents": { - "id": "e05b538a-1b5a-4aa5-84b1-fd2361289a81", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "denoise_mask": { - "id": "463a419e-df30-4382-8ffb-b25b25abe425", - "name": "denoise_mask", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "DenoiseMaskField" - } - } - }, - "outputs": { - "latents": { - "id": "559ee688-66cf-4139-8b82-3d3aa69995ce", - "name": "latents", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "0b4285c2-e8b9-48e5-98f6-0a49d3f98fd2", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "8b0881b9-45e5-47d5-b526-24b6661de0ee", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 705, - "position": { - "x": 1570.9941088179146, - "y": -407.6505491604564 - } - } - ], - "edges": [ - { - "id": "1b89067c-3f6b-42c8-991f-e3055789b251-fc9d0e35-a6de-4a19-84e1-c72497c823f6-collapsed", - "source": "1b89067c-3f6b-42c8-991f-e3055789b251", - "target": "fc9d0e35-a6de-4a19-84e1-c72497c823f6", - "type": "collapsed" - }, - { - "id": "dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5-0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77-collapsed", - "source": "dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5", - "target": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77", - "type": "collapsed" - }, - { - "id": "reactflow__edge-1b7e0df8-8589-4915-a4ea-c0088f15d642collection-1b89067c-3f6b-42c8-991f-e3055789b251collection", - "source": "1b7e0df8-8589-4915-a4ea-c0088f15d642", - "target": "1b89067c-3f6b-42c8-991f-e3055789b251", - "type": "default", - "sourceHandle": "collection", - "targetHandle": "collection" - }, - { - "id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426clip-fc9d0e35-a6de-4a19-84e1-c72497c823f6clip", - "source": "d6353b7f-b447-4e17-8f2e-80a88c91d426", - "target": "fc9d0e35-a6de-4a19-84e1-c72497c823f6", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-1b89067c-3f6b-42c8-991f-e3055789b251item-fc9d0e35-a6de-4a19-84e1-c72497c823f6prompt", - "source": "1b89067c-3f6b-42c8-991f-e3055789b251", - "target": "fc9d0e35-a6de-4a19-84e1-c72497c823f6", - "type": "default", - "sourceHandle": "item", - "targetHandle": "prompt" - }, - { - "id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426clip-c2eaf1ba-5708-4679-9e15-945b8b432692clip", - "source": "d6353b7f-b447-4e17-8f2e-80a88c91d426", - "target": "c2eaf1ba-5708-4679-9e15-945b8b432692", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5value-0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77seed", - "source": "dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5", - "target": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77", - "type": "default", - "sourceHandle": "value", - "targetHandle": "seed" - }, - { - "id": "reactflow__edge-fc9d0e35-a6de-4a19-84e1-c72497c823f6conditioning-2fb1577f-0a56-4f12-8711-8afcaaaf1d5epositive_conditioning", - "source": "fc9d0e35-a6de-4a19-84e1-c72497c823f6", - "target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "positive_conditioning" - }, - { - "id": "reactflow__edge-c2eaf1ba-5708-4679-9e15-945b8b432692conditioning-2fb1577f-0a56-4f12-8711-8afcaaaf1d5enegative_conditioning", - "source": "c2eaf1ba-5708-4679-9e15-945b8b432692", - "target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "negative_conditioning" - }, - { - "id": "reactflow__edge-0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77noise-2fb1577f-0a56-4f12-8711-8afcaaaf1d5enoise", - "source": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77", - "target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e", - "type": "default", - "sourceHandle": "noise", - "targetHandle": "noise" - }, - { - "id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426unet-2fb1577f-0a56-4f12-8711-8afcaaaf1d5eunet", - "source": "d6353b7f-b447-4e17-8f2e-80a88c91d426", - "target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e", - "type": "default", - "sourceHandle": "unet", - "targetHandle": "unet" - }, - { - "id": "reactflow__edge-2fb1577f-0a56-4f12-8711-8afcaaaf1d5elatents-491ec988-3c77-4c37-af8a-39a0c4e7a2a1latents", - "source": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e", - "target": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1", - "type": "default", - "sourceHandle": "latents", - "targetHandle": "latents" - }, - { - "id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426vae-491ec988-3c77-4c37-af8a-39a0c4e7a2a1vae", - "source": "d6353b7f-b447-4e17-8f2e-80a88c91d426", - "target": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1", - "type": "default", - "sourceHandle": "vae", - "targetHandle": "vae" - } - ] -} \ No newline at end of file diff --git a/docs/workflows/QR_Code_Monster.json b/docs/workflows/QR_Code_Monster.json deleted file mode 100644 index f0ae5d74fd4..00000000000 --- a/docs/workflows/QR_Code_Monster.json +++ /dev/null @@ -1,758 +0,0 @@ -{ - "name": "QR Code Monster", - "author": "InvokeAI", - "description": "Sample workflow for create images with QR code Monster ControlNet", - "version": "1.0.1", - "contact": "invoke@invoke.ai", - "tags": "qrcode, controlnet, default", - "notes": "", - "exposedFields": [ - { - "nodeId": "a6cc0986-f928-4a7e-8d44-ba2d4b36f54a", - "fieldName": "image" - }, - { - "nodeId": "aca3b054-bfba-4392-bd20-6476f59504df", - "fieldName": "prompt" - }, - { - "nodeId": "3db7cee0-31e2-4a3d-94a1-268cb16177dd", - "fieldName": "prompt" - } - ], - "meta": { - "version": "1.0.0" - }, - "nodes": [ - { - "id": "3db7cee0-31e2-4a3d-94a1-268cb16177dd", - "type": "invocation", - "data": { - "id": "3db7cee0-31e2-4a3d-94a1-268cb16177dd", - "type": "compel", - "inputs": { - "prompt": { - "id": "6a1fe244-5656-4f8c-91d1-1fb474e28807", - "name": "prompt", - "type": "string", - "fieldKind": "input", - "label": "Negative Prompt", - "value": "" - }, - "clip": { - "id": "f24688f3-29b8-4a2d-8603-046e5a5c7250", - "name": "clip", - "type": "ClipField", - "fieldKind": "input", - "label": "" - } - }, - "outputs": { - "conditioning": { - "id": "700528eb-3f8b-4745-b540-34f919b5b228", - "name": "conditioning", - "type": "ConditioningField", - "fieldKind": "output" - } - }, - "label": "Prompt", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 261, - "position": { - "x": 773.0502679628016, - "y": 1622.4836086770556 - } - }, - { - "id": "610384f1-6f0c-4847-a9a2-37ce7f456ed1", - "type": "invocation", - "data": { - "id": "610384f1-6f0c-4847-a9a2-37ce7f456ed1", - "type": "main_model_loader", - "inputs": { - "model": { - "id": "cb36b6d3-6c1f-4911-a200-646745b0ff74", - "name": "model", - "type": "MainModelField", - "fieldKind": "input", - "label": "", - "value": { - "model_name": "stable-diffusion-v1-5", - "base_model": "sd-1", - "model_type": "main" - } - } - }, - "outputs": { - "unet": { - "id": "7246895b-b252-49bc-b952-8d801b4672f7", - "name": "unet", - "type": "UNetField", - "fieldKind": "output" - }, - "clip": { - "id": "3c2aedb8-30d5-4d4b-99df-d06a0d7bedc6", - "name": "clip", - "type": "ClipField", - "fieldKind": "output" - }, - "vae": { - "id": "b9743815-5501-4bbb-8bde-8bd6ba298a4e", - "name": "vae", - "type": "VaeField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 226, - "position": { - "x": 211.58866462619744, - "y": 1376.0542388105248 - } - }, - { - "id": "aca3b054-bfba-4392-bd20-6476f59504df", - "type": "invocation", - "data": { - "id": "aca3b054-bfba-4392-bd20-6476f59504df", - "type": "compel", - "inputs": { - "prompt": { - "id": "6a1fe244-5656-4f8c-91d1-1fb474e28807", - "name": "prompt", - "type": "string", - "fieldKind": "input", - "label": "Positive Prompt", - "value": "" - }, - "clip": { - "id": "f24688f3-29b8-4a2d-8603-046e5a5c7250", - "name": "clip", - "type": "ClipField", - "fieldKind": "input", - "label": "" - } - }, - "outputs": { - "conditioning": { - "id": "700528eb-3f8b-4745-b540-34f919b5b228", - "name": "conditioning", - "type": "ConditioningField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 261, - "position": { - "x": 770.6491131680111, - "y": 1316.379247112241 - } - }, - { - "id": "a6cc0986-f928-4a7e-8d44-ba2d4b36f54a", - "type": "invocation", - "data": { - "id": "a6cc0986-f928-4a7e-8d44-ba2d4b36f54a", - "type": "image", - "inputs": { - "image": { - "id": "89ba5d58-28c9-4e04-a5df-79fb7a6f3531", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "QR Code / Hidden Image" - } - }, - "outputs": { - "image": { - "id": "54335653-0e17-42da-b9e8-83c5fb5af670", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "a3c65953-39ea-4d97-8858-d65154ff9d11", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "2c7db511-ebc9-4286-a46b-bc11e0fd779f", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 225, - "position": { - "x": 700.5034176864369, - "y": 1981.749600549388 - } - }, - { - "id": "280fd8a7-3b0c-49fe-8be4-6246e08b6c9a", - "type": "invocation", - "data": { - "id": "280fd8a7-3b0c-49fe-8be4-6246e08b6c9a", - "type": "noise", - "inputs": { - "seed": { - "id": "7c6c76dd-127b-4829-b1ec-430790cb7ed7", - "name": "seed", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "width": { - "id": "8ec6a525-a421-40d8-a17e-39e7b6836438", - "name": "width", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 512 - }, - "height": { - "id": "6af1e58a-e2ee-4ec4-9f06-d8d0412922ca", - "name": "height", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 512 - }, - "use_cpu": { - "id": "26662e99-5720-43a6-a5d8-06c9dab0e261", - "name": "use_cpu", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": true - } - }, - "outputs": { - "noise": { - "id": "cb4c4dfc-a744-49eb-af4f-677448e28407", - "name": "noise", - "type": "LatentsField", - "fieldKind": "output" - }, - "width": { - "id": "97e87be6-e81f-40a3-a522-28ebe4aad0ac", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "80784420-f1e1-47b0-bd1d-1d381a15e22d", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": false, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 32, - "position": { - "x": 1182.460291960481, - "y": 1759.592972960265 - } - }, - { - "id": "2ac03cf6-0326-454a-bed0-d8baef2bf30d", - "type": "invocation", - "data": { - "id": "2ac03cf6-0326-454a-bed0-d8baef2bf30d", - "type": "controlnet", - "inputs": { - "image": { - "id": "1f683889-9f14-40c8-af29-4b991b211a3a", - "name": "image", - "type": "ImageField", - "fieldKind": "input", - "label": "" - }, - "control_model": { - "id": "a933b21d-22c1-4e06-818f-15416b971282", - "name": "control_model", - "type": "ControlNetModelField", - "fieldKind": "input", - "label": "", - "value": { - "model_name": "qrcode_monster", - "base_model": "sd-1" - } - }, - "control_weight": { - "id": "198a0825-e55e-4496-bc54-c3d7b02f3d75", - "name": "control_weight", - "type": "FloatPolymorphic", - "fieldKind": "input", - "label": "", - "value": 1.4 - }, - "begin_step_percent": { - "id": "c85ce42f-22af-42a0-8993-676002fb275e", - "name": "begin_step_percent", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "end_step_percent": { - "id": "a61a65c4-9e6f-4fe2-96a5-1294d17ec6e4", - "name": "end_step_percent", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 1 - }, - "control_mode": { - "id": "1aa45cfa-0249-46b7-bf24-3e38e92f5fa0", - "name": "control_mode", - "type": "enum", - "fieldKind": "input", - "label": "", - "value": "balanced" - }, - "resize_mode": { - "id": "a89d3cb9-a141-4cea-bb49-977bf267377b", - "name": "resize_mode", - "type": "enum", - "fieldKind": "input", - "label": "", - "value": "just_resize" - } - }, - "outputs": { - "control": { - "id": "c9a1fc7e-cb25-45a9-adff-1a97c9ff04d6", - "name": "control", - "type": "ControlField", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 508, - "position": { - "x": 1165.434407461108, - "y": 1862.916856351665 - } - }, - { - "id": "28542b66-5a00-4780-a318-0a036d2df914", - "type": "invocation", - "data": { - "id": "28542b66-5a00-4780-a318-0a036d2df914", - "type": "l2i", - "inputs": { - "metadata": { - "id": "a38e8f55-7f2c-4fcc-a71f-d51e2eb0374a", - "name": "metadata", - "type": "MetadataField", - "fieldKind": "input", - "label": "" - }, - "latents": { - "id": "80e97bc8-e716-4175-9115-5b58495aa30c", - "name": "latents", - "type": "LatentsField", - "fieldKind": "input", - "label": "" - }, - "vae": { - "id": "5641bce6-ac2b-47eb-bb32-2f290026b7e1", - "name": "vae", - "type": "VaeField", - "fieldKind": "input", - "label": "" - }, - "tiled": { - "id": "9e75eb16-ae48-47ed-b180-e0409d377436", - "name": "tiled", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - }, - "fp32": { - "id": "0518b0ce-ee37-437b-8437-cc2976a3279f", - "name": "fp32", - "type": "boolean", - "fieldKind": "input", - "label": "", - "value": false - } - }, - "outputs": { - "image": { - "id": "ec2ff985-a7eb-401f-92c4-1217cddad6a2", - "name": "image", - "type": "ImageField", - "fieldKind": "output" - }, - "width": { - "id": "ba1d1720-6d67-4eca-9e9d-b97d08636774", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "10bcf8f4-6394-422f-b0c0-51680f3bfb25", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.0.0" - }, - "width": 320, - "height": 267, - "position": { - "x": 2110.8415693683014, - "y": 1487.253341116115 - } - }, - { - "id": "9755ae4c-ef30-4db3-80f6-a31f98979a11", - "type": "invocation", - "data": { - "id": "9755ae4c-ef30-4db3-80f6-a31f98979a11", - "type": "denoise_latents", - "inputs": { - "positive_conditioning": { - "id": "8e6aceaa-a986-4ab2-9c04-5b1027b3daf6", - "name": "positive_conditioning", - "type": "ConditioningField", - "fieldKind": "input", - "label": "" - }, - "negative_conditioning": { - "id": "fbbaa712-ca1a-420b-9016-763f2a29d68c", - "name": "negative_conditioning", - "type": "ConditioningField", - "fieldKind": "input", - "label": "" - }, - "noise": { - "id": "a3b3d5d2-c0f9-4b89-a9b3-8de9418f7bb5", - "name": "noise", - "type": "LatentsField", - "fieldKind": "input", - "label": "" - }, - "steps": { - "id": "e491e664-2f8c-4f49-b3e4-57b051fbb9c5", - "name": "steps", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 10 - }, - "cfg_scale": { - "id": "f0318abd-ed65-4cad-86a7-48d1c19a6d14", - "name": "cfg_scale", - "type": "FloatPolymorphic", - "fieldKind": "input", - "label": "", - "value": 7.5 - }, - "denoising_start": { - "id": "f7c24c51-496f-44c4-836a-c734e529fec0", - "name": "denoising_start", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "denoising_end": { - "id": "54f7656a-fb0d-4d9e-a459-f700f7dccd2e", - "name": "denoising_end", - "type": "float", - "fieldKind": "input", - "label": "", - "value": 1 - }, - "scheduler": { - "id": "363ee440-040d-499b-bf84-bf5391b08681", - "name": "scheduler", - "type": "Scheduler", - "fieldKind": "input", - "label": "", - "value": "euler" - }, - "unet": { - "id": "5c93d4e5-1064-4700-ab1d-d12e1e9b5ba7", - "name": "unet", - "type": "UNetField", - "fieldKind": "input", - "label": "" - }, - "control": { - "id": "e1948eb3-7407-43b0-93e3-139470f186b7", - "name": "control", - "type": "ControlPolymorphic", - "fieldKind": "input", - "label": "" - }, - "ip_adapter": { - "id": "5675b2c3-adfb-49ee-b33c-26bdbfab1fed", - "name": "ip_adapter", - "type": "IPAdapterPolymorphic", - "fieldKind": "input", - "label": "" - }, - "t2i_adapter": { - "id": "89cd4ab3-3bfc-4063-9de5-91d42305c651", - "name": "t2i_adapter", - "type": "T2IAdapterPolymorphic", - "fieldKind": "input", - "label": "" - }, - "latents": { - "id": "ec01df90-5042-418d-b6d6-86b251c13770", - "name": "latents", - "type": "LatentsField", - "fieldKind": "input", - "label": "" - }, - "denoise_mask": { - "id": "561cde00-cb20-42ae-9bd3-4f477f73fbe1", - "name": "denoise_mask", - "type": "DenoiseMaskField", - "fieldKind": "input", - "label": "" - } - }, - "outputs": { - "latents": { - "id": "f9addefe-efcc-4e01-8945-6ebbc934b002", - "name": "latents", - "type": "LatentsField", - "fieldKind": "output" - }, - "width": { - "id": "6d48f78b-d681-422a-8677-0111bd0625f1", - "name": "width", - "type": "integer", - "fieldKind": "output" - }, - "height": { - "id": "f25997b8-6316-44ce-b696-b82e4ed51ae5", - "name": "height", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": true, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": true, - "version": "1.4.0" - }, - "width": 320, - "height": 646, - "position": { - "x": 1597.9598293300219, - "y": 1420.4637727891632 - } - }, - { - "id": "59349822-af20-4e0e-a53f-3ba135d00c3f", - "type": "invocation", - "data": { - "id": "59349822-af20-4e0e-a53f-3ba135d00c3f", - "type": "rand_int", - "inputs": { - "low": { - "id": "051f22f9-2d4f-414f-bc51-84af2d626efa", - "name": "low", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 0 - }, - "high": { - "id": "77206186-f264-4224-9589-f925cf903dc9", - "name": "high", - "type": "integer", - "fieldKind": "input", - "label": "", - "value": 2147483647 - } - }, - "outputs": { - "value": { - "id": "a7ed9387-3a24-4d34-b7c5-f713bd544ab1", - "name": "value", - "type": "integer", - "fieldKind": "output" - } - }, - "label": "", - "isOpen": false, - "notes": "", - "embedWorkflow": false, - "isIntermediate": true, - "useCache": false, - "version": "1.0.0" - }, - "width": 320, - "height": 32, - "position": { - "x": 1178.16746986153, - "y": 1663.9433412808876 - } - } - ], - "edges": [ - { - "source": "59349822-af20-4e0e-a53f-3ba135d00c3f", - "target": "280fd8a7-3b0c-49fe-8be4-6246e08b6c9a", - "id": "59349822-af20-4e0e-a53f-3ba135d00c3f-280fd8a7-3b0c-49fe-8be4-6246e08b6c9a-collapsed", - "type": "collapsed" - }, - { - "source": "610384f1-6f0c-4847-a9a2-37ce7f456ed1", - "sourceHandle": "clip", - "target": "aca3b054-bfba-4392-bd20-6476f59504df", - "targetHandle": "clip", - "id": "reactflow__edge-610384f1-6f0c-4847-a9a2-37ce7f456ed1clip-aca3b054-bfba-4392-bd20-6476f59504dfclip", - "type": "default" - }, - { - "source": "610384f1-6f0c-4847-a9a2-37ce7f456ed1", - "sourceHandle": "clip", - "target": "3db7cee0-31e2-4a3d-94a1-268cb16177dd", - "targetHandle": "clip", - "id": "reactflow__edge-610384f1-6f0c-4847-a9a2-37ce7f456ed1clip-3db7cee0-31e2-4a3d-94a1-268cb16177ddclip", - "type": "default" - }, - { - "source": "a6cc0986-f928-4a7e-8d44-ba2d4b36f54a", - "sourceHandle": "image", - "target": "2ac03cf6-0326-454a-bed0-d8baef2bf30d", - "targetHandle": "image", - "id": "reactflow__edge-a6cc0986-f928-4a7e-8d44-ba2d4b36f54aimage-2ac03cf6-0326-454a-bed0-d8baef2bf30dimage", - "type": "default" - }, - { - "source": "610384f1-6f0c-4847-a9a2-37ce7f456ed1", - "sourceHandle": "vae", - "target": "28542b66-5a00-4780-a318-0a036d2df914", - "targetHandle": "vae", - "id": "reactflow__edge-610384f1-6f0c-4847-a9a2-37ce7f456ed1vae-28542b66-5a00-4780-a318-0a036d2df914vae", - "type": "default" - }, - { - "source": "280fd8a7-3b0c-49fe-8be4-6246e08b6c9a", - "sourceHandle": "noise", - "target": "9755ae4c-ef30-4db3-80f6-a31f98979a11", - "targetHandle": "noise", - "id": "reactflow__edge-280fd8a7-3b0c-49fe-8be4-6246e08b6c9anoise-9755ae4c-ef30-4db3-80f6-a31f98979a11noise", - "type": "default" - }, - { - "source": "3db7cee0-31e2-4a3d-94a1-268cb16177dd", - "sourceHandle": "conditioning", - "target": "9755ae4c-ef30-4db3-80f6-a31f98979a11", - "targetHandle": "negative_conditioning", - "id": "reactflow__edge-3db7cee0-31e2-4a3d-94a1-268cb16177ddconditioning-9755ae4c-ef30-4db3-80f6-a31f98979a11negative_conditioning", - "type": "default" - }, - { - "source": "aca3b054-bfba-4392-bd20-6476f59504df", - "sourceHandle": "conditioning", - "target": "9755ae4c-ef30-4db3-80f6-a31f98979a11", - "targetHandle": "positive_conditioning", - "id": "reactflow__edge-aca3b054-bfba-4392-bd20-6476f59504dfconditioning-9755ae4c-ef30-4db3-80f6-a31f98979a11positive_conditioning", - "type": "default" - }, - { - "source": "610384f1-6f0c-4847-a9a2-37ce7f456ed1", - "sourceHandle": "unet", - "target": "9755ae4c-ef30-4db3-80f6-a31f98979a11", - "targetHandle": "unet", - "id": "reactflow__edge-610384f1-6f0c-4847-a9a2-37ce7f456ed1unet-9755ae4c-ef30-4db3-80f6-a31f98979a11unet", - "type": "default" - }, - { - "source": "2ac03cf6-0326-454a-bed0-d8baef2bf30d", - "sourceHandle": "control", - "target": "9755ae4c-ef30-4db3-80f6-a31f98979a11", - "targetHandle": "control", - "id": "reactflow__edge-2ac03cf6-0326-454a-bed0-d8baef2bf30dcontrol-9755ae4c-ef30-4db3-80f6-a31f98979a11control", - "type": "default" - }, - { - "source": "9755ae4c-ef30-4db3-80f6-a31f98979a11", - "sourceHandle": "latents", - "target": "28542b66-5a00-4780-a318-0a036d2df914", - "targetHandle": "latents", - "id": "reactflow__edge-9755ae4c-ef30-4db3-80f6-a31f98979a11latents-28542b66-5a00-4780-a318-0a036d2df914latents", - "type": "default" - }, - { - "source": "59349822-af20-4e0e-a53f-3ba135d00c3f", - "sourceHandle": "value", - "target": "280fd8a7-3b0c-49fe-8be4-6246e08b6c9a", - "targetHandle": "seed", - "id": "reactflow__edge-59349822-af20-4e0e-a53f-3ba135d00c3fvalue-280fd8a7-3b0c-49fe-8be4-6246e08b6c9aseed", - "type": "default" - } - ] -} \ No newline at end of file diff --git a/docs/workflows/SDXL_Text_to_Image.json b/docs/workflows/SDXL_Text_to_Image.json deleted file mode 100644 index ef7810c1533..00000000000 --- a/docs/workflows/SDXL_Text_to_Image.json +++ /dev/null @@ -1,1320 +0,0 @@ -{ - "name": "SDXL Text to Image", - "author": "InvokeAI", - "description": "Sample text to image workflow for SDXL", - "version": "1.0.1", - "contact": "invoke@invoke.ai", - "tags": "text2image, SDXL, default", - "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" - } - ], - "meta": { - "category": "default", - "version": "2.0.0" - }, - "nodes": [ - { - "id": "3774ec24-a69e-4254-864c-097d07a6256f", - "type": "invocation", - "data": { - "id": "3774ec24-a69e-4254-864c-097d07a6256f", - "type": "string_join", - "label": "Positive Style Concat", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "inputs": { - "string_left": { - "id": "8d84be5c-4a96-46ad-a92c-eaf6fcae4a69", - "name": "string_left", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "string_right": { - "id": "c8e2a881-f675-4c6b-865b-a0892473b750", - "name": "string_right", - "fieldKind": "input", - "label": "Positive Style Concat", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - } - }, - "outputs": { - "value": { - "id": "196fad08-73ea-4fe5-8cc3-b55fd3cf40e5", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 750, - "y": -225 - } - }, - { - "id": "719dabe8-8297-4749-aea1-37be301cd425", - "type": "invocation", - "data": { - "id": "719dabe8-8297-4749-aea1-37be301cd425", - "type": "string", - "label": "Negative Prompt", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "inputs": { - "value": { - "id": "744a5f7c-6e3a-4fbc-ac66-ba0cf8559eeb", - "name": "value", - "fieldKind": "input", - "label": "Negative Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "photograph" - } - }, - "outputs": { - "value": { - "id": "3e0ddf7a-a5de-4dad-b726-5d0cb4e0baa6", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - } - } - } - }, - "width": 320, - "height": 258, - "position": { - "x": 750, - "y": -125 - } - }, - { - "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "type": "invocation", - "data": { - "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "type": "sdxl_compel_prompt", - "label": "SDXL Negative Compel Prompt", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "5a6889e6-95cb-462f-8f4a-6b93ae7afaec", - "name": "prompt", - "fieldKind": "input", - "label": "Negative Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "style": { - "id": "f240d0e6-3a1c-4320-af23-20ebb707c276", - "name": "style", - "fieldKind": "input", - "label": "Negative Style", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "original_width": { - "id": "05af07b0-99a0-4a68-8ad2-697bbdb7fc7e", - "name": "original_width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "original_height": { - "id": "2c771996-a998-43b7-9dd3-3792664d4e5b", - "name": "original_height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "crop_top": { - "id": "66519dca-a151-4e3e-ae1f-88f1f9877bde", - "name": "crop_top", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "crop_left": { - "id": "349cf2e9-f3d0-4e16-9ae2-7097d25b6a51", - "name": "crop_left", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "target_width": { - "id": "44499347-7bd6-4a73-99d6-5a982786db05", - "name": "target_width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "target_height": { - "id": "fda359b0-ab80-4f3c-805b-c9f61319d7d2", - "name": "target_height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "clip": { - "id": "b447adaf-a649-4a76-a827-046a9fc8d89b", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "clip2": { - "id": "86ee4e32-08f9-4baa-9163-31d93f5c0187", - "name": "clip2", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "7c10118e-7b4e-4911-b98e-d3ba6347dfd0", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 750, - "y": 200 - } - }, - { - "id": "55705012-79b9-4aac-9f26-c0b10309785b", - "type": "invocation", - "data": { - "id": "55705012-79b9-4aac-9f26-c0b10309785b", - "type": "noise", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.1", - "nodePack": "invokeai", - "inputs": { - "seed": { - "id": "6431737c-918a-425d-a3b4-5d57e2f35d4d", - "name": "seed", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "width": { - "id": "38fc5b66-fe6e-47c8-bba9-daf58e454ed7", - "name": "width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "height": { - "id": "16298330-e2bf-4872-a514-d6923df53cbb", - "name": "height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "use_cpu": { - "id": "c7c436d3-7a7a-4e76-91e4-c6deb271623c", - "name": "use_cpu", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": true - } - }, - "outputs": { - "noise": { - "id": "50f650dc-0184-4e23-a927-0497a96fe954", - "name": "noise", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "bb8a452b-133d-42d1-ae4a-3843d7e4109a", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "35cfaa12-3b8b-4b7a-a884-327ff3abddd9", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 388, - "position": { - "x": 375, - "y": 0 - } - }, - { - "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", - "type": "invocation", - "data": { - "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", - "type": "rand_int", - "label": "Random Seed", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": false, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "low": { - "id": "3ec65a37-60ba-4b6c-a0b2-553dd7a84b84", - "name": "low", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "high": { - "id": "085f853a-1a5f-494d-8bec-e4ba29a3f2d1", - "name": "high", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 2147483647 - } - }, - "outputs": { - "value": { - "id": "812ade4d-7699-4261-b9fc-a6c9d2ab55ee", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 375, - "y": -50 - } - }, - { - "id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "type": "invocation", - "data": { - "id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "type": "sdxl_model_loader", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "model": { - "id": "39f9e799-bc95-4318-a200-30eed9e60c42", - "name": "model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SDXLMainModelField" - }, - "value": null - } - }, - "outputs": { - "unet": { - "id": "2626a45e-59aa-4609-b131-2d45c5eaed69", - "name": "unet", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "clip": { - "id": "7c9c42fa-93d5-4639-ab8b-c4d9b0559baf", - "name": "clip", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "clip2": { - "id": "0dafddcf-a472-49c1-a47c-7b8fab4c8bc9", - "name": "clip2", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "vae": { - "id": "ee6a6997-1b3c-4ff3-99ce-1e7bfba2750c", - "name": "vae", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - } - } - }, - "width": 320, - "height": 257, - "position": { - "x": 375, - "y": -500 - } - }, - { - "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "type": "invocation", - "data": { - "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "type": "sdxl_compel_prompt", - "label": "SDXL Positive Compel Prompt", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "5a6889e6-95cb-462f-8f4a-6b93ae7afaec", - "name": "prompt", - "fieldKind": "input", - "label": "Positive Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "style": { - "id": "f240d0e6-3a1c-4320-af23-20ebb707c276", - "name": "style", - "fieldKind": "input", - "label": "Positive Style", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "original_width": { - "id": "05af07b0-99a0-4a68-8ad2-697bbdb7fc7e", - "name": "original_width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "original_height": { - "id": "2c771996-a998-43b7-9dd3-3792664d4e5b", - "name": "original_height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "crop_top": { - "id": "66519dca-a151-4e3e-ae1f-88f1f9877bde", - "name": "crop_top", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "crop_left": { - "id": "349cf2e9-f3d0-4e16-9ae2-7097d25b6a51", - "name": "crop_left", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "target_width": { - "id": "44499347-7bd6-4a73-99d6-5a982786db05", - "name": "target_width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "target_height": { - "id": "fda359b0-ab80-4f3c-805b-c9f61319d7d2", - "name": "target_height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "clip": { - "id": "b447adaf-a649-4a76-a827-046a9fc8d89b", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "clip2": { - "id": "86ee4e32-08f9-4baa-9163-31d93f5c0187", - "name": "clip2", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "7c10118e-7b4e-4911-b98e-d3ba6347dfd0", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 750, - "y": -175 - } - }, - { - "id": "63e91020-83b2-4f35-b174-ad9692aabb48", - "type": "invocation", - "data": { - "id": "63e91020-83b2-4f35-b174-ad9692aabb48", - "type": "l2i", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": false, - "useCache": false, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "88971324-3fdb-442d-b8b7-7612478a8622", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "latents": { - "id": "da0e40cb-c49f-4fa5-9856-338b91a65f6b", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "vae": { - "id": "ae5164ce-1710-4ec5-a83a-6113a0d1b5c0", - "name": "vae", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - }, - "tiled": { - "id": "2ccfd535-1a7b-4ecf-84db-9430a64fb3d7", - "name": "tiled", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - }, - "fp32": { - "id": "64f07d5a-54a2-429c-8c5b-0c2a3a8e5cd5", - "name": "fp32", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - } - }, - "outputs": { - "image": { - "id": "9b281eaa-6504-407d-a5ca-1e5e8020a4bf", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "98e545f3-b53b-490d-b94d-bed9418ccc75", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "4a74bd43-d7f7-4c7f-bb3b-d09bb2992c46", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 266, - "position": { - "x": 1475, - "y": -500 - } - }, - { - "id": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", - "type": "invocation", - "data": { - "id": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", - "type": "denoise_latents", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.5.0", - "nodePack": "invokeai", - "inputs": { - "positive_conditioning": { - "id": "29b73dfa-a06e-4b4a-a844-515b9eb93a81", - "name": "positive_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "negative_conditioning": { - "id": "a81e6f5b-f4de-4919-b483-b6e2f067465a", - "name": "negative_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "noise": { - "id": "4ba06bb7-eb45-4fb9-9984-31001b545587", - "name": "noise", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "steps": { - "id": "36ee8a45-ca69-44bc-9bc3-aa881e6045c0", - "name": "steps", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 32 - }, - "cfg_scale": { - "id": "2a2024e0-a736-46ec-933c-c1c1ebe96943", - "name": "cfg_scale", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 6 - }, - "denoising_start": { - "id": "be219d5e-41b7-430a-8fb5-bc21a31ad219", - "name": "denoising_start", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "denoising_end": { - "id": "3adfb7ae-c9f7-4a40-b6e0-4c2050bd1a99", - "name": "denoising_end", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1 - }, - "scheduler": { - "id": "14423e0d-7215-4ee0-b065-f9e95eaa8d7d", - "name": "scheduler", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SchedulerField" - }, - "value": "dpmpp_2m_sde_k" - }, - "unet": { - "id": "e73bbf98-6489-492b-b83c-faed215febac", - "name": "unet", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "control": { - "id": "dab351b3-0c86-4ea5-9782-4e8edbfb0607", - "name": "control", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "ControlField" - } - }, - "ip_adapter": { - "id": "192daea0-a90a-43cc-a2ee-0114a8e90318", - "name": "ip_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "IPAdapterField" - } - }, - "t2i_adapter": { - "id": "ee386a55-d4c7-48c1-ac57-7bc4e3aada7a", - "name": "t2i_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "T2IAdapterField" - } - }, - "cfg_rescale_multiplier": { - "id": "106bbe8d-e641-4034-9a39-d4e82c298da1", - "name": "cfg_rescale_multiplier", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "latents": { - "id": "3a922c6a-3d8c-4c9e-b3ec-2f4d81cda077", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "denoise_mask": { - "id": "cd7ce032-835f-495f-8b45-d57272f33132", - "name": "denoise_mask", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "DenoiseMaskField" - } - } - }, - "outputs": { - "latents": { - "id": "6260b84f-8361-470a-98d8-5b22a45c2d8c", - "name": "latents", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "aede0ecf-25b6-46be-aa30-b77f79715deb", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "519abf62-d475-48ef-ab8f-66136bc0e499", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 702, - "position": { - "x": 1125, - "y": -500 - } - }, - { - "id": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8", - "type": "invocation", - "data": { - "id": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8", - "type": "vae_loader", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "inputs": { - "vae_model": { - "id": "28a17000-b629-49c6-b945-77c591cf7440", - "name": "vae_model", - "fieldKind": "input", - "label": "VAE (use the FP16 model)", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VAEModelField" - }, - "value": null - } - }, - "outputs": { - "vae": { - "id": "a34892b6-ba6d-44eb-8a68-af1f40a84186", - "name": "vae", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - } - } - }, - "width": 320, - "height": 161, - "position": { - "x": 375, - "y": -225 - } - }, - { - "id": "ade2c0d3-0384-4157-b39b-29ce429cfa15", - "type": "invocation", - "data": { - "id": "ade2c0d3-0384-4157-b39b-29ce429cfa15", - "type": "string", - "label": "Positive Prompt", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "inputs": { - "value": { - "id": "744a5f7c-6e3a-4fbc-ac66-ba0cf8559eeb", - "name": "value", - "fieldKind": "input", - "label": "Positive Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "Super cute tiger cub, fierce, traditional chinese watercolor" - } - }, - "outputs": { - "value": { - "id": "3e0ddf7a-a5de-4dad-b726-5d0cb4e0baa6", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - } - } - } - }, - "width": 320, - "height": 258, - "position": { - "x": 750, - "y": -500 - } - }, - { - "id": "ad8fa655-3a76-43d0-9c02-4d7644dea650", - "type": "invocation", - "data": { - "id": "ad8fa655-3a76-43d0-9c02-4d7644dea650", - "type": "string_join", - "label": "Negative Style Concat", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "inputs": { - "string_left": { - "id": "8d84be5c-4a96-46ad-a92c-eaf6fcae4a69", - "name": "string_left", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "string_right": { - "id": "c8e2a881-f675-4c6b-865b-a0892473b750", - "name": "string_right", - "fieldKind": "input", - "label": "Negative Style Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - } - }, - "outputs": { - "value": { - "id": "196fad08-73ea-4fe5-8cc3-b55fd3cf40e5", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 750, - "y": 150 - } - } - ], - "edges": [ - { - "id": "3774ec24-a69e-4254-864c-097d07a6256f-faf965a4-7530-427b-b1f3-4ba6505c2a08-collapsed", - "source": "3774ec24-a69e-4254-864c-097d07a6256f", - "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "type": "collapsed" - }, - { - "id": "ad8fa655-3a76-43d0-9c02-4d7644dea650-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204-collapsed", - "source": "ad8fa655-3a76-43d0-9c02-4d7644dea650", - "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "type": "collapsed" - }, - { - "id": "reactflow__edge-ea94bc37-d995-4a83-aa99-4af42479f2f2value-55705012-79b9-4aac-9f26-c0b10309785bseed", - "source": "ea94bc37-d995-4a83-aa99-4af42479f2f2", - "target": "55705012-79b9-4aac-9f26-c0b10309785b", - "type": "default", - "sourceHandle": "value", - "targetHandle": "seed" - }, - { - "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip-faf965a4-7530-427b-b1f3-4ba6505c2a08clip", - "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip2-faf965a4-7530-427b-b1f3-4ba6505c2a08clip2", - "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "type": "default", - "sourceHandle": "clip2", - "targetHandle": "clip2" - }, - { - "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204clip", - "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip2-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204clip2", - "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "type": "default", - "sourceHandle": "clip2", - "targetHandle": "clip2" - }, - { - "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22unet-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbunet", - "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", - "type": "default", - "sourceHandle": "unet", - "targetHandle": "unet" - }, - { - "id": "reactflow__edge-faf965a4-7530-427b-b1f3-4ba6505c2a08conditioning-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbpositive_conditioning", - "source": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "positive_conditioning" - }, - { - "id": "reactflow__edge-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204conditioning-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbnegative_conditioning", - "source": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "negative_conditioning" - }, - { - "id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbnoise", - "source": "55705012-79b9-4aac-9f26-c0b10309785b", - "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", - "type": "default", - "sourceHandle": "noise", - "targetHandle": "noise" - }, - { - "id": "reactflow__edge-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfblatents-63e91020-83b2-4f35-b174-ad9692aabb48latents", - "source": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb", - "target": "63e91020-83b2-4f35-b174-ad9692aabb48", - "type": "default", - "sourceHandle": "latents", - "targetHandle": "latents" - }, - { - "id": "reactflow__edge-0093692f-9cf4-454d-a5b8-62f0e3eb3bb8vae-63e91020-83b2-4f35-b174-ad9692aabb48vae", - "source": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8", - "target": "63e91020-83b2-4f35-b174-ad9692aabb48", - "type": "default", - "sourceHandle": "vae", - "targetHandle": "vae" - }, - { - "id": "reactflow__edge-ade2c0d3-0384-4157-b39b-29ce429cfa15value-faf965a4-7530-427b-b1f3-4ba6505c2a08prompt", - "source": "ade2c0d3-0384-4157-b39b-29ce429cfa15", - "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "type": "default", - "sourceHandle": "value", - "targetHandle": "prompt" - }, - { - "id": "reactflow__edge-719dabe8-8297-4749-aea1-37be301cd425value-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204prompt", - "source": "719dabe8-8297-4749-aea1-37be301cd425", - "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "type": "default", - "sourceHandle": "value", - "targetHandle": "prompt" - }, - { - "id": "reactflow__edge-719dabe8-8297-4749-aea1-37be301cd425value-ad8fa655-3a76-43d0-9c02-4d7644dea650string_left", - "source": "719dabe8-8297-4749-aea1-37be301cd425", - "target": "ad8fa655-3a76-43d0-9c02-4d7644dea650", - "type": "default", - "sourceHandle": "value", - "targetHandle": "string_left" - }, - { - "id": "reactflow__edge-ad8fa655-3a76-43d0-9c02-4d7644dea650value-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204style", - "source": "ad8fa655-3a76-43d0-9c02-4d7644dea650", - "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "type": "default", - "sourceHandle": "value", - "targetHandle": "style" - }, - { - "id": "reactflow__edge-ade2c0d3-0384-4157-b39b-29ce429cfa15value-3774ec24-a69e-4254-864c-097d07a6256fstring_left", - "source": "ade2c0d3-0384-4157-b39b-29ce429cfa15", - "target": "3774ec24-a69e-4254-864c-097d07a6256f", - "type": "default", - "sourceHandle": "value", - "targetHandle": "string_left" - }, - { - "id": "reactflow__edge-3774ec24-a69e-4254-864c-097d07a6256fvalue-faf965a4-7530-427b-b1f3-4ba6505c2a08style", - "source": "3774ec24-a69e-4254-864c-097d07a6256f", - "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "type": "default", - "sourceHandle": "value", - "targetHandle": "style" - } - ] -} diff --git a/docs/workflows/SDXL_w_Refiner_Text_to_Image.json b/docs/workflows/SDXL_w_Refiner_Text_to_Image.json deleted file mode 100644 index 634b3fac4b8..00000000000 --- a/docs/workflows/SDXL_w_Refiner_Text_to_Image.json +++ /dev/null @@ -1,1900 +0,0 @@ -{ - "name": "SDXL w/Refiner Text to Image", - "author": "InvokeAI", - "description": "Sample text to image workflow for SDXL with the refiner", - "version": "1.0.1", - "contact": "invoke@invoke.ai", - "tags": "text2image, SDXL, default, refiner", - "notes": "", - "exposedFields": [ - { - "nodeId": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "fieldName": "model" - }, - { - "nodeId": "06a30867-1e9d-461f-bd58-14a63cc997dd", - "fieldName": "scheduler" - }, - { - "nodeId": "62bdf243-d98f-4508-b6b5-c3af00ef49f0", - "fieldName": "model" - }, - { - "nodeId": "b2b35add-929d-4538-aecb-02c661768b29", - "fieldName": "value" - }, - { - "nodeId": "f1a6a160-4c36-4902-8eeb-8b1c23e81bc8", - "fieldName": "value" - }, - { - "nodeId": "5639e3bc-b769-4ae5-9262-72db703c5a7b", - "fieldName": "value" - }, - { - "nodeId": "8d54b9db-3662-43af-8369-9a277e063f3b", - "fieldName": "value" - } - ], - "meta": { - "category": "user", - "version": "2.0.0" - }, - "nodes": [ - { - "id": "b2b35add-929d-4538-aecb-02c661768b29", - "type": "invocation", - "data": { - "id": "b2b35add-929d-4538-aecb-02c661768b29", - "type": "string", - "label": "Positive Prompt", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "value": { - "id": "89854c84-cbc1-4b60-921d-4bade11cab66", - "name": "value", - "fieldKind": "input", - "label": "Positive Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "super cute tiger" - } - }, - "outputs": { - "value": { - "id": "3617404e-e483-4e2e-8550-7080a1ef283f", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - } - } - } - }, - "position": { - "x": 700, - "y": -75 - }, - "width": 320, - "height": 24 - }, - { - "id": "8d54b9db-3662-43af-8369-9a277e063f3b", - "type": "invocation", - "data": { - "id": "8d54b9db-3662-43af-8369-9a277e063f3b", - "type": "string", - "label": "Negative Style", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "value": { - "id": "89854c84-cbc1-4b60-921d-4bade11cab66", - "name": "value", - "fieldKind": "input", - "label": "Negative Style", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - } - }, - "outputs": { - "value": { - "id": "3617404e-e483-4e2e-8550-7080a1ef283f", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - } - } - } - }, - "position": { - "x": 700, - "y": 75 - }, - "width": 320, - "height": 24 - }, - { - "id": "f1a6a160-4c36-4902-8eeb-8b1c23e81bc8", - "type": "invocation", - "data": { - "id": "f1a6a160-4c36-4902-8eeb-8b1c23e81bc8", - "type": "string", - "label": "Postive Style", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "value": { - "id": "89854c84-cbc1-4b60-921d-4bade11cab66", - "name": "value", - "fieldKind": "input", - "label": "Positive Style", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - } - }, - "outputs": { - "value": { - "id": "3617404e-e483-4e2e-8550-7080a1ef283f", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - } - } - } - }, - "position": { - "x": 700, - "y": -25 - }, - "width": 320, - "height": 24 - }, - { - "id": "fbb2f1a0-2e68-411d-a955-60c3b8a6f2d1", - "type": "invocation", - "data": { - "id": "fbb2f1a0-2e68-411d-a955-60c3b8a6f2d1", - "type": "sdxl_refiner_compel_prompt", - "label": "SDXL Refiner Negative Compel Prompt", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "style": { - "id": "c6f91ecf-370f-44d0-8243-63724e75510a", - "name": "style", - "fieldKind": "input", - "label": "Negative Style", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "original_width": { - "id": "956f6eca-8324-41eb-a8dd-fa9b34164ca6", - "name": "original_width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "original_height": { - "id": "a41bb3a1-7664-4dac-b6ae-6f4dff3828a9", - "name": "original_height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "crop_top": { - "id": "81936e19-0ae7-4006-9d7c-e359fc7c7d15", - "name": "crop_top", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "crop_left": { - "id": "be94ddb8-88cc-4d6b-a2c0-f2b43143bfa1", - "name": "crop_left", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "aesthetic_score": { - "id": "60f380de-87b4-4535-b3ef-545a6e57283e", - "name": "aesthetic_score", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 6 - }, - "clip2": { - "id": "773c4054-c005-46ad-92c4-5c1fa4506041", - "name": "clip2", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "63e16bfa-59d8-4d6f-abda-ad979b26adb5", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "position": { - "x": 1625, - "y": -925 - }, - "width": 320, - "height": 483 - }, - { - "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "type": "invocation", - "data": { - "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "type": "sdxl_compel_prompt", - "label": "SDXL Negative Compel Prompt", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "5a6889e6-95cb-462f-8f4a-6b93ae7afaec", - "name": "prompt", - "fieldKind": "input", - "label": "Negative Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "style": { - "id": "f240d0e6-3a1c-4320-af23-20ebb707c276", - "name": "style", - "fieldKind": "input", - "label": "Negative Style", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "original_width": { - "id": "05af07b0-99a0-4a68-8ad2-697bbdb7fc7e", - "name": "original_width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "original_height": { - "id": "2c771996-a998-43b7-9dd3-3792664d4e5b", - "name": "original_height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "crop_top": { - "id": "66519dca-a151-4e3e-ae1f-88f1f9877bde", - "name": "crop_top", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "crop_left": { - "id": "349cf2e9-f3d0-4e16-9ae2-7097d25b6a51", - "name": "crop_left", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "target_width": { - "id": "44499347-7bd6-4a73-99d6-5a982786db05", - "name": "target_width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "target_height": { - "id": "fda359b0-ab80-4f3c-805b-c9f61319d7d2", - "name": "target_height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "clip": { - "id": "b447adaf-a649-4a76-a827-046a9fc8d89b", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "clip2": { - "id": "86ee4e32-08f9-4baa-9163-31d93f5c0187", - "name": "clip2", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "7c10118e-7b4e-4911-b98e-d3ba6347dfd0", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "position": { - "x": 900, - "y": -925 - }, - "width": 320, - "height": 699 - }, - { - "id": "55705012-79b9-4aac-9f26-c0b10309785b", - "type": "invocation", - "data": { - "id": "55705012-79b9-4aac-9f26-c0b10309785b", - "type": "noise", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.1", - "nodePack": "invokeai", - "inputs": { - "seed": { - "id": "6431737c-918a-425d-a3b4-5d57e2f35d4d", - "name": "seed", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "width": { - "id": "38fc5b66-fe6e-47c8-bba9-daf58e454ed7", - "name": "width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "height": { - "id": "16298330-e2bf-4872-a514-d6923df53cbb", - "name": "height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "use_cpu": { - "id": "c7c436d3-7a7a-4e76-91e4-c6deb271623c", - "name": "use_cpu", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": true - } - }, - "outputs": { - "noise": { - "id": "50f650dc-0184-4e23-a927-0497a96fe954", - "name": "noise", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "bb8a452b-133d-42d1-ae4a-3843d7e4109a", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "35cfaa12-3b8b-4b7a-a884-327ff3abddd9", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "position": { - "x": 1275, - "y": -200 - }, - "width": 320, - "height": 24 - }, - { - "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", - "type": "invocation", - "data": { - "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", - "type": "rand_int", - "label": "Random Seed", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": false, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "low": { - "id": "3ec65a37-60ba-4b6c-a0b2-553dd7a84b84", - "name": "low", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "high": { - "id": "085f853a-1a5f-494d-8bec-e4ba29a3f2d1", - "name": "high", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 2147483647 - } - }, - "outputs": { - "value": { - "id": "812ade4d-7699-4261-b9fc-a6c9d2ab55ee", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "position": { - "x": 1275, - "y": -250 - }, - "width": 320, - "height": 24 - }, - { - "id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "type": "invocation", - "data": { - "id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "type": "sdxl_model_loader", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "model": { - "id": "39f9e799-bc95-4318-a200-30eed9e60c42", - "name": "model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SDXLMainModelField" - }, - "value": { - "model_name": "stable-diffusion-xl-base-1.0", - "base_model": "sdxl", - "model_type": "main" - } - } - }, - "outputs": { - "unet": { - "id": "2626a45e-59aa-4609-b131-2d45c5eaed69", - "name": "unet", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "clip": { - "id": "7c9c42fa-93d5-4639-ab8b-c4d9b0559baf", - "name": "clip", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "clip2": { - "id": "0dafddcf-a472-49c1-a47c-7b8fab4c8bc9", - "name": "clip2", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "vae": { - "id": "ee6a6997-1b3c-4ff3-99ce-1e7bfba2750c", - "name": "vae", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - } - } - }, - "position": { - "x": 700, - "y": 175 - }, - "width": 320, - "height": 24 - }, - { - "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "type": "invocation", - "data": { - "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "type": "sdxl_compel_prompt", - "label": "SDXL Positive Compel Prompt", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "5a6889e6-95cb-462f-8f4a-6b93ae7afaec", - "name": "prompt", - "fieldKind": "input", - "label": "Positive Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "style": { - "id": "f240d0e6-3a1c-4320-af23-20ebb707c276", - "name": "style", - "fieldKind": "input", - "label": "Positive Style", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "original_width": { - "id": "05af07b0-99a0-4a68-8ad2-697bbdb7fc7e", - "name": "original_width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "original_height": { - "id": "2c771996-a998-43b7-9dd3-3792664d4e5b", - "name": "original_height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "crop_top": { - "id": "66519dca-a151-4e3e-ae1f-88f1f9877bde", - "name": "crop_top", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "crop_left": { - "id": "349cf2e9-f3d0-4e16-9ae2-7097d25b6a51", - "name": "crop_left", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "target_width": { - "id": "44499347-7bd6-4a73-99d6-5a982786db05", - "name": "target_width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "target_height": { - "id": "fda359b0-ab80-4f3c-805b-c9f61319d7d2", - "name": "target_height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "clip": { - "id": "b447adaf-a649-4a76-a827-046a9fc8d89b", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "clip2": { - "id": "86ee4e32-08f9-4baa-9163-31d93f5c0187", - "name": "clip2", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "7c10118e-7b4e-4911-b98e-d3ba6347dfd0", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "position": { - "x": 550, - "y": -925 - }, - "width": 320, - "height": 699 - }, - { - "id": "f0e06b70-9f53-44e3-8f5f-63d813b6b579", - "type": "invocation", - "data": { - "id": "f0e06b70-9f53-44e3-8f5f-63d813b6b579", - "type": "sdxl_refiner_compel_prompt", - "label": "SDXL Refiner Positive Compel Prompt", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "style": { - "id": "c6f91ecf-370f-44d0-8243-63724e75510a", - "name": "style", - "fieldKind": "input", - "label": "Positive Style", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "original_width": { - "id": "956f6eca-8324-41eb-a8dd-fa9b34164ca6", - "name": "original_width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "original_height": { - "id": "a41bb3a1-7664-4dac-b6ae-6f4dff3828a9", - "name": "original_height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 1024 - }, - "crop_top": { - "id": "81936e19-0ae7-4006-9d7c-e359fc7c7d15", - "name": "crop_top", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "crop_left": { - "id": "be94ddb8-88cc-4d6b-a2c0-f2b43143bfa1", - "name": "crop_left", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "aesthetic_score": { - "id": "60f380de-87b4-4535-b3ef-545a6e57283e", - "name": "aesthetic_score", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 6 - }, - "clip2": { - "id": "773c4054-c005-46ad-92c4-5c1fa4506041", - "name": "clip2", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "63e16bfa-59d8-4d6f-abda-ad979b26adb5", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "position": { - "x": 1275, - "y": -925 - }, - "width": 320, - "height": 483 - }, - { - "id": "62bdf243-d98f-4508-b6b5-c3af00ef49f0", - "type": "invocation", - "data": { - "id": "62bdf243-d98f-4508-b6b5-c3af00ef49f0", - "type": "sdxl_refiner_model_loader", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "model": { - "id": "2f09431c-096b-4848-b580-b37be773839d", - "name": "model", - "fieldKind": "input", - "label": "Refiner Model", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SDXLRefinerModelField" - }, - "value": { - "model_name": "stable-diffusion-xl-refiner-1.0", - "base_model": "sdxl-refiner", - "model_type": "main" - } - } - }, - "outputs": { - "unet": { - "id": "c06b335a-7f65-4165-9ca2-40107eb9c85b", - "name": "unet", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "clip2": { - "id": "81ec105e-cc1f-4b78-a5bb-8df3a4dd2574", - "name": "clip2", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "vae": { - "id": "f516feab-873d-47ec-a946-b8f15eed4bed", - "name": "vae", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - } - } - }, - "position": { - "x": 700, - "y": 225 - }, - "width": 320, - "height": 24 - }, - { - "id": "5639e3bc-b769-4ae5-9262-72db703c5a7b", - "type": "invocation", - "data": { - "id": "5639e3bc-b769-4ae5-9262-72db703c5a7b", - "type": "string", - "label": "Negative Prompt", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "value": { - "id": "89854c84-cbc1-4b60-921d-4bade11cab66", - "name": "value", - "fieldKind": "input", - "label": "Negative Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - } - }, - "outputs": { - "value": { - "id": "3617404e-e483-4e2e-8550-7080a1ef283f", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - } - } - } - }, - "position": { - "x": 700, - "y": 25 - }, - "width": 320, - "height": 24 - }, - { - "id": "06a30867-1e9d-461f-bd58-14a63cc997dd", - "type": "invocation", - "data": { - "id": "06a30867-1e9d-461f-bd58-14a63cc997dd", - "type": "scheduler", - "label": "", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "scheduler": { - "id": "0be5a5a0-3388-41e1-a6c4-10d414d8fe5b", - "name": "scheduler", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SchedulerField" - }, - "value": "euler" - } - }, - "outputs": { - "scheduler": { - "id": "cafdef9d-61cd-4f43-be91-356a1d65afca", - "name": "scheduler", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SchedulerField" - } - } - } - }, - "position": { - "x": 700, - "y": 125 - }, - "width": 320, - "height": 24 - }, - { - "id": "84df8f00-ea7e-499f-ab86-d019ddea5393", - "type": "invocation", - "data": { - "id": "84df8f00-ea7e-499f-ab86-d019ddea5393", - "type": "denoise_latents", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.5.1", - "nodePack": "invokeai", - "inputs": { - "positive_conditioning": { - "id": "73b2ebc2-4a56-4809-b8ab-b78fde786961", - "name": "positive_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "negative_conditioning": { - "id": "04d1bfbb-6cdc-4c16-8e08-290ba86ca8ba", - "name": "negative_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "noise": { - "id": "39ea4659-ea69-415f-85c0-a06f94d53e14", - "name": "noise", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "steps": { - "id": "dfd3c295-adae-499a-8c94-3c6c6d9ece0e", - "name": "steps", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 10 - }, - "cfg_scale": { - "id": "2ae0c196-8c94-4ea8-a9fc-1be06938a0c3", - "name": "cfg_scale", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 7.5 - }, - "denoising_start": { - "id": "3d085ec1-14de-4eef-9853-2edf5d81daac", - "name": "denoising_start", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "denoising_end": { - "id": "1a820924-15ca-4ba5-b981-6b588e486a5b", - "name": "denoising_end", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0.8 - }, - "scheduler": { - "id": "d0d19fab-5001-4c5d-b664-031df1a65311", - "name": "scheduler", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SchedulerField" - }, - "value": "euler" - }, - "unet": { - "id": "efbdecd1-5c07-420c-bd58-52de43fcde4c", - "name": "unet", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "control": { - "id": "e1a457c4-5546-4c02-83e1-092776b27cd1", - "name": "control", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "ControlField" - } - }, - "ip_adapter": { - "id": "d4082d78-7f17-4f87-af05-5a76129737ba", - "name": "ip_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "IPAdapterField" - } - }, - "t2i_adapter": { - "id": "4841595c-f81b-440a-9377-fe89b26b42ac", - "name": "t2i_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "T2IAdapterField" - } - }, - "cfg_rescale_multiplier": { - "id": "679083b2-f137-416f-a9d8-ec38f6263c1f", - "name": "cfg_rescale_multiplier", - "type": { - "name": "FloatField", - "isCollection": false, - "isCollectionOrScalar": false - }, - "label": "", - "fieldKind": "input", - "value": 0 - }, - "latents": { - "id": "60bbfc7e-6641-4354-b678-12029c580aa9", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "denoise_mask": { - "id": "b2876171-e4c5-45cf-a352-852047c902fc", - "name": "denoise_mask", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "DenoiseMaskField" - } - } - }, - "outputs": { - "latents": { - "id": "62705a29-cc3a-4154-8f62-a8f821daf861", - "name": "latents", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "eb2e2312-1e64-4008-a64f-6783d49dde29", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "7bd8d012-edcf-4def-98eb-7ebdd724c7c5", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "position": { - "x": 1269.2683722842958, - "y": -119.4839111990423 - }, - "width": 320, - "height": 614 - }, - { - "id": "3d40eda5-ff7b-4dff-8d2e-4f44742faa1b", - "type": "invocation", - "data": { - "id": "3d40eda5-ff7b-4dff-8d2e-4f44742faa1b", - "type": "denoise_latents", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.5.1", - "nodePack": "invokeai", - "inputs": { - "positive_conditioning": { - "id": "a9c932a9-6164-4333-bade-3909c9a3ce59", - "name": "positive_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "negative_conditioning": { - "id": "a57fced5-aca6-40c9-8197-ce4f01433111", - "name": "negative_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "noise": { - "id": "b4cbec14-c24e-4ec2-bda3-8fc19c089717", - "name": "noise", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "steps": { - "id": "24dd36f7-fdf1-40c9-945c-216471b44a2f", - "name": "steps", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 10 - }, - "cfg_scale": { - "id": "5f3a7f0c-5088-49e9-b490-75822d0c20cc", - "name": "cfg_scale", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 7.5 - }, - "denoising_start": { - "id": "b326ffde-625c-49c5-b5e1-90b79df80979", - "name": "denoising_start", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0.8 - }, - "denoising_end": { - "id": "d4a458ef-5576-4c5d-8e6d-ee04c9c7c4dc", - "name": "denoising_end", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1 - }, - "scheduler": { - "id": "95aba2d0-c470-44f2-a25c-a192600be6da", - "name": "scheduler", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SchedulerField" - }, - "value": "euler" - }, - "unet": { - "id": "68a8636e-3b2f-4a95-bd05-d86a01edb74a", - "name": "unet", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "control": { - "id": "508e68a6-1cfc-4121-baed-b829b2886474", - "name": "control", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "ControlField" - } - }, - "ip_adapter": { - "id": "e976e72f-8bd1-44d4-ad75-8410db221e3f", - "name": "ip_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "IPAdapterField" - } - }, - "t2i_adapter": { - "id": "38e15c99-ff72-443a-bddc-440fab9ccefc", - "name": "t2i_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "T2IAdapterField" - } - }, - "cfg_rescale_multiplier": { - "id": "30c6aca9-333d-49fa-be99-89daf82fe850", - "name": "cfg_rescale_multiplier", - "type": { - "name": "FloatField", - "isCollection": false, - "isCollectionOrScalar": false - }, - "label": "", - "fieldKind": "input", - "value": 0 - }, - "latents": { - "id": "f6d628e8-05ca-4ee3-a5a4-35323ebeb853", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "denoise_mask": { - "id": "e2385456-d127-4793-98ca-d93b4aee3481", - "name": "denoise_mask", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "DenoiseMaskField" - } - } - }, - "outputs": { - "latents": { - "id": "a61efc39-ba21-468b-ae58-5922337cf399", - "name": "latents", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "9093043b-808b-4ac6-ab18-7d721a7e39d7", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "5ff472a4-ee22-4988-bcf7-7d6116a37e5a", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "position": { - "x": 1672.552348276784, - "y": -118.3156091718022 - }, - "width": 320, - "height": 614 - }, - { - "id": "17eb4b88-bdd8-4984-affa-26586b146866", - "type": "invocation", - "data": { - "id": "17eb4b88-bdd8-4984-affa-26586b146866", - "type": "l2i", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": false, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "04f49f70-8ee6-43e3-ac63-af02e5b34204", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "latents": { - "id": "d27c4313-01db-45cb-b9f4-6a827d0d766a", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "vae": { - "id": "3751b0f6-69f3-4f95-a7f9-476b2d31e9f9", - "name": "vae", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - }, - "tiled": { - "id": "07e5e79a-b452-4beb-b26f-715da2387ac7", - "name": "tiled", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - }, - "fp32": { - "id": "afe954f6-ecaf-4eac-98ee-23f4d0eb7a6b", - "name": "fp32", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - } - }, - "outputs": { - "image": { - "id": "98515f37-9fe7-420e-839b-6e349d9407df", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "5203f61f-02db-423c-9c85-80aa20816dea", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "499e6152-c604-4dad-84c3-8c5b26a39919", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "position": { - "x": 2045.2934900771834, - "y": -362.2916292593367 - }, - "width": 320, - "height": 225 - } - ], - "edges": [ - { - "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2-55705012-79b9-4aac-9f26-c0b10309785b-collapsed", - "type": "collapsed", - "source": "ea94bc37-d995-4a83-aa99-4af42479f2f2", - "target": "55705012-79b9-4aac-9f26-c0b10309785b" - }, - { - "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-62bdf243-d98f-4508-b6b5-c3af00ef49f0clip2-f0e06b70-9f53-44e3-8f5f-63d813b6b579clip2", - "type": "default", - "source": "62bdf243-d98f-4508-b6b5-c3af00ef49f0", - "target": "f0e06b70-9f53-44e3-8f5f-63d813b6b579", - "sourceHandle": "clip2", - "targetHandle": "clip2" - }, - { - "id": "reactflow__edge-62bdf243-d98f-4508-b6b5-c3af00ef49f0clip2-fbb2f1a0-2e68-411d-a955-60c3b8a6f2d1clip2", - "type": "default", - "source": "62bdf243-d98f-4508-b6b5-c3af00ef49f0", - "target": "fbb2f1a0-2e68-411d-a955-60c3b8a6f2d1", - "sourceHandle": "clip2", - "targetHandle": "clip2" - }, - { - "id": "reactflow__edge-5639e3bc-b769-4ae5-9262-72db703c5a7bvalue-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204prompt", - "type": "default", - "source": "5639e3bc-b769-4ae5-9262-72db703c5a7b", - "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "sourceHandle": "value", - "targetHandle": "prompt" - }, - { - "id": "reactflow__edge-f1a6a160-4c36-4902-8eeb-8b1c23e81bc8value-faf965a4-7530-427b-b1f3-4ba6505c2a08style", - "type": "default", - "source": "f1a6a160-4c36-4902-8eeb-8b1c23e81bc8", - "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "sourceHandle": "value", - "targetHandle": "style" - }, - { - "id": "reactflow__edge-b2b35add-929d-4538-aecb-02c661768b29value-faf965a4-7530-427b-b1f3-4ba6505c2a08prompt", - "type": "default", - "source": "b2b35add-929d-4538-aecb-02c661768b29", - "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "sourceHandle": "value", - "targetHandle": "prompt" - }, - { - "id": "reactflow__edge-f1a6a160-4c36-4902-8eeb-8b1c23e81bc8value-f0e06b70-9f53-44e3-8f5f-63d813b6b579style", - "type": "default", - "source": "f1a6a160-4c36-4902-8eeb-8b1c23e81bc8", - "target": "f0e06b70-9f53-44e3-8f5f-63d813b6b579", - "sourceHandle": "value", - "targetHandle": "style" - }, - { - "id": "reactflow__edge-8d54b9db-3662-43af-8369-9a277e063f3bvalue-fbb2f1a0-2e68-411d-a955-60c3b8a6f2d1style", - "type": "default", - "source": "8d54b9db-3662-43af-8369-9a277e063f3b", - "target": "fbb2f1a0-2e68-411d-a955-60c3b8a6f2d1", - "sourceHandle": "value", - "targetHandle": "style" - }, - { - "id": "reactflow__edge-8d54b9db-3662-43af-8369-9a277e063f3bvalue-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204style", - "type": "default", - "source": "8d54b9db-3662-43af-8369-9a277e063f3b", - "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "sourceHandle": "value", - "targetHandle": "style" - }, - { - "id": "reactflow__edge-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204conditioning-84df8f00-ea7e-499f-ab86-d019ddea5393negative_conditioning", - "type": "default", - "source": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "target": "84df8f00-ea7e-499f-ab86-d019ddea5393", - "sourceHandle": "conditioning", - "targetHandle": "negative_conditioning" - }, - { - "id": "reactflow__edge-faf965a4-7530-427b-b1f3-4ba6505c2a08conditioning-84df8f00-ea7e-499f-ab86-d019ddea5393positive_conditioning", - "type": "default", - "source": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "target": "84df8f00-ea7e-499f-ab86-d019ddea5393", - "sourceHandle": "conditioning", - "targetHandle": "positive_conditioning" - }, - { - "id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-84df8f00-ea7e-499f-ab86-d019ddea5393noise", - "type": "default", - "source": "55705012-79b9-4aac-9f26-c0b10309785b", - "target": "84df8f00-ea7e-499f-ab86-d019ddea5393", - "sourceHandle": "noise", - "targetHandle": "noise" - }, - { - "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22unet-84df8f00-ea7e-499f-ab86-d019ddea5393unet", - "type": "default", - "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "target": "84df8f00-ea7e-499f-ab86-d019ddea5393", - "sourceHandle": "unet", - "targetHandle": "unet" - }, - { - "id": "reactflow__edge-06a30867-1e9d-461f-bd58-14a63cc997ddscheduler-84df8f00-ea7e-499f-ab86-d019ddea5393scheduler", - "type": "default", - "source": "06a30867-1e9d-461f-bd58-14a63cc997dd", - "target": "84df8f00-ea7e-499f-ab86-d019ddea5393", - "sourceHandle": "scheduler", - "targetHandle": "scheduler" - }, - { - "id": "reactflow__edge-84df8f00-ea7e-499f-ab86-d019ddea5393latents-3d40eda5-ff7b-4dff-8d2e-4f44742faa1blatents", - "type": "default", - "source": "84df8f00-ea7e-499f-ab86-d019ddea5393", - "target": "3d40eda5-ff7b-4dff-8d2e-4f44742faa1b", - "sourceHandle": "latents", - "targetHandle": "latents" - }, - { - "id": "reactflow__edge-62bdf243-d98f-4508-b6b5-c3af00ef49f0unet-3d40eda5-ff7b-4dff-8d2e-4f44742faa1bunet", - "type": "default", - "source": "62bdf243-d98f-4508-b6b5-c3af00ef49f0", - "target": "3d40eda5-ff7b-4dff-8d2e-4f44742faa1b", - "sourceHandle": "unet", - "targetHandle": "unet" - }, - { - "id": "reactflow__edge-f0e06b70-9f53-44e3-8f5f-63d813b6b579conditioning-3d40eda5-ff7b-4dff-8d2e-4f44742faa1bpositive_conditioning", - "type": "default", - "source": "f0e06b70-9f53-44e3-8f5f-63d813b6b579", - "target": "3d40eda5-ff7b-4dff-8d2e-4f44742faa1b", - "sourceHandle": "conditioning", - "targetHandle": "positive_conditioning" - }, - { - "id": "reactflow__edge-fbb2f1a0-2e68-411d-a955-60c3b8a6f2d1conditioning-3d40eda5-ff7b-4dff-8d2e-4f44742faa1bnegative_conditioning", - "type": "default", - "source": "fbb2f1a0-2e68-411d-a955-60c3b8a6f2d1", - "target": "3d40eda5-ff7b-4dff-8d2e-4f44742faa1b", - "sourceHandle": "conditioning", - "targetHandle": "negative_conditioning" - }, - { - "id": "reactflow__edge-3d40eda5-ff7b-4dff-8d2e-4f44742faa1blatents-17eb4b88-bdd8-4984-affa-26586b146866latents", - "type": "default", - "source": "3d40eda5-ff7b-4dff-8d2e-4f44742faa1b", - "target": "17eb4b88-bdd8-4984-affa-26586b146866", - "sourceHandle": "latents", - "targetHandle": "latents" - }, - { - "id": "reactflow__edge-62bdf243-d98f-4508-b6b5-c3af00ef49f0vae-17eb4b88-bdd8-4984-affa-26586b146866vae", - "type": "default", - "source": "62bdf243-d98f-4508-b6b5-c3af00ef49f0", - "target": "17eb4b88-bdd8-4984-affa-26586b146866", - "sourceHandle": "vae", - "targetHandle": "vae" - } - ] -} \ No newline at end of file diff --git a/docs/workflows/Text_to_Image.json b/docs/workflows/Text_to_Image.json deleted file mode 100644 index 1e42df6e07a..00000000000 --- a/docs/workflows/Text_to_Image.json +++ /dev/null @@ -1,798 +0,0 @@ -{ - "name": "Text to Image - SD1.5", - "author": "InvokeAI", - "description": "Sample text to image workflow for Stable Diffusion 1.5/2", - "version": "1.1.0", - "contact": "invoke@invoke.ai", - "tags": "text2image, SD1.5, SD2, default", - "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" - } - ], - "meta": { - "category": "default", - "version": "2.0.0" - }, - "nodes": [ - { - "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402", - "type": "invocation", - "data": { - "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402", - "type": "compel", - "label": "Negative Compel Prompt", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "7739aff6-26cb-4016-8897-5a1fb2305e4e", - "name": "prompt", - "fieldKind": "input", - "label": "Negative Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "" - }, - "clip": { - "id": "48d23dce-a6ae-472a-9f8c-22a714ea5ce0", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "37cf3a9d-f6b7-4b64-8ff6-2558c5ecc447", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 259, - "position": { - "x": 1000, - "y": 350 - } - }, - { - "id": "55705012-79b9-4aac-9f26-c0b10309785b", - "type": "invocation", - "data": { - "id": "55705012-79b9-4aac-9f26-c0b10309785b", - "type": "noise", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.1", - "nodePack": "invokeai", - "inputs": { - "seed": { - "id": "6431737c-918a-425d-a3b4-5d57e2f35d4d", - "name": "seed", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "width": { - "id": "38fc5b66-fe6e-47c8-bba9-daf58e454ed7", - "name": "width", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "height": { - "id": "16298330-e2bf-4872-a514-d6923df53cbb", - "name": "height", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 512 - }, - "use_cpu": { - "id": "c7c436d3-7a7a-4e76-91e4-c6deb271623c", - "name": "use_cpu", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": true - } - }, - "outputs": { - "noise": { - "id": "50f650dc-0184-4e23-a927-0497a96fe954", - "name": "noise", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "bb8a452b-133d-42d1-ae4a-3843d7e4109a", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "35cfaa12-3b8b-4b7a-a884-327ff3abddd9", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 388, - "position": { - "x": 600, - "y": 325 - } - }, - { - "id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", - "type": "invocation", - "data": { - "id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", - "type": "main_model_loader", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "model": { - "id": "993eabd2-40fd-44fe-bce7-5d0c7075ddab", - "name": "model", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MainModelField" - }, - "value": { - "model_name": "stable-diffusion-v1-5", - "base_model": "sd-1", - "model_type": "main" - } - } - }, - "outputs": { - "unet": { - "id": "5c18c9db-328d-46d0-8cb9-143391c410be", - "name": "unet", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "clip": { - "id": "6effcac0-ec2f-4bf5-a49e-a2c29cf921f4", - "name": "clip", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - }, - "vae": { - "id": "57683ba3-f5f5-4f58-b9a2-4b83dacad4a1", - "name": "vae", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - } - } - }, - "width": 320, - "height": 226, - "position": { - "x": 600, - "y": 25 - } - }, - { - "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c", - "type": "invocation", - "data": { - "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c", - "type": "compel", - "label": "Positive Compel Prompt", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "prompt": { - "id": "7739aff6-26cb-4016-8897-5a1fb2305e4e", - "name": "prompt", - "fieldKind": "input", - "label": "Positive Prompt", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "StringField" - }, - "value": "Super cute tiger cub, national geographic award-winning photograph" - }, - "clip": { - "id": "48d23dce-a6ae-472a-9f8c-22a714ea5ce0", - "name": "clip", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ClipField" - } - } - }, - "outputs": { - "conditioning": { - "id": "37cf3a9d-f6b7-4b64-8ff6-2558c5ecc447", - "name": "conditioning", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - } - } - }, - "width": 320, - "height": 259, - "position": { - "x": 1000, - "y": 25 - } - }, - { - "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", - "type": "invocation", - "data": { - "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2", - "type": "rand_int", - "label": "Random Seed", - "isOpen": false, - "notes": "", - "isIntermediate": true, - "useCache": false, - "version": "1.0.0", - "nodePack": "invokeai", - "inputs": { - "low": { - "id": "3ec65a37-60ba-4b6c-a0b2-553dd7a84b84", - "name": "low", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 0 - }, - "high": { - "id": "085f853a-1a5f-494d-8bec-e4ba29a3f2d1", - "name": "high", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 2147483647 - } - }, - "outputs": { - "value": { - "id": "812ade4d-7699-4261-b9fc-a6c9d2ab55ee", - "name": "value", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 32, - "position": { - "x": 600, - "y": 275 - } - }, - { - "id": "eea2702a-19fb-45b5-9d75-56b4211ec03c", - "type": "invocation", - "data": { - "id": "eea2702a-19fb-45b5-9d75-56b4211ec03c", - "type": "denoise_latents", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": true, - "useCache": true, - "version": "1.5.0", - "nodePack": "invokeai", - "inputs": { - "positive_conditioning": { - "id": "90b7f4f8-ada7-4028-8100-d2e54f192052", - "name": "positive_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "negative_conditioning": { - "id": "9393779e-796c-4f64-b740-902a1177bf53", - "name": "negative_conditioning", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ConditioningField" - } - }, - "noise": { - "id": "8e17f1e5-4f98-40b1-b7f4-86aeeb4554c1", - "name": "noise", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "steps": { - "id": "9b63302d-6bd2-42c9-ac13-9b1afb51af88", - "name": "steps", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - }, - "value": 50 - }, - "cfg_scale": { - "id": "87dd04d3-870e-49e1-98bf-af003a810109", - "name": "cfg_scale", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "FloatField" - }, - "value": 7.5 - }, - "denoising_start": { - "id": "f369d80f-4931-4740-9bcd-9f0620719fab", - "name": "denoising_start", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "denoising_end": { - "id": "747d10e5-6f02-445c-994c-0604d814de8c", - "name": "denoising_end", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 1 - }, - "scheduler": { - "id": "1de84a4e-3a24-4ec8-862b-16ce49633b9b", - "name": "scheduler", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "SchedulerField" - }, - "value": "unipc" - }, - "unet": { - "id": "ffa6fef4-3ce2-4bdb-9296-9a834849489b", - "name": "unet", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "UNetField" - } - }, - "control": { - "id": "077b64cb-34be-4fcc-83f2-e399807a02bd", - "name": "control", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "ControlField" - } - }, - "ip_adapter": { - "id": "1d6948f7-3a65-4a65-a20c-768b287251aa", - "name": "ip_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "IPAdapterField" - } - }, - "t2i_adapter": { - "id": "75e67b09-952f-4083-aaf4-6b804d690412", - "name": "t2i_adapter", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": true, - "name": "T2IAdapterField" - } - }, - "cfg_rescale_multiplier": { - "id": "9101f0a6-5fe0-4826-b7b3-47e5d506826c", - "name": "cfg_rescale_multiplier", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "FloatField" - }, - "value": 0 - }, - "latents": { - "id": "334d4ba3-5a99-4195-82c5-86fb3f4f7d43", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "denoise_mask": { - "id": "0d3dbdbf-b014-4e95-8b18-ff2ff9cb0bfa", - "name": "denoise_mask", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "DenoiseMaskField" - } - } - }, - "outputs": { - "latents": { - "id": "70fa5bbc-0c38-41bb-861a-74d6d78d2f38", - "name": "latents", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "width": { - "id": "98ee0e6c-82aa-4e8f-8be5-dc5f00ee47f0", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "e8cb184a-5e1a-47c8-9695-4b8979564f5d", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 703, - "position": { - "x": 1400, - "y": 25 - } - }, - { - "id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", - "type": "invocation", - "data": { - "id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", - "type": "l2i", - "label": "", - "isOpen": true, - "notes": "", - "isIntermediate": false, - "useCache": true, - "version": "1.2.0", - "nodePack": "invokeai", - "inputs": { - "metadata": { - "id": "ab375f12-0042-4410-9182-29e30db82c85", - "name": "metadata", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "MetadataField" - } - }, - "latents": { - "id": "3a7e7efd-bff5-47d7-9d48-615127afee78", - "name": "latents", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "LatentsField" - } - }, - "vae": { - "id": "a1f5f7a1-0795-4d58-b036-7820c0b0ef2b", - "name": "vae", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "VaeField" - } - }, - "tiled": { - "id": "da52059a-0cee-4668-942f-519aa794d739", - "name": "tiled", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": false - }, - "fp32": { - "id": "c4841df3-b24e-4140-be3b-ccd454c2522c", - "name": "fp32", - "fieldKind": "input", - "label": "", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "BooleanField" - }, - "value": true - } - }, - "outputs": { - "image": { - "id": "72d667d0-cf85-459d-abf2-28bd8b823fe7", - "name": "image", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "ImageField" - } - }, - "width": { - "id": "c8c907d8-1066-49d1-b9a6-83bdcd53addc", - "name": "width", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - }, - "height": { - "id": "230f359c-b4ea-436c-b372-332d7dcdca85", - "name": "height", - "fieldKind": "output", - "type": { - "isCollection": false, - "isCollectionOrScalar": false, - "name": "IntegerField" - } - } - } - }, - "width": 320, - "height": 266, - "position": { - "x": 1800, - "y": 25 - } - } - ], - "edges": [ - { - "id": "reactflow__edge-ea94bc37-d995-4a83-aa99-4af42479f2f2value-55705012-79b9-4aac-9f26-c0b10309785bseed", - "source": "ea94bc37-d995-4a83-aa99-4af42479f2f2", - "target": "55705012-79b9-4aac-9f26-c0b10309785b", - "type": "default", - "sourceHandle": "value", - "targetHandle": "seed" - }, - { - "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8clip-7d8bf987-284f-413a-b2fd-d825445a5d6cclip", - "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", - "target": "7d8bf987-284f-413a-b2fd-d825445a5d6c", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8clip-93dc02a4-d05b-48ed-b99c-c9b616af3402clip", - "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", - "target": "93dc02a4-d05b-48ed-b99c-c9b616af3402", - "type": "default", - "sourceHandle": "clip", - "targetHandle": "clip" - }, - { - "id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-eea2702a-19fb-45b5-9d75-56b4211ec03cnoise", - "source": "55705012-79b9-4aac-9f26-c0b10309785b", - "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", - "type": "default", - "sourceHandle": "noise", - "targetHandle": "noise" - }, - { - "id": "reactflow__edge-7d8bf987-284f-413a-b2fd-d825445a5d6cconditioning-eea2702a-19fb-45b5-9d75-56b4211ec03cpositive_conditioning", - "source": "7d8bf987-284f-413a-b2fd-d825445a5d6c", - "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "positive_conditioning" - }, - { - "id": "reactflow__edge-93dc02a4-d05b-48ed-b99c-c9b616af3402conditioning-eea2702a-19fb-45b5-9d75-56b4211ec03cnegative_conditioning", - "source": "93dc02a4-d05b-48ed-b99c-c9b616af3402", - "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", - "type": "default", - "sourceHandle": "conditioning", - "targetHandle": "negative_conditioning" - }, - { - "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8unet-eea2702a-19fb-45b5-9d75-56b4211ec03cunet", - "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", - "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c", - "type": "default", - "sourceHandle": "unet", - "targetHandle": "unet" - }, - { - "id": "reactflow__edge-eea2702a-19fb-45b5-9d75-56b4211ec03clatents-58c957f5-0d01-41fc-a803-b2bbf0413d4flatents", - "source": "eea2702a-19fb-45b5-9d75-56b4211ec03c", - "target": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", - "type": "default", - "sourceHandle": "latents", - "targetHandle": "latents" - }, - { - "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8vae-58c957f5-0d01-41fc-a803-b2bbf0413d4fvae", - "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", - "target": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", - "type": "default", - "sourceHandle": "vae", - "targetHandle": "vae" - } - ] -} diff --git a/flake.lock b/flake.lock index 536c8259b1f..dedd56e74f3 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1690630721, - "narHash": "sha256-Y04onHyBQT4Erfr2fc82dbJTfXGYrf4V0ysLUYnPOP8=", + "lastModified": 1727955264, + "narHash": "sha256-lrd+7mmb5NauRoMa8+J1jFKYVa+rc8aq2qc9+CxPDKc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d2b52322f35597c62abf56de91b0236746b2a03d", + "rev": "71cd616696bd199ef18de62524f3df3ffe8b9333", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 3ccc6658121..07af19e93bf 100644 --- a/flake.nix +++ b/flake.nix @@ -34,7 +34,7 @@ cudaPackages.cudnn cudaPackages.cuda_nvrtc cudatoolkit - pkgconfig + pkg-config libconfig cmake blas @@ -66,7 +66,7 @@ black # Frontend. - yarn + pnpm_8 nodejs ]; LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs; diff --git a/installer/WinLongPathsEnabled.reg b/installer/WinLongPathsEnabled.reg deleted file mode 100644 index 778782b2724..00000000000 Binary files a/installer/WinLongPathsEnabled.reg and /dev/null differ diff --git a/installer/create_installer.sh b/installer/create_installer.sh deleted file mode 100755 index a71b0d9c417..00000000000 --- a/installer/create_installer.sh +++ /dev/null @@ -1,133 +0,0 @@ -#!/bin/bash - -set -e - -BCYAN="\033[1;36m" -BYELLOW="\033[1;33m" -BGREEN="\033[1;32m" -BRED="\033[1;31m" -RED="\033[31m" -RESET="\033[0m" - -function git_show { - git show -s --format=oneline --abbrev-commit "$1" | cat -} - -if [[ ! -z "${VIRTUAL_ENV}" ]]; then - # we can't just call 'deactivate' because this function is not exported - # to the environment of this script from the bash process that runs the script - echo -e "${BRED}A virtual environment is activated. Please deactivate it before proceeding.${RESET}" - exit -1 -fi - -cd "$(dirname "$0")" - -VERSION=$( - cd .. - python3 -c "from invokeai.version import __version__ as version; print(version)" -) -VERSION="v${VERSION}" - -if [[ ! -z ${CI} ]]; then - echo - echo -e "${BCYAN}CI environment detected${RESET}" - echo -else - echo - echo -e "${BYELLOW}This script must be run from the installer directory!${RESET}" - echo "The current working directory is $(pwd)" - read -p "If that looks right, press any key to proceed, or CTRL-C to exit..." - echo -fi - -echo -e "${BGREEN}HEAD${RESET}:" -git_show HEAD -echo - -# ---------------------- FRONTEND ---------------------- - -pushd ../invokeai/frontend/web >/dev/null -echo "Installing frontend dependencies..." -echo -pnpm i --frozen-lockfile -echo -if [[ ! -z ${CI} ]]; then - echo "Building frontend without checks..." - # In CI, we have already done the frontend checks and can just build - pnpm vite build -else - echo "Running checks and building frontend..." - # This runs all the frontend checks and builds - pnpm build -fi -echo -popd - -# ---------------------- BACKEND ---------------------- - -echo -echo "Building wheel..." -echo - -# install the 'build' package in the user site packages, if needed -# could be improved by using a temporary venv, but it's tiny and harmless -if [[ $(python3 -c 'from importlib.util import find_spec; print(find_spec("build") is None)') == "True" ]]; then - pip install --user build -fi - -rm -rf ../build - -python3 -m build --outdir dist/ ../. - -# ---------------------- - -echo -echo "Building installer zip files for InvokeAI ${VERSION}..." -echo - -# get rid of any old ones -rm -f *.zip -rm -rf InvokeAI-Installer - -# copy content -mkdir InvokeAI-Installer -for f in templates *.txt *.reg; do - cp -r ${f} InvokeAI-Installer/ -done -mkdir InvokeAI-Installer/lib -cp lib/*.py InvokeAI-Installer/lib - -# Install scripts -# Mac/Linux -cp install.sh.in InvokeAI-Installer/install.sh -chmod a+x InvokeAI-Installer/install.sh - -# Windows -cp install.bat.in InvokeAI-Installer/install.bat -cp WinLongPathsEnabled.reg InvokeAI-Installer/ - -FILENAME=InvokeAI-installer-$VERSION.zip - -# Zip everything up -zip -r ${FILENAME} InvokeAI-Installer - -echo -echo -e "${BGREEN}Built installer: ./${FILENAME}${RESET}" -echo -e "${BGREEN}Built PyPi distribution: ./dist${RESET}" - -# clean up, but only if we are not in a github action -if [[ -z ${CI} ]]; then - echo - echo "Cleaning up intermediate build files..." - rm -rf InvokeAI-Installer tmp ../invokeai/frontend/web/dist/ -fi - -if [[ ! -z ${CI} ]]; then - echo - echo "Setting GitHub action outputs..." - echo "INSTALLER_FILENAME=${FILENAME}" >>$GITHUB_OUTPUT - echo "INSTALLER_PATH=installer/${FILENAME}" >>$GITHUB_OUTPUT - echo "DIST_PATH=installer/dist/" >>$GITHUB_OUTPUT -fi - -exit 0 diff --git a/installer/install.bat.in b/installer/install.bat.in deleted file mode 100644 index b06aa97c98b..00000000000 --- a/installer/install.bat.in +++ /dev/null @@ -1,128 +0,0 @@ -@echo off -setlocal EnableExtensions EnableDelayedExpansion - -@rem This script requires the user to install Python 3.10 or higher. All other -@rem requirements are downloaded as needed. - -@rem change to the script's directory -PUSHD "%~dp0" - -set "no_cache_dir=--no-cache-dir" -if "%1" == "use-cache" ( - set "no_cache_dir=" -) - -@rem Config -@rem The version in the next line is replaced by an up to date release number -@rem when create_installer.sh is run. Change the release number there. -set INSTRUCTIONS=https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/ -set TROUBLESHOOTING=https://invoke-ai.github.io/InvokeAI/help/FAQ/ -set PYTHON_URL=https://www.python.org/downloads/windows/ -set MINIMUM_PYTHON_VERSION=3.10.0 -set PYTHON_URL=https://www.python.org/downloads/release/python-3109/ - -set err_msg=An error has occurred and the script could not continue. - -@rem --------------------------- Intro ------------------------------- -echo This script will install InvokeAI and its dependencies. -echo. -echo BEFORE YOU START PLEASE MAKE SURE TO DO THE FOLLOWING -echo 1. Install python 3.10 or 3.11. Python version 3.9 is no longer supported. -echo 2. Double-click on the file WinLongPathsEnabled.reg in order to -echo enable long path support on your system. -echo 3. Install the Visual C++ core libraries. -echo Please download and install the libraries from: -echo https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170 -echo. -echo See %INSTRUCTIONS% for more details. -echo. -echo FOR THE BEST USER EXPERIENCE WE SUGGEST MAXIMIZING THIS WINDOW NOW. -pause - -@rem ---------------------------- check Python version --------------- -echo ***** Checking and Updating Python ***** - -call python --version >.tmp1 2>.tmp2 -if %errorlevel% == 1 ( - set err_msg=Please install Python 3.10-11. See %INSTRUCTIONS% for details. - goto err_exit -) - -for /f "tokens=2" %%i in (.tmp1) do set python_version=%%i -if "%python_version%" == "" ( - set err_msg=No python was detected on your system. Please install Python version %MINIMUM_PYTHON_VERSION% or higher. We recommend Python 3.10.12 from %PYTHON_URL% - goto err_exit -) - -call :compareVersions %MINIMUM_PYTHON_VERSION% %python_version% -if %errorlevel% == 1 ( - set err_msg=Your version of Python is too low. You need at least %MINIMUM_PYTHON_VERSION% but you have %python_version%. We recommend Python 3.10.12 from %PYTHON_URL% - goto err_exit -) - -@rem Cleanup -del /q .tmp1 .tmp2 - -@rem -------------- Install and Configure --------------- - -call python .\lib\main.py -pause -exit /b - -@rem ------------------------ Subroutines --------------- -@rem routine to do comparison of semantic version numbers -@rem found at https://stackoverflow.com/questions/15807762/compare-version-numbers-in-batch-file -:compareVersions -:: -:: Compares two version numbers and returns the result in the ERRORLEVEL -:: -:: Returns 1 if version1 > version2 -:: 0 if version1 = version2 -:: -1 if version1 < version2 -:: -:: The nodes must be delimited by . or , or - -:: -:: Nodes are normally strictly numeric, without a 0 prefix. A letter suffix -:: is treated as a separate node -:: -setlocal enableDelayedExpansion -set "v1=%~1" -set "v2=%~2" -call :divideLetters v1 -call :divideLetters v2 -:loop -call :parseNode "%v1%" n1 v1 -call :parseNode "%v2%" n2 v2 -if %n1% gtr %n2% exit /b 1 -if %n1% lss %n2% exit /b -1 -if not defined v1 if not defined v2 exit /b 0 -if not defined v1 exit /b -1 -if not defined v2 exit /b 1 -goto :loop - - -:parseNode version nodeVar remainderVar -for /f "tokens=1* delims=.,-" %%A in ("%~1") do ( - set "%~2=%%A" - set "%~3=%%B" -) -exit /b - - -:divideLetters versionVar -for %%C in (a b c d e f g h i j k l m n o p q r s t u v w x y z) do set "%~1=!%~1:%%C=.%%C!" -exit /b - -:err_exit -echo %err_msg% -echo The installer will exit now. -pause -exit /b - -pause - -:Trim -SetLocal EnableDelayedExpansion -set Params=%* -for /f "tokens=1*" %%a in ("!Params!") do EndLocal & set %1=%%b -exit /b diff --git a/installer/install.sh.in b/installer/install.sh.in deleted file mode 100755 index 9cf41192bf1..00000000000 --- a/installer/install.sh.in +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -# make sure we are not already in a venv -# (don't need to check status) -deactivate >/dev/null 2>&1 -scriptdir=$(dirname "$0") -cd $scriptdir - -function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } - -MINIMUM_PYTHON_VERSION=3.10.0 -MAXIMUM_PYTHON_VERSION=3.11.100 -PYTHON="" -for candidate in python3.11 python3.10 python3 python ; do - if ppath=`which $candidate`; then - # when using `pyenv`, the executable for an inactive Python version will exist but will not be operational - # we check that this found executable can actually run - if [ $($candidate --version &>/dev/null; echo ${PIPESTATUS}) -gt 0 ]; then continue; fi - - python_version=$($ppath -V | awk '{ print $2 }') - if [ $(version $python_version) -ge $(version "$MINIMUM_PYTHON_VERSION") ]; then - if [ $(version $python_version) -le $(version "$MAXIMUM_PYTHON_VERSION") ]; then - PYTHON=$ppath - break - fi - fi - fi -done - -if [ -z "$PYTHON" ]; then - echo "A suitable Python interpreter could not be found" - echo "Please install Python $MINIMUM_PYTHON_VERSION or higher (maximum $MAXIMUM_PYTHON_VERSION) before running this script. See instructions at $INSTRUCTIONS for help." - echo "For the best user experience we suggest enlarging or maximizing this window now." - read -p "Press any key to exit" - exit -1 -fi - -exec $PYTHON ./lib/main.py ${@} -read -p "Press any key to exit" diff --git a/installer/lib/installer.py b/installer/lib/installer.py deleted file mode 100644 index 11823b413e0..00000000000 --- a/installer/lib/installer.py +++ /dev/null @@ -1,435 +0,0 @@ -# Copyright (c) 2023 Eugene Brodsky (https://github.com/ebr) -""" -InvokeAI installer script -""" - -import locale -import os -import platform -import re -import shutil -import subprocess -import sys -import venv -from pathlib import Path -from tempfile import TemporaryDirectory -from typing import Optional, Tuple - -SUPPORTED_PYTHON = ">=3.10.0,<=3.11.100" -INSTALLER_REQS = ["rich", "semver", "requests", "plumbum", "prompt-toolkit"] -BOOTSTRAP_VENV_PREFIX = "invokeai-installer-tmp" -DOCS_URL = "https://invoke-ai.github.io/InvokeAI/" -DISCORD_URL = "https://discord.gg/ZmtBAhwWhy" - -OS = platform.uname().system -ARCH = platform.uname().machine -VERSION = "latest" - - -def get_version_from_wheel_filename(wheel_filename: str) -> str: - match = re.search(r"-(\d+\.\d+\.\d+)", wheel_filename) - if match: - version = match.group(1) - return version - else: - raise ValueError(f"Could not extract version from wheel filename: {wheel_filename}") - - -class Installer: - """ - Deploys an InvokeAI installation into a given path - """ - - reqs: list[str] = INSTALLER_REQS - - def __init__(self) -> None: - if os.getenv("VIRTUAL_ENV") is not None: - print("A virtual environment is already activated. Please 'deactivate' before installation.") - sys.exit(-1) - self.bootstrap() - self.available_releases = get_github_releases() - - def mktemp_venv(self) -> TemporaryDirectory[str]: - """ - Creates a temporary virtual environment for the installer itself - - :return: path to the created virtual environment directory - :rtype: TemporaryDirectory - """ - - # Cleaning up temporary directories on Windows results in a race condition - # and a stack trace. - # `ignore_cleanup_errors` was only added in Python 3.10 - if OS == "Windows" and int(platform.python_version_tuple()[1]) >= 10: - venv_dir = TemporaryDirectory(prefix=BOOTSTRAP_VENV_PREFIX, ignore_cleanup_errors=True) - else: - venv_dir = TemporaryDirectory(prefix=BOOTSTRAP_VENV_PREFIX) - - venv.create(venv_dir.name, with_pip=True) - self.venv_dir = venv_dir - set_sys_path(Path(venv_dir.name)) - - return venv_dir - - def bootstrap(self, verbose: bool = False) -> TemporaryDirectory[str] | None: - """ - Bootstrap the installer venv with packages required at install time - """ - - print("Initializing the installer. This may take a minute - please wait...") - - venv_dir = self.mktemp_venv() - pip = get_pip_from_venv(Path(venv_dir.name)) - - cmd = [pip, "install", "--require-virtualenv", "--use-pep517"] - cmd.extend(self.reqs) - - try: - # upgrade pip to the latest version to avoid a confusing message - res = upgrade_pip(Path(venv_dir.name)) - if verbose: - print(res) - - # run the install prerequisites installation - res = subprocess.check_output(cmd).decode() - - if verbose: - print(res) - - return venv_dir - except subprocess.CalledProcessError as e: - print(e) - - def app_venv(self, venv_parent: Path) -> Path: - """ - Create a virtualenv for the InvokeAI installation - """ - - venv_dir = venv_parent / ".venv" - - # Prefer to copy python executables - # so that updates to system python don't break InvokeAI - try: - venv.create(venv_dir, with_pip=True) - # If installing over an existing environment previously created with symlinks, - # the executables will fail to copy. Keep symlinks in that case - except shutil.SameFileError: - venv.create(venv_dir, with_pip=True, symlinks=True) - - return venv_dir - - def install( - self, - root: str = "~/invokeai", - yes_to_all: bool = False, - find_links: Optional[str] = None, - wheel: Optional[Path] = None, - ) -> None: - """Install the InvokeAI application into the given runtime path - - Args: - root: Destination path for the installation - yes_to_all: Accept defaults to all questions - find_links: A local directory to search for requirement wheels before going to remote indexes - wheel: A wheel file to install - """ - - import messages - - if wheel: - messages.installing_from_wheel(wheel.name) - version = get_version_from_wheel_filename(wheel.name) - else: - messages.welcome(self.available_releases) - version = messages.choose_version(self.available_releases) - - auto_dest = Path(os.environ.get("INVOKEAI_ROOT", root)).expanduser().resolve() - destination = auto_dest if yes_to_all else messages.dest_path(root) - if destination is None: - print("Could not find or create the destination directory. Installation cancelled.") - sys.exit(0) - - # create the venv for the app - self.venv = self.app_venv(venv_parent=destination) - - self.instance = InvokeAiInstance(runtime=destination, venv=self.venv, version=version) - - # install dependencies and the InvokeAI application - (extra_index_url, optional_modules) = get_torch_source() if not yes_to_all else (None, None) - self.instance.install(extra_index_url, optional_modules, find_links, wheel) - - # install the launch/update scripts into the runtime directory - self.instance.install_user_scripts() - - message = f""" -*** Installation Successful *** - -To start the application, run: - {destination}/invoke.{"bat" if sys.platform == "win32" else "sh"} - -For more information, troubleshooting and support, visit our docs at: - {DOCS_URL} - -Join the community on Discord: - {DISCORD_URL} -""" - print(message) - - -class InvokeAiInstance: - """ - Manages an installed instance of InvokeAI, comprising a virtual environment and a runtime directory. - The virtual environment *may* reside within the runtime directory. - A single runtime directory *may* be shared by multiple virtual environments, though this isn't currently tested or supported. - """ - - def __init__(self, runtime: Path, venv: Path, version: str = "stable") -> None: - self.runtime = runtime - self.venv = venv - self.pip = get_pip_from_venv(venv) - self.version = version - - set_sys_path(venv) - os.environ["INVOKEAI_ROOT"] = str(self.runtime.expanduser().resolve()) - os.environ["VIRTUAL_ENV"] = str(self.venv.expanduser().resolve()) - upgrade_pip(venv) - - def get(self) -> tuple[Path, Path]: - """ - Get the location of the virtualenv directory for this installation - - :return: Paths of the runtime and the venv directory - :rtype: tuple[Path, Path] - """ - - return (self.runtime, self.venv) - - def install( - self, - extra_index_url: Optional[str] = None, - optional_modules: Optional[str] = None, - find_links: Optional[str] = None, - wheel: Optional[Path] = None, - ): - """Install the package from PyPi or a wheel, if provided. - - Args: - extra_index_url: the "--extra-index-url ..." line for pip to look in extra indexes. - optional_modules: optional modules to install using "[module1,module2]" format. - find_links: path to a directory containing wheels to be searched prior to going to the internet - wheel: a wheel file to install - """ - - import messages - - # not currently used, but may be useful for "install most recent version" option - if self.version == "prerelease": - version = None - pre_flag = "--pre" - elif self.version == "stable": - version = None - pre_flag = None - else: - version = self.version - pre_flag = None - - src = "invokeai" - if optional_modules: - src += optional_modules - if version: - src += f"=={version}" - - messages.simple_banner("Installing the InvokeAI Application :art:") - - from plumbum import FG, ProcessExecutionError, local - - pip = local[self.pip] - - pipeline = pip[ - "install", - "--require-virtualenv", - "--force-reinstall", - "--use-pep517", - str(src) if not wheel else str(wheel), - "--find-links" if find_links is not None else None, - find_links, - "--extra-index-url" if extra_index_url is not None else None, - extra_index_url, - pre_flag if not wheel else None, # Ignore the flag if we are installing a wheel - ] - - try: - _ = pipeline & FG - except ProcessExecutionError as e: - print(f"Error: {e}") - print( - "Could not install InvokeAI. Please try downloading the latest version of the installer and install again." - ) - sys.exit(1) - - def install_user_scripts(self): - """ - Copy the launch and update scripts to the runtime dir - """ - - ext = "bat" if OS == "Windows" else "sh" - - scripts = ["invoke"] - - for script in scripts: - src = Path(__file__).parent / ".." / "templates" / f"{script}.{ext}.in" - dest = self.runtime / f"{script}.{ext}" - shutil.copy(src, dest) - os.chmod(dest, 0o0755) - - def update(self): - pass - - def remove(self): - pass - - -### Utility functions ### - - -def get_pip_from_venv(venv_path: Path) -> str: - """ - Given a path to a virtual environment, get the absolute path to the `pip` executable - in a cross-platform fashion. Does not validate that the pip executable - actually exists in the virtualenv. - - :param venv_path: Path to the virtual environment - :type venv_path: Path - :return: Absolute path to the pip executable - :rtype: str - """ - - pip = "Scripts\\pip.exe" if OS == "Windows" else "bin/pip" - return str(venv_path.expanduser().resolve() / pip) - - -def upgrade_pip(venv_path: Path) -> str | None: - """ - Upgrade the pip executable in the given virtual environment - """ - - python = "Scripts\\python.exe" if OS == "Windows" else "bin/python" - python = str(venv_path.expanduser().resolve() / python) - - try: - result = subprocess.check_output([python, "-m", "pip", "install", "--upgrade", "pip"]).decode( - encoding=locale.getpreferredencoding() - ) - except subprocess.CalledProcessError as e: - print(e) - result = None - - return result - - -def set_sys_path(venv_path: Path) -> None: - """ - Given a path to a virtual environment, set the sys.path, in a cross-platform fashion, - such that packages from the given venv may be imported in the current process. - Ensure that the packages from system environment are not visible (emulate - the virtual env 'activate' script) - this doesn't work on Windows yet. - - :param venv_path: Path to the virtual environment - :type venv_path: Path - """ - - # filter out any paths in sys.path that may be system- or user-wide - # but leave the temporary bootstrap virtualenv as it contains packages we - # temporarily need at install time - sys.path = list(filter(lambda p: not p.endswith("-packages") or p.find(BOOTSTRAP_VENV_PREFIX) != -1, sys.path)) - - # determine site-packages/lib directory location for the venv - lib = "Lib" if OS == "Windows" else f"lib/python{sys.version_info.major}.{sys.version_info.minor}" - - # add the site-packages location to the venv - sys.path.append(str(Path(venv_path, lib, "site-packages").expanduser().resolve())) - - -def get_github_releases() -> tuple[list[str], list[str]] | None: - """ - Query Github for published (pre-)release versions. - Return a tuple where the first element is a list of stable releases and the second element is a list of pre-releases. - Return None if the query fails for any reason. - """ - - import requests - - ## get latest releases using github api - url = "https://api.github.com/repos/invoke-ai/InvokeAI/releases" - releases: list[str] = [] - pre_releases: list[str] = [] - try: - res = requests.get(url) - res.raise_for_status() - tag_info = res.json() - for tag in tag_info: - if not tag["prerelease"]: - releases.append(tag["tag_name"].lstrip("v")) - else: - pre_releases.append(tag["tag_name"].lstrip("v")) - except requests.HTTPError as e: - print(f"Error: {e}") - print("Could not fetch version information from GitHub. Please check your network connection and try again.") - return - except Exception as e: - print(f"Error: {e}") - print("An unexpected error occurred while trying to fetch version information from GitHub. Please try again.") - return - - releases.sort(reverse=True) - pre_releases.sort(reverse=True) - - return releases, pre_releases - - -def get_torch_source() -> Tuple[str | None, str | None]: - """ - Determine the extra index URL for pip to use for torch installation. - This depends on the OS and the graphics accelerator in use. - This is only applicable to Windows and Linux, since PyTorch does not - offer accelerated builds for macOS. - - Prefer CUDA-enabled wheels if the user wasn't sure of their GPU, as it will fallback to CPU if possible. - - A NoneType return means just go to PyPi. - - :return: tuple consisting of (extra index url or None, optional modules to load or None) - :rtype: list - """ - - from messages import select_gpu - - # device can be one of: "cuda", "rocm", "cpu", "cuda_and_dml, autodetect" - device = select_gpu() - - # The correct extra index URLs for torch are inconsistent, see https://pytorch.org/get-started/locally/#start-locally - - url = None - optional_modules: str | None = None - if OS == "Linux": - if device.value == "rocm": - url = "https://download.pytorch.org/whl/rocm5.6" - elif device.value == "cpu": - url = "https://download.pytorch.org/whl/cpu" - elif device.value == "cuda": - # CUDA uses the default PyPi index - optional_modules = "[xformers,onnx-cuda]" - elif OS == "Windows": - if device.value == "cuda": - url = "https://download.pytorch.org/whl/cu121" - optional_modules = "[xformers,onnx-cuda]" - elif device.value == "cpu": - # CPU uses the default PyPi index, no optional modules - pass - elif OS == "Darwin": - # macOS uses the default PyPi index, no optional modules - pass - - # Fall back to defaults - - return (url, optional_modules) diff --git a/installer/lib/main.py b/installer/lib/main.py deleted file mode 100644 index be9dc18f966..00000000000 --- a/installer/lib/main.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -InvokeAI Installer -""" - -import argparse -import os -from pathlib import Path - -from installer import Installer - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - - parser.add_argument( - "-r", - "--root", - dest="root", - type=str, - help="Destination path for installation", - default=os.environ.get("INVOKEAI_ROOT") or "~/invokeai", - ) - parser.add_argument( - "-y", - "--yes", - "--yes-to-all", - dest="yes_to_all", - action="store_true", - help="Assume default answers to all questions", - default=False, - ) - - parser.add_argument( - "--find-links", - dest="find_links", - help="Specifies a directory of local wheel files to be searched prior to searching the online repositories.", - type=Path, - default=None, - ) - - parser.add_argument( - "--wheel", - dest="wheel", - help="Specifies a wheel for the InvokeAI package. Used for troubleshooting or testing prereleases.", - type=Path, - default=None, - ) - - args = parser.parse_args() - - inst = Installer() - - try: - inst.install(**args.__dict__) - except KeyboardInterrupt: - print("\n") - print("Ctrl-C pressed. Aborting.") - print("Come back soon!") diff --git a/installer/lib/messages.py b/installer/lib/messages.py deleted file mode 100644 index dcd65a98137..00000000000 --- a/installer/lib/messages.py +++ /dev/null @@ -1,338 +0,0 @@ -# Copyright (c) 2023 Eugene Brodsky (https://github.com/ebr) -""" -Installer user interaction -""" - -import os -import platform -from enum import Enum -from pathlib import Path -from typing import Optional - -from prompt_toolkit import prompt -from prompt_toolkit.completion import FuzzyWordCompleter, PathCompleter -from prompt_toolkit.validation import Validator -from rich import box, print -from rich.console import Console, Group, group -from rich.panel import Panel -from rich.prompt import Confirm -from rich.style import Style -from rich.syntax import Syntax -from rich.text import Text - -OS = platform.uname().system -ARCH = platform.uname().machine - -if OS == "Windows": - # Windows terminals look better without a background colour - console = Console(style=Style(color="grey74")) -else: - console = Console(style=Style(color="grey74", bgcolor="grey19")) - - -def welcome(available_releases: tuple[list[str], list[str]] | None = None) -> None: - @group() - def text(): - if (platform_specific := _platform_specific_help()) is not None: - yield platform_specific - yield "" - yield Text.from_markup( - "Some of the installation steps take a long time to run. Please be patient. If the script appears to hang for more than 10 minutes, please interrupt with [i]Control-C[/] and retry.", - justify="center", - ) - if available_releases is not None: - latest_stable = available_releases[0][0] - last_pre = available_releases[1][0] - yield "" - yield Text.from_markup( - f"[red3]🠶[/] Latest stable release (recommended): [b bright_white]{latest_stable}", justify="center" - ) - yield Text.from_markup( - f"[red3]🠶[/] Last published pre-release version: [b bright_white]{last_pre}", justify="center" - ) - - console.rule() - print( - Panel( - title="[bold wheat1]Welcome to the InvokeAI Installer", - renderable=text(), - box=box.DOUBLE, - expand=True, - padding=(1, 2), - style=Style(bgcolor="grey23", color="orange1"), - subtitle=f"[bold grey39]{OS}-{ARCH}", - ) - ) - console.line() - - -def installing_from_wheel(wheel_filename: str) -> None: - """Display a message about installing from a wheel""" - - @group() - def text(): - yield Text.from_markup(f"You are installing from a wheel file: [bold]{wheel_filename}\n") - yield Text.from_markup( - "[bold orange3]If you are not sure why you are doing this, you should cancel and install InvokeAI normally." - ) - - console.print( - Panel( - title="Installing from Wheel", - renderable=text(), - box=box.DOUBLE, - expand=True, - padding=(1, 2), - ) - ) - - should_proceed = Confirm.ask("Do you want to proceed?") - - if not should_proceed: - console.print("Installation cancelled.") - exit() - - -def choose_version(available_releases: tuple[list[str], list[str]] | None = None) -> str: - """ - Prompt the user to choose an Invoke version to install - """ - - # short circuit if we couldn't get a version list - # still try to install the latest stable version - if available_releases is None: - return "stable" - - console.print(":grey_question: [orange3]Please choose an Invoke version to install.") - - choices = available_releases[0] + available_releases[1] - - response = prompt( - message=f" to install the recommended release ({choices[0]}). or type to pick a version: ", - complete_while_typing=True, - completer=FuzzyWordCompleter(choices), - ) - console.print(f" Version {choices[0] if response == '' else response} will be installed.") - - console.line() - - return "stable" if response == "" else response - - -def confirm_install(dest: Path) -> bool: - if dest.exists(): - print(f":stop_sign: Directory {dest} already exists!") - print(" Is this location correct?") - default = False - else: - print(f":file_folder: InvokeAI will be installed in {dest}") - default = True - - dest_confirmed = Confirm.ask(" Please confirm:", default=default) - - console.line() - - return dest_confirmed - - -def dest_path(dest: Optional[str | Path] = None) -> Path | None: - """ - Prompt the user for the destination path and create the path - - :param dest: a filesystem path, defaults to None - :type dest: str, optional - :return: absolute path to the created installation directory - :rtype: Path - """ - - if dest is not None: - dest = Path(dest).expanduser().resolve() - else: - dest = Path.cwd().expanduser().resolve() - prev_dest = init_path = dest - dest_confirmed = False - - while not dest_confirmed: - browse_start = (dest or Path.cwd()).expanduser().resolve() - - path_completer = PathCompleter( - only_directories=True, - expanduser=True, - get_paths=lambda: [str(browse_start)], # noqa: B023 - # get_paths=lambda: [".."].extend(list(browse_start.iterdir())) - ) - - console.line() - - console.print(f":grey_question: [orange3]Please select the install destination:[/] \\[{browse_start}]: ") - selected = prompt( - ">>> ", - complete_in_thread=True, - completer=path_completer, - default=str(browse_start) + os.sep, - vi_mode=True, - complete_while_typing=True, - # Test that this is not needed on Windows - # complete_style=CompleteStyle.READLINE_LIKE, - ) - prev_dest = dest - dest = Path(selected) - - console.line() - - dest_confirmed = confirm_install(dest.expanduser().resolve()) - - if not dest_confirmed: - dest = prev_dest - - dest = dest.expanduser().resolve() - - try: - dest.mkdir(exist_ok=True, parents=True) - return dest - except PermissionError: - console.print( - f"Failed to create directory {dest} due to insufficient permissions", - style=Style(color="red"), - highlight=True, - ) - except OSError: - console.print_exception() - - if Confirm.ask("Would you like to try again?"): - dest_path(init_path) - else: - console.rule("Goodbye!") - - -class GpuType(Enum): - CUDA = "cuda" - ROCM = "rocm" - CPU = "cpu" - - -def select_gpu() -> GpuType: - """ - Prompt the user to select the GPU driver - """ - - if ARCH == "arm64" and OS != "Darwin": - print(f"Only CPU acceleration is available on {ARCH} architecture. Proceeding with that.") - return GpuType.CPU - - nvidia = ( - "an [gold1 b]NVIDIA[/] GPU (using CUDA™)", - GpuType.CUDA, - ) - amd = ( - "an [gold1 b]AMD[/] GPU (using ROCm™)", - GpuType.ROCM, - ) - cpu = ( - "Do not install any GPU support, use CPU for generation (slow)", - GpuType.CPU, - ) - - options = [] - if OS == "Windows": - options = [nvidia, cpu] - if OS == "Linux": - options = [nvidia, amd, cpu] - elif OS == "Darwin": - options = [cpu] - - if len(options) == 1: - print(f'Your platform [gold1]{OS}-{ARCH}[/] only supports the "{options[0][1]}" driver. Proceeding with that.') - return options[0][1] - - options = {str(i): opt for i, opt in enumerate(options, 1)} - - console.rule(":space_invader: GPU (Graphics Card) selection :space_invader:") - console.print( - Panel( - Group( - "\n".join( - [ - f"Detected the [gold1]{OS}-{ARCH}[/] platform", - "", - "See [deep_sky_blue1]https://invoke-ai.github.io/InvokeAI/#system[/] to ensure your system meets the minimum requirements.", - "", - "[red3]🠶[/] [b]Your GPU drivers must be correctly installed before using InvokeAI![/] [red3]🠴[/]", - ] - ), - "", - "Please select the type of GPU installed in your computer.", - Panel( - "\n".join([f"[dark_goldenrod b i]{i}[/] [dark_red]🢒[/]{opt[0]}" for (i, opt) in options.items()]), - box=box.MINIMAL, - ), - ), - box=box.MINIMAL, - padding=(1, 1), - ) - ) - choice = prompt( - "Please make your selection: ", - validator=Validator.from_callable( - lambda n: n in options.keys(), error_message="Please select one the above options" - ), - ) - - return options[choice][1] - - -def simple_banner(message: str) -> None: - """ - A simple banner with a message, defined here for styling consistency - - :param message: The message to display - :type message: str - """ - - console.rule(message) - - -# TODO this does not yet work correctly -def windows_long_paths_registry() -> None: - """ - Display a message about applying the Windows long paths registry fix - """ - - with open(str(Path(__file__).parent / "WinLongPathsEnabled.reg"), "r", encoding="utf-16le") as code: - syntax = Syntax(code.read(), line_numbers=True, lexer="regedit") - - console.print( - Panel( - Group( - "\n".join( - [ - "We will now apply a registry fix to enable long paths on Windows. InvokeAI needs this to function correctly. We are asking your permission to modify the Windows Registry on your behalf.", - "", - "This is the change that will be applied:", - str(syntax), - ] - ) - ), - title="Windows Long Paths registry fix", - box=box.HORIZONTALS, - padding=(1, 1), - ) - ) - - -def _platform_specific_help() -> Text | None: - if OS == "Darwin": - text = Text.from_markup( - """[b wheat1]macOS Users![/]\n\nPlease be sure you have the [b wheat1]Xcode command-line tools[/] installed before continuing.\nIf not, cancel with [i]Control-C[/] and follow the Xcode install instructions at [deep_sky_blue1]https://www.freecodecamp.org/news/install-xcode-command-line-tools/[/].""" - ) - elif OS == "Windows": - text = Text.from_markup( - """[b wheat1]Windows Users![/]\n\nBefore you start, please do the following: - 1. Double-click on the file [b wheat1]WinLongPathsEnabled.reg[/] in order to - enable long path support on your system. - 2. Make sure you have the [b wheat1]Visual C++ core libraries[/] installed. If not, install from - [deep_sky_blue1]https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170[/]""" - ) - else: - return - return text diff --git a/installer/readme.txt b/installer/readme.txt deleted file mode 100644 index ef040c3913c..00000000000 --- a/installer/readme.txt +++ /dev/null @@ -1,52 +0,0 @@ -InvokeAI - -Project homepage: https://github.com/invoke-ai/InvokeAI - -Preparations: - - You will need to install Python 3.10 or higher for this installer - to work. Instructions are given here: - https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/ - - Before you start the installer, please open up your system's command - line window (Terminal or Command) and type the commands: - - python --version - - If all is well, it will print "Python 3.X.X", where the version number - is at least 3.10.*, and not higher than 3.11.*. - - If this works, check the version of the Python package manager, pip: - - pip --version - - You should get a message that indicates that the pip package - installer was derived from Python 3.10 or 3.11. For example: - "pip 22.0.1 from /usr/bin/pip (python 3.10)" - -Long Paths on Windows: - - If you are on Windows, you will need to enable Windows Long Paths to - run InvokeAI successfully. If you're not sure what this is, you - almost certainly need to do this. - - Simply double-click the "WinLongPathsEnabled.reg" file located in - this directory, and approve the Windows warnings. Note that you will - need to have admin privileges in order to do this. - -Launching the installer: - - Windows: double-click the 'install.bat' file (while keeping it inside - the InvokeAI-Installer folder). - - Linux and Mac: Please open the terminal application and run - './install.sh' (while keeping it inside the InvokeAI-Installer - folder). - -The installer will create a directory of your choice and install the -InvokeAI application within it. This directory contains everything you need to run -invokeai. Once InvokeAI is up and running, you may delete the -InvokeAI-Installer folder at your convenience. - -For more information, please see -https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/ diff --git a/installer/templates/invoke.bat.in b/installer/templates/invoke.bat.in deleted file mode 100644 index c8ef19710bc..00000000000 --- a/installer/templates/invoke.bat.in +++ /dev/null @@ -1,54 +0,0 @@ -@echo off - -PUSHD "%~dp0" -setlocal - -call .venv\Scripts\activate.bat -set INVOKEAI_ROOT=. - -:start -echo Desired action: -echo 1. Generate images with the browser-based interface -echo 2. Open the developer console -echo 3. Command-line help -echo Q - Quit -echo. -echo To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest. -echo. -set /P choice="Please enter 1-4, Q: [1] " -if not defined choice set choice=1 -IF /I "%choice%" == "1" ( - echo Starting the InvokeAI browser-based UI.. - python .venv\Scripts\invokeai-web.exe %* -) ELSE IF /I "%choice%" == "2" ( - echo Developer Console - echo Python command is: - where python - echo Python version is: - python --version - echo ************************* - echo You are now in the system shell, with the local InvokeAI Python virtual environment activated, - echo so that you can troubleshoot this InvokeAI installation as necessary. - echo ************************* - echo *** Type `exit` to quit this shell and deactivate the Python virtual environment *** - call cmd /k -) ELSE IF /I "%choice%" == "3" ( - echo Displaying command line help... - python .venv\Scripts\invokeai-web.exe --help %* - pause - exit /b -) ELSE IF /I "%choice%" == "q" ( - echo Goodbye! - goto ending -) ELSE ( - echo Invalid selection - pause - exit /b -) -goto start - -endlocal -pause - -:ending -exit /b diff --git a/installer/templates/invoke.sh.in b/installer/templates/invoke.sh.in deleted file mode 100644 index b8d5a7af23e..00000000000 --- a/installer/templates/invoke.sh.in +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash - -# MIT License - -# Coauthored by Lincoln Stein, Eugene Brodsky and Joshua Kimsey -# Copyright 2023, The InvokeAI Development Team - -#### -# This launch script assumes that: -# 1. it is located in the runtime directory, -# 2. the .venv is also located in the runtime directory and is named exactly that -# -# If both of the above are not true, this script will likely not work as intended. -# Activate the virtual environment and run `invoke.py` directly. -#### - -set -eu - -# Ensure we're in the correct folder in case user's CWD is somewhere else -scriptdir=$(dirname "$0") -cd "$scriptdir" - -. .venv/bin/activate - -export INVOKEAI_ROOT="$scriptdir" - -# Stash the CLI args - when we prompt for user input, `$@` is overwritten -PARAMS=$@ - -# This setting allows torch to fall back to CPU for operations that are not supported by MPS on macOS. -if [ "$(uname -s)" == "Darwin" ]; then - export PYTORCH_ENABLE_MPS_FALLBACK=1 -fi - -# Primary function for the case statement to determine user input -do_choice() { - case $1 in - 1) - clear - printf "Generate images with a browser-based interface\n" - invokeai-web $PARAMS - ;; - 2) - clear - printf "Open the developer console\n" - file_name=$(basename "${BASH_SOURCE[0]}") - bash --init-file "$file_name" - ;; - 3) - clear - printf "Command-line help\n" - invokeai-web --help - ;; - *) - clear - printf "Exiting...\n" - exit - ;; - esac - clear -} - -# Command-line interface for launching Invoke functions -do_line_input() { - clear - printf "What would you like to do?\n" - printf "1: Generate images using the browser-based interface\n" - printf "2: Open the developer console\n" - printf "3: Command-line help\n" - printf "Q: Quit\n\n" - printf "To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest.\n\n" - read -p "Please enter 1-4, Q: [1] " yn - choice=${yn:='1'} - do_choice $choice - clear -} - -# Main IF statement for launching Invoke, and for checking if the user is in the developer console -if [ "$0" != "bash" ]; then - while true; do - do_line_input - done -else # in developer console - python --version - printf "Press ^D to exit\n" - export PS1="(InvokeAI) \u@\h \w> " -fi 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 index 19a7bb083dc..e7468c1bca4 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -1,40 +1,72 @@ # 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.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData +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__ -from ..services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage -from ..services.board_images.board_images_default import BoardImagesService -from ..services.board_records.board_records_sqlite import SqliteBoardRecordStorage -from ..services.boards.boards_default import BoardService -from ..services.bulk_download.bulk_download_default import BulkDownloadService -from ..services.config import InvokeAIAppConfig -from ..services.download import DownloadQueueService -from ..services.events.events_fastapievents import FastAPIEventService -from ..services.image_files.image_files_disk import DiskImageFileStorage -from ..services.image_records.image_records_sqlite import SqliteImageRecordStorage -from ..services.images.images_default import ImageService -from ..services.invocation_cache.invocation_cache_memory import MemoryInvocationCache -from ..services.invocation_services import InvocationServices -from ..services.invocation_stats.invocation_stats_default import InvocationStatsService -from ..services.invoker import Invoker -from ..services.model_images.model_images_default import ModelImageFileStorageDisk -from ..services.model_manager.model_manager_default import ModelManagerService -from ..services.model_records import ModelRecordServiceSQL -from ..services.names.names_default import SimpleNameService -from ..services.session_processor.session_processor_default import DefaultSessionProcessor, DefaultSessionRunner -from ..services.session_queue.session_queue_sqlite import SqliteSessionQueue -from ..services.urls.urls_default import LocalUrlService -from ..services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage - # TODO: is there a better way to achieve this? def check_internet() -> bool: @@ -61,7 +93,12 @@ class ApiDependencies: invoker: Invoker @staticmethod - def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger = logger) -> None: + 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)}") @@ -72,9 +109,17 @@ def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger 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 @@ -82,31 +127,67 @@ def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger board_images = BoardImagesService() board_records = SqliteBoardRecordStorage(db=db) boards = BoardService() - events = FastAPIEventService(event_handler_id) + 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", ephemeral=True) + ObjectSerializerDisk[torch.Tensor]( + output_folder / "tensors", + safe_globals=[torch.Tensor], + ephemeral=True, + ), ) conditioning = ObjectSerializerForwardCache( - ObjectSerializerDisk[ConditioningFieldData](output_folder / "conditioning", ephemeral=True) + 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_images_service = ModelImageFileStorageDisk(model_images_folder / "model_images") + model_record_service = ModelRecordServiceSQL(db=db, logger=logger) model_manager = ModelManagerService.build_model_manager( app_config=configuration, - model_record_service=ModelRecordServiceSQL(db=db), + 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, @@ -123,7 +204,10 @@ def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger 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, @@ -132,9 +216,24 @@ def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger 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 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..642a4b1bce1 --- /dev/null +++ b/invokeai/app/api/extract_metadata_from_image.py @@ -0,0 +1,134 @@ +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 + if invokeai_metadata_override is not None + else 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 + if invokeai_workflow_override is not None + else 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 if invokeai_graph_override is not None else 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 index 15a53270f1d..cbf82d99c71 100644 --- a/invokeai/app/api/no_cache_staticfiles.py +++ b/invokeai/app/api/no_cache_staticfiles.py @@ -1,7 +1,9 @@ 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): @@ -12,6 +14,10 @@ class NoCacheStaticFiles(StaticFiles): 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): @@ -26,3 +32,19 @@ def file_response(self, *args: Any, **kwargs: Any) -> Response: 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 index c3bc98a0387..a8e0c68d781 100644 --- a/invokeai/app/api/routers/app_info.py +++ b/invokeai/app/api/routers/app_info.py @@ -1,23 +1,36 @@ -import typing +import locale +import re from enum import Enum -from importlib.metadata import PackageNotFoundError, version -from pathlib import Path -from platform import python_version -from typing import Optional +from importlib.metadata import distributions +from pathlib import Path as FilePath +from threading import Lock +from typing import Any, Literal, Union import torch -from fastapi import Body +import yaml +from fastapi import Body, HTTPException, Path from fastapi.routing import APIRouter -from pydantic import BaseModel, Field - -from invokeai.app.invocations.upscale import ESRGAN_MODELS +from pydantic import BaseModel, Field, field_validator, 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__ -from ..dependencies import ApiDependencies - class LogLevel(int, Enum): NotSet = logging.NOTSET @@ -28,11 +41,6 @@ class LogLevel(int, Enum): Critical = logging.CRITICAL -class Upscaler(BaseModel): - upscaling_method: str = Field(description="Name of upscaling method") - upscaling_models: list[str] = Field(description="List of upscaling models for this method") - - app_router = APIRouter(prefix="/v1/app", tags=["app"]) @@ -42,84 +50,346 @@ class AppVersion(BaseModel): version: str = Field(description="App version") -class AppDependencyVersions(BaseModel): - """App depencency Versions Response""" +@app_router.get("/version", operation_id="app_version", status_code=200, response_model=AppVersion) +async def get_version() -> AppVersion: + return AppVersion(version=__version__) - accelerate: str = Field(description="accelerate version") - compel: str = Field(description="compel version") - cuda: Optional[str] = Field(description="CUDA version") - diffusers: str = Field(description="diffusers version") - numpy: str = Field(description="Numpy version") - opencv: str = Field(description="OpenCV version") - onnx: str = Field(description="ONNX version") - pillow: str = Field(description="Pillow (PIL) version") - python: str = Field(description="Python version") - torch: str = Field(description="PyTorch version") - torchvision: str = Field(description="PyTorch Vision version") - transformers: str = Field(description="transformers version") - xformers: Optional[str] = Field(description="xformers 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" -class AppConfig(BaseModel): - """App Config Response""" + deps["CUDA"] = cuda - infill_methods: list[str] = Field(description="List of available infill methods") - upscaling_methods: list[Upscaler] = Field(description="List of upscaling methods") - nsfw_methods: list[str] = Field(description="List of NSFW checking methods") - watermarking_methods: list[str] = Field(description="List of invisible watermark methods") + sorted_deps = dict(sorted(deps.items(), key=lambda item: item[0].lower())) + return sorted_deps -@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("/patchmatch_status", operation_id="get_patchmatch_status", status_code=200, response_model=bool) +async def get_patchmatch_status() -> bool: + return PatchMatch.patchmatch_available() -@app_router.get("/app_deps", operation_id="get_app_deps", status_code=200, response_model=AppDependencyVersions) -async def get_app_deps() -> AppDependencyVersions: - try: - xformers = version("xformers") - except PackageNotFoundError: - xformers = None - return AppDependencyVersions( - accelerate=version("accelerate"), - compel=version("compel"), - cuda=torch.version.cuda, - diffusers=version("diffusers"), - numpy=version("numpy"), - opencv=version("opencv-python"), - onnx=version("onnx"), - pillow=version("pillow"), - python=python_version(), - torch=torch.version.__version__, - torchvision=version("torchvision"), - transformers=version("transformers"), - xformers=xformers, + +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]) + + +_GENERATION_DEVICE_PATTERN = re.compile(r"^(cpu|mps|cuda(:\d+)?)$") + + +class GenerationDeviceOption(BaseModel): + """A device that may be selected for generation.""" + + device: str = Field(description="The device identifier, e.g. 'cuda:0', 'mps', or 'cpu'") + name: str = Field(description="Human-readable device name") + + +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.", + ) + generation_devices: Union[Literal["auto"], list[str]] | None = Field( + default=None, + description="Devices to use for parallel generation. `auto` uses every available GPU; provide an explicit list (e.g. `[cuda:0, cuda:1]`) to use specific devices. Takes effect after restarting InvokeAI.", + json_schema_extra=_remove_nullable_default_from_schema, + ) + + @field_validator("generation_devices") + @classmethod + def validate_generation_devices( + cls, v: Union[Literal["auto"], list[str], None] + ) -> Union[Literal["auto"], list[str], None]: + if v is None or v == "auto": + return v + for device in v: + if not _GENERATION_DEVICE_PATTERN.match(device): + raise ValueError( + f"Invalid generation device '{device}'. Valid values are 'auto', 'cpu', 'mps', 'cuda', or 'cuda:N'." + ) + return v + + @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") + if "generation_devices" in self.model_fields_set and self.generation_devices is None: + raise ValueError("generation_devices may not be null") + return self + + +@app_router.get( + "/generation_device_options", + operation_id="get_generation_device_options", + status_code=200, + response_model=list[GenerationDeviceOption], +) +async def get_generation_device_options() -> list[GenerationDeviceOption]: + """List the devices available for generation, for use with the `generation_devices` setting.""" + options: list[GenerationDeviceOption] = [] + if torch.cuda.is_available(): + for index in range(torch.cuda.device_count()): + device = f"cuda:{index}" + try: + name = torch.cuda.get_device_name(index) + except Exception: + name = device + options.append(GenerationDeviceOption(device=device, name=name)) + elif torch.backends.mps.is_available(): + options.append(GenerationDeviceOption(device="mps", name="Apple MPS")) + else: + options.append(GenerationDeviceOption(device="cpu", name="CPU")) + return options + + +@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, ) -@app_router.get("/config", operation_id="get_config", status_code=200, response_model=AppConfig) -async def get_config() -> AppConfig: - infill_methods = ["tile", "lama", "cv2", "color"] # TODO: add mosaic back - if PatchMatch.patchmatch_available(): - infill_methods.append("patchmatch") +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) + - upscaling_models = [] - for model in typing.get_args(ESRGAN_MODELS): - upscaling_models.append(str(Path(model).stem)) - upscaler = Upscaler(upscaling_method="esrgan", upscaling_models=upscaling_models) +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() - nsfw_methods = ["nsfw_checker"] + 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} - watermarking_methods = ["invisible_watermark"] + if non_provider_updates: + file_config.update_config(non_provider_updates) - return AppConfig( - infill_methods=infill_methods, - upscaling_methods=[upscaler], - nsfw_methods=nsfw_methods, - watermarking_methods=watermarking_methods, + 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", 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 index 8e36a682d26..f94e4f2437c 100644 --- a/invokeai/app/api/routers/board_images.py +++ b/invokeai/app/api/routers/board_images.py @@ -1,19 +1,51 @@ from fastapi import Body, HTTPException from fastapi.routing import APIRouter -from pydantic import BaseModel, Field -from ..dependencies import ApiDependencies +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"]) -class AddImagesToBoardResult(BaseModel): - board_id: str = Field(description="The id of the board the images were added to") - added_image_names: list[str] = Field(description="The image names that were added to the board") +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 -class RemoveImagesFromBoardResult(BaseModel): - removed_image_names: list[str] = Field(description="The image names that were removed from their board") + 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( @@ -23,17 +55,29 @@ class RemoveImagesFromBoardResult(BaseModel): 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: - result = ApiDependencies.invoker.services.board_images.add_image_to_board( - board_id=board_id, image_name=image_name + 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), ) - return result except Exception: raise HTTPException(status_code=500, detail="Failed to add image to board") @@ -45,14 +89,30 @@ async def add_image_to_board( 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: - result = ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name) - return result + 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") @@ -67,21 +127,39 @@ async def remove_image_from_board( 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_image_names: list[str] = [] + 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 + board_id=board_id, + image_name=image_name, ) - added_image_names.append(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(board_id=board_id, added_image_names=added_image_names) + 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") @@ -96,17 +174,31 @@ async def add_images_to_board( 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_image_names: list[str] = [] + 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_image_names.append(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_image_names=removed_image_names) + 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 index 69f965da64a..6897e90aff4 100644 --- a/invokeai/app/api/routers/boards.py +++ b/invokeai/app/api/routers/boards.py @@ -4,11 +4,13 @@ from fastapi.routing import APIRouter from pydantic import BaseModel, Field -from invokeai.app.services.board_records.board_records_common import BoardChanges +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 ..dependencies import ApiDependencies +from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection boards_router = APIRouter(prefix="/v1/boards", tags=["boards"]) @@ -31,11 +33,12 @@ class DeleteBoardResult(BaseModel): response_model=BoardDTO, ) async def create_board( - board_name: str = Query(description="The name of the board to create"), + current_user: CurrentUserOrDefault, + board_name: str = Query(description="The name of the board to create", max_length=300), ) -> BoardDTO: - """Creates a board""" + """Creates a board for the current user""" try: - result = ApiDependencies.invoker.services.boards.create(board_name=board_name) + 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") @@ -43,16 +46,28 @@ async def 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""" + """Gets a board (user must have access to it)""" try: result = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id) - return result 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}", @@ -66,10 +81,19 @@ async def get_board( 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""" + """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 @@ -79,14 +103,25 @@ async def 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""" + """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 + 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) @@ -97,7 +132,9 @@ async def delete_board( ) else: deleted_board_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board( - board_id=board_id + board_id=board_id, + categories=None, + is_intermediate=None, ) ApiDependencies.invoker.services.boards.delete(board_id=board_id) return DeleteBoardResult( @@ -115,17 +152,22 @@ async def delete_board( 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""" + """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() + 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( - offset, - limit, + current_user.user_id, current_user.is_admin, order_by, direction, offset, limit, include_archived ) else: raise HTTPException( @@ -140,11 +182,40 @@ async def list_boards( response_model=list[str], ) async def list_all_board_image_names( - board_id: str = Path(description="The id of the board"), + 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 index a6e53c7a5c4..305eaf9273e 100644 --- a/invokeai/app/api/routers/download_queue.py +++ b/invokeai/app/api/routers/download_queue.py @@ -1,6 +1,8 @@ # 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 @@ -8,21 +10,42 @@ 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, ) -from ..dependencies import ApiDependencies - 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() -> List[DownloadJob]: +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() @@ -36,7 +59,7 @@ async def list_downloads() -> List[DownloadJob]: 400: {"description": "Bad request"}, }, ) -async def prune_downloads() -> Response: +async def prune_downloads(current_user: AdminUserOrDefault) -> Response: """Prune completed and errored jobs.""" queue = ApiDependencies.invoker.services.download_queue queue.prune_jobs() @@ -48,14 +71,16 @@ async def prune_downloads() -> Response: 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, Path(dest), priority, access_token) + return queue.download(source, FsPath(validated_dest), priority, access_token) @download_queue_router.get( @@ -67,6 +92,7 @@ async def download( }, ) 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.""" @@ -86,6 +112,7 @@ async def get_download_job( }, ) 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.""" @@ -105,7 +132,7 @@ async def cancel_download_job( 204: {"description": "Download jobs have been cancelled"}, }, ) -async def cancel_all_download_jobs() -> Response: +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 index a947b83abe9..976434c68f2 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -1,19 +1,44 @@ import io +import json import traceback -from typing import Optional +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, JsonValue +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, ImageRecordChanges, ResourceOrigin -from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO +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 ..dependencies import ApiDependencies +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"]) @@ -22,6 +47,19 @@ 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", @@ -33,6 +71,7 @@ response_model=ImageDTO, ) async def upload_image( + current_user: CurrentUserOrDefault, file: UploadFile, request: Request, response: Response, @@ -41,52 +80,74 @@ async def upload_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"), - metadata: Optional[JsonValue] = Body( - default=None, description="The metadata to associate with the image", embed=True + 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""" + """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") - _metadata = None - _workflow = None - _graph = None - contents = await file.read() try: pil_image = Image.open(io.BytesIO(contents)) - if crop_visible: - bbox = pil_image.getbbox() - pil_image = pil_image.crop(bbox) except Exception: ApiDependencies.invoker.services.logger.error(traceback.format_exc()) raise HTTPException(status_code=415, detail="Failed to read image") - # TODO: retain non-invokeai metadata on upload? - # attempt to parse metadata from image - metadata_raw = metadata if isinstance(metadata, str) else pil_image.info.get("invokeai_metadata", None) - if isinstance(metadata_raw, str): - _metadata = metadata_raw - else: - ApiDependencies.invoker.services.logger.debug("Failed to parse metadata for uploaded image") - pass - - # attempt to parse workflow from image - workflow_raw = pil_image.info.get("invokeai_workflow", None) - if isinstance(workflow_raw, str): - _workflow = workflow_raw - else: - ApiDependencies.invoker.services.logger.debug("Failed to parse workflow for uploaded image") - pass - - # attempt to extract graph from image - graph_raw = pil_image.info.get("invokeai_graph", None) - if isinstance(graph_raw, str): - _graph = graph_raw - else: - ApiDependencies.invoker.services.logger.debug("Failed to parse graph for uploaded image") - pass + 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( @@ -95,10 +156,11 @@ async def upload_image( image_category=image_category, session_id=session_id, board_id=board_id, - metadata=_metadata, - workflow=_workflow, - graph=_graph, + 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 @@ -110,40 +172,75 @@ async def upload_image( raise HTTPException(status_code=500, detail="Failed to create image") -@images_router.delete("/i/{image_name}", operation_id="delete_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"), -) -> None: +) -> 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() -> int: - """Clears all 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") - pass @images_router.get("/intermediates", operation_id="get_intermediates_count") -async def get_intermediates_count() -> int: - """Gets the count of intermediate images""" +async def get_intermediates_count( + current_user: CurrentUserOrDefault, +) -> int: + """Gets the count of intermediate images. Non-admin users only see their own intermediates.""" try: - return ApiDependencies.invoker.services.images.get_intermediates_count() + 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") - pass @images_router.patch( @@ -152,10 +249,12 @@ async def get_intermediates_count() -> int: 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) @@ -169,9 +268,11 @@ async def update_image( 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) @@ -185,9 +286,11 @@ async def get_image_dto( 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) @@ -204,8 +307,11 @@ class WorkflowAndGraphResponse(BaseModel): "/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) @@ -214,9 +320,8 @@ async def get_image_workflow( raise HTTPException(status_code=404) -@images_router.api_route( +@images_router.get( "/i/{image_name}/full", - methods=["GET", "HEAD"], operation_id="get_image_full", response_class=Response, responses={ @@ -227,24 +332,34 @@ async def get_image_workflow( 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"), -) -> FileResponse: - """Gets a full-resolution image file""" +) -> 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) - - if not ApiDependencies.invoker.services.images.validate_path(path): - raise HTTPException(status_code=404) - - response = FileResponse( - path, - media_type="image/png", - filename=image_name, - content_disposition_type="inline", - ) + 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) @@ -264,15 +379,18 @@ async def get_image_full( ) async def get_image_thumbnail( image_name: str = Path(description="The name of thumbnail image file to get"), -) -> FileResponse: - """Gets a thumbnail image file""" +) -> 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) - if not ApiDependencies.invoker.services.images.validate_path(path): - raise HTTPException(status_code=404) - - response = FileResponse(path, media_type="image/webp", content_disposition_type="inline") + 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: @@ -285,9 +403,11 @@ async def get_image_thumbnail( 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) @@ -307,6 +427,7 @@ async def get_image_urls( 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."), @@ -316,38 +437,91 @@ async def list_image_dtos( ), 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""" + """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 -class DeleteImagesFromListResult(BaseModel): - deleted_images: list[str] - - -@images_router.post("/delete", operation_id="delete_images_from_list", response_model=DeleteImagesFromListResult) +@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), -) -> DeleteImagesFromListResult: +) -> DeleteImagesResult: try: - deleted_images: list[str] = [] + 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.append(image_name) + deleted_images.add(image_name) + affected_boards.add(board_id) + except HTTPException: + raise except Exception: pass - return DeleteImagesFromListResult(deleted_images=deleted_images) + 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") @@ -356,36 +530,62 @@ 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=ImagesUpdatedFromListResult) +@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), -) -> ImagesUpdatedFromListResult: +) -> StarredImagesResult: try: - updated_image_names: list[str] = [] + starred_images: set[str] = set() + affected_boards: set[str] = set() for image_name in image_names: try: - ApiDependencies.invoker.services.images.update(image_name, changes=ImageRecordChanges(starred=True)) - updated_image_names.append(image_name) + _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 ImagesUpdatedFromListResult(updated_image_names=updated_image_names) + 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=ImagesUpdatedFromListResult) +@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), -) -> ImagesUpdatedFromListResult: +) -> UnstarredImagesResult: try: - updated_image_names: list[str] = [] + unstarred_images: set[str] = set() + affected_boards: set[str] = set() for image_name in image_names: try: - ApiDependencies.invoker.services.images.update(image_name, changes=ImageRecordChanges(starred=False)) - updated_image_names.append(image_name) + _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 ImagesUpdatedFromListResult(updated_image_names=updated_image_names) + 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") @@ -403,6 +603,7 @@ class ImagesDownloaded(BaseModel): "/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 @@ -413,6 +614,16 @@ async def download_images_from_list( ) -> 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( @@ -420,6 +631,7 @@ async def download_images_from_list( image_names, board_id, bulk_download_item_id, + current_user.user_id, ) return ImagesDownloaded(bulk_download_item_name=bulk_download_item_id + ".zip") @@ -438,11 +650,21 @@ async def download_images_from_list( }, ) 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""" + """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( @@ -454,5 +676,77 @@ async def get_bulk_download_item( 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 index 99f00423c6c..53c4c68981f 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -1,13 +1,16 @@ # Copyright (c) 2023 Lincoln D. Stein """FastAPI route for model configuration records.""" +import contextlib import io import pathlib -import shutil import traceback from copy import deepcopy -from typing import Any, Dict, List, Optional, Type +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 @@ -16,28 +19,40 @@ 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 ( - DuplicateModelException, InvalidModelException, ModelRecordChanges, + ModelRecordOrderBy, UnknownModelException, ) -from invokeai.backend.model_manager.config import ( - AnyModelConfig, - BaseModelType, - MainCheckpointConfig, - ModelFormat, - ModelType, - SubModelType, +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_MODELS, StarterModel, StarterModelWithoutDependencies - -from ..dependencies import ApiDependencies +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"]) @@ -53,11 +68,46 @@ class ModelsList(BaseModel): 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) - config.cover_image = cover_image - return config + 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) ############################################################################## @@ -73,6 +123,7 @@ def add_cover_image_to_model_config(config: AnyModelConfig, dependencies: Type[A "config_path": "string", "key": "string", "hash": "string", + "file_size": 1, "description": "string", "source": "string", "converted_at": 0, @@ -110,6 +161,8 @@ async def list_model_records( 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 @@ -118,18 +171,53 @@ async def list_model_records( 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 + 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) + record_store.search_by_attr( + model_type=model_type, + model_name=model_name, + model_format=model_format, + order_by=order_by, + direction=direction, + ) ) - for model in found_models: - model = add_cover_image_to_model_config(model, ApiDependencies) + 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", @@ -148,7 +236,24 @@ async def get_model_records_by_attrs( if not configs: raise HTTPException(status_code=404, detail="No model found with these attributes") - return configs[0] + 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( @@ -169,21 +274,55 @@ async def get_model_record( """Get a model record""" try: config = ApiDependencies.invoker.services.model_manager.store.get_model(key) - return add_cover_image_to_model_config(config, ApiDependencies) + return prepare_model_config_for_response(config, ApiDependencies) except UnknownModelException as e: raise HTTPException(status_code=404, detail=str(e)) -# @model_manager_router.get("/summary", operation_id="list_model_summary") -# async def list_model_summary( -# page: int = Query(default=0, description="The page to get"), -# per_page: int = Query(default=10, description="The number of models per page"), -# order_by: ModelRecordOrderBy = Query(default=ModelRecordOrderBy.Default, description="The attribute to order by"), -# ) -> PaginatedResults[ModelSummary]: -# """Gets a page of model summary data.""" -# record_store = ApiDependencies.invoker.services.model_manager.store -# results: PaginatedResults[ModelSummary] = record_store.list_models(page=page, per_page=per_page, order_by=order_by) -# return results +@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): @@ -233,9 +372,10 @@ async def scan_for_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: {e}", + detail=f"An error occurred while searching the directory: {error_type}", ) return scan_results @@ -290,16 +430,29 @@ async def get_hugging_face_models( ) async def update_model_record( key: Annotated[str, Path(description="Unique key of model")], - changes: Annotated[ModelRecordChanges, Body(description="Model config", example=example_model_input)], + 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 - installer = ApiDependencies.invoker.services.model_manager.install try: - record_store.update_model(key, changes=changes) - config = installer.sync_model_path(key) - config = add_cover_image_to_model_config(config, ApiDependencies) + 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): + # Drop the model from every per-device cache so the next load on any GPU rebuilds it. + dropped = sum( + cache.drop_model(key) + for cache in ApiDependencies.invoker.services.model_manager.load.ram_caches.values() + ) + 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)) @@ -309,6 +462,26 @@ async def update_model_record( 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", @@ -355,6 +528,7 @@ async def get_model_image( 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") @@ -388,6 +562,7 @@ async def update_model_image( status_code=204, ) async def delete_model( + current_admin: AdminUserOrDefault, key: str = Path(description="Unique key of model to remove from model registry."), ) -> Response: """ @@ -408,6 +583,134 @@ async def delete_model( 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", @@ -418,6 +721,7 @@ async def delete_model( 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 @@ -443,15 +747,14 @@ async def delete_model_image( 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), - # TODO(MM2): Can we type this? - config: Optional[Dict[str, Any]] = Body( - description="Dict of fields that override auto-probed values in the model config record, such as name, description and prediction_type ", - default=None, - example={"name": "string", "description": "string"}, + 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"}], ), - access_token: Optional[str] = None, ) -> ModelInstallJob: """Install a model using a string identifier. @@ -466,8 +769,9 @@ async def install_model( - model/name:fp16:path/to/model.safetensors - model/name::path/to/model.safetensors - `config` is an optional dict containing model configuration values that will override - the ones that are probed automatically. + `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. @@ -514,6 +818,7 @@ async def install_model( 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.""" @@ -633,7 +938,7 @@ def generate_html(title: str, heading: str, repo_id: str, is_error: bool, messag "/install", operation_id="list_model_installs", ) -async def list_model_installs() -> List[ModelInstallJob]: +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 @@ -642,6 +947,7 @@ async def list_model_installs() -> List[ModelInstallJob]: * "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. @@ -664,7 +970,9 @@ async def list_model_installs() -> List[ModelInstallJob]: 404: {"description": "No such job"}, }, ) -async def get_model_install_job(id: int = Path(description="Model install id")) -> ModelInstallJob: +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. @@ -685,7 +993,10 @@ async def get_model_install_job(id: int = Path(description="Model install id")) }, status_code=201, ) -async def cancel_model_install_job(id: int = Path(description="Model install job ID")) -> None: +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: @@ -695,6 +1006,96 @@ async def cancel_model_install_job(id: int = Path(description="Model install job 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", @@ -703,7 +1104,7 @@ async def cancel_model_install_job(id: int = Path(description="Model install job 400: {"description": "Bad request"}, }, ) -async def prune_model_install_jobs() -> Response: +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) @@ -723,6 +1124,7 @@ async def prune_model_install_jobs() -> Response: }, ) async def convert_model( + current_admin: AdminUserOrDefault, key: str = Path(description="Unique key of the safetensors main model to convert to diffusers format."), ) -> AnyModelConfig: """ @@ -742,43 +1144,49 @@ async def convert_model( logger.error(str(e)) raise HTTPException(status_code=424, detail=str(e)) - if not isinstance(model_config, MainCheckpointConfig): - logger.error(f"The model with key {key} is not a main checkpoint model.") - raise HTTPException(400, f"The model with key {key} is not a main checkpoint model.") - - # loading the model will convert it into a cached diffusers file - try: - cc_size = loader.convert_cache.max_size - if cc_size == 0: # temporary set the convert cache to a positive number so that cached model is written - loader._convert_cache.max_size = 1.0 - loader.load_model(model_config, submodel_type=SubModelType.Scheduler) - finally: - loader._convert_cache.max_size = cc_size - - # Get the path of the converted model from the loader - cache_path = loader.convert_cache.cache_path(key) - assert cache_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( - cache_path, - config={ - "name": original_name, - "description": model_config.description, - "hash": model_config.hash, - "source": model_config.source, - }, - ) - except DuplicateModelException as e: - logger.error(str(e)) - raise HTTPException(status_code=409, 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: @@ -791,28 +1199,254 @@ async def convert_model( # delete the original safetensors file installer.delete(key) - # delete the cached version - shutil.rmtree(cache_path) + # 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 = add_cover_image_to_model_config(new_config, ApiDependencies) + new_config = prepare_model_config_for_response(new_config, ApiDependencies) return new_config -@model_manager_router.get("/starter_models", operation_id="get_starter_models", response_model=list[StarterModel]) -async def get_starter_models() -> list[StarterModel]: +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() - installed_model_sources = {m.source for m in installed_models} starter_models = deepcopy(STARTER_MODELS) + starter_bundles = deepcopy(STARTER_BUNDLES) for model in starter_models: - if model.source in installed_model_sources: - model.is_installed = True + model.is_installed = get_is_installed(model, installed_models) # Remove already-installed dependencies missing_deps: list[StarterModelWithoutDependencies] = [] + for dep in model.dependencies or []: - if dep.source not in installed_model_sources: + if not get_is_installed(dep, installed_models): missing_deps.append(dep) model.dependencies = missing_deps - return starter_models + 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 each per-device cache to drop all models. + ApiDependencies.invoker.services.logger.info("Emptying model cache.") + for cache in ApiDependencies.invoker.services.model_manager.load.ram_caches.values(): + 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..d79045d02a5 --- /dev/null +++ b/invokeai/app/api/routers/recall_parameters.py @@ -0,0 +1,618 @@ +"""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).", + ), + append: bool = Query( + default=False, + description=( + "When true, recalled reference images (ip_adapters and reference_images) are " + "appended to the frontend's existing reference-image list instead of replacing it. " + "Mutually exclusive with strict." + ), + ), +) -> 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. + append: When true, recalled reference images (``ip_adapters`` and + ``reference_images``) are appended to whatever reference images the + frontend already has, instead of replacing the whole list. Mutually + exclusive with ``strict`` (which clears omitted parameters). + + 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 + + if strict and append: + raise HTTPException( + status_code=400, + detail="The 'strict' and 'append' query parameters are mutually exclusive", + ) + + # 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)") + + # Append mode rides along inside the event's parameters dict rather + # than as a new event field so the generated client schema (which + # types parameters as a free-form object) doesn't need regenerating. + # Added after the persistence loop above, so the flag itself is never + # stored as a recall parameter. + if append: + provided_params["append"] = True + + # 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 index 7161e54a41d..cd2260d2271 100644 --- a/invokeai/app/api/routers/session_queue.py +++ b/invokeai/app/api/routers/session_queue.py @@ -1,25 +1,32 @@ from typing import Optional -from fastapi import Body, Path, Query +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 ( - QUEUE_ITEM_STATUS, Batch, BatchStatus, + CancelAllExceptCurrentResult, CancelByBatchIDsResult, + CancelByDestinationResult, ClearResult, + DeleteAllExceptCurrentResult, + DeleteByDestinationResult, EnqueueBatchResult, + ItemIdsResult, PruneResult, + RetryItemsResult, + SessionQueueCountsByDestination, SessionQueueItem, - SessionQueueItemDTO, + SessionQueueItemNotFoundError, SessionQueueStatus, ) -from invokeai.app.services.shared.pagination import CursorPaginatedResults - -from ..dependencies import ApiDependencies +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"]) @@ -31,6 +38,51 @@ class SessionQueueAndProcessorStatus(BaseModel): 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", @@ -39,34 +91,105 @@ class SessionQueueAndProcessorStatus(BaseModel): }, ) 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.""" + """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}") + - return ApiDependencies.invoker.services.session_queue.enqueue_batch(queue_id=queue_id, batch=batch, prepend=prepend) +@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}/list", - operation_id="list_queue_items", + "/{queue_id}/item_ids", + operation_id="get_queue_item_ids", responses={ - 200: {"model": CursorPaginatedResults[SessionQueueItemDTO]}, + 200: {"model": ItemIdsResult}, }, ) -async def list_queue_items( +async def get_queue_item_ids( + current_user: CurrentUserOrDefault, queue_id: str = Path(description="The queue id to perform this operation on"), - limit: int = Query(default=50, description="The number of items to fetch"), - status: Optional[QUEUE_ITEM_STATUS] = Query(default=None, description="The status of items to fetch"), - cursor: Optional[int] = Query(default=None, description="The pagination cursor"), - priority: int = Query(default=0, description="The pagination cursor priority"), -) -> CursorPaginatedResults[SessionQueueItemDTO]: - """Gets all queue items (without graphs)""" + order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"), +) -> ItemIdsResult: + """Gets all queue item ids that match the given parameters. - return ApiDependencies.invoker.services.session_queue.list_queue_items( - queue_id=queue_id, limit=limit, status=status, cursor=cursor, priority=priority - ) + IDs for every user's items are returned (item ids carry no sensitive data on their own). + When the corresponding items are hydrated via get_queue_items_by_item_ids, those belonging + to other users are redacted by sanitize_queue_item_for_user. This lets a non-admin see + partially-redacted entries for other users' jobs in the queue list, while still revealing + only timestamps and status for items they do not own. + + current_user is required so the endpoint stays behind authentication in multiuser mode. + """ + try: + return ApiDependencies.invoker.services.session_queue.get_queue_item_ids(queue_id=queue_id, order_dir=order_dir) + 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( @@ -75,10 +198,14 @@ async def list_queue_items( 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""" - return ApiDependencies.invoker.services.session_processor.resume() + """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( @@ -86,11 +213,55 @@ async def resume( operation_id="pause", responses={200: {"model": SessionProcessorStatus}}, ) -async def Pause( +async def pause( + current_user: AdminUserOrDefault, queue_id: str = Path(description="The queue id to perform this operation on"), ) -> SessionProcessorStatus: - """Pauses session processor""" - return ApiDependencies.invoker.services.session_processor.pause() + """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( @@ -99,11 +270,72 @@ async def Pause( 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""" - return ApiDependencies.invoker.services.session_queue.cancel_by_batch_ids(queue_id=queue_id, batch_ids=batch_ids) + """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( @@ -114,14 +346,27 @@ async def cancel_by_batch_ids( }, ) async def clear( + current_user: CurrentUserOrDefault, queue_id: str = Path(description="The queue id to perform this operation on"), ) -> ClearResult: - """Clears the queue entirely, immediately canceling the currently-executing session""" - queue_item = ApiDependencies.invoker.services.session_queue.get_current(queue_id) - if queue_item is not None: - ApiDependencies.invoker.services.session_queue.cancel_queue_item(queue_item.item_id) - clear_result = ApiDependencies.invoker.services.session_queue.clear(queue_id) - return clear_result + """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( @@ -132,10 +377,16 @@ async def clear( }, ) 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""" - return ApiDependencies.invoker.services.session_queue.prune(queue_id) + """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( @@ -146,10 +397,17 @@ async def prune( }, ) 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""" - return ApiDependencies.invoker.services.session_queue.get_current(queue_id) + 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( @@ -160,10 +418,17 @@ async def get_current_queue_item( }, ) 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""" - return ApiDependencies.invoker.services.session_queue.get_next(queue_id) + 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( @@ -174,12 +439,17 @@ async def get_next_queue_item( }, ) 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""" - queue = ApiDependencies.invoker.services.session_queue.get_queue_status(queue_id) - processor = ApiDependencies.invoker.services.session_processor.get_status() - return SessionQueueAndProcessorStatus(queue=queue, processor=processor) + """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( @@ -190,11 +460,18 @@ async def get_queue_status( }, ) 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 the session queue""" - return ApiDependencies.invoker.services.session_queue.get_batch_status(queue_id=queue_id, batch_id=batch_id) + """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( @@ -206,11 +483,48 @@ async def get_batch_status( 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""" - return ApiDependencies.invoker.services.session_queue.get_queue_item(item_id) + 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( @@ -221,9 +535,64 @@ async def get_queue_item( }, ) 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: - """Deletes a queue item""" + """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) + 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 index 2a912dfacf2..568546603ab 100644 --- a/invokeai/app/api/routers/utilities.py +++ b/invokeai/app/api/routers/utilities.py @@ -1,13 +1,34 @@ +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 +from fastapi import Body, HTTPException from fastapi.routing import APIRouter -from pydantic import BaseModel +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] @@ -22,9 +43,11 @@ class DynamicPromptsResponse(BaseModel): }, ) 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) @@ -35,9 +58,170 @@ async def parse_dynamicprompts( generator = CombinatorialPromptGenerator() prompts = generator.generate(prompt, max_prompts=max_prompts) else: - generator = RandomPromptGenerator() + 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 index 6e93d6d0ce5..eb893251953 100644 --- a/invokeai/app/api/routers/workflows.py +++ b/invokeai/app/api/routers/workflows.py @@ -1,7 +1,12 @@ +import io +import traceback from typing import Optional -from fastapi import APIRouter, Body, HTTPException, Path, Query +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 @@ -10,11 +15,14 @@ WorkflowCategory, WorkflowNotFoundError, WorkflowRecordDTO, - WorkflowRecordListItemDTO, + 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"]) @@ -22,18 +30,29 @@ "/i/{workflow_id}", operation_id="get_workflow", responses={ - 200: {"model": WorkflowRecordDTO}, + 200: {"model": WorkflowRecordWithThumbnailDTO}, }, ) async def get_workflow( + current_user: CurrentUserOrDefault, workflow_id: str = Path(description="The workflow to get"), -) -> WorkflowRecordDTO: +) -> WorkflowRecordWithThumbnailDTO: """Gets a workflow""" try: - return ApiDependencies.invoker.services.workflow_records.get(workflow_id) + 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}", @@ -43,10 +62,21 @@ async def get_workflow( }, ) async def update_workflow( + current_user: CurrentUserOrDefault, workflow: Workflow = Body(description="The updated workflow", embed=True), ) -> WorkflowRecordDTO: """Updates a workflow""" - return ApiDependencies.invoker.services.workflow_records.update(workflow=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( @@ -54,10 +84,25 @@ async def update_workflow( operation_id="delete_workflow", ) async def delete_workflow( + current_user: CurrentUserOrDefault, workflow_id: str = Path(description="The workflow to delete"), ) -> None: """Deletes a workflow""" - ApiDependencies.invoker.services.workflow_records.delete(workflow_id) + 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( @@ -68,30 +113,287 @@ async def delete_workflow( }, ) async def create_workflow( + current_user: CurrentUserOrDefault, workflow: WorkflowWithoutID = Body(description="The workflow to create", embed=True), ) -> WorkflowRecordDTO: """Creates a workflow""" - return ApiDependencies.invoker.services.workflow_records.create(workflow=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[WorkflowRecordListItemDTO]}, + 200: {"model": PaginatedResults[WorkflowRecordListItemWithThumbnailDTO]}, }, ) async def list_workflows( + current_user: CurrentUserOrDefault, page: int = Query(default=0, description="The page to get"), - per_page: int = Query(default=10, description="The number of workflows per page"), + 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"), - category: WorkflowCategory = Query(default=WorkflowCategory.User, description="The category of workflow to get"), + 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)"), -) -> PaginatedResults[WorkflowRecordListItemDTO]: + 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""" - return ApiDependencies.invoker.services.workflow_records.get_many( - page=page, per_page=per_page, order_by=order_by, direction=direction, query=query, category=category + 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 index b39922c69bf..7e93c332d64 100644 --- a/invokeai/app/api/sockets.py +++ b/invokeai/app/api/sockets.py @@ -6,6 +6,7 @@ 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, @@ -20,8 +21,8 @@ DownloadStartedEvent, FastAPIEvent, InvocationCompleteEvent, - InvocationDenoiseProgressEvent, InvocationErrorEvent, + InvocationProgressEvent, InvocationStartedEvent, ModelEventBase, ModelInstallCancelledEvent, @@ -35,8 +36,12 @@ QueueClearedEvent, QueueEventBase, QueueItemStatusChangedEvent, + RecallParametersUpdatedEvent, register_events, ) +from invokeai.backend.util.logging import InvokeAILogger + +logger = InvokeAILogger.get_logger() class QueueSubscriptionEvent(BaseModel): @@ -55,12 +60,13 @@ class BulkDownloadSubscriptionEvent(BaseModel): QUEUE_EVENTS = { InvocationStartedEvent, - InvocationDenoiseProgressEvent, + InvocationProgressEvent, InvocationCompleteEvent, InvocationErrorEvent, QueueItemStatusChangedEvent, BatchEnqueuedEvent, QueueClearedEvent, + RecallParametersUpdatedEvent, } MODEL_EVENTS = { @@ -94,6 +100,13 @@ def __init__(self, app: FastAPI): 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) @@ -103,23 +116,258 @@ def __init__(self, app: FastAPI): 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: - await self._sio.enter_room(sid, QueueSubscriptionEvent(**data).queue_id) + """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]): - await self._sio.emit(event=event[0], data=event[1].model_dump(mode="json"), room=event[1].queue_id) + """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. + # + # Emit to the union of the owner room and the admin room in a SINGLE + # call. python-socketio deduplicates recipients across a room list, + # so a socket that belongs to BOTH rooms — e.g. the "system" user in + # single-user mode, which is also an admin — receives the event + # exactly once. Two separate emits would deliver it twice: harmless + # for the idempotent scalar recall fields (the frontend just re-sets + # them), but the append-mode reference-image recall *pushes* rather + # than replaces, so a double delivery adds the same reference image + # twice. + 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, "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: - await self._sio.emit(event=event[0], data=event[1].model_dump(mode="json"), room=event[1].bulk_download_id) + 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 index e69d95af714..4b79e1eeb0c 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -1,66 +1,70 @@ import asyncio import logging -import mimetypes -import socket from contextlib import asynccontextmanager from pathlib import Path -import torch -import uvicorn -from fastapi import FastAPI +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 +from fastapi.responses import HTMLResponse, RedirectResponse from fastapi_events.handlers.local import local_handler from fastapi_events.middleware import EventHandlerASGIMiddleware -from torch.backends.mps import is_available as is_mps_available +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint -# for PyCharm: -# noinspection PyUnresolvedReferences -import invokeai.backend.util.hotfixes # noqa: F401 (monkeypatching on import) 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.services.config.config_default import get_config -from invokeai.app.util.custom_openapi import get_openapi_func -from invokeai.backend.util.devices import TorchDevice - -from ..backend.util.logging import InvokeAILogger -from .api.dependencies import ApiDependencies -from .api.routers import ( +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 .api.sockets import SocketIO +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() - - -if is_mps_available(): - import invokeai.backend.util.mps_fixes # noqa: F401 (monkeypatching on import) - - logger = InvokeAILogger.get_logger(config=app_config) -# 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") -torch_device_name = TorchDevice.get_torch_device_name() -logger.info(f"Using torch device: {torch_device_name}") +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, logger=logger) + 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() @@ -76,6 +80,74 @@ async def lifespan(app: FastAPI): 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( @@ -92,21 +164,30 @@ async def lifespan(app: FastAPI): 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) @@ -131,81 +212,16 @@ def overridden_redoc() -> HTMLResponse: 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.warn(f"No UI found at {web_root_path}/dist, skipping UI mount") + 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 - - -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 invoke_api() -> None: - def find_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: - if s.connect_ex(("localhost", port)) == 0: - return find_port(port=port + 1) - else: - return port - - if app_config.dev_reload: - try: - import jurigged - except ImportError as e: - logger.error( - 'Can\'t start `--dev_reload` because jurigged is not found; `pip install -e ".[dev]"` to include development dependencies.', - exc_info=e, - ) - else: - jurigged.watch(logger=InvokeAILogger.get_logger(name="jurigged").info) - - port = find_port(app_config.port) - if port != app_config.port: - logger.warn(f"Port {app_config.port} in use, using port {port}") - - check_cudnn(logger) - - # Start our own event loop for eventing usage - loop = asyncio.new_event_loop() - config = uvicorn.Config( - app=app, - host=app_config.host, - port=port, - loop="asyncio", - log_level=app_config.log_level, - 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 - for logname in ["uvicorn.access", "uvicorn"]: - log = InvokeAILogger.get_logger(logname) - log.handlers.clear() - for ch in logger.handlers: - log.addHandler(ch) - - loop.run_until_complete(server.serve()) - - -if __name__ == "__main__": - invoke_api() diff --git a/invokeai/app/invocations/__init__.py b/invokeai/app/invocations/__init__.py index cb1caa167ef..c8d64437524 100644 --- a/invokeai/app/invocations/__init__.py +++ b/invokeai/app/invocations/__init__.py @@ -1,28 +1,5 @@ -import shutil -import sys -from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path -from invokeai.app.services.config.config_default import get_config - -custom_nodes_path = Path(get_config().custom_nodes_path) -custom_nodes_path.mkdir(parents=True, exist_ok=True) - -custom_nodes_init_path = str(custom_nodes_path / "__init__.py") -custom_nodes_readme_path = str(custom_nodes_path / "README.md") - -# copy our custom nodes __init__.py to the custom nodes directory -shutil.copy(Path(__file__).parent / "custom_nodes/init.py", custom_nodes_init_path) -shutil.copy(Path(__file__).parent / "custom_nodes/README.md", custom_nodes_readme_path) - -# Import custom nodes, see https://docs.python.org/3/library/importlib.html#importing-programmatically -spec = spec_from_file_location("custom_nodes", custom_nodes_init_path) -if spec is None or spec.loader is None: - raise RuntimeError(f"Could not load custom nodes from {custom_nodes_init_path}") -module = module_from_spec(spec) -sys.modules[spec.name] = module -spec.loader.exec_module(module) - # add core nodes to __all__ python_files = filter(lambda f: not f.name.startswith("_"), Path(__file__).parent.glob("*.py")) __all__ = [f.stem for f in python_files] # type: ignore diff --git a/invokeai/app/invocations/anima_denoise.py b/invokeai/app/invocations/anima_denoise.py new file mode 100644 index 00000000000..b301e817f9c --- /dev/null +++ b/invokeai/app/invocations/anima_denoise.py @@ -0,0 +1,736 @@ +"""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=f"Denoising (Anima){TorchDevice.get_session_device_label()}") + 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=f"Denoising (Anima){TorchDevice.get_session_device_label()}" + ): + 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..c9bad65f3d0 --- /dev/null +++ b/invokeai/app/invocations/anima_text_encoder.py @@ -0,0 +1,217 @@ +"""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, + idle_gpu_offloadable=True, +) +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 index 1d169f0a82d..95cac4065a3 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -4,9 +4,13 @@ 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, @@ -18,20 +22,23 @@ Literal, Optional, Type, + TypedDict, TypeVar, Union, cast, ) import semver -from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model +from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter, create_model from pydantic.fields import FieldInfo from pydantic_core import PydanticUndefined -from typing_extensions import TypeAliasType 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 @@ -40,12 +47,10 @@ from invokeai.backend.util.logging import InvokeAILogger if TYPE_CHECKING: - from ..services.invocation_services import InvocationServices + from invokeai.app.services.invocation_services import InvocationServices logger = InvokeAILogger.get_logger() -CUSTOM_NODE_PACK_SUFFIX = "__invokeai-custom-node" - class InvalidVersionError(ValueError): pass @@ -61,11 +66,28 @@ class Classification(str, Enum, metaclass=MetaEnum): - `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): @@ -74,13 +96,13 @@ class UIConfigBase(BaseModel): This is used internally by the @invocation decorator logic. Do not use this directly. """ - tags: Optional[list[str]] = Field(default_factory=None, description="The node's tags") + 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: Optional[str] = Field(default=None, description="Whether or not this is a custom node") + 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( @@ -89,6 +111,11 @@ class UIConfigBase(BaseModel): ) +class OriginalModelField(TypedDict): + annotation: Any + field_info: FieldInfo + + class BaseInvocationOutput(BaseModel): """ Base class for all invocation outputs. @@ -96,36 +123,11 @@ class BaseInvocationOutput(BaseModel): All invocation outputs must use the `@invocation_output` decorator to provide their unique type. """ - _output_classes: ClassVar[set[BaseInvocationOutput]] = set() - _typeadapter: ClassVar[Optional[TypeAdapter[Any]]] = None - _typeadapter_needs_update: ClassVar[bool] = False - - @classmethod - def register_output(cls, output: BaseInvocationOutput) -> None: - """Registers an invocation output.""" - cls._output_classes.add(output) - cls._typeadapter_needs_update = True - - @classmethod - def get_outputs(cls) -> Iterable[BaseInvocationOutput]: - """Gets all invocation outputs.""" - return cls._output_classes - - @classmethod - def get_typeadapter(cls) -> TypeAdapter[Any]: - """Gets a pydantc TypeAdapter for the union of all invocation output types.""" - if not cls._typeadapter or cls._typeadapter_needs_update: - AnyInvocationOutput = TypeAliasType( - "AnyInvocationOutput", Annotated[Union[tuple(cls._output_classes)], Field(discriminator="type")] - ) - cls._typeadapter = TypeAdapter(AnyInvocationOutput) - cls._typeadapter_needs_update = False - return cls._typeadapter - - @classmethod - def get_output_types(cls) -> Iterable[str]: - """Gets all invocation output types.""" - return (i.get_type() for i in BaseInvocationOutput.get_outputs()) + 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: @@ -142,6 +144,9 @@ 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, @@ -169,79 +174,29 @@ class BaseInvocation(ABC, BaseModel): All invocations must use the `@invocation` decorator to provide their unique type. """ - _invocation_classes: ClassVar[set[BaseInvocation]] = set() - _typeadapter: ClassVar[Optional[TypeAdapter[Any]]] = None - _typeadapter_needs_update: ClassVar[bool] = False - @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 register_invocation(cls, invocation: BaseInvocation) -> None: - """Registers an invocation.""" - cls._invocation_classes.add(invocation) - cls._typeadapter_needs_update = True - - @classmethod - def get_typeadapter(cls) -> TypeAdapter[Any]: - """Gets a pydantc TypeAdapter for the union of all invocation types.""" - if not cls._typeadapter or cls._typeadapter_needs_update: - AnyInvocation = TypeAliasType( - "AnyInvocation", Annotated[Union[tuple(cls._invocation_classes)], Field(discriminator="type")] - ) - cls._typeadapter = TypeAdapter(AnyInvocation) - cls._typeadapter_needs_update = False - return cls._typeadapter - - @classmethod - def get_invocations(cls) -> Iterable[BaseInvocation]: - """Gets all invocations, respecting the allowlist and denylist.""" - app_config = get_config() - allowed_invocations: set[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, BaseInvocation]: - """Gets a map of all invocation types to their invocation classes.""" - return {i.get_type(): i for i in BaseInvocation.get_invocations()} - - @classmethod - def get_invocation_types(cls) -> Iterable[str]: - """Gets all invocation types.""" - return (i.get_type() for i in BaseInvocation.get_invocations()) - - @classmethod - def get_output_annotation(cls) -> BaseInvocationOutput: + 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.""" - uiconfig = cast(UIConfigBase | None, getattr(model_class, "UIConfig", None)) - if uiconfig is not None: - if uiconfig.title is not None: - schema["title"] = uiconfig.title - if uiconfig.tags is not None: - schema["tags"] = uiconfig.tags - if uiconfig.category is not None: - schema["category"] = uiconfig.category - if uiconfig.node_pack is not None: - schema["node_pack"] = uiconfig.node_pack - schema["classification"] = uiconfig.classification - schema["version"] = uiconfig.version + 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" @@ -257,7 +212,7 @@ def invoke_internal(self, context: InvocationContext, services: "InvocationServi 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 self.model_fields.items(): + 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 @@ -272,9 +227,9 @@ def invoke_internal(self, context: InvocationContext, services: "InvocationServi setattr(self, field_name, orig_default) if orig_required and orig_default is PydanticUndefined and getattr(self, field_name) is None: if input_ == Input.Connection: - raise RequiredConnectionException(self.model_fields["type"].default, field_name) + raise RequiredConnectionException(type(self).model_fields["type"].default, field_name) elif input_ == Input.Any: - raise MissingInputException(self.model_fields["type"].default, field_name) + 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: @@ -304,7 +259,9 @@ def invoke_internal(self, context: InvocationContext, services: "InvocationServi is_intermediate: bool = Field( default=False, description="Whether or not this is an intermediate invocation.", - json_schema_extra={"ui_type": "IsIntermediate", "field_kind": FieldKind.NodeAttribute}, + 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, @@ -312,7 +269,15 @@ def invoke_internal(self, context: InvocationContext, services: "InvocationServi json_schema_extra={"field_kind": FieldKind.NodeAttribute}, ) - UIConfig: ClassVar[Type[UIConfigBase]] + bottleneck: ClassVar[Bottleneck] + + idle_gpu_offloadable: ClassVar[bool] = False + """Whether this node's entire execution may be temporarily re-pinned to an idle GPU when + `offload_text_encoders_to_idle_gpus` is enabled in multi-GPU mode. Only set this to True on nodes + that exclusively load encoder model(s), run a forward pass, and store their result on the CPU — + i.e. nodes that do no work tied to the session's own GPU. Set via the `@invocation` decorator.""" + + UIConfig: ClassVar[UIConfigBase] model_config = ConfigDict( protected_namespaces=(), @@ -322,21 +287,190 @@ def invoke_internal(self, context: InvocationContext, services: "InvocationServi 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", + "idle_gpu_offloadable", } RESERVED_INPUT_FIELD_NAMES = {"metadata", "board"} -RESERVED_OUTPUT_FIELD_NAMES = {"type"} +RESERVED_OUTPUT_FIELD_NAMES = {"type", "output_meta"} class _Model(BaseModel): @@ -349,6 +483,15 @@ class _Model(BaseModel): 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: @@ -360,54 +503,144 @@ def validate_fields(model_fields: dict[str, FieldInfo], model_type: str) -> None """ for name, field in model_fields.items(): if name in RESERVED_PYDANTIC_FIELD_NAMES: - raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved by pydantic)') + raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (reserved by pydantic)") if not field.annotation: - raise InvalidFieldError(f'Invalid field type "{name}" on "{model_type}" (missing annotation)') + raise InvalidFieldError(f"{model_type}.{name}: Invalid field type (missing annotation)") if not isinstance(field.json_schema_extra, dict): - raise InvalidFieldError( - f'Invalid field definition for "{name}" on "{model_type}" (missing 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 isinstance(field_kind, FieldKind): + if not is_enum_member(field_kind, FieldKind): raise InvalidFieldError( - f'Invalid field definition for "{name}" on "{model_type}" (maybe it\'s not an InputField or OutputField?)' + f"{model_type}.{name}: Invalid field definition for (maybe it's not an InputField or OutputField?)" ) - if field_kind is FieldKind.Input and ( + if field_kind == FieldKind.Input.value and ( name in RESERVED_NODE_ATTRIBUTE_FIELD_NAMES or name in RESERVED_INPUT_FIELD_NAMES ): - raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved input field name)') + raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (reserved input field name)") - if field_kind is FieldKind.Output and name in RESERVED_OUTPUT_FIELD_NAMES: - raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved output 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 is FieldKind.Internal) and name not in RESERVED_INPUT_FIELD_NAMES: - raise InvalidFieldError( - f'Invalid field name "{name}" on "{model_type}" (internal field without reserved 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 is FieldKind.NodeAttribute + 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'Invalid field name "{name}" on "{model_type}" (node attribute field without reserved name)' + f"{model_type}.{name}: Invalid field name (node attribute field without reserved name)" ) ui_type = field.json_schema_extra.get("ui_type", None) - if isinstance(ui_type, str) and ui_type.startswith("DEPRECATED_"): - logger.warn(f"\"UIType.{ui_type.split('_')[-1]}\" is deprecated, ignoring") - field.json_schema_extra.pop("ui_type") + 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, @@ -416,6 +649,8 @@ def invocation( version: Optional[str] = None, use_cache: Optional[bool] = True, classification: Classification = Classification.Stable, + bottleneck: Bottleneck = Bottleneck.GPU, + idle_gpu_offloadable: bool = False, ) -> Callable[[Type[TBaseInvocation]], Type[TBaseInvocation]]: """ Registers an invocation. @@ -427,6 +662,8 @@ def invocation( :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. + :param bool idle_gpu_offloadable: Whether this node's whole execution may run on a borrowed idle GPU when `offload_text_encoders_to_idle_gpus` is enabled. Only set True for encoder-only nodes that store their result on the CPU and do no work on the session's own GPU. Defaults to False. """ def wrapper(cls: Type[TBaseInvocation]) -> Type[TBaseInvocation]: @@ -435,40 +672,57 @@ def wrapper(cls: Type[TBaseInvocation]) -> Type[TBaseInvocation]: if re.compile(r"^\S+$").match(invocation_type) is None: raise ValueError(f'"invocation_type" must consist of non-whitespace characters, got "{invocation_type}"') - if invocation_type in BaseInvocation.get_invocation_types(): - raise ValueError(f'Invocation type "{invocation_type}" already exists') + # 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_name = cls.__qualname__ + ".UIConfig" - if not hasattr(cls, "UIConfig") or cls.UIConfig.__qualname__ != uiconfig_name: - cls.UIConfig = type(uiconfig_name, (UIConfigBase,), {}) - cls.UIConfig.title = title - cls.UIConfig.tags = tags - cls.UIConfig.category = category - cls.UIConfig.classification = classification - - # Grab the node pack's name from the module name, if it's a custom node - is_custom_node = cls.__module__.rsplit(".", 1)[0] == "invokeai.app.invocations" - if is_custom_node: - cls.UIConfig.node_pack = cls.__module__.split(".")[0] - else: - cls.UIConfig.node_pack = None + 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 - cls.UIConfig.version = version + uiconfig["version"] = version else: - logger.warn(f'No version specified for node "{invocation_type}", using "1.0.0"') - cls.UIConfig.version = "1.0.0" + 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 + cls.idle_gpu_offloadable = idle_gpu_offloadable + # Add the invocation type to the model. # You'd be tempted to just add the type field and rebuild the model, like this: @@ -478,24 +732,55 @@ def wrapper(cls: Type[TBaseInvocation]) -> Type[TBaseInvocation]: # 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] # type: ignore - invocation_type_field = Field( - title="type", default=invocation_type, json_schema_extra={"field_kind": FieldKind.NodeAttribute} + 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__ - cls = create_model( - cls.__qualname__, - __base__=cls, - __module__=cls.__module__, - type=(invocation_type_annotation, invocation_type_field), - ) - cls.__doc__ = docstring + 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 - # TODO: how to type this correctly? it's typed as ModelMetaclass, a private class in pydantic - BaseInvocation.register_invocation(cls) # type: ignore + InvocationRegistry.register_invocation(new_class) - return cls + return new_class return wrapper @@ -518,29 +803,41 @@ def wrapper(cls: Type[TBaseInvocationOutput]) -> Type[TBaseInvocationOutput]: if re.compile(r"^\S+$").match(output_type) is None: raise ValueError(f'"output_type" must consist of non-whitespace characters, got "{output_type}"') - if output_type in BaseInvocationOutput.get_output_types(): - raise ValueError(f'Invocation type "{output_type}" already exists') - 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] - output_type_annotation = Literal[output_type] # type: ignore - output_type_field = Field( - title="type", default=output_type, json_schema_extra={"field_kind": FieldKind.NodeAttribute} + # 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__ - cls = create_model( - cls.__qualname__, - __base__=cls, - __module__=cls.__module__, - type=(output_type_annotation, output_type_field), - ) - cls.__doc__ = docstring + new_class = create_model(cls.__qualname__, __base__=cls, __module__=cls.__module__, **fields) + new_class.__doc__ = docstring - BaseInvocationOutput.register_output(cls) # type: ignore # TODO: how to type this correctly? + InvocationRegistry.register_output(new_class) - return cls + 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 index 9238f4b34c5..9f4e0f5563c 100644 --- a/invokeai/app/invocations/blend_latents.py +++ b/invokeai/app/invocations/blend_latents.py @@ -1,98 +1,120 @@ -from typing import Any, Union +from typing import Optional, Union import numpy as np -import numpy.typing as npt 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, Input, InputField, LatentsField +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"], + tags=["latents", "blend", "mask"], category="latents", - version="1.0.3", + version="1.1.0", ) class BlendLatentsInvocation(BaseInvocation): - """Blend two latents using a given alpha. Latents must have same size.""" - - latents_a: LatentsField = InputField( - description=FieldDescriptions.latents, - input=Input.Connection, - ) - latents_b: LatentsField = InputField( - description=FieldDescriptions.latents, - input=Input.Connection, - ) - alpha: float = InputField(default=0.5, description=FieldDescriptions.blend_alpha) + """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 Exception("Latents to blend must be the same size.") + raise ValueError("Latents to blend must be the same size.") device = TorchDevice.choose_torch_device() - def slerp( - t: Union[float, npt.NDArray[Any]], # FIXME: maybe use np.float32 here? - v0: Union[torch.Tensor, npt.NDArray[Any]], - v1: Union[torch.Tensor, npt.NDArray[Any]], - DOT_THRESHOLD: float = 0.9995, - ) -> Union[torch.Tensor, npt.NDArray[Any]]: - """ - 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: torch.Tensor = torch.from_numpy(v2).to(device) - return v2_torch - else: - assert isinstance(v2, np.ndarray) - return v2 - # blend - bl = slerp(self.alpha, latents_a, latents_b) - assert isinstance(bl, torch.Tensor) - blended_latents: torch.Tensor = bl # for type checking convenience + 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") - - TorchDevice.empty_cache() + torch.cuda.empty_cache() name = context.tensors.save(tensor=blended_latents) - return LatentsOutput.build(latents_name=name, latents=blended_latents, seed=self.latents_a.seed) + 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..cb06d2b3ff6 --- /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), desc=f"Denoising{TorchDevice.get_session_device_label()}"): + 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..c303e55b828 --- /dev/null +++ b/invokeai/app/invocations/cogview4_text_encoder.py @@ -0,0 +1,101 @@ +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, + idle_gpu_offloadable=True, +) +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 index e02291980f9..39e77f5b637 100644 --- a/invokeai/app/invocations/collections.py +++ b/invokeai/app/invocations/collections.py @@ -4,17 +4,14 @@ 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 -from .baseinvocation import BaseInvocation, invocation -from .fields import InputField - -@invocation( - "range", title="Integer Range", tags=["collection", "integer", "range"], category="collections", version="1.0.0" -) +@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""" @@ -36,7 +33,7 @@ def invoke(self, context: InvocationContext) -> IntegerCollectionOutput: "range_of_size", title="Integer Range of Size", tags=["collection", "integer", "size", "range"], - category="collections", + category="batch", version="1.0.0", ) class RangeOfSizeInvocation(BaseInvocation): @@ -56,7 +53,7 @@ def invoke(self, context: InvocationContext) -> IntegerCollectionOutput: "random_range", title="Random Range", tags=["range", "integer", "random", "collection"], - category="collections", + category="batch", version="1.0.1", use_cache=False, ) 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 index 1e78e10d380..428f72d3964 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -1,10 +1,11 @@ from typing import Iterator, List, Optional, Tuple, Union, cast import torch -from compel import Compel, ReturnedEmbeddingsType +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, @@ -14,11 +15,14 @@ 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.lora import LoRAModelRaw +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, @@ -26,9 +30,6 @@ ) from invokeai.backend.util.devices import TorchDevice -from .baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output -from .model import CLIPField - # unconditioned: Optional[torch.Tensor] @@ -40,10 +41,11 @@ @invocation( "compel", - title="Prompt", + title="Prompt - SD1.5", tags=["prompt", "compel"], - category="conditioning", - version="1.2.0", + category="prompt", + version="1.2.1", + idle_gpu_offloadable=True, ) class CompelInvocation(BaseInvocation): """Parse prompt using compel package to conditioning.""" @@ -56,7 +58,6 @@ class CompelInvocation(BaseInvocation): clip: CLIPField = InputField( title="CLIP", description=FieldDescriptions.clip, - input=Input.Connection, ) mask: Optional[TensorField] = InputField( default=None, description="A mask defining the region that this conditioning prompt applies to." @@ -64,29 +65,29 @@ class CompelInvocation(BaseInvocation): @torch.no_grad() def invoke(self, context: InvocationContext) -> ConditioningOutput: - tokenizer_info = context.models.load(self.clip.tokenizer) - text_encoder_info = context.models.load(self.clip.text_encoder) - - def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: + 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, LoRAModelRaw) + 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 (model_state_dict, text_encoder), - tokenizer_info as tokenizer, - ModelPatcher.apply_lora_text_encoder( - text_encoder, - loras=_lora_loader(), - model_state_dict=model_state_dict, + 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), @@ -95,6 +96,7 @@ def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: ti_manager, ), ): + context.util.signal_progress("Building conditioning") assert isinstance(text_encoder, CLIPTextModel) assert isinstance(tokenizer, CLIPTokenizer) compel = Compel( @@ -103,6 +105,8 @@ def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: 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) @@ -112,6 +116,13 @@ def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: 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)]) @@ -137,9 +148,7 @@ def run_clip_compel( lora_prefix: str, zero_on_empty: bool, ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: - tokenizer_info = context.models.load(clip_field.tokenizer) 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 @@ -161,11 +170,11 @@ def run_clip_compel( c_pooled = None return c, c_pooled - def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: + 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, LoRAModelRaw) + assert isinstance(lora_model, ModelPatchRaw) yield (lora_model, lora.weight) del lora_info return @@ -176,13 +185,14 @@ def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: with ( # apply all patches while the model is on the target device - text_encoder_info.model_on_device() as (state_dict, text_encoder), - tokenizer_info as tokenizer, - ModelPatcher.apply_lora( - text_encoder, - loras=_lora_loader(), + 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, - model_state_dict=state_dict, + 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), @@ -191,6 +201,7 @@ def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: ti_manager, ), ): + context.util.signal_progress("Building conditioning") assert isinstance(text_encoder, (CLIPTextModel, CLIPTextModelWithProjection)) assert isinstance(tokenizer, CLIPTokenizer) @@ -203,6 +214,8 @@ def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: 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) @@ -218,9 +231,11 @@ def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: else: c_pooled = None + del compel + del patched_tokenizer del tokenizer + del ti_manager del text_encoder - del tokenizer_info del text_encoder_info c = c.detach().to("cpu") @@ -232,10 +247,11 @@ def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: @invocation( "sdxl_compel_prompt", - title="SDXL Prompt", + title="Prompt - SDXL", tags=["sdxl", "compel", "prompt"], - category="conditioning", - version="1.2.0", + category="prompt", + version="1.2.1", + idle_gpu_offloadable=True, ) class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase): """Parse prompt using compel package to conditioning.""" @@ -326,10 +342,11 @@ def invoke(self, context: InvocationContext) -> ConditioningOutput: @invocation( "sdxl_refiner_compel_prompt", - title="SDXL Refiner Prompt", + title="Prompt - SDXL Refiner", tags=["sdxl", "compel", "prompt"], - category="conditioning", - version="1.1.1", + category="prompt", + version="1.1.2", + idle_gpu_offloadable=True, ) class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase): """Parse prompt using compel package to conditioning.""" @@ -375,10 +392,10 @@ class CLIPSkipInvocationOutput(BaseInvocationOutput): @invocation( "clip_skip", - title="CLIP Skip", + title="Apply CLIP Skip - SD1.5, SDXL", tags=["clipskip", "clip", "skip"], - category="conditioning", - version="1.1.0", + category="prompt", + version="1.1.1", ) class CLIPSkipInvocation(BaseInvocation): """Skip layers in clip text_encoder model.""" @@ -512,7 +529,7 @@ def log_tokenization_for_text( usedTokens += 1 if usedTokens > 0: - print(f'\n>> [TOKENLOG] Tokens {display_label or ""} ({usedTokens}):') + print(f"\n>> [TOKENLOG] Tokens {display_label or ''} ({usedTokens}):") print(f"{tokenized}\x1b[0m") if discarded != "": 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 index e01589be812..314890a0f8f 100644 --- a/invokeai/app/invocations/constants.py +++ b/invokeai/app/invocations/constants.py @@ -1,8 +1,5 @@ from typing import Literal -from invokeai.backend.stable_diffusion.schedulers import SCHEDULER_MAP -from invokeai.backend.util.devices import TorchDevice - 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 @@ -11,10 +8,5 @@ The ratio of image:latent dimensions is LATENT_SCALE_FACTOR:1, or 8:1. """ -SCHEDULER_NAME_VALUES = Literal[tuple(SCHEDULER_MAP.keys())] -"""A literal type representing the valid scheduler names.""" - IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"] """A literal type for PIL image modes supported by Invoke""" - -DEFAULT_PRECISION = TorchDevice.choose_torch_dtype() 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/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py deleted file mode 100644 index c0b332f27b6..00000000000 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ /dev/null @@ -1,673 +0,0 @@ -# Invocations for ControlNet image preprocessors -# initial implementation by Gregg Helt, 2023 -# heavily leverages controlnet_aux package: https://github.com/patrickvonplaten/controlnet_aux -from builtins import bool, float -from pathlib import Path -from typing import Dict, List, Literal, Union - -import cv2 -import numpy as np -from controlnet_aux import ( - ContentShuffleDetector, - LeresDetector, - MediapipeFaceDetector, - MidasDetector, - MLSDdetector, - NormalBaeDetector, - PidiNetDetector, - SamDetector, - ZoeDetector, -) -from controlnet_aux.util import HWC3, ade_palette -from PIL import Image -from pydantic import BaseModel, Field, field_validator, model_validator - -from invokeai.app.invocations.fields import ( - FieldDescriptions, - ImageField, - InputField, - OutputField, - UIType, - WithBoard, - WithMetadata, -) -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 -from invokeai.backend.image_util.canny import get_canny_edges -from invokeai.backend.image_util.depth_anything import DEPTH_ANYTHING_MODELS, DepthAnythingDetector -from invokeai.backend.image_util.dw_openpose import DWPOSE_MODELS, DWOpenposeDetector -from invokeai.backend.image_util.hed import HEDProcessor -from invokeai.backend.image_util.lineart import LineartProcessor -from invokeai.backend.image_util.lineart_anime import LineartAnimeProcessor -from invokeai.backend.image_util.util import np_to_pil, pil_to_np -from invokeai.backend.util.devices import TorchDevice - -from .baseinvocation import BaseInvocation, BaseInvocationOutput, Classification, invocation, invocation_output - - -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", tags=["controlnet"], category="controlnet", version="1.1.2") -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_type=UIType.ControlNetModel - ) - 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, - ), - ) - - -# This invocation exists for other invocations to subclass it - do not register with @invocation! -class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithBoard): - """Base class for invocations that preprocess images for ControlNet""" - - image: ImageField = InputField(description="The image to process") - - def run_processor(self, image: Image.Image) -> Image.Image: - # superclass just passes through image without processing - return image - - def load_image(self, context: InvocationContext) -> Image.Image: - # allows override for any special formatting specific to the preprocessor - return context.images.get_pil(self.image.image_name, "RGB") - - def invoke(self, context: InvocationContext) -> ImageOutput: - self._context = context - raw_image = self.load_image(context) - # image type should be PIL.PngImagePlugin.PngImageFile ? - processed_image = self.run_processor(raw_image) - - # currently can't see processed image in node UI without a showImage node, - # so for now setting image_type to RESULT instead of INTERMEDIATE so will get saved in gallery - image_dto = context.images.save(image=processed_image) - - """Builds an ImageOutput and its ImageField""" - processed_image_field = ImageField(image_name=image_dto.image_name) - return ImageOutput( - image=processed_image_field, - # width=processed_image.width, - width=image_dto.width, - # height=processed_image.height, - height=image_dto.height, - # mode=processed_image.mode, - ) - - -@invocation( - "canny_image_processor", - title="Canny Processor", - tags=["controlnet", "canny"], - category="controlnet", - version="1.3.3", -) -class CannyImageProcessorInvocation(ImageProcessorInvocation): - """Canny edge detection for ControlNet""" - - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - 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 load_image(self, context: InvocationContext) -> Image.Image: - # Keep alpha channel for Canny processing to detect edges of transparent areas - return context.images.get_pil(self.image.image_name, "RGBA") - - def run_processor(self, image: Image.Image) -> Image.Image: - processed_image = get_canny_edges( - image, - self.low_threshold, - self.high_threshold, - detect_resolution=self.detect_resolution, - image_resolution=self.image_resolution, - ) - return processed_image - - -@invocation( - "hed_image_processor", - title="HED (softedge) Processor", - tags=["controlnet", "hed", "softedge"], - category="controlnet", - version="1.2.3", -) -class HedImageProcessorInvocation(ImageProcessorInvocation): - """Applies HED edge detection to image""" - - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - # safe not supported in controlnet_aux v0.0.3 - # safe: bool = InputField(default=False, description=FieldDescriptions.safe_mode) - scribble: bool = InputField(default=False, description=FieldDescriptions.scribble_mode) - - def run_processor(self, image: Image.Image) -> Image.Image: - hed_processor = HEDProcessor() - processed_image = hed_processor.run( - image, - detect_resolution=self.detect_resolution, - image_resolution=self.image_resolution, - # safe not supported in controlnet_aux v0.0.3 - # safe=self.safe, - scribble=self.scribble, - ) - return processed_image - - -@invocation( - "lineart_image_processor", - title="Lineart Processor", - tags=["controlnet", "lineart"], - category="controlnet", - version="1.2.3", -) -class LineartImageProcessorInvocation(ImageProcessorInvocation): - """Applies line art processing to image""" - - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - coarse: bool = InputField(default=False, description="Whether to use coarse mode") - - def run_processor(self, image: Image.Image) -> Image.Image: - lineart_processor = LineartProcessor() - processed_image = lineart_processor.run( - image, detect_resolution=self.detect_resolution, image_resolution=self.image_resolution, coarse=self.coarse - ) - return processed_image - - -@invocation( - "lineart_anime_image_processor", - title="Lineart Anime Processor", - tags=["controlnet", "lineart", "anime"], - category="controlnet", - version="1.2.3", -) -class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation): - """Applies line art anime processing to image""" - - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - - def run_processor(self, image: Image.Image) -> Image.Image: - processor = LineartAnimeProcessor() - processed_image = processor.run( - image, - detect_resolution=self.detect_resolution, - image_resolution=self.image_resolution, - ) - return processed_image - - -@invocation( - "midas_depth_image_processor", - title="Midas Depth Processor", - tags=["controlnet", "midas"], - category="controlnet", - version="1.2.4", -) -class MidasDepthImageProcessorInvocation(ImageProcessorInvocation): - """Applies Midas depth processing to image""" - - a_mult: float = InputField(default=2.0, ge=0, description="Midas parameter `a_mult` (a = a_mult * PI)") - bg_th: float = InputField(default=0.1, ge=0, description="Midas parameter `bg_th`") - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - # depth_and_normal not supported in controlnet_aux v0.0.3 - # depth_and_normal: bool = InputField(default=False, description="whether to use depth and normal mode") - - def run_processor(self, image: Image.Image) -> Image.Image: - # TODO: replace from_pretrained() calls with context.models.download_and_cache() (or similar) - midas_processor = MidasDetector.from_pretrained("lllyasviel/Annotators") - processed_image = midas_processor( - image, - a=np.pi * self.a_mult, - bg_th=self.bg_th, - image_resolution=self.image_resolution, - detect_resolution=self.detect_resolution, - # dept_and_normal not supported in controlnet_aux v0.0.3 - # depth_and_normal=self.depth_and_normal, - ) - return processed_image - - -@invocation( - "normalbae_image_processor", - title="Normal BAE Processor", - tags=["controlnet"], - category="controlnet", - version="1.2.3", -) -class NormalbaeImageProcessorInvocation(ImageProcessorInvocation): - """Applies NormalBae processing to image""" - - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - - def run_processor(self, image: Image.Image) -> Image.Image: - normalbae_processor = NormalBaeDetector.from_pretrained("lllyasviel/Annotators") - processed_image = normalbae_processor( - image, detect_resolution=self.detect_resolution, image_resolution=self.image_resolution - ) - return processed_image - - -@invocation( - "mlsd_image_processor", title="MLSD Processor", tags=["controlnet", "mlsd"], category="controlnet", version="1.2.3" -) -class MlsdImageProcessorInvocation(ImageProcessorInvocation): - """Applies MLSD processing to image""" - - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - thr_v: float = InputField(default=0.1, ge=0, description="MLSD parameter `thr_v`") - thr_d: float = InputField(default=0.1, ge=0, description="MLSD parameter `thr_d`") - - def run_processor(self, image: Image.Image) -> Image.Image: - mlsd_processor = MLSDdetector.from_pretrained("lllyasviel/Annotators") - processed_image = mlsd_processor( - image, - detect_resolution=self.detect_resolution, - image_resolution=self.image_resolution, - thr_v=self.thr_v, - thr_d=self.thr_d, - ) - return processed_image - - -@invocation( - "pidi_image_processor", title="PIDI Processor", tags=["controlnet", "pidi"], category="controlnet", version="1.2.3" -) -class PidiImageProcessorInvocation(ImageProcessorInvocation): - """Applies PIDI processing to image""" - - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - safe: bool = InputField(default=False, description=FieldDescriptions.safe_mode) - scribble: bool = InputField(default=False, description=FieldDescriptions.scribble_mode) - - def run_processor(self, image: Image.Image) -> Image.Image: - pidi_processor = PidiNetDetector.from_pretrained("lllyasviel/Annotators") - processed_image = pidi_processor( - image, - detect_resolution=self.detect_resolution, - image_resolution=self.image_resolution, - safe=self.safe, - scribble=self.scribble, - ) - return processed_image - - -@invocation( - "content_shuffle_image_processor", - title="Content Shuffle Processor", - tags=["controlnet", "contentshuffle"], - category="controlnet", - version="1.2.3", -) -class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation): - """Applies content shuffle processing to image""" - - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - h: int = InputField(default=512, ge=0, description="Content shuffle `h` parameter") - w: int = InputField(default=512, ge=0, description="Content shuffle `w` parameter") - f: int = InputField(default=256, ge=0, description="Content shuffle `f` parameter") - - def run_processor(self, image: Image.Image) -> Image.Image: - content_shuffle_processor = ContentShuffleDetector() - processed_image = content_shuffle_processor( - image, - detect_resolution=self.detect_resolution, - image_resolution=self.image_resolution, - h=self.h, - w=self.w, - f=self.f, - ) - return processed_image - - -# should work with controlnet_aux >= 0.0.4 and timm <= 0.6.13 -@invocation( - "zoe_depth_image_processor", - title="Zoe (Depth) Processor", - tags=["controlnet", "zoe", "depth"], - category="controlnet", - version="1.2.3", -) -class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation): - """Applies Zoe depth processing to image""" - - def run_processor(self, image: Image.Image) -> Image.Image: - zoe_depth_processor = ZoeDetector.from_pretrained("lllyasviel/Annotators") - processed_image = zoe_depth_processor(image) - return processed_image - - -@invocation( - "mediapipe_face_processor", - title="Mediapipe Face Processor", - tags=["controlnet", "mediapipe", "face"], - category="controlnet", - version="1.2.4", -) -class MediapipeFaceProcessorInvocation(ImageProcessorInvocation): - """Applies mediapipe face processing to image""" - - 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") - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - - def run_processor(self, image: Image.Image) -> Image.Image: - mediapipe_face_processor = MediapipeFaceDetector() - processed_image = mediapipe_face_processor( - image, - max_faces=self.max_faces, - min_confidence=self.min_confidence, - image_resolution=self.image_resolution, - detect_resolution=self.detect_resolution, - ) - return processed_image - - -@invocation( - "leres_image_processor", - title="Leres (Depth) Processor", - tags=["controlnet", "leres", "depth"], - category="controlnet", - version="1.2.3", -) -class LeresImageProcessorInvocation(ImageProcessorInvocation): - """Applies leres processing to image""" - - thr_a: float = InputField(default=0, description="Leres parameter `thr_a`") - thr_b: float = InputField(default=0, description="Leres parameter `thr_b`") - boost: bool = InputField(default=False, description="Whether to use boost mode") - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - - def run_processor(self, image: Image.Image) -> Image.Image: - leres_processor = LeresDetector.from_pretrained("lllyasviel/Annotators") - processed_image = leres_processor( - image, - thr_a=self.thr_a, - thr_b=self.thr_b, - boost=self.boost, - detect_resolution=self.detect_resolution, - image_resolution=self.image_resolution, - ) - return processed_image - - -@invocation( - "tile_image_processor", - title="Tile Resample Processor", - tags=["controlnet", "tile"], - category="controlnet", - version="1.2.3", -) -class TileResamplerProcessorInvocation(ImageProcessorInvocation): - """Tile resampler processor""" - - # res: int = InputField(default=512, ge=0, le=1024, description="The pixel resolution for each tile") - down_sampling_rate: float = InputField(default=1.0, ge=1.0, le=8.0, description="Down sampling rate") - - # tile_resample copied from sd-webui-controlnet/scripts/processor.py - def tile_resample( - self, - np_img: np.ndarray, - res=512, # never used? - down_sampling_rate=1.0, - ): - np_img = HWC3(np_img) - if down_sampling_rate < 1.1: - return np_img - H, W, C = np_img.shape - H = int(float(H) / float(down_sampling_rate)) - W = int(float(W) / float(down_sampling_rate)) - np_img = cv2.resize(np_img, (W, H), interpolation=cv2.INTER_AREA) - return np_img - - def run_processor(self, image: Image.Image) -> Image.Image: - np_img = np.array(image, dtype=np.uint8) - processed_np_image = self.tile_resample( - np_img, - # res=self.tile_size, - down_sampling_rate=self.down_sampling_rate, - ) - processed_image = Image.fromarray(processed_np_image) - return processed_image - - -@invocation( - "segment_anything_processor", - title="Segment Anything Processor", - tags=["controlnet", "segmentanything"], - category="controlnet", - version="1.2.4", -) -class SegmentAnythingProcessorInvocation(ImageProcessorInvocation): - """Applies segment anything processing to image""" - - detect_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.detect_res) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - - def run_processor(self, image: Image.Image) -> Image.Image: - # segment_anything_processor = SamDetector.from_pretrained("ybelkada/segment-anything", subfolder="checkpoints") - segment_anything_processor = SamDetectorReproducibleColors.from_pretrained( - "ybelkada/segment-anything", subfolder="checkpoints" - ) - np_img = np.array(image, dtype=np.uint8) - processed_image = segment_anything_processor( - np_img, image_resolution=self.image_resolution, detect_resolution=self.detect_resolution - ) - return processed_image - - -class SamDetectorReproducibleColors(SamDetector): - # overriding SamDetector.show_anns() method to use reproducible colors for segmentation image - # base class show_anns() method randomizes colors, - # which seems to also lead to non-reproducible image generation - # so using ADE20k color palette instead - def show_anns(self, anns: List[Dict]): - if len(anns) == 0: - return - sorted_anns = sorted(anns, key=(lambda x: x["area"]), reverse=True) - h, w = anns[0]["segmentation"].shape - final_img = Image.fromarray(np.zeros((h, w, 3), dtype=np.uint8), mode="RGB") - palette = ade_palette() - for i, ann in enumerate(sorted_anns): - m = ann["segmentation"] - img = np.empty((m.shape[0], m.shape[1], 3), dtype=np.uint8) - # doing modulo just in case number of annotated regions exceeds number of colors in palette - ann_color = palette[i % len(palette)] - img[:, :] = ann_color - final_img.paste(Image.fromarray(img, mode="RGB"), (0, 0), Image.fromarray(np.uint8(m * 255))) - return np.array(final_img, dtype=np.uint8) - - -@invocation( - "color_map_image_processor", - title="Color Map Processor", - tags=["controlnet"], - category="controlnet", - version="1.2.3", -) -class ColorMapImageProcessorInvocation(ImageProcessorInvocation): - """Generates a color map from the provided image""" - - color_map_tile_size: int = InputField(default=64, ge=1, description=FieldDescriptions.tile_size) - - def run_processor(self, image: Image.Image) -> Image.Image: - np_image = np.array(image, dtype=np.uint8) - height, width = np_image.shape[:2] - - width_tile_size = min(self.color_map_tile_size, width) - height_tile_size = min(self.color_map_tile_size, height) - - color_map = cv2.resize( - np_image, - (width // width_tile_size, height // height_tile_size), - interpolation=cv2.INTER_CUBIC, - ) - color_map = cv2.resize(color_map, (width, height), interpolation=cv2.INTER_NEAREST) - color_map = Image.fromarray(color_map) - return color_map - - -DEPTH_ANYTHING_MODEL_SIZES = Literal["large", "base", "small"] - - -@invocation( - "depth_anything_image_processor", - title="Depth Anything Processor", - tags=["controlnet", "depth", "depth anything"], - category="controlnet", - version="1.1.2", -) -class DepthAnythingImageProcessorInvocation(ImageProcessorInvocation): - """Generates a depth map based on the Depth Anything algorithm""" - - model_size: DEPTH_ANYTHING_MODEL_SIZES = InputField( - default="small", description="The size of the depth model to use" - ) - resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - - def run_processor(self, image: Image.Image) -> Image.Image: - def loader(model_path: Path): - return DepthAnythingDetector.load_model( - model_path, model_size=self.model_size, device=TorchDevice.choose_torch_device() - ) - - with self._context.models.load_remote_model( - source=DEPTH_ANYTHING_MODELS[self.model_size], loader=loader - ) as model: - depth_anything_detector = DepthAnythingDetector(model, TorchDevice.choose_torch_device()) - processed_image = depth_anything_detector(image=image, resolution=self.resolution) - return processed_image - - -@invocation( - "dw_openpose_image_processor", - title="DW Openpose Image Processor", - tags=["controlnet", "dwpose", "openpose"], - category="controlnet", - version="1.1.1", -) -class DWOpenposeImageProcessorInvocation(ImageProcessorInvocation): - """Generates an openpose pose from an image using DWPose""" - - draw_body: bool = InputField(default=True) - draw_face: bool = InputField(default=False) - draw_hands: bool = InputField(default=False) - image_resolution: int = InputField(default=512, ge=1, description=FieldDescriptions.image_res) - - def run_processor(self, image: Image.Image) -> Image.Image: - onnx_det = self._context.models.download_and_cache_model(DWPOSE_MODELS["yolox_l.onnx"]) - onnx_pose = self._context.models.download_and_cache_model(DWPOSE_MODELS["dw-ll_ucoco_384.onnx"]) - - dw_openpose = DWOpenposeDetector(onnx_det=onnx_det, onnx_pose=onnx_pose) - processed_image = dw_openpose( - image, - draw_face=self.draw_face, - draw_hands=self.draw_hands, - draw_body=self.draw_body, - resolution=self.image_resolution, - ) - return processed_image - - -@invocation( - "heuristic_resize", - title="Heuristic Resize", - tags=["image, controlnet"], - category="image", - version="1.0.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(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 index 2d66c20dbd4..419a516bcdc 100644 --- a/invokeai/app/invocations/create_denoise_mask.py +++ b/invokeai/app/invocations/create_denoise_mask.py @@ -6,7 +6,6 @@ from torchvision.transforms.functional import resize as tv_resize from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation -from invokeai.app.invocations.constants import DEFAULT_PRECISION 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 @@ -19,7 +18,7 @@ "create_denoise_mask", title="Create Denoise Mask", tags=["mask", "denoise"], - category="latents", + category="mask", version="1.0.2", ) class CreateDenoiseMaskInvocation(BaseInvocation): @@ -29,11 +28,7 @@ class CreateDenoiseMaskInvocation(BaseInvocation): 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=DEFAULT_PRECISION == torch.float32, - description=FieldDescriptions.fp32, - ui_order=4, - ) + 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": @@ -65,6 +60,7 @@ def invoke(self, context: InvocationContext) -> DenoiseMaskOutput: 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) diff --git a/invokeai/app/invocations/create_gradient_mask.py b/invokeai/app/invocations/create_gradient_mask.py index 089313463bf..08826cc5efc 100644 --- a/invokeai/app/invocations/create_gradient_mask.py +++ b/invokeai/app/invocations/create_gradient_mask.py @@ -1,13 +1,14 @@ from typing import Literal, Optional +import cv2 import numpy as np import torch import torchvision.transforms as T -from PIL import Image, ImageFilter +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 DEFAULT_PRECISION +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR from invokeai.app.invocations.fields import ( DenoiseMaskField, FieldDescriptions, @@ -19,8 +20,7 @@ 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 import LoadedModel -from invokeai.backend.model_manager.config import MainConfigBase, ModelVariantType +from invokeai.backend.model_manager.taxonomy import FluxVariantType, ModelType, ModelVariantType from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor @@ -28,7 +28,10 @@ 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") + 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." ) @@ -38,16 +41,14 @@ class GradientMaskOutput(BaseInvocationOutput): "create_gradient_mask", title="Create Gradient Mask", tags=["mask", "denoise"], - category="latents", - version="1.1.0", + category="mask", + version="1.3.0", ) class CreateGradientMaskInvocation(BaseInvocation): - """Creates mask for denoising model run.""" + """Creates mask for denoising.""" - mask: ImageField = InputField(default=None, description="Image which will be masked", ui_order=1) - edge_radius: int = InputField( - default=16, ge=0, description="How far to blur/expand the edges of the mask", ui_order=2 - ) + 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 @@ -73,60 +74,124 @@ class CreateGradientMaskInvocation(BaseInvocation): ui_order=7, ) tiled: bool = InputField(default=False, description=FieldDescriptions.tiled, ui_order=8) - fp32: bool = InputField( - default=DEFAULT_PRECISION == torch.float32, - description=FieldDescriptions.fp32, - ui_order=9, - ) + 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") - if self.edge_radius > 0: - if self.coherence_mode == "Box Blur": - blur_mask = mask_image.filter(ImageFilter.BoxBlur(self.edge_radius)) - else: # Gaussian Blur OR Staged - # Gaussian Blur uses standard deviation. 1/2 radius is a good approximation - blur_mask = mask_image.filter(ImageFilter.GaussianBlur(self.edge_radius / 2)) - blur_tensor: torch.Tensor = image_resized_to_grid_as_tensor(blur_mask, normalize=False) + # 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, + ) - # redistribute blur so that the original edges are 0 and blur outwards to 1 - blur_tensor = (blur_tensor - 0.5) * 2 + mask_np_orig = np.array(mask_image, dtype=np.float32) - threshold = 1 - self.minimum_denoise + 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 the blur_tensor is less than fully masked, convert it to threshold - blur_tensor = torch.where((blur_tensor < 1) & (blur_tensor > 0), threshold, blur_tensor) - else: - # wherever the blur_tensor is above threshold but less than 1, drop it to threshold - blur_tensor = torch.where((blur_tensor > threshold) & (blur_tensor < 1), threshold, blur_tensor) + # 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: - blur_tensor: torch.Tensor = image_resized_to_grid_as_tensor(mask_image, normalize=False) + 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) - mask_name = context.tensors.save(tensor=blur_tensor.unsqueeze(1)) + # 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, + ) - # compute a [0, 1] mask from the blur_tensor - expanded_mask = torch.where((blur_tensor < 1), 0, 1) - expanded_mask_image = Image.fromarray((expanded_mask.squeeze(0).numpy() * 255).astype(np.uint8), mode="L") - expanded_image_dto = context.images.save(expanded_mask_image) + # 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 isinstance(main_model_config, MainConfigBase) - if main_model_config.variant is ModelVariantType.Inpaint: - mask = blur_tensor - vae_info: LoadedModel = context.models.load(self.vae.vae) + 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() ) @@ -136,3 +201,29 @@ def invoke(self, context: InvocationContext) -> 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/custom_nodes/init.py b/invokeai/app/invocations/custom_nodes/init.py deleted file mode 100644 index efddede72fc..00000000000 --- a/invokeai/app/invocations/custom_nodes/init.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Invoke-managed custom node loader. See README.md for more information. -""" - -import sys -import traceback -from importlib.util import module_from_spec, spec_from_file_location -from pathlib import Path - -from invokeai.backend.util.logging import InvokeAILogger - -logger = InvokeAILogger.get_logger() -loaded_count = 0 - - -for d in Path(__file__).parent.iterdir(): - # skip files - if not d.is_dir(): - continue - - # skip hidden directories - if d.name.startswith("_") or d.name.startswith("."): - continue - - # skip directories without an `__init__.py` - init = d / "__init__.py" - if not init.exists(): - continue - - module_name = init.parent.stem - - # skip if already imported - if module_name in globals(): - continue - - # load the module, appending adding a suffix to identify it as a custom node pack - spec = spec_from_file_location(module_name, init.absolute()) - - if spec is None or spec.loader is None: - logger.warn(f"Could not load {init}") - continue - - 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_count += 1 - except Exception: - full_error = traceback.format_exc() - logger.error(f"Failed to load node pack {module_name}:\n{full_error}") - - del init, module_name - -if loaded_count > 0: - logger.info(f"Loaded {loaded_count} node packs from {Path(__file__).parent}") diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py index a7c394deb22..f7951ccfeb2 100644 --- a/invokeai/app/invocations/cv.py +++ b/invokeai/app/invocations/cv.py @@ -5,13 +5,11 @@ import numpy from PIL import Image, ImageOps -from invokeai.app.invocations.fields import ImageField +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 .baseinvocation import BaseInvocation, invocation -from .fields import InputField, WithBoard, WithMetadata - @invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.3.1") class CvInpaintInvocation(BaseInvocation, WithMetadata, WithBoard): diff --git a/invokeai/app/invocations/denoise_latents.py b/invokeai/app/invocations/denoise_latents.py index e94daf70bdd..bb114263e23 100644 --- a/invokeai/app/invocations/denoise_latents.py +++ b/invokeai/app/invocations/denoise_latents.py @@ -1,5 +1,6 @@ # 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 @@ -9,16 +10,19 @@ 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, SCHEDULER_NAME_VALUES -from invokeai.app.invocations.controlnet_image_processors import ControlField +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR +from invokeai.app.invocations.controlnet import ControlField from invokeai.app.invocations.fields import ( ConditioningField, DenoiseMaskField, @@ -35,10 +39,13 @@ 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.lora import LoRAModelRaw -from invokeai.backend.model_manager import BaseModelType +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.stable_diffusion import PipelineIntermediateState, set_seamless +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, @@ -53,8 +60,23 @@ 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 @@ -64,9 +86,14 @@ def get_scheduler( 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 @@ -78,10 +105,17 @@ def get_scheduler( "_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 @@ -93,10 +127,10 @@ def get_scheduler( @invocation( "denoise_latents", - title="Denoise Latents", + title="Denoise - SD1.5, SDXL", tags=["latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"], category="latents", - version="1.5.3", + version="1.5.4", ) class DenoiseLatentsInvocation(BaseInvocation): """Denoises noisy latents to decodable images""" @@ -165,7 +199,7 @@ class DenoiseLatentsInvocation(BaseInvocation): ) denoise_mask: Optional[DenoiseMaskField] = InputField( default=None, - description=FieldDescriptions.mask, + description=FieldDescriptions.denoise_mask, input=Input.Connection, ui_order=8, ) @@ -182,8 +216,8 @@ def ge_one(cls, v: Union[List[float], float]) -> Union[List[float], float]: raise ValueError("cfg_scale must be greater than 1") return v + @staticmethod def _get_text_embeddings_and_masks( - self, cond_list: list[ConditioningField], context: InvocationContext, device: torch.device, @@ -203,8 +237,9 @@ def _get_text_embeddings_and_masks( return text_embeddings, text_embeddings_masks + @staticmethod def _preprocess_regional_prompt_mask( - self, mask: Optional[torch.Tensor], target_height: int, target_width: int, dtype: torch.dtype + 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. @@ -228,8 +263,8 @@ def _preprocess_regional_prompt_mask( resized_mask = tf(mask) return resized_mask + @staticmethod def _concat_regional_text_embeddings( - self, text_conditionings: Union[list[BasicConditioningInfo], list[SDXLConditioningInfo]], masks: Optional[list[Optional[torch.Tensor]]], latent_height: int, @@ -279,7 +314,9 @@ def _concat_regional_text_embeddings( ) ) processed_masks.append( - self._preprocess_regional_prompt_mask(mask, latent_height, latent_width, dtype=dtype) + DenoiseLatentsInvocation._preprocess_regional_prompt_mask( + mask, latent_height, latent_width, dtype=dtype + ) ) cur_text_embedding_len += text_embedding_info.embeds.shape[1] @@ -301,60 +338,64 @@ def _concat_regional_text_embeddings( ) return BasicConditioningInfo(embeds=text_embedding), regions + @staticmethod def get_conditioning_data( - self, context: InvocationContext, - unet: UNet2DConditionModel, + 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 self.positive_conditioning and self.negative_conditioning to lists. - cond_list = self.positive_conditioning + # 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 = self.negative_conditioning + uncond_list = negative_conditioning_field if not isinstance(uncond_list, list): uncond_list = [uncond_list] - cond_text_embeddings, cond_text_embedding_masks = self._get_text_embeddings_and_masks( - cond_list, context, unet.device, unet.dtype + 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 = self._get_text_embeddings_and_masks( - uncond_list, context, unet.device, unet.dtype + uncond_text_embeddings, uncond_text_embedding_masks = DenoiseLatentsInvocation._get_text_embeddings_and_masks( + uncond_list, context, device, dtype ) - cond_text_embedding, cond_regions = self._concat_regional_text_embeddings( + 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=unet.dtype, + dtype=dtype, ) - uncond_text_embedding, uncond_regions = self._concat_regional_text_embeddings( + 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=unet.dtype, + dtype=dtype, ) - if isinstance(self.cfg_scale, list): - assert ( - len(self.cfg_scale) == self.steps - ), "cfg_scale (list) must have the same length as the number of steps" + 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=self.cfg_scale, - guidance_rescale_multiplier=self.cfg_rescale_multiplier, + guidance_scale=cfg_scale, + guidance_rescale_multiplier=cfg_rescale_multiplier, ) return conditioning_data + @staticmethod def create_pipeline( - self, unet: UNet2DConditionModel, scheduler: Scheduler, ) -> StableDiffusionGeneratorPipeline: @@ -377,38 +418,39 @@ def __init__(self) -> None: requires_safety_checker=False, ) + @staticmethod def prep_control_data( - self, context: InvocationContext, - control_input: Optional[Union[ControlField, List[ControlField]]], + control_input: ControlField | list[ControlField] | None, latents_shape: List[int], + device: torch.device, exit_stack: ExitStack, do_classifier_free_guidance: bool = True, - ) -> Optional[List[ControlNetData]]: - # Assuming fixed dimensional scaling of LATENT_SCALE_FACTOR. - control_height_resize = latents_shape[2] * LATENT_SCALE_FACTOR - control_width_resize = latents_shape[3] * LATENT_SCALE_FACTOR - if control_input is None: - control_list = None - elif isinstance(control_input, list) and len(control_input) == 0: - control_list = None - elif isinstance(control_input, ControlField): + ) -> 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) and len(control_input) > 0 and isinstance(control_input[0], ControlField): + elif isinstance(control_input, list): control_list = control_input + elif control_input is None: + control_list = [] else: - control_list = None - if control_list is None: + raise ValueError(f"Unexpected control_input type: {type(control_input)}") + + if len(control_list) == 0: return None - # After above handling, any control that is not None should now be of type list[ControlField]. - # FIXME: add checks to skip entry if model or image is None - # and if weight is None, populate with default 1.0? - controlnet_data = [] + # 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_models.append(control_model) 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 @@ -423,13 +465,13 @@ def prep_control_data( height=control_height_resize, # batch_size=batch_size * num_images_per_prompt, # num_images_per_prompt=num_images_per_prompt, - device=control_model.device, + device=device, dtype=control_model.dtype, control_mode=control_info.control_mode, resize_mode=control_info.resize_mode, ) control_item = ControlNetData( - model=control_model, # model object + model=control_model, image_tensor=control_image, weight=control_info.control_weight, begin_step_percent=control_info.begin_step_percent, @@ -444,6 +486,70 @@ def prep_control_data( 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, @@ -454,14 +560,15 @@ def prep_ip_adapter_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) - image_encoder_model_info = context.models.load(single_ip_adapter.image_encoder_model) # `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) for image in single_ipa_image_fields] - with image_encoder_model_info as image_encoder_model: + 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( @@ -501,6 +608,7 @@ def prep_ip_adapter_data( 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, ) ) @@ -511,6 +619,7 @@ def run_t2i_adapters( 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: @@ -526,44 +635,57 @@ def run_t2i_adapters( 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) - t2i_adapter_loaded_model = context.models.load(t2i_adapter_field.t2i_adapter_model) - image = context.images.get_pil(t2i_adapter_field.image.image_name) + 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 t2i_adapter_loaded_model as t2i_adapter_model: + with context.models.load(t2i_adapter_field.t2i_adapter_model) as t2i_adapter_model: total_downscale_factor = t2i_adapter_model.total_downscale_factor - # Resize the T2I-Adapter input image. - # We select the resize dimensions so that after the T2I-Adapter's total_downscale_factor is applied, the - # result will match the latent image's dimensions after max_unet_downscale is applied. - t2i_input_height = latents_shape[2] // max_unet_downscale * total_downscale_factor - t2i_input_width = latents_shape[3] // max_unet_downscale * total_downscale_factor - # Note: We have hard-coded `do_classifier_free_guidance=False`. This is because we only want to prepare # a single image. If CFG is enabled, we will duplicate the resultant tensor after applying the # T2I-Adapter model. # # Note: We re-use the `prepare_control_image(...)` from ControlNet for T2I-Adapter, because it has many # of the same requirements (e.g. preserving binary masks during resize). + + # 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=t2i_input_width, - height=t2i_input_height, + width=control_width_resize, + height=control_height_resize, num_channels=t2i_adapter_model.config["in_channels"], # mypy treats this as a FrozenDict - device=t2i_adapter_model.device, + 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: @@ -583,15 +705,15 @@ def run_t2i_adapters( # original idea by https://github.com/AmericanPresidentJimmyCarter # TODO: research more for second order schedulers timesteps + @staticmethod def init_scheduler( - self, scheduler: Union[Scheduler, ConfigMixin], device: torch.device, steps: int, denoising_start: float, denoising_end: float, seed: int, - ) -> Tuple[int, List[int], int, Dict[str, Any]]: + ) -> 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") @@ -617,7 +739,6 @@ def init_scheduler( init_timestep = timesteps[t_start_idx : t_start_idx + 1] timesteps = timesteps[t_start_idx : t_start_idx + t_end_idx] - num_inference_steps = len(timesteps) // scheduler.order scheduler_step_kwargs: Dict[str, Any] = {} scheduler_step_signature = inspect.signature(scheduler.step) @@ -639,7 +760,7 @@ def init_scheduler( if isinstance(scheduler, TCDScheduler): scheduler_step_kwargs.update({"eta": 1.0}) - return num_inference_steps, timesteps, init_timestep, scheduler_step_kwargs + return timesteps, init_timestep, scheduler_step_kwargs def prep_inpaint_mask( self, context: InvocationContext, latents: torch.Tensor @@ -654,34 +775,201 @@ def prep_inpaint_mask( else: masked_latents = torch.where(mask < 0.5, 0.0, latents) - return 1 - mask, masked_latents, self.denoise_mask.gradient + return mask, masked_latents, self.denoise_mask.gradient - @torch.no_grad() - @SilenceWarnings() # This quenches the NSFW nag from diffusers. - def invoke(self, context: InvocationContext) -> LatentsOutput: - seed = None - noise = None - if self.noise is not None: - noise = context.tensors.load(self.noise.latents_name) - seed = self.noise.seed + @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. - if self.latents is not None: - latents = context.tensors.load(self.latents.latents_name) - if seed is None: - seed = self.latents.seed + 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. - if noise is not None and noise.shape[1:] != latents.shape[1:]: - raise Exception(f"Incompatable 'noise' and 'latents' shapes: {latents.shape=} {noise.shape=}") + 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 Exception("'latents' or 'noise' must be provided!") + raise ValueError("'latents' or 'noise' must be provided!") - if seed is None: + 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. @@ -689,6 +977,7 @@ def invoke(self, context: InvocationContext) -> LatentsOutput: context, self.t2i_adapter, latents.shape, + device=device, do_classifier_free_guidance=True, ) @@ -706,61 +995,72 @@ def invoke(self, context: InvocationContext) -> LatentsOutput: # 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 dispatch_progress() + # 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[LoRAModelRaw, float]]: + 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, LoRAModelRaw) + assert isinstance(lora_info.model, ModelPatchRaw) yield (lora_info.model, lora.weight) del lora_info return - unet_info = context.models.load(self.unet.unet) - assert isinstance(unet_info.model, UNet2DConditionModel) with ( ExitStack() as exit_stack, - unet_info.model_on_device() as (model_state_dict, unet), + context.models.load(self.unet.unet).model_on_device() as (cached_weights, unet), ModelPatcher.apply_freeu(unet, self.unet.freeu_config), - set_seamless(unet, self.unet.seamless_axes), # FIXME + 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. - ModelPatcher.apply_lora_unet( - unet, - loras=_lora_loader(), - model_state_dict=model_state_dict, + 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=unet.device, dtype=unet.dtype) + latents = latents.to(device=device, dtype=unet.dtype) if noise is not None: - noise = noise.to(device=unet.device, dtype=unet.dtype) + noise = noise.to(device=device, dtype=unet.dtype) if mask is not None: - mask = mask.to(device=unet.device, dtype=unet.dtype) + mask = mask.to(device=device, dtype=unet.dtype) if masked_latents is not None: - masked_latents = masked_latents.to(device=unet.device, dtype=unet.dtype) + 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, unet=unet, latent_height=latent_height, latent_width=latent_width + 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, @@ -776,9 +1076,9 @@ def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: dtype=unet.dtype, ) - num_inference_steps, timesteps, init_timestep, scheduler_step_kwargs = self.init_scheduler( + timesteps, init_timestep, scheduler_step_kwargs = self.init_scheduler( scheduler, - device=unet.device, + device=device, steps=self.steps, denoising_start=self.denoising_start, denoising_end=self.denoising_end, @@ -793,8 +1093,7 @@ def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: seed=seed, mask=mask, masked_latents=masked_latents, - gradient_mask=gradient_mask, - num_inference_steps=num_inference_steps, + is_gradient_mask=gradient_mask, scheduler_step_kwargs=scheduler_step_kwargs, conditioning_data=conditioning_data, control_data=controlnet_data, 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 index 987f1b1e406..1092a67ce95 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -435,7 +435,9 @@ def get_faces_list( return all_faces -@invocation("face_off", title="FaceOff", tags=["image", "faceoff", "face", "mask"], category="image", version="1.2.2") +@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""" @@ -514,7 +516,9 @@ def invoke(self, context: InvocationContext) -> FaceOffOutput: return output -@invocation("face_mask_detection", title="FaceMask", tags=["image", "face", "mask"], category="image", version="1.2.2") +@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""" @@ -617,7 +621,11 @@ def invoke(self, context: InvocationContext) -> FaceMaskOutput: @invocation( - "face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.2.2" + "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.""" diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py index 0fa0216f1c7..e53aeb417b2 100644 --- a/invokeai/app/invocations/fields.py +++ b/invokeai/app/invocations/fields.py @@ -6,6 +6,14 @@ 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() @@ -38,18 +46,6 @@ class UIType(str, Enum, metaclass=MetaEnum): used, and the type will be ignored. They are included here for backwards compatibility. """ - # region Model Field Types - MainModel = "MainModelField" - SDXLMainModel = "SDXLMainModelField" - SDXLRefinerModel = "SDXLRefinerModelField" - ONNXModel = "ONNXModelField" - VAEModel = "VAEModelField" - LoRAModel = "LoRAModelField" - ControlNetModel = "ControlNetModelField" - IPAdapterModel = "IPAdapterModelField" - T2IAdapterModel = "T2IAdapterModelField" - # endregion - # region Misc Field Types Scheduler = "SchedulerField" Any = "AnyField" @@ -58,6 +54,7 @@ class UIType(str, Enum, metaclass=MetaEnum): # region Internal Field Types _Collection = "CollectionField" _CollectionItem = "CollectionItemField" + _IsIntermediate = "IsIntermediate" # endregion # region DEPRECATED @@ -95,13 +92,44 @@ class UIType(str, Enum, metaclass=MetaEnum): CollectionItem = "DEPRECATED_CollectionItem" Enum = "DEPRECATED_Enum" WorkflowField = "DEPRECATED_WorkflowField" - IsIntermediate = "DEPRECATED_IsIntermediate" 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): """ @@ -124,16 +152,32 @@ class FieldDescriptions: 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)" @@ -160,6 +204,7 @@ class FieldDescriptions: 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" @@ -170,7 +215,7 @@ class FieldDescriptions: ) num_1 = "The first number" num_2 = "The second number" - mask = "The mask to use for the operation" + 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" @@ -181,6 +226,12 @@ class FieldDescriptions: 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): @@ -195,6 +246,12 @@ class BoardField(BaseModel): 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""" @@ -228,6 +285,85 @@ 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""" @@ -239,6 +375,18 @@ class ConditioningField(BaseModel): ) +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]. @@ -295,8 +443,8 @@ class InputFieldJSONSchemaExtra(BaseModel): """ input: Input - orig_required: bool field_kind: FieldKind + orig_required: bool = True default: Optional[Any] = None orig_default: Optional[Any] = None ui_hidden: bool = False @@ -304,10 +452,16 @@ class InputFieldJSONSchemaExtra(BaseModel): 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, ) @@ -331,7 +485,7 @@ class WithWorkflow: workflow = None def __init_subclass__(cls) -> None: - logger.warn( + logger.warning( f"{cls.__module__.split('.')[0]}.{cls.__name__}: WithWorkflow is deprecated. Use `context.workflow` to access the workflow." ) super().__init_subclass__() @@ -360,16 +514,100 @@ class OutputFieldJSONSchemaExtra(BaseModel): """ field_kind: FieldKind - ui_hidden: bool - ui_type: Optional[UIType] - ui_order: Optional[int] + 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? @@ -393,51 +631,115 @@ def InputField( input: Input = Input.Any, ui_type: Optional[UIType] = None, ui_component: Optional[UIComponent] = None, - ui_hidden: bool = False, + 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) \ + 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. - :param Input input: [Input.Any] 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. + 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. - :param UIType ui_type: [None] 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, model selection fields should render a dropdown UI component to select a model. \ - Internally, there is no difference between SD-1, SD-2 and SDXL model fields, they all use \ - `MainModelField`. So to ensure the base-model-specific UI is rendered, you can use \ - `UIType.SDXLMainModelField` to indicate that the field is an SDXL main model field. + 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. - :param UIComponent ui_component: [None] 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. \ - For this case, you could provide `UIComponent.Textarea`. + 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. - :param bool ui_hidden: [False] Specifies whether or not this field should be hidden in the UI. + 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`. - :param int ui_order: [None] Specifies the order in which this field should be rendered in the UI. + ui_hidden: Specifies whether or not this field should be hidden in the UI. - :param dict[str, str] ui_choice_labels: [None] Specifies the labels to use for the choices in an enum field. + 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, - ui_type=ui_type, - ui_component=ui_component, - ui_hidden=ui_hidden, - ui_order=ui_order, - ui_choice_labels=ui_choice_labels, field_kind=FieldKind.Input, - orig_required=True, ) + 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. @@ -467,7 +769,7 @@ def InputField( if default_factory is not _Unset and default_factory is not None: default = default_factory() - logger.warn('"default_factory" is not supported, calling it now to set "default"') + 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 = { @@ -509,7 +811,7 @@ def InputField( return Field( **provided_args, - json_schema_extra=json_schema_extra_.model_dump(exclude_none=True), + json_schema_extra=json_schema_extra_.model_dump(exclude_unset=True), ) @@ -538,20 +840,20 @@ def OutputField( """ 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) \ + 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. - :param UIType ui_type: [None] 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, model selection fields should render a dropdown UI component to select a model. \ - Internally, there is no difference between SD-1, SD-2 and SDXL model fields, they all use \ - `MainModelField`. So to ensure the base-model-specific UI is rendered, you can use \ - `UIType.SDXLMainModelField` to indicate that the field is an SDXL main model field. + 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. - :param bool ui_hidden: [False] Specifies whether or not this field should be hidden in the UI. \ + ui_hidden: Specifies whether or not this field should be hidden in the UI. - :param int ui_order: [None] Specifies the order in which this field should be rendered 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, @@ -569,9 +871,9 @@ def OutputField( min_length=min_length, max_length=max_length, json_schema_extra=OutputFieldJSONSchemaExtra( - ui_type=ui_type, 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..2b4b53faf72 --- /dev/null +++ b/invokeai/app/invocations/flux2_klein_text_encoder.py @@ -0,0 +1,201 @@ +"""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, + idle_gpu_offloadable=True, +) +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..ac1f5764d78 --- /dev/null +++ b/invokeai/app/invocations/flux_redux.py @@ -0,0 +1,167 @@ +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, + idle_gpu_offloadable=True, +) +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..e3f28e57d72 --- /dev/null +++ b/invokeai/app/invocations/flux_text_encoder.py @@ -0,0 +1,270 @@ +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", + idle_gpu_offloadable=True, +) +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 index 120f8c1ba01..5cfa9c04d01 100644 --- a/invokeai/app/invocations/ideal_size.py +++ b/invokeai/app/invocations/ideal_size.py @@ -6,7 +6,7 @@ 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.config import BaseModelType +from invokeai.backend.model_manager.taxonomy import BaseModelType @invocation_output("ideal_size_output") @@ -19,16 +19,17 @@ class IdealSizeOutput(BaseInvocationOutput): @invocation( "ideal_size", - title="Ideal Size", + title="Ideal Size - SD1.5, SDXL", tags=["latents", "math", "ideal_size"], - version="1.0.3", + 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(default=None, description=FieldDescriptions.unet) + 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 " @@ -41,11 +42,21 @@ def trim_to_multiple_of(self, *args: int, multiple_of: int = LATENT_SCALE_FACTOR def invoke(self, context: InvocationContext) -> IdealSizeOutput: unet_config = context.models.get_config(self.unet.unet.key) aspect = self.width / self.height - dimension: float = 512 - if unet_config.base == BaseModelType.StableDiffusion2: + + if unet_config.base == BaseModelType.StableDiffusion1: + dimension = 512 + elif unet_config.base == BaseModelType.StableDiffusion2: dimension = 768 - elif unet_config.base == BaseModelType.StableDiffusionXL: + 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 diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 65e7ce5e067..18709a25091 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1,13 +1,21 @@ # 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, @@ -15,13 +23,43 @@ WithBoard, WithMetadata, ) -from invokeai.app.invocations.primitives import ImageOutput +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 -from .baseinvocation import BaseInvocation, Classification, invocation + +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") @@ -158,12 +196,12 @@ class ImagePasteInvocation(BaseInvocation, WithMetadata, WithBoard): 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) - image = context.images.get_pil(self.image.image_name) + 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) - mask = ImageOps.invert(mask.convert("L")) + 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) @@ -173,7 +211,11 @@ def invoke(self, context: InvocationContext) -> ImageOutput: new_image = Image.new(mode="RGBA", size=(max_x - min_x, max_y - min_y), color=(0, 0, 0, 0)) new_image.paste(base_image, (abs(min_x), abs(min_y))) - new_image.paste(image, (max(0, self.x), max(0, self.y)), mask=mask) + + # 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 @@ -188,7 +230,7 @@ def invoke(self, context: InvocationContext) -> ImageOutput: "tomask", title="Mask from Alpha", tags=["image", "mask"], - category="image", + category="mask", version="1.2.2", ) class MaskFromAlphaInvocation(BaseInvocation, WithMetadata, WithBoard): @@ -298,14 +340,44 @@ class ImageBlurInvocation(BaseInvocation, WithMetadata, WithBoard): 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) + 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) ) - blur_image = image.filter(blur) + 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 - image_dto = context.images.save(image=blur_image) + # 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) @@ -316,7 +388,6 @@ def invoke(self, context: InvocationContext) -> ImageOutput: tags=["image", "unsharp_mask"], category="image", version="1.2.2", - classification=Classification.Beta, ) class UnsharpMaskInvocation(BaseInvocation, WithMetadata, WithBoard): """Applies an unsharp mask filter to an image""" @@ -335,7 +406,7 @@ def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) mode = image.mode - alpha_channel = image.getchannel("A") if mode == "RGBA" else None + alpha_channel = _extract_alpha_channel(image) image = image.convert("RGB") image_blurred = self.array_from_pil(image.filter(ImageFilter.GaussianBlur(radius=self.radius))) @@ -359,6 +430,53 @@ def invoke(self, context: InvocationContext) -> ImageOutput: ) +@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", @@ -543,11 +661,30 @@ def invoke(self, context: InvocationContext) -> ImageOutput: 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="image", + category="mask", version="1.2.2", ) class MaskEdgeInvocation(BaseInvocation, WithMetadata, WithBoard): @@ -586,7 +723,7 @@ def invoke(self, context: InvocationContext) -> ImageOutput: "mask_combine", title="Combine Masks", tags=["image", "mask", "multiply"], - category="image", + category="mask", version="1.2.2", ) class MaskCombineInvocation(BaseInvocation, WithMetadata, WithBoard): @@ -611,102 +748,104 @@ def invoke(self, context: InvocationContext) -> ImageOutput: title="Color Correct", tags=["image", "color"], category="image", - version="1.2.2", + version="2.0.0", ) class ColorCorrectInvocation(BaseInvocation, WithMetadata, WithBoard): """ - Shifts the colors of a target image to match the reference image, optionally - using a mask to only color-correct certain regions of the target image. + 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. """ - image: ImageField = InputField(description="The image to color-correct") - reference: ImageField = InputField(description="Reference image for color-correction") - mask: Optional[ImageField] = InputField(default=None, description="Mask to use when applying color-correction") - mask_blur_radius: float = InputField(default=8, description="Mask blur radius") + 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: - pil_init_mask = None - if self.mask is not None: - pil_init_mask = context.images.get_pil(self.mask.image_name).convert("L") - - init_image = context.images.get_pil(self.reference.image_name) - - result = context.images.get_pil(self.image.image_name).convert("RGBA") - - # if init_image is None or init_mask is None: - # return result - - # Get the original alpha channel of the mask if there is one. - # Otherwise it is some other black/white image format ('1', 'L' or 'RGB') - # pil_init_mask = ( - # init_mask.getchannel("A") - # if init_mask.mode == "RGBA" - # else init_mask.convert("L") - # ) - pil_init_image = init_image.convert("RGBA") # Add an alpha channel if one doesn't exist - - # Build an image with only visible pixels from source to use as reference for color-matching. - init_rgb_pixels = numpy.asarray(init_image.convert("RGB"), dtype=numpy.uint8) - init_a_pixels = numpy.asarray(pil_init_image.getchannel("A"), dtype=numpy.uint8) - init_mask_pixels = numpy.asarray(pil_init_mask, dtype=numpy.uint8) - - # Get numpy version of result - np_image = numpy.asarray(result.convert("RGB"), dtype=numpy.uint8) - - # Mask and calculate mean and standard deviation - mask_pixels = init_a_pixels * init_mask_pixels > 0 - np_init_rgb_pixels_masked = init_rgb_pixels[mask_pixels, :] - np_image_masked = np_image[mask_pixels, :] - - if np_init_rgb_pixels_masked.size > 0: - init_means = np_init_rgb_pixels_masked.mean(axis=0) - init_std = np_init_rgb_pixels_masked.std(axis=0) - gen_means = np_image_masked.mean(axis=0) - gen_std = np_image_masked.std(axis=0) - - # Color correct - np_matched_result = np_image.copy() - np_matched_result[:, :, :] = ( - ( - ( - (np_matched_result[:, :, :].astype(numpy.float32) - gen_means[None, None, :]) - / gen_std[None, None, :] - ) - * init_std[None, None, :] - + init_means[None, None, :] - ) - .clip(0, 255) - .astype(numpy.uint8) - ) - matched_result = Image.fromarray(np_matched_result, mode="RGB") + # 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: - matched_result = Image.fromarray(np_image, mode="RGB") - - # Blur the mask out (into init image) by specified amount - if self.mask_blur_radius > 0: - nm = numpy.asarray(pil_init_mask, dtype=numpy.uint8) - inverted_nm = 255 - nm - dilation_size = int(round(self.mask_blur_radius) + 20) - dilating_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (dilation_size, dilation_size)) - inverted_dilated_nm = cv2.dilate(inverted_nm, dilating_kernel) - dilated_nm = 255 - inverted_dilated_nm - nmd = cv2.erode( - dilated_nm, - kernel=numpy.ones((3, 3), dtype=numpy.uint8), - iterations=int(self.mask_blur_radius / 2), + # 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] ) - pmd = Image.fromarray(nmd, mode="L") - blurred_init_mask = pmd.filter(ImageFilter.BoxBlur(self.mask_blur_radius)) - else: - blurred_init_mask = pil_init_mask - multiplied_blurred_init_mask = ImageChops.multiply(blurred_init_mask, result.split()[-1]) + # 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") - # Paste original on color-corrected generation (using blurred mask) - matched_result.paste(init_image, (0, 0), mask=multiplied_blurred_init_mask) + # 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 - image_dto = context.images.save(image=matched_result) + # 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) @@ -743,6 +882,47 @@ def invoke(self, context: InvocationContext) -> ImageOutput: 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)", @@ -804,7 +984,7 @@ def invoke(self, context: InvocationContext) -> ImageOutput: "value", ], category="image", - version="1.2.2", + version="1.2.3", ) class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata, WithBoard): """Add or subtract a value from a specific color channel of an image.""" @@ -814,18 +994,22 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata, WithBoard): offset: int = InputField(default=0, ge=-255, le=255, description="The amount to adjust the channel by") def invoke(self, context: InvocationContext) -> ImageOutput: - pil_image = context.images.get_pil(self.image.image_name) + 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(pil_image.convert(mode)).astype(int) + converted_image = numpy.array(image.convert(mode)).astype(int) image_channel = converted_image[:, :, channel_number] - # Adjust the value, clipping to 0..255 - image_channel = numpy.clip(image_channel + self.offset, 0, 255) + 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 @@ -833,6 +1017,10 @@ def invoke(self, context: InvocationContext) -> ImageOutput: # 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) @@ -860,7 +1048,7 @@ def invoke(self, context: InvocationContext) -> ImageOutput: "value", ], category="image", - version="1.2.2", + version="1.2.3", ) class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata, WithBoard): """Scale a specific color channel of an image.""" @@ -871,14 +1059,14 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata, WithBoard): invert_channel: bool = InputField(default=False, description="Invert the channel after scaling") def invoke(self, context: InvocationContext) -> ImageOutput: - pil_image = context.images.get_pil(self.image.image_name) + 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(pil_image.convert(mode)).astype(float) + converted_image = numpy.array(image.convert(mode)).astype(float) image_channel = converted_image[:, :, channel_number] # Adjust the value, clipping to 0..255 @@ -894,6 +1082,10 @@ def invoke(self, context: InvocationContext) -> ImageOutput: # 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) @@ -903,7 +1095,7 @@ def invoke(self, context: InvocationContext) -> ImageOutput: "save_image", title="Save Image", tags=["primitives", "image"], - category="primitives", + category="image", version="1.2.2", use_cache=False, ) @@ -920,18 +1112,114 @@ def invoke(self, context: InvocationContext) -> ImageOutput: 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="image", - version="1.0.0", + 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(default=None, description="The target image") + target_image: ImageField = InputField(description="The target image") mask: ImageField = InputField( description="The mask to use when pasting", ) @@ -959,10 +1247,10 @@ def invoke(self, context: InvocationContext) -> ImageOutput: @invocation( "mask_from_id", - title="Mask from ID", + title="Mask from Segmented Image", tags=["image", "mask", "id"], - category="image", - version="1.0.0", + category="mask", + version="1.0.1", ) class MaskFromIDInvocation(BaseInvocation, WithMetadata, WithBoard): """Generate a mask for a particular color in an ID Map""" @@ -972,39 +1260,421 @@ class MaskFromIDInvocation(BaseInvocation, WithMetadata, WithBoard): threshold: int = InputField(default=100, description="Threshold for color detection") invert: bool = InputField(default=False, description="Whether or not to invert the mask") - def rgba_to_hex(self, rgba_color: tuple[int, int, int, int]): - r, g, b, a = rgba_color - hex_code = "#{:02X}{:02X}{:02X}{:02X}".format(r, g, b, int(a * 255)) - return hex_code - - def id_to_mask(self, id_mask: Image.Image, color: tuple[int, int, int, int], threshold: int = 100): - if id_mask.mode != "RGB": - id_mask = id_mask.convert("RGB") + def invoke(self, context: InvocationContext) -> ImageOutput: + image = context.images.get_pil(self.image.image_name, mode="RGBA") - # Can directly just use the tuple but I'll leave this rgba_to_hex here - # incase anyone prefers using hex codes directly instead of the color picker - hex_color_str = self.rgba_to_hex(color) - rgb_color = numpy.array([int(hex_color_str[i : i + 2], 16) for i in (1, 3, 5)]) + 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(id_mask - rgb_color, axis=-1) + 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 < threshold).astype(numpy.uint8) * 255 + binary_mask = (color_distance < self.threshold).astype(numpy.uint8) * 255 # Convert the mask back to PIL binary_mask_pil = Image.fromarray(binary_mask) - return binary_mask_pil + 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) - mask = self.id_to_mask(image, self.color.tuple(), self.threshold) + bounding_box = self.bounding_box.tuple() if self.bounding_box is not None else image.getbbox() - if self.invert: - mask = ImageOps.invert(mask) + 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. - image_dto = context.images.save(image=mask, image_category=ImageCategory.MASK) + 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 index 06de530154e..8dc5ceba0b0 100644 --- a/invokeai/app/invocations/image_to_latents.py +++ b/invokeai/app/invocations/image_to_latents.py @@ -1,4 +1,6 @@ +from contextlib import nullcontext from functools import singledispatchmethod +from typing import Literal import einops import torch @@ -12,26 +14,37 @@ from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation -from invokeai.app.invocations.constants import DEFAULT_PRECISION +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 VAEField +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 import LoadedModel +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", + title="Image to Latents - SD1.5, SDXL", tags=["latents", "image", "vae", "i2l"], category="latents", - version="1.0.2", + version="1.2.0", ) class ImageToLatentsInvocation(BaseInvocation): """Encodes an image into latents.""" @@ -44,12 +57,34 @@ class ImageToLatentsInvocation(BaseInvocation): input=Input.Connection, ) tiled: bool = InputField(default=False, description=FieldDescriptions.tiled) - fp32: bool = InputField(default=DEFAULT_PRECISION == torch.float32, description=FieldDescriptions.fp32) + # 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).", + ) - @staticmethod - def vae_encode(vae_info: LoadedModel, upcast: bool, tiled: bool, image_tensor: torch.Tensor) -> torch.Tensor: - with vae_info as vae: - assert isinstance(vae, torch.nn.Module) + @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) @@ -81,9 +116,18 @@ def vae_encode(vae_info: LoadedModel, upcast: bool, tiled: bool, image_tensor: t 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=vae.device, dtype=vae.dtype) - with torch.inference_mode(): + 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 @@ -96,12 +140,25 @@ 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") - latents = self.vae_encode(vae_info, self.fp32, self.tiled, image_tensor) + 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) diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py index 7e1a2ee322f..dd7b2c87b0a 100644 --- a/invokeai/app/invocations/infill.py +++ b/invokeai/app/invocations/infill.py @@ -3,7 +3,9 @@ from PIL import Image -from invokeai.app.invocations.fields import ColorField, ImageField +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 @@ -14,10 +16,6 @@ from invokeai.backend.image_util.infill_methods.tile import infill_tile from invokeai.backend.util.logging import InvokeAILogger -from .baseinvocation import BaseInvocation, invocation -from .fields import InputField, WithBoard, WithMetadata -from .image import PIL_RESAMPLING_MAP, PIL_RESAMPLING_MODES - logger = InvokeAILogger.get_logger() @@ -129,13 +127,16 @@ def infill(self, image: Image.Image): 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="https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt", + source=LAMA_MODEL_URL, loader=LaMA.load_jit_model, ) as model: lama = LaMA(model) diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py index de40879eef8..711f910d587 100644 --- a/invokeai/app/invocations/ip_adapter.py +++ b/invokeai/app/invocations/ip_adapter.py @@ -5,18 +5,24 @@ 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, UIType +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.config import ( - AnyModelConfig, - BaseModelType, - IPAdapterCheckpointConfig, - IPAdapterInvokeAIConfig, - ModelType, +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): @@ -25,6 +31,7 @@ class IPAdapterField(BaseModel): 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)" ) @@ -55,10 +62,20 @@ class IPAdapterOutput(BaseInvocationOutput): ip_adapter: IPAdapterField = OutputField(description=FieldDescriptions.ip_adapter, title="IP-Adapter") -CLIP_VISION_MODEL_MAP = {"ViT-H": "ip_adapter_sd_image_encoder", "ViT-G": "ip_adapter_sdxl_image_encoder"} +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", tags=["ip_adapter", "control"], category="ip_adapter", version="1.4.1") +@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.""" @@ -68,9 +85,10 @@ class IPAdapterInvocation(BaseInvocation): description="The IP-Adapter model.", title="IP-Adapter Model", ui_order=-1, - ui_type=UIType.IPAdapterModel, + ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusionXL], + ui_model_type=ModelType.IPAdapter, ) - clip_vision_model: Literal["ViT-H", "ViT-G"] = InputField( + 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, @@ -78,7 +96,7 @@ class IPAdapterInvocation(BaseInvocation): weight: Union[float, List[float]] = InputField( default=1, description="The weight given to the IP-Adapter", title="Weight" ) - method: Literal["full", "style", "composition"] = InputField( + 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( @@ -105,15 +123,17 @@ def validate_begin_end_step_percent(self) -> 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, (IPAdapterInvokeAIConfig, IPAdapterCheckpointConfig)) + assert isinstance(ip_adapter_info, (IPAdapter_InvokeAI_Config_Base, IPAdapter_Checkpoint_Config_Base)) - if isinstance(ip_adapter_info, IPAdapterInvokeAIConfig): + 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_model_name = CLIP_VISION_MODEL_MAP[self.clip_vision_model] + 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_image_encoder(context, image_encoder_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": @@ -129,6 +149,38 @@ def invoke(self, context: InvocationContext) -> IPAdapterOutput: 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: @@ -144,10 +196,14 @@ def invoke(self, context: InvocationContext) -> IPAdapterOutput: begin_step_percent=self.begin_step_percent, end_step_percent=self.end_step_percent, mask=self.mask, + method=self.method, ), ) - def _get_image_encoder(self, context: InvocationContext, image_encoder_model_name: str) -> AnyModelConfig: + @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 ) @@ -159,7 +215,11 @@ def _get_image_encoder(self, context: InvocationContext, image_encoder_model_nam ) installer = context._services.model_manager.install - job = installer.heuristic_import(f"InvokeAI/{image_encoder_model_name}") + # 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 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 index 202e8bfa1bc..608485a078b 100644 --- a/invokeai/app/invocations/latents_to_image.py +++ b/invokeai/app/invocations/latents_to_image.py @@ -1,17 +1,12 @@ +from contextlib import nullcontext + import torch from diffusers.image_processor import VaeImageProcessor -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 diffusers.models.unets.unet_2d_condition import UNet2DConditionModel from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation -from invokeai.app.invocations.constants import DEFAULT_PRECISION +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR from invokeai.app.invocations.fields import ( FieldDescriptions, Input, @@ -23,16 +18,18 @@ 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 import set_seamless +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", + title="Latents to Image - SD1.5, SDXL", tags=["latents", "image", "vae", "l2i"], category="latents", - version="1.2.2", + version="1.3.2", ) class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard): """Generates an image from latents.""" @@ -46,51 +43,59 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard): input=Input.Connection, ) tiled: bool = InputField(default=False, description=FieldDescriptions.tiled) - fp32: bool = InputField(default=DEFAULT_PRECISION == torch.float32, description=FieldDescriptions.fp32) + # 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, (UNet2DConditionModel, AutoencoderKL, AutoencoderTiny)) - with set_seamless(vae_info.model, self.vae.seamless_axes), vae_info as vae: - assert isinstance(vae, torch.nn.Module) - latents = latents.to(vae.device) + 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) - - 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(latents.dtype) - vae.decoder.conv_in.to(latents.dtype) - vae.decoder.mid_block.to(latents.dtype) - else: - latents = latents.float() - + latents = latents.float() else: vae.to(dtype=torch.float16) latents = latents.half() - if self.tiled or context.config.get().force_tiled_decode: + 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(): + with torch.inference_mode(), tiling_context: # copied from diffusers pipeline latents = latents / vae.config.scaling_factor image = vae.decode(latents, return_dict=False)[0] 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 index 6f54660847a..49749f43b64 100644 --- a/invokeai/app/invocations/mask.py +++ b/invokeai/app/invocations/mask.py @@ -1,16 +1,30 @@ import numpy as np import torch +from PIL import Image -from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, InvocationContext, invocation -from invokeai.app.invocations.fields import ImageField, InputField, TensorField, WithMetadata -from invokeai.app.invocations.primitives import MaskOutput +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="conditioning", + category="mask", version="1.0.1", ) class RectangleMaskInvocation(BaseInvocation, WithMetadata): @@ -41,9 +55,8 @@ def invoke(self, context: InvocationContext) -> MaskOutput: "alpha_mask_to_tensor", title="Alpha Mask to Tensor", tags=["conditioning"], - category="conditioning", + category="mask", version="1.0.0", - classification=Classification.Beta, ) class AlphaMaskToTensorInvocation(BaseInvocation): """Convert a mask image to a tensor. Opaque regions are 1 and transparent regions are 0.""" @@ -52,7 +65,7 @@ class AlphaMaskToTensorInvocation(BaseInvocation): 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) + 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) @@ -70,9 +83,8 @@ def invoke(self, context: InvocationContext) -> MaskOutput: "invert_tensor_mask", title="Invert Tensor Mask", tags=["conditioning"], - category="conditioning", - version="1.0.0", - classification=Classification.Beta, + category="mask", + version="1.1.0", ) class InvertTensorMaskInvocation(BaseInvocation): """Inverts a tensor mask.""" @@ -81,6 +93,15 @@ class InvertTensorMaskInvocation(BaseInvocation): 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( @@ -94,7 +115,7 @@ def invoke(self, context: InvocationContext) -> MaskOutput: "image_mask_to_tensor", title="Image Mask to Tensor", tags=["conditioning"], - category="conditioning", + category="mask", version="1.0.0", ) class ImageMaskToTensorInvocation(BaseInvocation, WithMetadata): @@ -118,3 +139,128 @@ def invoke(self, context: InvocationContext) -> MaskOutput: 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 index dad000d411e..5d3988031ba 100644 --- a/invokeai/app/invocations/math.py +++ b/invokeai/app/invocations/math.py @@ -5,12 +5,11 @@ 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 -from .baseinvocation import BaseInvocation, invocation - @invocation("add", title="Add Integers", tags=["math", "add"], category="math", version="1.0.1") class AddInvocation(BaseInvocation): 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 index 9c7264a9bbb..da24d8802bb 100644 --- a/invokeai/app/invocations/metadata.py +++ b/invokeai/app/invocations/metadata.py @@ -2,7 +2,13 @@ from pydantic import BaseModel, ConfigDict, Field -from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output +from invokeai.app.invocations.baseinvocation import ( + BaseInvocation, + BaseInvocationOutput, + Classification, + invocation, + invocation_output, +) from invokeai.app.invocations.fields import ( FieldDescriptions, ImageField, @@ -12,10 +18,10 @@ 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 ...version import __version__ +from invokeai.version.invokeai_version import __version__ class MetadataItemField(BaseModel): @@ -35,8 +41,10 @@ class IPAdapterMetadataField(BaseModel): image: ImageField = Field(description="The IP-Adapter image prompt.") ip_adapter_model: ModelIdentifierField = Field(description="The IP-Adapter model.") - clip_vision_model: Literal["ViT-H", "ViT-G"] = Field(description="The CLIP Vision model") - method: Literal["full", "style", "composition"] = Field(description="Method to apply IP Weights with") + 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)") @@ -130,13 +138,55 @@ def invoke(self, context: InvocationContext) -> MetadataOutput: GENERATION_MODES = Literal[ - "txt2img", "img2img", "inpaint", "outpaint", "sdxl_txt2img", "sdxl_img2img", "sdxl_inpaint", "sdxl_outpaint" + "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.0.0") +@invocation( + "core_metadata", + title="Core Metadata", + tags=["metadata"], + category="metadata", + version="2.1.0", + classification=Classification.Internal, +) class CoreMetadataInvocation(BaseInvocation): - """Collects core generation metadata into a MetadataField""" + """Used internally by Invoke to collect metadata for generations.""" generation_mode: Optional[GENERATION_MODES] = InputField( default=None, @@ -183,6 +233,10 @@ class CoreMetadataInvocation(BaseInvocation): 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( @@ -248,3 +302,34 @@ def invoke(self, context: InvocationContext) -> MetadataOutput: 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 index 94a6136fcb9..0c96cdb1d9d 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -3,18 +3,17 @@ from pydantic import BaseModel, Field -from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType -from invokeai.app.services.shared.invocation_context import InvocationContext -from invokeai.app.shared.models import FreeUConfig -from invokeai.backend.model_manager.config import AnyModelConfig, BaseModelType, ModelType, SubModelType - -from .baseinvocation import ( +from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Classification, 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): @@ -23,8 +22,9 @@ class ModelIdentifierField(BaseModel): 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: Optional[SubModelType] = Field( - description="The submodel to load, if this is a main model", default=None + submodel_type: SubModelType | None = Field( + description="The submodel to load, if this is a main model", + default=None, ) @classmethod @@ -61,11 +61,46 @@ class CLIPField(BaseModel): 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.""" @@ -103,11 +138,10 @@ class ModelIdentifierOutput(BaseInvocationOutput): @invocation( "model_identifier", - title="Model identifier", + title="Any Model", tags=["model"], category="model", - version="1.0.0", - classification=Classification.Prototype, + 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 @@ -125,15 +159,19 @@ def invoke(self, context: InvocationContext) -> ModelIdentifierOutput: @invocation( "main_model_loader", - title="Main Model", + title="Main Model - SD1.5, SD2", tags=["model"], category="model", - version="1.0.3", + version="1.0.4", ) class MainModelLoaderInvocation(BaseInvocation): """Loads a main model, outputting its submodels.""" - model: ModelIdentifierField = InputField(description=FieldDescriptions.main_model, ui_type=UIType.MainModel) + 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: @@ -162,12 +200,15 @@ class LoRALoaderOutput(BaseInvocationOutput): clip: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP") -@invocation("lora_loader", title="LoRA", tags=["model"], category="model", version="1.0.3") +@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_type=UIType.LoRAModel + 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( @@ -187,7 +228,7 @@ def invoke(self, context: InvocationContext) -> LoRALoaderOutput: lora_key = self.lora.key if not context.models.exists(lora_key): - raise Exception(f"Unkown lora: {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') @@ -225,12 +266,14 @@ class LoRASelectorOutput(BaseInvocationOutput): lora: LoRAField = OutputField(description="LoRA model and weight", title="LoRA") -@invocation("lora_selector", title="LoRA Selector", tags=["model"], category="model", version="1.0.1") +@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_type=UIType.LoRAModel + description=FieldDescriptions.lora_model, + title="LoRA", + ui_model_type=ModelType.LoRA, ) weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight) @@ -238,12 +281,14 @@ def invoke(self, context: InvocationContext) -> LoRASelectorOutput: return LoRASelectorOutput(lora=LoRAField(lora=self.lora, weight=self.weight)) -@invocation("lora_collection_loader", title="LoRA Collection Loader", tags=["model"], category="model", version="1.0.0") +@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: LoRAField | list[LoRAField] = InputField( - description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs" + 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, @@ -263,7 +308,14 @@ def invoke(self, context: InvocationContext) -> 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 @@ -274,14 +326,10 @@ def invoke(self, context: InvocationContext) -> LoRALoaderOutput: added_loras.append(lora.lora.key) - if self.unet is not None: - if output.unet is None: - output.unet = self.unet.model_copy(deep=True) + if self.unet is not None and output.unet is not None: output.unet.loras.append(lora) - if self.clip is not None: - if output.clip is None: - output.clip = self.clip.model_copy(deep=True) + if self.clip is not None and output.clip is not None: output.clip.loras.append(lora) return output @@ -298,16 +346,19 @@ class SDXLLoRALoaderOutput(BaseInvocationOutput): @invocation( "sdxl_lora_loader", - title="SDXL LoRA", + title="Apply LoRA - SDXL", tags=["lora", "model"], category="model", - version="1.0.3", + 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_type=UIType.LoRAModel + 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( @@ -378,16 +429,16 @@ def invoke(self, context: InvocationContext) -> SDXLLoRALoaderOutput: @invocation( "sdxl_lora_collection_loader", - title="SDXL LoRA Collection Loader", + title="Apply LoRA Collection - SDXL", tags=["model"], category="model", - version="1.0.0", + version="1.1.2", ) class SDXLLoRACollectionLoader(BaseInvocation): """Applies a collection of SDXL LoRAs to the provided UNet and CLIP models.""" - loras: LoRAField | list[LoRAField] = InputField( - description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs" + 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, @@ -413,7 +464,18 @@ def invoke(self, context: InvocationContext) -> 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 @@ -424,37 +486,47 @@ def invoke(self, context: InvocationContext) -> SDXLLoRALoaderOutput: added_loras.append(lora.lora.key) - if self.unet is not None: - if output.unet is None: - output.unet = self.unet.model_copy(deep=True) + if self.unet is not None and output.unet is not None: output.unet.loras.append(lora) - if self.clip is not None: - if output.clip is None: - output.clip = self.clip.model_copy(deep=True) + if self.clip is not None and output.clip is not None: output.clip.loras.append(lora) - if self.clip2 is not None: - if output.clip2 is None: - output.clip2 = self.clip2.model_copy(deep=True) + if self.clip2 is not None and output.clip2 is not None: output.clip2.loras.append(lora) return output -@invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.3") +@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_type=UIType.VAEModel + 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"Unkown vae: {key}!") + raise Exception(f"Unknown vae: {key}!") return VAEOutput(vae=VAEField(vae=self.vae_model)) @@ -469,10 +541,10 @@ class SeamlessModeOutput(BaseInvocationOutput): @invocation( "seamless", - title="Seamless", + title="Apply Seamless - SD1.5, SDXL", tags=["seamless", "model"], category="model", - version="1.0.1", + version="1.0.2", ) class SeamlessModeInvocation(BaseInvocation): """Applies the seamless transformation to the Model UNet and VAE.""" @@ -512,7 +584,7 @@ def invoke(self, context: InvocationContext) -> SeamlessModeOutput: return SeamlessModeOutput(unet=unet, vae=vae) -@invocation("freeu", title="FreeU", tags=["freeu"], category="unet", version="1.0.1") +@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): diff --git a/invokeai/app/invocations/noise.py b/invokeai/app/invocations/noise.py index 931e6391063..cfac3f112a9 100644 --- a/invokeai/app/invocations/noise.py +++ b/invokeai/app/invocations/noise.py @@ -1,62 +1,16 @@ -# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) & the InvokeAI Team - - 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 ...backend.util.devices import TorchDevice -from .baseinvocation import ( - BaseInvocation, - BaseInvocationOutput, - invocation, - invocation_output, -) - -""" -Utilities -""" - - -def get_noise( - width: int, - height: int, - device: torch.device, - seed: int = 0, - latent_channels: int = 4, - downsampling_factor: int = 8, - use_cpu: bool = True, - perlin: float = 0.0, -): - """Generate noise for a given image size.""" - noise_device_type = "cpu" if use_cpu else device.type - - # limit noise to only the diffusion image channels, not the mask channels - input_channels = min(latent_channels, 4) - generator = torch.Generator(device=noise_device_type).manual_seed(seed) - - noise_tensor = torch.randn( - [ - 1, - input_channels, - height // downsampling_factor, - width // downsampling_factor, - ], - dtype=TorchDevice.choose_torch_dtype(device=device), - device=noise_device_type, - generator=generator, - ).to("cpu") - - return noise_tensor - - -""" -Nodes -""" +from invokeai.backend.util.devices import TorchDevice @invocation_output("noise_output") @@ -71,20 +25,22 @@ class NoiseOutput(BaseInvocationOutput): def build(cls, latents_name: str, latents: torch.Tensor, seed: int) -> "NoiseOutput": return cls( noise=LatentsField(latents_name=latents_name, seed=seed), - width=latents.size()[3] * LATENT_SCALE_FACTOR, - height=latents.size()[2] * LATENT_SCALE_FACTOR, + width=latents.shape[-1] * LATENT_SCALE_FACTOR, + height=latents.shape[-2] * LATENT_SCALE_FACTOR, ) @invocation( "noise", - title="Noise", + title="Create Latent Noise", tags=["latents", "noise"], category="latents", - version="1.0.2", + version="1.1.0", ) class NoiseInvocation(BaseInvocation): - """Generates latent noise.""" + """Generates latent noise for supported denoiser architectures.""" + + noise_type: LatentNoiseType = InputField(default="SD", description="Architecture-specific noise type.") seed: int = InputField( default=0, @@ -115,11 +71,13 @@ def modulo_seed(cls, v): return v % (SEED_MAX + 1) def invoke(self, context: InvocationContext) -> NoiseOutput: - noise = get_noise( + 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) 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 index 0e590f4e2bd..ed4318d95d1 100644 --- a/invokeai/app/invocations/param_easing.py +++ b/invokeai/app/invocations/param_easing.py @@ -1,50 +1,10 @@ -import io -from typing import Literal, Optional - -import matplotlib.pyplot as plt import numpy as np -import PIL.Image -from easing_functions import ( - BackEaseIn, - BackEaseInOut, - BackEaseOut, - BounceEaseIn, - BounceEaseInOut, - BounceEaseOut, - CircularEaseIn, - CircularEaseInOut, - CircularEaseOut, - CubicEaseIn, - CubicEaseInOut, - CubicEaseOut, - ElasticEaseIn, - ElasticEaseInOut, - ElasticEaseOut, - ExponentialEaseIn, - ExponentialEaseInOut, - ExponentialEaseOut, - LinearInOut, - QuadEaseIn, - QuadEaseInOut, - QuadEaseOut, - QuarticEaseIn, - QuarticEaseInOut, - QuarticEaseOut, - QuinticEaseIn, - QuinticEaseInOut, - QuinticEaseOut, - SineEaseIn, - SineEaseInOut, - SineEaseOut, -) -from matplotlib.ticker import MaxNLocator +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 -from .baseinvocation import BaseInvocation, invocation -from .fields import InputField - @invocation( "float_range", @@ -66,191 +26,3 @@ class FloatLinearRangeInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> FloatCollectionOutput: param_list = list(np.linspace(self.start, self.stop, self.steps)) return FloatCollectionOutput(collection=param_list) - - -EASING_FUNCTIONS_MAP = { - "Linear": LinearInOut, - "QuadIn": QuadEaseIn, - "QuadOut": QuadEaseOut, - "QuadInOut": QuadEaseInOut, - "CubicIn": CubicEaseIn, - "CubicOut": CubicEaseOut, - "CubicInOut": CubicEaseInOut, - "QuarticIn": QuarticEaseIn, - "QuarticOut": QuarticEaseOut, - "QuarticInOut": QuarticEaseInOut, - "QuinticIn": QuinticEaseIn, - "QuinticOut": QuinticEaseOut, - "QuinticInOut": QuinticEaseInOut, - "SineIn": SineEaseIn, - "SineOut": SineEaseOut, - "SineInOut": SineEaseInOut, - "CircularIn": CircularEaseIn, - "CircularOut": CircularEaseOut, - "CircularInOut": CircularEaseInOut, - "ExponentialIn": ExponentialEaseIn, - "ExponentialOut": ExponentialEaseOut, - "ExponentialInOut": ExponentialEaseInOut, - "ElasticIn": ElasticEaseIn, - "ElasticOut": ElasticEaseOut, - "ElasticInOut": ElasticEaseInOut, - "BackIn": BackEaseIn, - "BackOut": BackEaseOut, - "BackInOut": BackEaseInOut, - "BounceIn": BounceEaseIn, - "BounceOut": BounceEaseOut, - "BounceInOut": BounceEaseInOut, -} - -EASING_FUNCTION_KEYS = Literal[tuple(EASING_FUNCTIONS_MAP.keys())] - - -# actually I think for now could just use CollectionOutput (which is list[Any] -@invocation( - "step_param_easing", - title="Step Param Easing", - tags=["step", "easing"], - category="step", - version="1.0.2", -) -class StepParamEasingInvocation(BaseInvocation): - """Experimental per-step parameter easing for denoising steps""" - - easing: EASING_FUNCTION_KEYS = InputField(default="Linear", description="The easing function to use") - num_steps: int = InputField(default=20, description="number of denoising steps") - start_value: float = InputField(default=0.0, description="easing starting value") - end_value: float = InputField(default=1.0, description="easing ending value") - start_step_percent: float = InputField(default=0.0, description="fraction of steps at which to start easing") - end_step_percent: float = InputField(default=1.0, description="fraction of steps after which to end easing") - # if None, then start_value is used prior to easing start - pre_start_value: Optional[float] = InputField(default=None, description="value before easing start") - # if None, then end value is used prior to easing end - post_end_value: Optional[float] = InputField(default=None, description="value after easing end") - mirror: bool = InputField(default=False, description="include mirror of easing function") - # FIXME: add alt_mirror option (alternative to default or mirror), or remove entirely - # alt_mirror: bool = InputField(default=False, description="alternative mirroring by dual easing") - show_easing_plot: bool = InputField(default=False, description="show easing plot") - - def invoke(self, context: InvocationContext) -> FloatCollectionOutput: - log_diagnostics = False - # convert from start_step_percent to nearest step <= (steps * start_step_percent) - # start_step = int(np.floor(self.num_steps * self.start_step_percent)) - start_step = int(np.round(self.num_steps * self.start_step_percent)) - # convert from end_step_percent to nearest step >= (steps * end_step_percent) - # end_step = int(np.ceil((self.num_steps - 1) * self.end_step_percent)) - end_step = int(np.round((self.num_steps - 1) * self.end_step_percent)) - - # end_step = int(np.ceil(self.num_steps * self.end_step_percent)) - num_easing_steps = end_step - start_step + 1 - - # num_presteps = max(start_step - 1, 0) - num_presteps = start_step - num_poststeps = self.num_steps - (num_presteps + num_easing_steps) - prelist = list(num_presteps * [self.pre_start_value]) - postlist = list(num_poststeps * [self.post_end_value]) - - if log_diagnostics: - context.logger.debug("start_step: " + str(start_step)) - context.logger.debug("end_step: " + str(end_step)) - context.logger.debug("num_easing_steps: " + str(num_easing_steps)) - context.logger.debug("num_presteps: " + str(num_presteps)) - context.logger.debug("num_poststeps: " + str(num_poststeps)) - context.logger.debug("prelist size: " + str(len(prelist))) - context.logger.debug("postlist size: " + str(len(postlist))) - context.logger.debug("prelist: " + str(prelist)) - context.logger.debug("postlist: " + str(postlist)) - - easing_class = EASING_FUNCTIONS_MAP[self.easing] - if log_diagnostics: - context.logger.debug("easing class: " + str(easing_class)) - easing_list = [] - if self.mirror: # "expected" mirroring - # if number of steps is even, squeeze duration down to (number_of_steps)/2 - # and create reverse copy of list to append - # if number of steps is odd, squeeze duration down to ceil(number_of_steps/2) - # and create reverse copy of list[1:end-1] - # but if even then number_of_steps/2 === ceil(number_of_steps/2), so can just use ceil always - - base_easing_duration = int(np.ceil(num_easing_steps / 2.0)) - if log_diagnostics: - context.logger.debug("base easing duration: " + str(base_easing_duration)) - even_num_steps = num_easing_steps % 2 == 0 # even number of steps - easing_function = easing_class( - start=self.start_value, - end=self.end_value, - duration=base_easing_duration - 1, - ) - base_easing_vals = [] - for step_index in range(base_easing_duration): - easing_val = easing_function.ease(step_index) - base_easing_vals.append(easing_val) - if log_diagnostics: - context.logger.debug("step_index: " + str(step_index) + ", easing_val: " + str(easing_val)) - if even_num_steps: - mirror_easing_vals = list(reversed(base_easing_vals)) - else: - mirror_easing_vals = list(reversed(base_easing_vals[0:-1])) - if log_diagnostics: - context.logger.debug("base easing vals: " + str(base_easing_vals)) - context.logger.debug("mirror easing vals: " + str(mirror_easing_vals)) - easing_list = base_easing_vals + mirror_easing_vals - - # FIXME: add alt_mirror option (alternative to default or mirror), or remove entirely - # elif self.alt_mirror: # function mirroring (unintuitive behavior (at least to me)) - # # half_ease_duration = round(num_easing_steps - 1 / 2) - # half_ease_duration = round((num_easing_steps - 1) / 2) - # easing_function = easing_class(start=self.start_value, - # end=self.end_value, - # duration=half_ease_duration, - # ) - # - # mirror_function = easing_class(start=self.end_value, - # end=self.start_value, - # duration=half_ease_duration, - # ) - # for step_index in range(num_easing_steps): - # if step_index <= half_ease_duration: - # step_val = easing_function.ease(step_index) - # else: - # step_val = mirror_function.ease(step_index - half_ease_duration) - # easing_list.append(step_val) - # if log_diagnostics: logger.debug(step_index, step_val) - # - - else: # no mirroring (default) - easing_function = easing_class( - start=self.start_value, - end=self.end_value, - duration=num_easing_steps - 1, - ) - for step_index in range(num_easing_steps): - step_val = easing_function.ease(step_index) - easing_list.append(step_val) - if log_diagnostics: - context.logger.debug("step_index: " + str(step_index) + ", easing_val: " + str(step_val)) - - if log_diagnostics: - context.logger.debug("prelist size: " + str(len(prelist))) - context.logger.debug("easing_list size: " + str(len(easing_list))) - context.logger.debug("postlist size: " + str(len(postlist))) - - param_list = prelist + easing_list + postlist - - if self.show_easing_plot: - plt.figure() - plt.xlabel("Step") - plt.ylabel("Param Value") - plt.title("Per-Step Values Based On Easing: " + self.easing) - plt.bar(range(len(param_list)), param_list) - # plt.plot(param_list) - ax = plt.gca() - ax.xaxis.set_major_locator(MaxNLocator(integer=True)) - buf = io.BytesIO() - plt.savefig(buf, format="png") - buf.seek(0) - im = PIL.Image.open(buf) - im.show() - buf.close() - - # output array of size steps, each entry list[i] is param value for step i - 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 index 28f72fb377a..7ec6c3dc149 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -4,30 +4,36 @@ 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 -from .baseinvocation import ( - BaseInvocation, - BaseInvocationOutput, - invocation, - invocation_output, -) - """ Primitives: Boolean, Integer, Float, String, Image, Latents, Conditioning, Color - primitive nodes @@ -263,13 +269,9 @@ class ImageInvocation(BaseInvocation): image: ImageField = InputField(description="The image to load") def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.images.get_pil(self.image.image_name) + image_dto = context.images.get_dto(self.image.image_name) - return ImageOutput( - image=ImageField(image_name=self.image.image_name), - width=image.width, - height=image.height, - ) + return ImageOutput.build(image_dto=image_dto) @invocation( @@ -414,11 +416,87 @@ def invoke(self, context: InvocationContext) -> ColorOutput: 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""" @@ -475,3 +553,42 @@ def invoke(self, context: InvocationContext) -> ConditioningCollectionOutput: # 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 index 64a06d2f184..48eec0ac0ef 100644 --- a/invokeai/app/invocations/prompt.py +++ b/invokeai/app/invocations/prompt.py @@ -5,12 +5,11 @@ 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 -from .baseinvocation import BaseInvocation, invocation -from .fields import InputField, UIComponent - @invocation( "dynamic_prompt", 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..cac536f00c9 --- /dev/null +++ b/invokeai/app/invocations/qwen_image_image_to_latents.py @@ -0,0 +1,102 @@ +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 +from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_qwen_image + + +@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: + # Reserve working memory for the encode so the cache offloads any large resident model first; + # otherwise the encode's activations OOM (the VAE weights themselves are tiny). + estimated_working_memory = estimate_vae_working_memory_qwen_image("encode", image_tensor, vae_info.model) + with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) 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..c418fe43cbe --- /dev/null +++ b/invokeai/app/invocations/qwen_image_latents_to_image.py @@ -0,0 +1,97 @@ +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 +from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_qwen_image + + +@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) + # Reserve working memory for the decode so the cache offloads any large resident model (e.g. + # the transformer) first; otherwise the decode's activations OOM. See estimator for details. + estimated_working_memory = estimate_vae_working_memory_qwen_image("decode", latents, 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, AutoencoderKLQwenImage) + latents = latents.to(device=TorchDevice.choose_torch_device(), dtype=vae.dtype) + + # Honor the global force_tiled_decode setting, like the SD/SDXL l2i node. Tiling bounds the + # VAE's per-tile memory, which is the scalable way to decode very large outputs that would + # exceed VRAM even after offloading the transformer/text encoder. For normal sizes, leave + # it off (faster, no tile blending) — the reserved working memory offloads other models so + # the full-frame decode fits. + if context.config.get().force_tiled_decode: + vae.enable_tiling() + else: + 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..9d9347a8cf9 --- /dev/null +++ b/invokeai/app/invocations/qwen_image_text_encoder.py @@ -0,0 +1,323 @@ +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, + idle_gpu_offloadable=True, +) +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/scheduler.py b/invokeai/app/invocations/scheduler.py index 52af20378ef..a870a442ef8 100644 --- a/invokeai/app/invocations/scheduler.py +++ b/invokeai/app/invocations/scheduler.py @@ -1,5 +1,4 @@ from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output -from invokeai.app.invocations.constants import SCHEDULER_NAME_VALUES from invokeai.app.invocations.fields import ( FieldDescriptions, InputField, @@ -7,6 +6,7 @@ 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") diff --git a/invokeai/app/invocations/sd3_denoise.py b/invokeai/app/invocations/sd3_denoise.py new file mode 100644 index 00000000000..10c9080ac5e --- /dev/null +++ b/invokeai/app/invocations/sd3_denoise.py @@ -0,0 +1,354 @@ +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))), + desc=f"Denoising{TorchDevice.get_session_device_label()}", + ): + # 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..d9f5c3f1f15 --- /dev/null +++ b/invokeai/app/invocations/sd3_text_encoder.py @@ -0,0 +1,207 @@ +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", + idle_gpu_offloadable=True, +) +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 index 1c0817cb928..0f509828c13 100644 --- a/invokeai/app/invocations/sdxl.py +++ b/invokeai/app/invocations/sdxl.py @@ -1,14 +1,8 @@ -from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField, UIType +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 import SubModelType - -from .baseinvocation import ( - BaseInvocation, - BaseInvocationOutput, - invocation, - invocation_output, -) -from .model import CLIPField, ModelIdentifierField, UNetField, VAEField +from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType @invocation_output("sdxl_model_loader_output") @@ -30,12 +24,14 @@ class SDXLRefinerModelLoaderOutput(BaseInvocationOutput): vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE") -@invocation("sdxl_model_loader", title="SDXL Main Model", tags=["model", "sdxl"], category="model", version="1.0.3") +@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_type=UIType.SDXLMainModel + description=FieldDescriptions.sdxl_main_model, + ui_model_base=BaseModelType.StableDiffusionXL, + ui_model_type=ModelType.Main, ) # TODO: precision? @@ -64,16 +60,18 @@ def invoke(self, context: InvocationContext) -> SDXLModelLoaderOutput: @invocation( "sdxl_refiner_model_loader", - title="SDXL Refiner Model", + title="Refiner Model - SDXL", tags=["model", "sdxl", "refiner"], category="model", - version="1.0.3", + version="1.0.4", ) class SDXLRefinerModelLoaderInvocation(BaseInvocation): """Loads an sdxl refiner model, outputting its submodels.""" model: ModelIdentifierField = InputField( - description=FieldDescriptions.sdxl_refiner_model, ui_type=UIType.SDXLRefinerModel + description=FieldDescriptions.sdxl_refiner_model, + ui_model_base=BaseModelType.StableDiffusionXLRefiner, + ui_model_type=ModelType.Main, ) # TODO: precision? 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 index 46ef35cbbfa..6d64e8771ad 100644 --- a/invokeai/app/invocations/strings.py +++ b/invokeai/app/invocations/strings.py @@ -2,17 +2,11 @@ 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 -from .baseinvocation import ( - BaseInvocation, - BaseInvocationOutput, - invocation, - invocation_output, -) -from .fields import InputField, OutputField, UIComponent -from .primitives import StringOutput - @invocation_output("string_pos_neg_output") class StringPosNegOutput(BaseInvocationOutput): @@ -26,7 +20,7 @@ class StringPosNegOutput(BaseInvocationOutput): "string_split_neg", title="String Split Negative", tags=["string", "split", "negative"], - category="string", + category="strings", version="1.0.1", ) class StringSplitNegInvocation(BaseInvocation): @@ -69,7 +63,7 @@ class String2Output(BaseInvocationOutput): string_2: str = OutputField(description="string 2") -@invocation("string_split", title="String Split", tags=["string", "split"], category="string", version="1.0.1") +@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""" @@ -89,7 +83,7 @@ def invoke(self, context: InvocationContext) -> String2Output: return String2Output(string_1=part1, string_2=part2) -@invocation("string_join", title="String Join", tags=["string", "join"], category="string", version="1.0.1") +@invocation("string_join", title="String Join", tags=["string", "join"], category="strings", version="1.0.1") class StringJoinInvocation(BaseInvocation): """Joins string left to string right""" @@ -100,7 +94,9 @@ 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="string", version="1.0.1") +@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""" @@ -113,7 +109,7 @@ def invoke(self, context: InvocationContext) -> StringOutput: @invocation( - "string_replace", title="String Replace", tags=["string", "replace", "regex"], category="string", version="1.0.1" + "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""" diff --git a/invokeai/app/invocations/t2i_adapter.py b/invokeai/app/invocations/t2i_adapter.py index 04f9a6c6954..cf4b7cda474 100644 --- a/invokeai/app/invocations/t2i_adapter.py +++ b/invokeai/app/invocations/t2i_adapter.py @@ -8,11 +8,12 @@ invocation, invocation_output, ) -from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField, UIType +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): @@ -45,7 +46,11 @@ class T2IAdapterOutput(BaseInvocationOutput): @invocation( - "t2i_adapter", title="T2I-Adapter", tags=["t2i_adapter", "control"], category="t2i_adapter", version="1.0.3" + "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.""" @@ -56,7 +61,8 @@ class T2IAdapterInvocation(BaseInvocation): description="The T2I-Adapter model.", title="T2I-Adapter Model", ui_order=-1, - ui_type=UIType.T2IAdapterModel, + 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" 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 index a54001a0a79..a631f3ba4ec 100644 --- a/invokeai/app/invocations/tiles.py +++ b/invokeai/app/invocations/tiles.py @@ -7,7 +7,6 @@ from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Classification, invocation, invocation_output, ) @@ -40,7 +39,6 @@ class CalculateImageTilesOutput(BaseInvocationOutput): tags=["tiles"], category="tiles", version="1.0.1", - classification=Classification.Beta, ) class CalculateImageTilesInvocation(BaseInvocation): """Calculate the coordinates and overlaps of tiles that cover a target image shape.""" @@ -74,7 +72,6 @@ def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput: tags=["tiles"], category="tiles", version="1.1.1", - classification=Classification.Beta, ) class CalculateImageTilesEvenSplitInvocation(BaseInvocation): """Calculate the coordinates and overlaps of tiles that cover a target image shape.""" @@ -117,7 +114,6 @@ def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput: tags=["tiles"], category="tiles", version="1.0.1", - classification=Classification.Beta, ) class CalculateImageTilesMinimumOverlapInvocation(BaseInvocation): """Calculate the coordinates and overlaps of tiles that cover a target image shape.""" @@ -168,7 +164,6 @@ class TileToPropertiesOutput(BaseInvocationOutput): tags=["tiles"], category="tiles", version="1.0.1", - classification=Classification.Beta, ) class TileToPropertiesInvocation(BaseInvocation): """Split a Tile into its individual properties.""" @@ -201,7 +196,6 @@ class PairTileImageOutput(BaseInvocationOutput): tags=["tiles"], category="tiles", version="1.0.1", - classification=Classification.Beta, ) class PairTileImageInvocation(BaseInvocation): """Pair an image with its tile properties.""" @@ -230,7 +224,6 @@ def invoke(self, context: InvocationContext) -> PairTileImageOutput: tags=["tiles"], category="tiles", version="1.1.1", - classification=Classification.Beta, ) class MergeTilesToImageInvocation(BaseInvocation, WithMetadata, WithBoard): """Merge multiple tile images into a single image.""" diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index f93060f8d34..64e372a0f6b 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -6,15 +6,13 @@ from PIL import Image from pydantic import ConfigDict -from invokeai.app.invocations.fields import ImageField +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 -from .baseinvocation import BaseInvocation, invocation -from .fields import InputField, WithBoard, WithMetadata - # TODO: Populate this from disk? # TODO: Use model manager to load? ESRGAN_MODELS = Literal[ @@ -32,7 +30,7 @@ } -@invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="esrgan", version="1.3.2") +@invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="upscale", version="1.3.2") class ESRGANInvocation(BaseInvocation, WithMetadata, WithBoard): """Upscales an image using RealESRGAN.""" diff --git a/invokeai/app/invocations/util.py b/invokeai/app/invocations/util.py index c69c32eed01..3ae3e17ae67 100644 --- a/invokeai/app/invocations/util.py +++ b/invokeai/app/invocations/util.py @@ -9,6 +9,6 @@ def validate_weights(weights: Union[float, list[float]]) -> None: def validate_begin_end_step(begin_step_percent: float, end_step_percent: float) -> None: - """Validate that begin_step_percent is less than end_step_percent""" - if begin_step_percent >= end_step_percent: + """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..50b41f6121e --- /dev/null +++ b/invokeai/app/invocations/z_image_denoise.py @@ -0,0 +1,813 @@ +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, + positive_cap_feats=pos_prompt_embeds, + ) + ) + + # 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=f"Denoising{TorchDevice.get_session_device_label()}") + 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), desc=f"Denoising{TorchDevice.get_session_device_label()}"): + 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..148cff5c269 --- /dev/null +++ b/invokeai/app/invocations/z_image_text_encoder.py @@ -0,0 +1,210 @@ +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, + idle_gpu_offloadable=True, +) +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 index 701f1dab739..389b61e7347 100644 --- a/invokeai/app/run_app.py +++ b/invokeai/app/run_app.py @@ -1,12 +1,145 @@ -"""This is a wrapper around the main app entrypoint, to allow for CLI args to be parsed before running the app.""" +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: - # Before doing _anything_, parse CLI args! + """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() - from invokeai.app.api_app import invoke_api + 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_generation_devices_summary(app_config.generation_devices) + 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() - invoke_api() + # 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/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/invokeai/app/services/board_image_records/board_image_records_base.py b/invokeai/app/services/board_image_records/board_image_records_base.py index c8f7b359085..4ccbaa952db 100644 --- a/invokeai/app/services/board_image_records/board_image_records_base.py +++ b/invokeai/app/services/board_image_records/board_image_records_base.py @@ -1,6 +1,8 @@ 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.""" @@ -26,6 +28,8 @@ def remove_image_from_board( 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 @@ -45,3 +49,11 @@ def get_image_count_for_board( ) -> 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 index cde810a7396..b249bb67334 100644 --- a/invokeai/app/services/board_image_records/board_image_records_sqlite.py +++ b/invokeai/app/services/board_image_records/board_image_records_sqlite.py @@ -1,33 +1,30 @@ import sqlite3 -import threading from typing import Optional, cast -from invokeai.app.services.image_records.image_records_common import ImageRecord, deserialize_image_record +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 -from .board_image_records_base import BoardImageRecordStorageBase - class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase): - _conn: sqlite3.Connection - _cursor: sqlite3.Cursor - _lock: threading.RLock - def __init__(self, db: SqliteDatabase) -> None: super().__init__() - self._lock = db.lock - self._conn = db.conn - self._cursor = self._conn.cursor() + self._db = db def add_image_to_board( self, board_id: str, image_name: str, ) -> None: - try: - self._lock.acquire() - self._cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql INSERT INTO board_images (board_id, image_name) VALUES (?, ?) @@ -35,32 +32,19 @@ def add_image_to_board( """, (board_id, image_name, board_id), ) - self._conn.commit() - except sqlite3.Error as e: - self._conn.rollback() - raise e - finally: - self._lock.release() def remove_image_from_board( self, image_name: str, ) -> None: - try: - self._lock.acquire() - self._cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql DELETE FROM board_images WHERE image_name = ?; """, (image_name,), ) - self._conn.commit() - except sqlite3.Error as e: - self._conn.rollback() - raise e - finally: - self._lock.release() def get_images_for_board( self, @@ -68,10 +52,8 @@ def get_images_for_board( offset: int = 0, limit: int = 10, ) -> OffsetPaginatedResults[ImageRecord]: - # TODO: this isn't paginated yet? - try: - self._lock.acquire() - self._cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql SELECT images.* FROM board_images @@ -81,80 +63,128 @@ def get_images_for_board( """, (board_id,), ) - result = cast(list[sqlite3.Row], self._cursor.fetchall()) + result = cast(list[sqlite3.Row], cursor.fetchall()) images = [deserialize_image_record(dict(r)) for r in result] - self._cursor.execute( + cursor.execute( """--sql SELECT COUNT(*) FROM images WHERE 1=1; """ ) - count = cast(int, self._cursor.fetchone()[0]) + count = cast(int, cursor.fetchone()[0]) - except sqlite3.Error as e: - self._conn.rollback() - raise e - finally: - self._lock.release() return OffsetPaginatedResults(items=images, offset=offset, limit=limit, total=count) - def get_all_board_image_names_for_board(self, board_id: str) -> list[str]: - try: - self._lock.acquire() - self._cursor.execute( - """--sql - SELECT image_name - FROM board_images - WHERE board_id = ?; - """, - (board_id,), - ) - result = cast(list[sqlite3.Row], self._cursor.fetchall()) - image_names = [r[0] for r in result] - return image_names - except sqlite3.Error as e: - self._conn.rollback() - raise e - finally: - self._lock.release() + 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]: - try: - self._lock.acquire() - self._cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql - SELECT board_id - FROM board_images - WHERE image_name = ?; - """, + SELECT board_id + FROM board_images + WHERE image_name = ?; + """, (image_name,), ) - result = self._cursor.fetchone() - if result is None: - return None - return cast(str, result[0]) - except sqlite3.Error as e: - self._conn.rollback() - raise e - finally: - self._lock.release() + 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: - try: - self._lock.acquire() - self._cursor.execute( - """--sql - SELECT COUNT(*) FROM board_images WHERE board_id = ?; - """, - (board_id,), + 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, self._cursor.fetchone()[0]) - return count - except sqlite3.Error as e: - self._conn.rollback() - raise e - finally: - self._lock.release() + count = cast(int, cursor.fetchone()[0]) + return count diff --git a/invokeai/app/services/board_images/board_images_base.py b/invokeai/app/services/board_images/board_images_base.py index 356ff7068b6..c16d971cd28 100644 --- a/invokeai/app/services/board_images/board_images_base.py +++ b/invokeai/app/services/board_images/board_images_base.py @@ -1,6 +1,8 @@ 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.""" @@ -26,6 +28,8 @@ def remove_image_from_board( 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 diff --git a/invokeai/app/services/board_images/board_images_default.py b/invokeai/app/services/board_images/board_images_default.py index 85e478619c1..437495189f3 100644 --- a/invokeai/app/services/board_images/board_images_default.py +++ b/invokeai/app/services/board_images/board_images_default.py @@ -1,9 +1,9 @@ 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 -from .board_images_base import BoardImagesServiceABC - class BoardImagesService(BoardImagesServiceABC): __invoker: Invoker @@ -27,8 +27,14 @@ def remove_image_from_board( 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) + 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, diff --git a/invokeai/app/services/board_records/board_records_base.py b/invokeai/app/services/board_records/board_records_base.py index 30f819618a4..20981f2c7d7 100644 --- a/invokeai/app/services/board_records/board_records_base.py +++ b/invokeai/app/services/board_records/board_records_base.py @@ -1,8 +1,8 @@ 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 .board_records_common import BoardChanges, BoardRecord +from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection class BoardRecordStorageBase(ABC): @@ -17,8 +17,9 @@ def delete(self, board_id: str) -> None: def save( self, board_name: str, + user_id: str, ) -> BoardRecord: - """Saves a board record.""" + """Saves a board record for a specific user.""" pass @abstractmethod @@ -41,15 +42,25 @@ def update( @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.""" + """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.""" + """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 index d08951b499c..b263f264cb8 100644 --- a/invokeai/app/services/board_records/board_records_common.py +++ b/invokeai/app/services/board_records/board_records_common.py @@ -1,12 +1,25 @@ 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.""" @@ -14,6 +27,8 @@ class BoardRecord(BaseModelExcludeNull): """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.") @@ -22,6 +37,12 @@ class BoardRecord(BaseModelExcludeNull): """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: @@ -31,24 +52,44 @@ def deserialize_board_record(board_dict: dict) -> BoardRecord: 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.") + 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): diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py index a3836cb6c7d..1e3e11c8a36 100644 --- a/invokeai/app/services/board_records/board_records_sqlite.py +++ b/invokeai/app/services/board_records/board_records_sqlite.py @@ -1,96 +1,77 @@ import sqlite3 -import threading from typing import Union, cast -from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase -from invokeai.app.util.misc import uuid_string - -from .board_records_base import BoardRecordStorageBase -from .board_records_common import ( +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): - _conn: sqlite3.Connection - _cursor: sqlite3.Cursor - _lock: threading.RLock - def __init__(self, db: SqliteDatabase) -> None: super().__init__() - self._lock = db.lock - self._conn = db.conn - self._cursor = self._conn.cursor() + self._db = db def delete(self, board_id: str) -> None: - try: - self._lock.acquire() - self._cursor.execute( - """--sql - DELETE FROM boards - WHERE board_id = ?; - """, - (board_id,), - ) - self._conn.commit() - except sqlite3.Error as e: - self._conn.rollback() - raise BoardRecordDeleteException from e - except Exception as e: - self._conn.rollback() - raise BoardRecordDeleteException from e - finally: - self._lock.release() + 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: - try: - board_id = uuid_string() - self._lock.acquire() - self._cursor.execute( - """--sql - INSERT OR IGNORE INTO boards (board_id, board_name) - VALUES (?, ?); - """, - (board_id, board_name), - ) - self._conn.commit() - except sqlite3.Error as e: - self._conn.rollback() - raise BoardRecordSaveException from e - finally: - self._lock.release() + 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: - try: - self._lock.acquire() - self._cursor.execute( - """--sql - SELECT * - FROM boards - WHERE board_id = ?; - """, - (board_id,), - ) - - result = cast(Union[sqlite3.Row, None], self._cursor.fetchone()) - except sqlite3.Error as e: - self._conn.rollback() - raise BoardRecordNotFoundException from e - finally: - self._lock.release() + 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)) @@ -100,102 +81,210 @@ def update( board_id: str, changes: BoardChanges, ) -> BoardRecord: - try: - self._lock.acquire() + 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 name of a board - if changes.board_name is not None: - self._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 cover image of a board - if changes.cover_image_name is not None: - self._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), + ) - self._conn.commit() - except sqlite3.Error as e: - self._conn.rollback() - raise BoardRecordSaveException from e - finally: - self._lock.release() + 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]: - try: - self._lock.acquire() - - # Get all the boards - self._cursor.execute( - """--sql - SELECT * - FROM boards - ORDER BY created_at DESC - LIMIT ? OFFSET ?; - """, - (limit, offset), - ) - - result = cast(list[sqlite3.Row], self._cursor.fetchall()) - boards = [deserialize_board_record(dict(r)) for r in result] + 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" - # Get the total number of boards - self._cursor.execute( - """--sql - SELECT COUNT(*) - FROM boards - WHERE 1=1; + 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 ?; """ - ) - count = cast(int, self._cursor.fetchone()[0]) + # 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)) - return OffsetPaginatedResults[BoardRecord](items=boards, offset=offset, limit=limit, total=count) + result = cast(list[sqlite3.Row], cursor.fetchall()) + boards = [deserialize_board_record(dict(r)) for r in result] - except sqlite3.Error as e: - self._conn.rollback() - raise e - finally: - self._lock.release() + # 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]: - try: - self._lock.acquire() - - # Get all the boards - self._cursor.execute( - """--sql - SELECT * - FROM boards - ORDER BY created_at DESC - """ - ) + 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} + """ - result = cast(list[sqlite3.Row], self._cursor.fetchall()) - boards = [deserialize_board_record(dict(r)) for r in result] + 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)) - return boards + result = cast(list[sqlite3.Row], cursor.fetchall()) + boards = [deserialize_board_record(dict(r)) for r in result] - except sqlite3.Error as e: - self._conn.rollback() - raise e - finally: - self._lock.release() + return boards diff --git a/invokeai/app/services/boards/boards_base.py b/invokeai/app/services/boards/boards_base.py index 6f90334d53e..914dfa3d0d7 100644 --- a/invokeai/app/services/boards/boards_base.py +++ b/invokeai/app/services/boards/boards_base.py @@ -1,9 +1,9 @@ from abc import ABC, abstractmethod -from invokeai.app.services.board_records.board_records_common import BoardChanges +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 .boards_common import BoardDTO +from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection class BoardServiceABC(ABC): @@ -13,8 +13,9 @@ class BoardServiceABC(ABC): def create( self, board_name: str, + user_id: str, ) -> BoardDTO: - """Creates a board.""" + """Creates a board for a specific user.""" pass @abstractmethod @@ -45,15 +46,25 @@ def delete( @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.""" + """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.""" + """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 index 0cb54102bb3..99952fec134 100644 --- a/invokeai/app/services/boards/boards_common.py +++ b/invokeai/app/services/boards/boards_common.py @@ -2,7 +2,7 @@ from pydantic import Field -from ..board_records.board_records_common import BoardRecord +from invokeai.app.services.board_records.board_records_common import BoardRecord class BoardDTO(BoardRecord): @@ -12,12 +12,24 @@ class BoardDTO(BoardRecord): """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) -> BoardDTO: +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 index 5b37d6c7ad2..71465815ef9 100644 --- a/invokeai/app/services/boards/boards_default.py +++ b/invokeai/app/services/boards/boards_default.py @@ -1,10 +1,9 @@ -from invokeai.app.services.board_records.board_records_common import BoardChanges -from invokeai.app.services.boards.boards_common import BoardDTO +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 .boards_base import BoardServiceABC -from .boards_common import board_record_to_dto +from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection class BoardService(BoardServiceABC): @@ -16,9 +15,10 @@ def start(self, invoker: Invoker) -> None: def create( self, board_name: str, + user_id: str, ) -> BoardDTO: - board_record = self.__invoker.services.board_records.save(board_name) - return board_record_to_dto(board_record, None, 0) + 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) @@ -28,7 +28,8 @@ def get_dto(self, board_id: str) -> BoardDTO: else: cover_image_name = None image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id) - return board_record_to_dto(board_record, cover_image_name, image_count) + 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, @@ -43,13 +44,25 @@ def update( cover_image_name = None image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id) - return board_record_to_dto(board_record, cover_image_name, image_count) + 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, offset: int = 0, limit: int = 10) -> OffsetPaginatedResults[BoardDTO]: - board_records = self.__invoker.services.board_records.get_many(offset, limit) + 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) @@ -59,12 +72,30 @@ def get_many(self, offset: int = 0, limit: int = 10) -> OffsetPaginatedResults[B cover_image_name = None image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id) - board_dtos.append(board_record_to_dto(r, cover_image_name, image_count)) + 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) -> list[BoardDTO]: - board_records = self.__invoker.services.board_records.get_all() + 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) @@ -74,6 +105,15 @@ def get_all(self) -> list[BoardDTO]: cover_image_name = None image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id) - board_dtos.append(board_record_to_dto(r, cover_image_name, image_count)) + 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/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index 617b611f566..6cd4ed0cbaf 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -7,7 +7,11 @@ class BulkDownloadBase(ABC): @abstractmethod def handler( - self, image_names: Optional[list[str]], board_id: Optional[str], bulk_download_item_id: Optional[str] + 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. @@ -15,6 +19,7 @@ def handler( :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 @@ -42,3 +47,12 @@ def delete(self, bulk_download_item_name: str) -> None: :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_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index d4bf059b8f0..c037e9c5c15 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -4,6 +4,7 @@ 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, @@ -15,8 +16,6 @@ from invokeai.app.services.invoker import Invoker from invokeai.app.util.misc import uuid_string -from .bulk_download_base import BulkDownloadBase - class BulkDownloadService(BulkDownloadBase): def start(self, invoker: Invoker) -> None: @@ -26,15 +25,24 @@ 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] + 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" - self._signal_job_started(bulk_download_id, bulk_download_item_id, bulk_download_item_name) + # 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] = [] @@ -47,16 +55,16 @@ def handler( 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) + 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) + 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) + 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 @@ -64,7 +72,11 @@ 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) + 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: @@ -100,43 +112,60 @@ def _clean_string_to_path_safe(self, s: str) -> str: 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 + 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 + 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 + 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 + 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 + 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) + 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) @@ -147,4 +176,15 @@ def get_path(self, bulk_download_item_name: str) -> str: 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) - return path.exists() + + # 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 index 126692f08a8..df1acbf1047 100644 --- a/invokeai/app/services/config/__init__.py +++ b/invokeai/app/services/config/__init__.py @@ -1,7 +1,6 @@ """Init file for InvokeAI configure package.""" from invokeai.app.services.config.config_common import PagingArgumentParser - -from .config_default import InvokeAIAppConfig, get_config +from invokeai.app.services.config.config_default import InvokeAIAppConfig, get_config __all__ = ["InvokeAIAppConfig", "get_config", "PagingArgumentParser"] diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index 1dc75add1d6..d5c8a9634a5 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -3,15 +3,16 @@ 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 +from typing import Any, Literal, Optional, Union -import psutil import yaml from pydantic import BaseModel, Field, PrivateAttr, field_validator from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict @@ -21,36 +22,26 @@ 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") -DEFAULT_RAM_CACHE = 10.0 -DEFAULT_VRAM_CACHE = 0.25 -DEFAULT_CONVERT_CACHE = 20.0 -DEVICE = Literal["auto", "cpu", "cuda", "cuda:1", "mps"] 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"] -CONFIG_SCHEMA_VERSION = "4.0.1" - - -def get_default_ram_cache_size() -> float: - """Run a heuristic for the default RAM cache based on installed RAM.""" - - # On some machines, psutil.virtual_memory().total gives a value that is slightly less than the actual RAM, so the - # limits are set slightly lower than than what we expect the actual RAM to be. - - GB = 1024**3 - max_ram = psutil.virtual_memory().total / GB - - if max_ram >= 60: - return 15.0 - if max_ram >= 30: - return 7.5 - if max_ram >= 14: - return 4.0 - return 2.1 # 2.1 is just large enough for sd 1.5 ;-) +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): @@ -80,32 +71,42 @@ class InvokeAIAppConfig(BaseSettings): 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.org/settings/#https. - ssl_keyfile: SSL key file for HTTPS. See https://www.uvicorn.org/settings/#https. + 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. When loading a non-diffusers model, it will be converted and store on disk at this location. + 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. - ram: Maximum memory amount used by memory model cache for rapid switching (GB). - vram: Amount of VRAM reserved for model storage (GB). - convert_cache: Maximum size of on-disk converted models cache (GB). - lazy_offload: Keep models in VRAM until their space is needed. + 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. - device: Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities.
Valid values: `auto`, `cpu`, `cuda`, `cuda:1`, `mps` + 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` @@ -113,13 +114,26 @@ class InvokeAIAppConfig(BaseSettings): 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. + 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) @@ -139,8 +153,8 @@ class InvokeAIAppConfig(BaseSettings): 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.org/settings/#https.") - ssl_keyfile: Optional[Path] = Field(default=None, description="SSL key file for HTTPS. See https://www.uvicorn.org/settings/#https.") + 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.") @@ -148,12 +162,15 @@ class InvokeAIAppConfig(BaseSettings): # 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. When loading a non-diffusers model, it will be converted and store on disk at this location.") + 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=".') @@ -161,6 +178,7 @@ class InvokeAIAppConfig(BaseSettings): 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.") @@ -170,14 +188,25 @@ class InvokeAIAppConfig(BaseSettings): profiles_dir: Path = Field(default=Path("profiles"), description="Path to profiles output directory.") # CACHE - ram: float = Field(default_factory=get_default_ram_cache_size, gt=0, description="Maximum memory amount used by memory model cache for rapid switching (GB).") - vram: float = Field(default=DEFAULT_VRAM_CACHE, ge=0, description="Amount of VRAM reserved for model storage (GB).") - convert_cache: float = Field(default=DEFAULT_CONVERT_CACHE, ge=0, description="Maximum size of on-disk converted models cache (GB).") - lazy_offload: bool = Field(default=True, description="Keep models in VRAM until their space is needed.") + 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: DEVICE = Field(default="auto", description="Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities.") + 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+)?)$") + generation_devices: Union[Literal["auto"], list[str]] = Field(default="auto", description="Devices to use for parallel generation. `auto` (the default) uses every available GPU, running one generation session per GPU concurrently and distributing jobs fairly across users. Provide an explicit list (e.g. `[cuda:0, cuda:1]`) to use specific devices, or a single-device list (e.g. `[cuda:0]`) to run serially. On systems without a GPU, `auto` resolves to the single `cpu`/`mps` device.
Valid values: `auto`, or a list whose entries are each `cpu`, `cuda`, `mps`, or `cuda:N` (where N is a device number)") + offload_text_encoders_to_idle_gpus: bool = Field(default=True, description="When running on multiple GPUs, load text encoders onto a currently-idle GPU instead of the one running the denoise pipeline. This avoids churning the denoise model in and out of VRAM to make room for the encoder, and lets a cached encoder be reused across generations. Has no effect unless at least two `generation_devices` are configured and a GPU is idle; under full load encoders run on the session's own GPU as before.") 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 @@ -187,7 +216,8 @@ class InvokeAIAppConfig(BaseSettings): 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.") + 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.") @@ -198,11 +228,58 @@ class InvokeAIAppConfig(BaseSettings): 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) + @field_validator("generation_devices") + @classmethod + def validate_generation_devices(cls, v: Union[str, list[str]]) -> Union[str, list[str]]: + if v == "auto": + return v + # A non-"auto" string would otherwise be iterated character-by-character below (rejecting + # 'c' from "cuda:0"), producing a confusing error. Require an explicit list instead. + if isinstance(v, str): + raise ValueError( + f"Invalid generation_devices value '{v}'. Use 'auto' or a list of devices, e.g. ['cuda:0', 'cuda:1']." + ) + if len(v) == 0: + raise ValueError("generation_devices cannot be an empty list. Use 'auto' or a list of devices.") + pattern = re.compile(r"^(cpu|mps|cuda(:\d+)?)$") + for device in v: + if not pattern.match(device): + raise ValueError( + f"Invalid generation device '{device}'. Valid values are 'auto', 'cpu', 'mps', 'cuda', or 'cuda:N'." + ) + return v + def update_config(self, config: dict[str, Any] | InvokeAIAppConfig, clobber: bool = True) -> None: """Updates the config, overwriting existing values. @@ -250,13 +327,13 @@ def write_file(self, dest_path: Path, as_example: bool = False) -> None: ) if as_example: - file.write( - "# This is an example file with default and example settings. Use the values here as a baseline.\n\n" - ) + 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.github.io/InvokeAI/features/CONFIGURATION/:\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)) @@ -280,6 +357,13 @@ def config_file_path(self) -> Path: 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..""" @@ -302,6 +386,16 @@ 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..""" @@ -357,14 +451,14 @@ def settings_customise_sources( return (init_settings,) -def migrate_v3_config_dict(config_dict: dict[str, Any]) -> InvokeAIAppConfig: - """Migrate a v3 config dictionary to a current config object. +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 instance of `InvokeAIAppConfig` with the migrated settings. + An `InvokeAIAppConfig` config dict. """ parsed_config_dict: dict[str, Any] = {} @@ -398,32 +492,55 @@ def migrate_v3_config_dict(config_dict: dict[str, Any]) -> InvokeAIAppConfig: elif k in InvokeAIAppConfig.model_fields: # skip unknown fields parsed_config_dict[k] = v - # When migrating the config file, we should not include currently-set environment variables. - config = DefaultInvokeAIAppConfig.model_validate(parsed_config_dict) - - return config + parsed_config_dict["schema_version"] = "4.0.0" + return parsed_config_dict -def migrate_v4_0_0_config_dict(config_dict: dict[str, Any]) -> InvokeAIAppConfig: - """Migrate v4.0.0 config dictionary to a current config object. +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: - An instance of `InvokeAIAppConfig` with the migrated settings. + A config dict with the settings migrated to v4.0.1. """ - parsed_config_dict: dict[str, Any] = {} - for k, v in config_dict.items(): - # autocast was removed from precision in v4.0.1 - if k == "precision" and v == "autocast": - parsed_config_dict["precision"] = "auto" - else: - parsed_config_dict[k] = v - if k == "schema_version": - parsed_config_dict[k] = CONFIG_SCHEMA_VERSION - config = DefaultInvokeAIAppConfig.model_validate(parsed_config_dict) - return config + 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: @@ -437,38 +554,75 @@ def load_and_migrate_config(config_path: Path) -> InvokeAIAppConfig: """ assert config_path.suffix == ".yaml" with open(config_path, "rt", encoding=locale.getpreferredencoding()) as file: - loaded_config_dict = yaml.safe_load(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: - # This is a v3 config file, attempt to migrate it + 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: - # loaded_config_dict could be the wrong shape, but we will catch all exceptions below - migrated_config = migrate_v3_config_dict(loaded_config_dict) # pyright: ignore [reportUnknownArgumentType] + # 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 - migrated_config.write_file(config_path) - return migrated_config - if loaded_config_dict["schema_version"] == "4.0.0": - loaded_config_dict = migrate_v4_0_0_config_dict(loaded_config_dict) - loaded_config_dict.write_file(config_path) - - # Attempt to load as a v4 config file 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}" + 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. @@ -485,6 +639,7 @@ def get_config() -> InvokeAIAppConfig: """ # This object includes environment variables, as parsed by pydantic-settings config = InvokeAIAppConfig() + env_fields_set = set(config.model_fields_set) args = InvokeAIArgs.args @@ -507,9 +662,35 @@ def get_config() -> InvokeAIAppConfig: ] example_config.write_file(config.config_file_path.with_suffix(".example.yaml"), as_example=True) - # Copy all legacy configs - We know `__path__[0]` is correct here + # 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] - shutil.copytree(configs_src, config.legacy_conf_path, dirs_exist_ok=True) + 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) @@ -520,4 +701,11 @@ def get_config() -> InvokeAIAppConfig: 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 index 33b0025809c..48ded7d5496 100644 --- a/invokeai/app/services/download/__init__.py +++ b/invokeai/app/services/download/__init__.py @@ -1,13 +1,13 @@ """Init file for download queue.""" -from .download_base import ( +from invokeai.app.services.download.download_base import ( DownloadJob, DownloadJobStatus, DownloadQueueServiceBase, MultiFileDownloadJob, UnknownJobIDException, ) -from .download_default import DownloadQueueService, TqdmProgress +from invokeai.app.services.download.download_default import DownloadQueueService, TqdmProgress __all__ = [ "DownloadJob", diff --git a/invokeai/app/services/download/download_base.py b/invokeai/app/services/download/download_base.py index 4880ab98b89..1798fd69df5 100644 --- a/invokeai/app/services/download/download_base.py +++ b/invokeai/app/services/download/download_base.py @@ -18,6 +18,7 @@ class DownloadJobStatus(str, Enum): 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 @@ -61,6 +62,7 @@ class DownloadJobBase(BaseModel): # 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) @@ -72,6 +74,12 @@ class DownloadJobBase(BaseModel): 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 @@ -80,6 +88,11 @@ 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.""" @@ -161,6 +174,17 @@ class DownloadJob(DownloadJobBase): 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.""" @@ -321,6 +345,10 @@ 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.""" diff --git a/invokeai/app/services/download/download_default.py b/invokeai/app/services/download/download_default.py index f6c7c1a1a0c..13e86d18284 100644 --- a/invokeai/app/services/download/download_default.py +++ b/invokeai/app/services/download/download_default.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023, Lincoln D. Stein +# Copyright (c) 2023,2026 Lincoln D. Stein """Implementation of multithreaded download queue for invokeai.""" import os @@ -8,7 +8,9 @@ import traceback from pathlib import Path from queue import Empty, PriorityQueue -from typing import Any, Dict, List, Literal, Optional, Set +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 @@ -16,12 +18,7 @@ from tqdm import tqdm from invokeai.app.services.config import InvokeAIAppConfig, get_config -from invokeai.app.services.events.events_base import EventServiceBase -from invokeai.app.util.misc import get_iso_timestamp -from invokeai.backend.model_manager.metadata import RemoteModelFile -from invokeai.backend.util.logging import InvokeAILogger - -from .download_base import ( +from invokeai.app.services.download.download_base import ( DownloadEventHandler, DownloadExceptionHandler, DownloadJob, @@ -33,6 +30,12 @@ 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 @@ -57,7 +60,9 @@ def __init__( """ self._app_config = app_config or get_config() self._jobs: Dict[int, DownloadJob] = {} - self._download_part2parent: Dict[AnyHttpUrl, MultiFileDownloadJob] = {} + 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() @@ -83,7 +88,7 @@ def stop(self, *args: Any, **kwargs: Any) -> None: """Stop the download worker threads.""" with self._lock: if not self._worker_pool: - raise Exception("Attempt to stop the download service before it was started") + 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] @@ -113,7 +118,8 @@ def submit_download_job( raise ServiceInactiveException( "The download service is not currently accepting requests. Please call start() to initialize the service." ) - job.id = self._next_id() + if job.id == -1: + job.id = self._next_id() job.set_callbacks( on_start=on_start, on_progress=on_progress, @@ -124,6 +130,11 @@ def submit_download_job( 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, @@ -185,24 +196,41 @@ def multifile_download( job = DownloadJob( source=url, dest=path, - access_token=access_token, + 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.source] = mfdj + self._download_part2parent[job.id] = mfdj if submit_job: self.submit_multifile_download(mfdj) return mfdj def submit_multifile_download(self, job: MultiFileDownloadJob) -> None: - for download_job in job.download_parts: - 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, - ) + 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.""" @@ -282,12 +310,18 @@ def _download_next_item(self) -> None: except Empty: continue try: + if job.cancelled: + raise DownloadJobCancelledException("Job was cancelled before start") job.job_started = get_iso_timestamp() self._do_download(job) - self._signal_job_complete(job) + if job.status != DownloadJobStatus.COMPLETED: + self._signal_job_complete(job) except DownloadJobCancelledException: - self._signal_job_cancelled(job) - self._cleanup_cancelled_job(job) + 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() @@ -295,8 +329,7 @@ def _download_next_item(self) -> None: finally: job.job_ended = get_iso_timestamp() self._job_terminated_event.set() # signal a change to terminal state - self._download_part2parent.pop(job.source, None) # if this is a subpart of a multipart job, remove it - self._job_terminated_event.set() + 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.") @@ -304,22 +337,130 @@ def _download_next_item(self) -> None: def _do_download(self, job: DownloadJob) -> None: """Do the actual download.""" - url = job.source + 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: - raise HTTPError(resp.reason) + 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)) - job.total_bytes = content_length if job.dest.is_dir(): - file_name = os.path.basename(str(url.path)) # default is to use the last bit of the URL + 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) @@ -334,31 +475,83 @@ def _do_download(self, job: DownloadJob) -> None: 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(): + if job.download_path.exists() and resume_from == 0: raise OSError(f"[Errno 17] File {job.download_path} exists") # append ".downloading" to the path - in_progress_path = self._in_progress_path(job.download_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 (content_length > 0 and job.bytes >= content_length): - self._logger.warning(f"{job.download_path}: complete file found. Skipping.") + 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.warning(f"{job.download_path}: partial file found. Resuming") + self._logger.info(f"{job.download_path}: partial file found. Resuming") # some other error elif resp.status_code != 200: - raise HTTPError(resp.reason) + 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 @@ -375,6 +568,12 @@ def _do_download(self, job: DownloadJob) -> None: 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) @@ -430,11 +629,28 @@ def _signal_job_cancelled(self, job: DownloadJob) -> None: self._event_bus.emit_download_cancelled(job) # if multifile download, then signal the parent - if parent_job := self._download_part2parent.get(job.source, None): + 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)}") @@ -444,6 +660,8 @@ def _signal_job_error(self, job: DownloadJob, excp: Optional[Exception] = None) 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: @@ -458,7 +676,7 @@ def _cleanup_cancelled_job(self, job: DownloadJob) -> None: 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.source] + 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 @@ -471,44 +689,62 @@ def _mfd_started(self, download_job: DownloadJob) -> None: def _mfd_progress(self, download_job: DownloadJob) -> None: with self._lock: - mf_job = self._download_part2parent[download_job.source] + 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.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.source] + 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 mf_job.running and all(x.complete for x in mf_job.download_parts): + 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.source] + 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: - self._logger.warning(f"Download cancelled: {download_job.source}") - mf_job.cancel() - + 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.source] + 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 @@ -519,7 +755,7 @@ def _mfd_error(self, download_job: DownloadJob, excp: Optional[Exception] = None ) for s in [x for x in mf_job.download_parts if x.running]: self.cancel_job(s) - self._download_part2parent.pop(download_job.source) + self._mfd_pending.pop(mf_job.id, None) self._job_terminated_event.set() def _execute_cb( diff --git a/invokeai/app/services/events/__init__.py b/invokeai/app/services/events/__init__.py index 17407d3b72a..e69de29bb2d 100644 --- a/invokeai/app/services/events/__init__.py +++ b/invokeai/app/services/events/__init__.py @@ -1 +0,0 @@ -from .events_base import EventServiceBase # noqa F401 diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index bb578c23e8c..935b422a732 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -11,12 +11,13 @@ DownloadCancelledEvent, DownloadCompleteEvent, DownloadErrorEvent, + DownloadPausedEvent, DownloadProgressEvent, DownloadStartedEvent, EventBase, InvocationCompleteEvent, - InvocationDenoiseProgressEvent, InvocationErrorEvent, + InvocationProgressEvent, InvocationStartedEvent, ModelInstallCancelledEvent, ModelInstallCompleteEvent, @@ -28,9 +29,10 @@ ModelLoadCompleteEvent, ModelLoadStartedEvent, QueueClearedEvent, + QueueItemsRetriedEvent, QueueItemStatusChangedEvent, + RecallParametersUpdatedEvent, ) -from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState if TYPE_CHECKING: from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput @@ -40,10 +42,12 @@ from invokeai.app.services.session_queue.session_queue_common import ( BatchStatus, EnqueueBatchResult, + RetryItemsResult, SessionQueueItem, SessionQueueStatus, ) - from invokeai.backend.model_manager.config import AnyModelConfig, SubModelType + from invokeai.backend.model_manager.configs.factory import AnyModelConfig + from invokeai.backend.model_manager.taxonomy import SubModelType class EventServiceBase: @@ -58,15 +62,16 @@ def emit_invocation_started(self, queue_item: "SessionQueueItem", invocation: "B """Emitted when an invocation is started""" self.dispatch(InvocationStartedEvent.build(queue_item, invocation)) - def emit_invocation_denoise_progress( + def emit_invocation_progress( self, queue_item: "SessionQueueItem", invocation: "BaseInvocation", - intermediate_state: PipelineIntermediateState, - progress_image: "ProgressImage", + message: str, + percentage: float | None = None, + image: "ProgressImage | None" = None, ) -> None: - """Emitted at each step during denoising of an invocation.""" - self.dispatch(InvocationDenoiseProgressEvent.build(queue_item, invocation, intermediate_state, progress_image)) + """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" @@ -95,14 +100,22 @@ def emit_queue_item_status_changed( """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") -> None: + 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)) + 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 @@ -123,6 +136,10 @@ 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)) @@ -177,23 +194,42 @@ def emit_model_install_error(self, job: "ModelInstallJob") -> None: # region Bulk image download def emit_bulk_download_started( - self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str + 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)) + 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 + 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)) + 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 + 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) + 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 index c6a867fb081..c30fa31b75c 100644 --- a/invokeai/app/services/events/events_common.py +++ b/invokeai/app/services/events/events_common.py @@ -1,26 +1,27 @@ -from math import floor 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.config import AnyModelConfig, SubModelType -from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState +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 + from invokeai.app.services.model_install.model_install_common import ModelInstallJob, ModelSource class EventBase(BaseModel): @@ -88,6 +89,9 @@ class QueueItemEventBase(QueueEventBase): 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): @@ -95,8 +99,6 @@ class InvocationEventBase(QueueItemEventBase): session_id: str = Field(description="The ID of the session (aka graph execution state)") queue_id: str = Field(description="The ID of the queue") - item_id: int = Field(description="The ID of the queue item") - batch_id: str = Field(description="The ID of the queue batch") 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") @@ -114,6 +116,9 @@ def build(cls, queue_item: SessionQueueItem, invocation: AnyInvocation) -> "Invo 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], @@ -121,52 +126,55 @@ def build(cls, queue_item: SessionQueueItem, invocation: AnyInvocation) -> "Invo @payload_schema.register -class InvocationDenoiseProgressEvent(InvocationEventBase): - """Event model for invocation_denoise_progress""" +class InvocationProgressEvent(InvocationEventBase): + """Event model for invocation_progress""" - __event_name__ = "invocation_denoise_progress" + __event_name__ = "invocation_progress" - progress_image: ProgressImage = Field(description="The progress image sent at each step during processing") - step: int = Field(description="The current step of the invocation") - total_steps: int = Field(description="The total number of steps in the invocation") - order: int = Field(description="The order of the invocation in the session") - percentage: float = Field(description="The percentage of completion of the invocation") + 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" + ) + device: str | None = Field( + default=None, + description="The device processing this session, e.g. 'cuda:1' (set only when running on a CUDA GPU)", + ) @classmethod def build( cls, queue_item: SessionQueueItem, invocation: AnyInvocation, - intermediate_state: PipelineIntermediateState, - progress_image: ProgressImage, - ) -> "InvocationDenoiseProgressEvent": - step = intermediate_state.step - total_steps = intermediate_state.total_steps - order = intermediate_state.order + message: str, + percentage: float | None = None, + image: ProgressImage | None = None, + ) -> "InvocationProgressEvent": + # This is emitted from the session-processor worker thread, which pins its CUDA device via + # TorchDevice.set_session_device(). Resolve that here so the UI can label progress by GPU. + from invokeai.backend.util.devices import TorchDevice + + session_device = TorchDevice.get_session_device() + device = str(session_device) if session_device is not None and session_device.type == "cuda" else None + 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], - progress_image=progress_image, - step=step, - total_steps=total_steps, - order=order, - percentage=cls.calc_percentage(step, total_steps, order), + percentage=percentage, + image=image, + message=message, + device=device, ) - @staticmethod - def calc_percentage(step: int, total_steps: int, scheduler_order: float) -> float: - """Calculate the percentage of completion of denoising.""" - if total_steps == 0: - return 0.0 - if scheduler_order == 2: - return floor((step + 1 + 1) / 2) / floor((total_steps + 1) / 2) - # order == 1 - return (step + 1 + 1) / (total_steps + 1) - @payload_schema.register class InvocationCompleteEvent(InvocationEventBase): @@ -184,6 +192,9 @@ def build( 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], @@ -200,8 +211,6 @@ class InvocationErrorEvent(InvocationEventBase): error_type: str = Field(description="The error type") error_message: str = Field(description="The error message") error_traceback: str = Field(description="The error traceback") - user_id: Optional[str] = Field(default=None, description="The ID of the user who created the invocation") - project_id: Optional[str] = Field(default=None, description="The ID of the user who created the invocation") @classmethod def build( @@ -216,14 +225,15 @@ def build( 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, - user_id=getattr(queue_item, "user_id", None), - project_id=getattr(queue_item, "project_id", None), ) @@ -234,11 +244,15 @@ class QueueItemStatusChangedEvent(QueueItemEventBase): __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: Optional[str] = Field(default=None, description="The timestamp when the queue item was created") - updated_at: Optional[str] = Field(default=None, description="The timestamp when the queue item was last updated") + 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") @@ -253,13 +267,17 @@ def build( 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) if queue_item.created_at else None, - updated_at=str(queue_item.updated_at) if queue_item.updated_at else None, + 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, @@ -279,15 +297,35 @@ class BatchEnqueuedEvent(QueueEventBase): 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) -> "BatchEnqueuedEvent": + 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, ) @@ -369,6 +407,17 @@ 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""" @@ -424,7 +473,7 @@ class ModelInstallDownloadStartedEvent(ModelEventBase): __event_name__ = "model_install_download_started" id: int = Field(description="The ID of the install job") - source: str = Field(description="Source of the model; local path, repo_id or url") + 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") @@ -445,7 +494,7 @@ def build(cls, job: "ModelInstallJob") -> "ModelInstallDownloadStartedEvent": ] return cls( id=job.id, - source=str(job.source), + source=job.source, local_path=job.local_path.as_posix(), parts=parts, bytes=job.bytes, @@ -460,7 +509,7 @@ class ModelInstallDownloadProgressEvent(ModelEventBase): __event_name__ = "model_install_download_progress" id: int = Field(description="The ID of the install job") - source: str = Field(description="Source of the model; local path, repo_id or url") + 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") @@ -481,7 +530,7 @@ def build(cls, job: "ModelInstallJob") -> "ModelInstallDownloadProgressEvent": ] return cls( id=job.id, - source=str(job.source), + source=job.source, local_path=job.local_path.as_posix(), parts=parts, bytes=job.bytes, @@ -496,11 +545,11 @@ class ModelInstallDownloadsCompleteEvent(ModelEventBase): __event_name__ = "model_install_downloads_complete" id: int = Field(description="The ID of the install job") - source: str = Field(description="Source of the model; local path, repo_id or url") + 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=str(job.source)) + return cls(id=job.id, source=job.source) @payload_schema.register @@ -510,11 +559,11 @@ class ModelInstallStartedEvent(ModelEventBase): __event_name__ = "model_install_started" id: int = Field(description="The ID of the install job") - source: str = Field(description="Source of the model; local path, repo_id or url") + 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=str(job.source)) + return cls(id=job.id, source=job.source) @payload_schema.register @@ -524,14 +573,21 @@ class ModelInstallCompleteEvent(ModelEventBase): __event_name__ = "model_install_complete" id: int = Field(description="The ID of the install job") - source: str = Field(description="Source of the model; local path, repo_id or url") + 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=str(job.source), key=(job.config_out.key), total_bytes=job.total_bytes) + 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 @@ -541,11 +597,11 @@ class ModelInstallCancelledEvent(ModelEventBase): __event_name__ = "model_install_cancelled" id: int = Field(description="The ID of the install job") - source: str = Field(description="Source of the model; local path, repo_id or url") + 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=str(job.source)) + return cls(id=job.id, source=job.source) @payload_schema.register @@ -555,7 +611,7 @@ class ModelInstallErrorEvent(ModelEventBase): __event_name__ = "model_install_error" id: int = Field(description="The ID of the install job") - source: str = Field(description="Source of the model; local path, repo_id or url") + 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") @@ -563,7 +619,7 @@ class ModelInstallErrorEvent(ModelEventBase): 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=str(job.source), error_type=job.error_type, error=job.error) + return cls(id=job.id, source=job.source, error_type=job.error_type, error=job.error) class BulkDownloadEventBase(EventBase): @@ -572,6 +628,7 @@ class BulkDownloadEventBase(EventBase): 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 @@ -582,12 +639,17 @@ class BulkDownloadStartedEvent(BulkDownloadEventBase): @classmethod def build( - cls, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str + 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, ) @@ -599,12 +661,17 @@ class BulkDownloadCompleteEvent(BulkDownloadEventBase): @classmethod def build( - cls, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str + 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, ) @@ -618,11 +685,31 @@ class BulkDownloadErrorEvent(BulkDownloadEventBase): @classmethod def build( - cls, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str, error: str + 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 index 8279d3bb344..90e1402773d 100644 --- a/invokeai/app/services/events/events_fastapievents.py +++ b/invokeai/app/services/events/events_fastapievents.py @@ -1,47 +1,54 @@ -# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) - import asyncio import threading -from queue import Empty, Queue from fastapi_events.dispatcher import dispatch -from invokeai.app.services.events.events_common import ( - EventBase, -) - -from .events_base import EventServiceBase +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) -> None: + def __init__(self, event_handler_id: int, loop: asyncio.AbstractEventLoop) -> None: self.event_handler_id = event_handler_id - self._queue = Queue[EventBase | None]() + self._queue = asyncio.Queue[EventBase | None]() self._stop_event = threading.Event() - asyncio.create_task(self._dispatch_from_queue(stop_event=self._stop_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._queue.put(None) + self._loop.call_soon_threadsafe(self._queue.put_nowait, None) def dispatch(self, event: EventBase) -> None: - self._queue.put(event) + 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 = self._queue.get(block=False) + 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 Empty: - await asyncio.sleep(0.1) - pass - 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/image_files_base.py b/invokeai/app/services/image_files/image_files_base.py index dc6609aa48c..7464cd7941d 100644 --- a/invokeai/app/services/image_files/image_files_base.py +++ b/invokeai/app/services/image_files/image_files_base.py @@ -9,12 +9,12 @@ class ImageFileStorageBase(ABC): """Low-level service responsible for storing and retrieving image files.""" @abstractmethod - def get(self, image_name: str) -> PILImageType: + 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) -> Path: + def get_path(self, image_name: str, thumbnail: bool = False, image_subfolder: str = "") -> Path: """Gets the internal path to an image or thumbnail.""" pass @@ -34,21 +34,22 @@ def save( 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) -> None: + 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) -> Optional[str]: + 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) -> Optional[str]: + 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_disk.py b/invokeai/app/services/image_files/image_files_disk.py index 15d0be31f8d..ec84439547a 100644 --- a/invokeai/app/services/image_files/image_files_disk.py +++ b/invokeai/app/services/image_files/image_files_disk.py @@ -1,34 +1,34 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team +import threading from pathlib import Path from queue import Queue -from typing import Dict, Optional, Union +from typing import Optional, Union from PIL import Image, PngImagePlugin from PIL.Image import Image as PILImageType -from send2trash import send2trash +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 -from .image_files_base import ImageFileStorageBase -from .image_files_common import ImageFileDeleteException, ImageFileNotFoundException, ImageFileSaveException - class DiskImageFileStorage(ImageFileStorageBase): """Stores images on disk""" - __output_folder: Path - __cache_ids: Queue # TODO: this is an incredibly naive cache - __cache: Dict[Path, PILImageType] - __max_cache_size: int - __invoker: Invoker - def __init__(self, output_folder: Union[str, Path]): - self.__cache = {} - self.__cache_ids = Queue() + self.__cache: dict[Path, PILImageType] = {} + self.__cache_ids = Queue[Path]() self.__max_cache_size = 10 # TODO: get this from config + # Guards the cache structures (__cache / __cache_ids), which are read and mutated from + # multiple session-processor worker threads in multi-GPU parallel mode. + self.__cache_lock = threading.Lock() - self.__output_folder: Path = output_folder if isinstance(output_folder, Path) else Path(output_folder) + 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() @@ -36,15 +36,22 @@ def __init__(self, output_folder: Union[str, Path]): def start(self, invoker: Invoker) -> None: self.__invoker = invoker - def get(self, image_name: str) -> PILImageType: + def get(self, image_name: str, image_subfolder: str = "") -> PILImageType: try: - image_path = self.get_path(image_name) + 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) + # Image.open() is lazy: it reads the header but defers pixel decoding (and holds the + # file handle open) until the first .load()/.copy()/.convert(). The opened object is + # cached and the SAME object is handed to every caller, so in multi-GPU parallel mode + # two worker threads can call .copy() on it concurrently and race on the shared file + # handle and decoder state, producing "broken data stream" / "self.png is not None" + # errors. Forcing the decode here makes the cached object safe for concurrent reads. + image.load() self.__set_cache(image_path, image) return image except FileNotFoundError as e: @@ -58,10 +65,14 @@ def save( 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_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 = {} @@ -86,7 +97,11 @@ def save( ) thumbnail_name = get_thumbnail_name(image_name) - thumbnail_path = self.get_path(thumbnail_name, thumbnail=True) + 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) @@ -95,49 +110,82 @@ def save( except Exception as e: raise ImageFileSaveException from e - def delete(self, image_name: str) -> None: + def delete(self, image_name: str, image_subfolder: str = "") -> None: try: - image_path = self.get_path(image_name) + image_path = self.get_path(image_name, image_subfolder=image_subfolder) if image_path.exists(): - send2trash(image_path) - if image_path in self.__cache: - del self.__cache[image_path] + image_path.unlink() thumbnail_name = get_thumbnail_name(image_name) - thumbnail_path = self.get_path(thumbnail_name, True) + thumbnail_path = self.get_path(thumbnail_name, True, image_subfolder=image_subfolder) if thumbnail_path.exists(): - send2trash(thumbnail_path) - if thumbnail_path in self.__cache: - del self.__cache[thumbnail_path] + thumbnail_path.unlink() + + with self.__cache_lock: + if image_path in self.__cache: + del self.__cache[image_path] + if thumbnail_path in self.__cache: + del self.__cache[thumbnail_path] except Exception as e: raise ImageFileDeleteException from e - # TODO: make this a bit more flexible for e.g. cloud storage - def get_path(self, image_name: str, thumbnail: bool = False) -> Path: - path = self.__output_folder / image_name - - if thumbnail: - thumbnail_name = get_thumbnail_name(image_name) - path = self.__thumbnails_folder / thumbnail_name - - return path + 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) -> str | None: - image = self.get(image_name) + 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) -> str | None: - image = self.get(image_name) + 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 @@ -150,13 +198,15 @@ def __validate_storage_folders(self) -> None: 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] + with self.__cache_lock: + 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] + with self.__cache_lock: + 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/image_records_base.py b/invokeai/app/services/image_records/image_records_base.py index 45c0705090f..8c71dfba9e7 100644 --- a/invokeai/app/services/image_records/image_records_base.py +++ b/invokeai/app/services/image_records/image_records_base.py @@ -3,9 +3,16 @@ 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 .image_records_common import ImageCategory, ImageRecord, ImageRecordChanges, ResourceOrigin +from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection +from invokeai.app.services.virtual_boards.virtual_boards_common import VirtualSubBoardDTO class ImageRecordStorageBase(ABC): @@ -37,12 +44,17 @@ 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.""" + """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. @@ -58,13 +70,13 @@ def delete_many(self, image_names: list[str]) -> None: pass @abstractmethod - def delete_intermediates(self) -> list[str]: - """Deletes all intermediate image records, returning a list of deleted image names.""" + 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) -> int: - """Gets a count of all intermediate images.""" + 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 @@ -81,11 +93,57 @@ def save( 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 index af681e90e11..3d4650b77ae 100644 --- a/invokeai/app/services/image_records/image_records_common.py +++ b/invokeai/app/services/image_records/image_records_common.py @@ -3,7 +3,7 @@ from enum import Enum from typing import Optional, Union -from pydantic import Field, StrictBool, StrictStr +from pydantic import BaseModel, Field, StrictBool, StrictStr from invokeai.app.util.metaenum import MetaEnum from invokeai.app.util.misc import get_iso_timestamp @@ -58,6 +58,15 @@ class ImageCategory(str, Enum, metaclass=MetaEnum): """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. @@ -106,6 +115,7 @@ def __init__(self, message="Image record not deleted"): "updated_at", "deleted_at", "starred", + "image_subfolder", ] ] ) @@ -147,6 +157,7 @@ class ImageRecord(BaseModelExcludeNull): 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"): @@ -191,6 +202,7 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord: 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, @@ -206,4 +218,18 @@ def deserialize_image_record(image_dict: dict) -> ImageRecord: 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 index ef73e79fa19..1eb3857dba6 100644 --- a/invokeai/app/services/image_records/image_records_sqlite.py +++ b/invokeai/app/services/image_records/image_records_sqlite.py @@ -1,16 +1,13 @@ import sqlite3 -import threading from datetime import datetime from typing import Optional, Union, cast from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator -from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase - -from .image_records_base import ImageRecordStorageBase -from .image_records_common import ( +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, @@ -19,56 +16,66 @@ 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): - _conn: sqlite3.Connection - _cursor: sqlite3.Cursor - _lock: threading.RLock - def __init__(self, db: SqliteDatabase) -> None: super().__init__() - self._lock = db.lock - self._conn = db.conn - self._cursor = self._conn.cursor() + self._db = db def get(self, image_name: str) -> ImageRecord: - try: - self._lock.acquire() - - self._cursor.execute( - f"""--sql - SELECT {IMAGE_DTO_COLS} FROM images - WHERE image_name = ?; - """, - (image_name,), - ) + 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], self._cursor.fetchone()) - except sqlite3.Error as e: - self._conn.rollback() - raise ImageRecordNotFoundException from e - finally: - self._lock.release() + 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_metadata(self, image_name: str) -> Optional[MetadataField]: - try: - self._lock.acquire() - - self._cursor.execute( + def get_user_id(self, image_name: str) -> Optional[str]: + with self._db.transaction() as cursor: + cursor.execute( """--sql - SELECT metadata FROM images + 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")) - result = cast(Optional[sqlite3.Row], self._cursor.fetchone()) + 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 @@ -76,82 +83,76 @@ def get_metadata(self, image_name: str) -> Optional[MetadataField]: 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 - except sqlite3.Error as e: - self._conn.rollback() - raise ImageRecordNotFoundException from e - finally: - self._lock.release() def update( self, image_name: str, changes: ImageRecordChanges, ) -> None: - try: - self._lock.acquire() - # Change the category of the image - if changes.image_category is not None: - self._cursor.execute( - """--sql - UPDATE images - SET image_category = ? - WHERE image_name = ?; - """, - (changes.image_category, image_name), - ) + 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: - self._cursor.execute( - """--sql - UPDATE images - SET session_id = ? - WHERE image_name = ?; - """, - (changes.session_id, 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: - self._cursor.execute( - """--sql - UPDATE images - SET is_intermediate = ? - WHERE image_name = ?; - """, - (changes.is_intermediate, 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: - self._cursor.execute( - """--sql - UPDATE images - SET starred = ? - WHERE image_name = ?; - """, - (changes.starred, 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), + ) - self._conn.commit() - except sqlite3.Error as e: - self._conn.rollback() - raise ImageRecordSaveException from e - finally: - self._lock.release() + 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]: - try: - self._lock.acquire() - + with self._db.transaction() as cursor: # Manually build two queries - one for the count, one for the records count_query = """--sql SELECT COUNT(*) @@ -202,15 +203,38 @@ def get_many( 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) - query_pagination = """--sql - ORDER BY images.starred DESC, images.created_at DESC LIMIT ? OFFSET ? - """ + # 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 + ";" @@ -220,101 +244,81 @@ def get_many( images_params.extend([limit, offset]) # Build the list of images, deserializing each row - self._cursor.execute(images_query, images_params) - result = cast(list[sqlite3.Row], self._cursor.fetchall()) + 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() - self._cursor.execute(count_query, count_params) - count = cast(int, self._cursor.fetchone()[0]) - except sqlite3.Error as e: - self._conn.rollback() - raise e - finally: - self._lock.release() + 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: - try: - self._lock.acquire() - self._cursor.execute( - """--sql - DELETE FROM images - WHERE image_name = ?; - """, - (image_name,), - ) - self._conn.commit() - except sqlite3.Error as e: - self._conn.rollback() - raise ImageRecordDeleteException from e - finally: - self._lock.release() + 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: - try: - placeholders = ",".join("?" for _ in image_names) - - self._lock.acquire() - - # 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 - self._cursor.execute(query, image_names) - - self._conn.commit() - except sqlite3.Error as e: - self._conn.rollback() - raise ImageRecordDeleteException from e - finally: - self._lock.release() - - def get_intermediates_count(self) -> int: - try: - self._lock.acquire() - self._cursor.execute( - """--sql - SELECT COUNT(*) FROM images - WHERE is_intermediate = TRUE; - """ - ) - count = cast(int, self._cursor.fetchone()[0]) - self._conn.commit() - return count - except sqlite3.Error as e: - self._conn.rollback() - raise ImageRecordDeleteException from e - finally: - self._lock.release() - - def delete_intermediates(self) -> list[str]: - try: - self._lock.acquire() - self._cursor.execute( - """--sql - SELECT image_name FROM images - WHERE is_intermediate = TRUE; - """ - ) - result = cast(list[sqlite3.Row], self._cursor.fetchall()) - image_names = [r[0] for r in result] - self._cursor.execute( - """--sql - DELETE FROM images - WHERE is_intermediate = TRUE; - """ - ) - self._conn.commit() - return image_names - except sqlite3.Error as e: - self._conn.rollback() - raise ImageRecordDeleteException from e - finally: - self._lock.release() + 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, @@ -329,79 +333,319 @@ def save( session_id: Optional[str] = None, node_id: Optional[str] = None, metadata: Optional[str] = None, + user_id: Optional[str] = None, + image_subfolder: str = "", ) -> datetime: - try: - self._lock.acquire() - self._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 - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); - """, - ( - image_name, - image_origin.value, - image_category.value, - width, - height, - node_id, - session_id, - metadata, - is_intermediate, - starred, - has_workflow, - ), - ) - self._conn.commit() + 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, + ), + ) - self._cursor.execute( - """--sql - SELECT created_at - FROM images - WHERE image_name = ?; - """, - (image_name,), - ) + cursor.execute( + """--sql + SELECT created_at + FROM images + WHERE image_name = ?; + """, + (image_name,), + ) - created_at = datetime.fromisoformat(self._cursor.fetchone()[0]) + created_at = datetime.fromisoformat(cursor.fetchone()[0]) - return created_at - except sqlite3.Error as e: - self._conn.rollback() - raise ImageRecordSaveException from e - finally: - self._lock.release() + 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]: - try: - self._lock.acquire() - self._cursor.execute( + 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], self._cursor.fetchone()) - finally: - self._lock.release() + 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/images_base.py b/invokeai/app/services/images/images_base.py index 9175fc4809e..aebbead2f35 100644 --- a/invokeai/app/services/images/images_base.py +++ b/invokeai/app/services/images/images_base.py @@ -6,12 +6,14 @@ 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): @@ -53,6 +55,7 @@ def create( 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 @@ -116,12 +119,17 @@ 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.""" + """Gets a paginated list of image DTOs with starred images first when starred_first=True.""" pass @abstractmethod @@ -135,11 +143,27 @@ def delete_intermediates(self) -> int: pass @abstractmethod - def get_intermediates_count(self) -> int: - """Gets the number of intermediate images.""" + 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 index 0464244b944..311f6e556d2 100644 --- a/invokeai/app/services/images/images_common.py +++ b/invokeai/app/services/images/images_common.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import Field +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 @@ -39,3 +39,27 @@ def image_record_to_dto( 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 index 1206526bd58..4a190f37edc 100644 --- a/invokeai/app/services/images/images_default.py +++ b/invokeai/app/services/images/images_default.py @@ -3,16 +3,15 @@ from PIL.Image import Image as PILImageType from invokeai.app.invocations.fields import MetadataField -from invokeai.app.services.invoker import Invoker -from invokeai.app.services.shared.pagination import OffsetPaginatedResults - -from ..image_files.image_files_common import ( +from invokeai.app.services.image_files.image_files_common import ( ImageFileDeleteException, ImageFileNotFoundException, ImageFileSaveException, ) -from ..image_records.image_records_common import ( +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, @@ -22,8 +21,11 @@ InvalidOriginException, ResourceOrigin, ) -from .images_base import ImageServiceABC -from .images_common import ImageDTO, image_record_to_dto +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): @@ -44,6 +46,7 @@ def create( 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 @@ -53,6 +56,11 @@ def create( 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: @@ -71,11 +79,23 @@ def create( node_id=node_id, metadata=metadata, session_id=session_id, + user_id=user_id, + image_subfolder=image_subfolder, ) if board_id is not None: - self.__invoker.services.board_image_records.add_image_to_board(board_id=board_id, image_name=image_name) + 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_name=image_name, + image=image, + metadata=metadata, + workflow=workflow, + graph=graph, + image_subfolder=image_subfolder, ) image_dto = self.get_dto(image_name) @@ -110,7 +130,8 @@ def update( def get_pil_image(self, image_name: str) -> PILImageType: try: - return self.__invoker.services.image_files.get(image_name) + 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 @@ -159,7 +180,8 @@ def get_metadata(self, image_name: str) -> Optional[MetadataField]: def get_workflow(self, image_name: str) -> Optional[str]: try: - return self.__invoker.services.image_files.get_workflow(image_name) + 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 @@ -169,7 +191,8 @@ def get_workflow(self, image_name: str) -> Optional[str]: def get_graph(self, image_name: str) -> Optional[str]: try: - return self.__invoker.services.image_files.get_graph(image_name) + 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 @@ -179,7 +202,12 @@ def get_graph(self, image_name: str) -> Optional[str]: def get_path(self, image_name: str, thumbnail: bool = False) -> str: try: - return str(self.__invoker.services.image_files.get_path(image_name, thumbnail)) + 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 @@ -202,19 +230,29 @@ 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 = [ @@ -239,7 +277,8 @@ def get_many( def delete(self, image_name: str): try: - self.__invoker.services.image_files.delete(image_name) + 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: @@ -254,9 +293,17 @@ def delete(self, image_name: str): 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) + 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: - self.__invoker.services.image_files.delete(image_name) + 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) @@ -267,15 +314,15 @@ def delete_images_on_board(self, board_id: str): 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") + self.__invoker.services.logger.error(f"Problem deleting image records and files: {str(e)}") raise e def delete_intermediates(self) -> int: try: - image_names = self.__invoker.services.image_records.delete_intermediates() - count = len(image_names) - for image_name in image_names: - self.__invoker.services.image_files.delete(image_name) + 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: @@ -288,9 +335,37 @@ def delete_intermediates(self) -> int: self.__invoker.services.logger.error("Problem deleting image records and files") raise e - def get_intermediates_count(self) -> int: + def get_intermediates_count(self, user_id: Optional[str] = None) -> int: try: - return self.__invoker.services.image_records.get_intermediates_count() + 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_services.py b/invokeai/app/services/invocation_services.py index f4fce6098f3..2c95f87b41d 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -4,35 +4,44 @@ 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 - from .board_image_records.board_image_records_base import BoardImageRecordStorageBase - from .board_images.board_images_base import BoardImagesServiceABC - from .board_records.board_records_base import BoardRecordStorageBase - from .boards.boards_base import BoardServiceABC - from .bulk_download.bulk_download_base import BulkDownloadBase - from .config import InvokeAIAppConfig - from .download import DownloadQueueServiceBase - from .events.events_base import EventServiceBase - from .image_files.image_files_base import ImageFileStorageBase - from .image_records.image_records_base import ImageRecordStorageBase - from .images.images_base import ImageServiceABC - from .invocation_cache.invocation_cache_base import InvocationCacheBase - from .invocation_stats.invocation_stats_base import InvocationStatsServiceBase - from .model_images.model_images_base import ModelImageFileStorageBase - from .model_manager.model_manager_base import ModelManagerServiceBase - from .names.names_base import NameServiceBase - from .session_processor.session_processor_base import SessionProcessorBase - from .session_queue.session_queue_base import SessionQueueBase - from .urls.urls_base import UrlServiceBase - from .workflow_records.workflow_records_base import WorkflowRecordsStorageBase - class InvocationServices: """Services that can be used by invocations""" @@ -52,7 +61,10 @@ def __init__( 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", @@ -62,6 +74,11 @@ def __init__( 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 @@ -76,7 +93,10 @@ def __init__( 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 @@ -86,3 +106,8 @@ def __init__( 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/invocation_stats_base.py b/invokeai/app/services/invocation_stats/invocation_stats_base.py index 3266d985fef..1ada23c79b3 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_base.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_base.py @@ -60,7 +60,7 @@ def collect_stats( pass @abstractmethod - def reset_stats(self): + def reset_stats(self, graph_execution_state_id: str) -> None: """Reset all stored statistics.""" pass diff --git a/invokeai/app/services/invocation_stats/invocation_stats_common.py b/invokeai/app/services/invocation_stats/invocation_stats_common.py index f4c906a58f7..4fec6d7bcf0 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_common.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_common.py @@ -14,7 +14,7 @@ class NodeExecutionStatsSummary: node_type: str num_calls: int time_used_seconds: float - peak_vram_gb: float + delta_vram_gb: float @dataclass @@ -58,10 +58,10 @@ class InvocationStatsSummary: 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 Used':>10}\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.peak_vram_gb:>9.3f}G\n" + _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" @@ -100,7 +100,7 @@ class NodeExecutionStats: start_ram_gb: float # GB end_ram_gb: float # GB - peak_vram_gb: float # GB + delta_vram_gb: float # GB def total_time(self) -> float: return self.end_time - self.start_time @@ -174,9 +174,9 @@ def get_node_stats_summaries(self) -> list[NodeExecutionStatsSummary]: 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]) - peak_vram = max([n.peak_vram_gb 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, peak_vram_gb=peak_vram + node_type=node_type, num_calls=num_calls, time_used_seconds=time_used, delta_vram_gb=delta_vram ) summaries.append(summary) diff --git a/invokeai/app/services/invocation_stats/invocation_stats_default.py b/invokeai/app/services/invocation_stats/invocation_stats_default.py index 5a41f1f5d6b..9245d372d2e 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_default.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_default.py @@ -9,11 +9,8 @@ import invokeai.backend.util.logging as logger from invokeai.app.invocations.baseinvocation import BaseInvocation -from invokeai.app.services.invoker import Invoker -from invokeai.backend.model_manager.load.model_cache import CacheStats - -from .invocation_stats_base import InvocationStatsServiceBase -from .invocation_stats_common import ( +from invokeai.app.services.invocation_stats.invocation_stats_base import InvocationStatsServiceBase +from invokeai.app.services.invocation_stats.invocation_stats_common import ( GESStatsNotFoundError, GraphExecutionStats, GraphExecutionStatsSummary, @@ -22,6 +19,8 @@ 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 @@ -53,8 +52,9 @@ def collect_stats(self, invocation: BaseInvocation, graph_execution_state_id: st # Record state before the invocation. start_time = time.time() start_ram = psutil.Process().memory_info().rss - if torch.cuda.is_available(): - torch.cuda.reset_peak_memory_stats() + + # 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] @@ -63,25 +63,29 @@ def collect_stats(self, invocation: BaseInvocation, graph_execution_state_id: st # Let the invocation run. yield None finally: - # Record state after the invocation. + # 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, - peak_vram_gb=torch.cuda.max_memory_allocated() / GB if torch.cuda.is_available() else 0.0, + delta_vram_gb=delta_vram_gb, ) self._stats[graph_execution_state_id].add_node_execution_stats(node_stats) - def reset_stats(self): - self._stats = {} - self._cache_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( diff --git a/invokeai/app/services/invoker.py b/invokeai/app/services/invoker.py index 527afb37f44..64f83725a1d 100644 --- a/invokeai/app/services/invoker.py +++ b/invokeai/app/services/invoker.py @@ -1,7 +1,7 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from .invocation_services import InvocationServices +from invokeai.app.services.invocation_services import InvocationServices class Invoker: diff --git a/invokeai/app/services/model_images/model_images_common.py b/invokeai/app/services/model_images/model_images_common.py index 0d856f2dfe6..4853a06f6a0 100644 --- a/invokeai/app/services/model_images/model_images_common.py +++ b/invokeai/app/services/model_images/model_images_common.py @@ -1,4 +1,4 @@ -# TODO: Should these excpetions subclass existing python exceptions? +# TODO: Should these exceptions subclass existing python exceptions? class ModelImageFileNotFoundException(Exception): """Raised when an image file is not found in storage.""" diff --git a/invokeai/app/services/model_images/model_images_default.py b/invokeai/app/services/model_images/model_images_default.py index 0ab79df3ed5..5fe8086c6a5 100644 --- a/invokeai/app/services/model_images/model_images_default.py +++ b/invokeai/app/services/model_images/model_images_default.py @@ -2,18 +2,16 @@ from PIL import Image from PIL.Image import Image as PILImageType -from send2trash import send2trash from invokeai.app.services.invoker import Invoker -from invokeai.app.util.misc import uuid_string -from invokeai.app.util.thumbnails import make_thumbnail - -from .model_images_base import ModelImageFileStorageBase -from .model_images_common import ( +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): @@ -71,7 +69,7 @@ def delete(self, model_key: str) -> None: if not self._validate_path(path): raise ModelImageFileNotFoundException - send2trash(path) + path.unlink() except Exception as e: raise ModelImageFileDeleteException from e diff --git a/invokeai/app/services/model_install/__init__.py b/invokeai/app/services/model_install/__init__.py index 941485a1345..d96e86cbfed 100644 --- a/invokeai/app/services/model_install/__init__.py +++ b/invokeai/app/services/model_install/__init__.py @@ -1,9 +1,7 @@ """Initialization file for model install service package.""" -from .model_install_base import ( - ModelInstallServiceBase, -) -from .model_install_common import ( +from invokeai.app.services.model_install.model_install_base import ModelInstallServiceBase +from invokeai.app.services.model_install.model_install_common import ( HFModelSource, InstallStatus, LocalModelSource, @@ -12,7 +10,7 @@ UnknownInstallJobException, URLModelSource, ) -from .model_install_default import ModelInstallService +from invokeai.app.services.model_install.model_install_default import ModelInstallService __all__ = [ "ModelInstallServiceBase", diff --git a/invokeai/app/services/model_install/model_install_base.py b/invokeai/app/services/model_install/model_install_base.py index 20afaeaa505..96e1c351415 100644 --- a/invokeai/app/services/model_install/model_install_base.py +++ b/invokeai/app/services/model_install/model_install_base.py @@ -3,17 +3,18 @@ from abc import ABC, abstractmethod from pathlib import Path -from typing import Any, Dict, List, Optional, Union +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.events.events_base import EventServiceBase 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 ModelRecordServiceBase -from invokeai.backend.model_manager import AnyModelConfig +from invokeai.app.services.model_records import ModelRecordChanges, ModelRecordServiceBase + +if TYPE_CHECKING: + from invokeai.app.services.events.events_base import EventServiceBase class ModelInstallServiceBase(ABC): @@ -64,7 +65,7 @@ def event_bus(self) -> Optional["EventServiceBase"]: def register_path( self, model_path: Union[Path, str], - config: Optional[Dict[str, Any]] = None, + config: Optional[ModelRecordChanges] = None, ) -> str: """ Probe and register the model at model_path. @@ -72,7 +73,7 @@ def register_path( This keeps the model in its current location. :param model_path: Filesystem Path to the model. - :param config: Dict of attributes that will override autoassigned values. + :param config: ModelRecordChanges object that will override autoassigned model record values. :returns id: The string ID of the registered model. """ @@ -92,7 +93,7 @@ def unconditionally_delete(self, key: str) -> None: def install_path( self, model_path: Union[Path, str], - config: Optional[Dict[str, Any]] = None, + config: Optional[ModelRecordChanges] = None, ) -> str: """ Probe, register and install the model in the models directory. @@ -101,7 +102,7 @@ def install_path( the models directory handled by InvokeAI. :param model_path: Filesystem Path to the model. - :param config: Dict of attributes that will override autoassigned values. + :param config: ModelRecordChanges object that will override autoassigned model record values. :returns id: The string ID of the registered model. """ @@ -109,14 +110,14 @@ def install_path( def heuristic_import( self, source: str, - config: Optional[Dict[str, Any]] = None, + 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 dict. Any fields in this dict + :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. @@ -147,7 +148,7 @@ def heuristic_import( def import_model( self, source: ModelSource, - config: Optional[Dict[str, Any]] = None, + config: Optional[ModelRecordChanges] = None, ) -> ModelInstallJob: """Install the indicated model. @@ -204,6 +205,22 @@ def prune_jobs(self) -> None: 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. @@ -229,19 +246,6 @@ def wait_for_installs(self, timeout: int = 0) -> List[ModelInstallJob]: will block indefinitely until the installs complete. """ - @abstractmethod - def sync_model_path(self, key: str) -> AnyModelConfig: - """ - Move model into the location indicated by its basetype, type and name. - - Call this after updating a model's attributes in order to move - the model's path into the location indicated by its basetype, type and - name. Applies only to models whose paths are within the root `models_dir` - directory. - - May raise an UnknownModelException. - """ - @abstractmethod def download_and_cache_model(self, source: str | AnyHttpUrl) -> Path: """ @@ -254,7 +258,7 @@ def download_and_cache_model(self, source: str | AnyHttpUrl) -> Path: is periodically cleared of infrequently-used entries when the model converter runs. - Note that this doesn't automaticallly install or register the model, but is + 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 index c1538f543dc..f223c4698c2 100644 --- a/invokeai/app/services/model_install/model_install_common.py +++ b/invokeai/app/services/model_install/model_install_common.py @@ -2,16 +2,23 @@ import traceback from enum import Enum from pathlib import Path -from typing import Any, Dict, Literal, Optional, Set, Union +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.backend.model_manager import AnyModelConfig, ModelRepoVariant -from invokeai.backend.model_manager.config import ModelSourceType +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): @@ -21,6 +28,7 @@ class InstallStatus(str, Enum): 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 @@ -78,9 +86,12 @@ def __str__(self) -> str: class HFModelSource(StringLikeSource): """ - A HuggingFace repo_id with optional variant, sub-folder and access token. + 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 @@ -96,13 +107,23 @@ def proper_repo_id(cls, v: str) -> str: # noqa D102 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}" + base += f"::{self.subfolder.as_posix()}" return base @@ -118,12 +139,27 @@ def __str__(self) -> str: return str(self.url) -ModelSource = Annotated[Union[LocalModelSource, HFModelSource, URLModelSource], Field(discriminator="type")] +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, } @@ -133,8 +169,9 @@ class ModelInstallJob(BaseModel): 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: Dict[str, Any] = Field( - default_factory=dict, description="Configuration information (e.g. 'description') to apply to model." + 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." @@ -164,6 +201,7 @@ class ModelInstallJob(BaseModel): _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.""" @@ -211,6 +249,11 @@ 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.""" diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index dd1b44d8999..7c9fdeee11b 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -1,37 +1,57 @@ """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 copyfile, copytree, move, rmtree +from shutil import move, rmtree from tempfile import mkdtemp -from typing import Any, Dict, List, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union import torch import yaml -from huggingface_hub import HfFolder +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.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_records import DuplicateModelException, ModelRecordServiceBase +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.backend.model_manager.config import ( +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, - CheckpointConfigBase, - InvalidModelConfigException, - ModelRepoVariant, - ModelSourceType, + ModelConfigFactory, ) +from invokeai.backend.model_manager.configs.unknown import Unknown_Config from invokeai.backend.model_manager.metadata import ( AnyModelRepoMetadata, HuggingFaceMetadataFetch, @@ -40,25 +60,28 @@ RemoteModelFile, ) from invokeai.backend.model_manager.metadata.metadata_base import HuggingFaceMetadata -from invokeai.backend.model_manager.probe import ModelProbe 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 -from .model_install_common import ( - MODEL_SOURCE_TO_TYPE_MAP, - HFModelSource, - InstallStatus, - LocalModelSource, - ModelInstallJob, - ModelSource, - StringLikeSource, - URLModelSource, -) +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): @@ -89,13 +112,209 @@ def __init__( 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] = {} + # Per-source locks serializing download_and_cache_model() so parallel (multi-GPU) sessions + # that need the same remote model (e.g. the LaMa infill model) don't race to download into + # the same cache directory. _download_cache_locks_guard protects the dict itself. + self._download_cache_locks: Dict[str, threading.Lock] = {} + self._download_cache_locks_guard = threading.Lock() 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 @@ -131,10 +350,13 @@ def start(self, invoker: Optional[Invoker] = None) -> None: 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: - raise Exception("Attempt to stop the install service before it was started") + return self._logger.debug("calling stop_event.set()") self._stop_event.set() self._clear_pending_jobs() @@ -143,11 +365,23 @@ def stop(self, invoker: Optional[Invoker] = None) -> 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: - self._logger.warning("Cancelling job {job.id}") - self.cancel_job(job) + 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) @@ -164,40 +398,61 @@ def _put_in_queue(self, job: ModelInstallJob) -> None: def register_path( self, model_path: Union[Path, str], - config: Optional[Dict[str, Any]] = None, + config: Optional[ModelRecordChanges] = None, ) -> str: # noqa D102 model_path = Path(model_path) - config = config or {} - if not config.get("source"): - config["source"] = model_path.resolve().as_posix() - config["source_type"] = ModelSourceType.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[Dict[str, Any]] = None, - ) -> str: # noqa D102 + config: Optional[ModelRecordChanges] = None, + ) -> str: model_path = Path(model_path) - config = config or {} - - info: AnyModelConfig = ModelProbe.probe(Path(model_path), config, hash_algo=self._app_config.hashing_algorithm) - - if preferred_name := config.get("name"): - preferred_name = Path(preferred_name).with_suffix(model_path.suffix) + config = config or ModelRecordChanges() + info: AnyModelConfig = self._probe(Path(model_path), config) # type: ignore - dest_path = ( - self.app_config.models_path / info.base.value / info.type.value / (preferred_name or model_path.name) - ) + dest_dir = self.app_config.models_path / info.key try: - new_path = self._copy_model(model_path, dest_path) - except FileExistsError as excp: + 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_path.as_posix()}" - ) from excp + f"A model named {model_path.name} is already installed at {dest_dir.as_posix()}" + ) from e return self._register( - new_path, + dest_path, config, info, ) @@ -205,7 +460,7 @@ def install_path( def heuristic_import( self, source: str, - config: Optional[Dict[str, Any]] = None, + config: Optional[ModelRecordChanges] = None, access_token: Optional[str] = None, inplace: Optional[bool] = False, ) -> ModelInstallJob: @@ -217,7 +472,9 @@ def heuristic_import( source_obj.access_token = access_token return self.import_model(source_obj, config) - def import_model(self, source: ModelSource, config: Optional[Dict[str, Any]] = None) -> ModelInstallJob: # noqa D102 + 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.") @@ -230,6 +487,9 @@ def import_model(self, source: ModelSource, config: Optional[Dict[str, Any]] = N 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)}'") @@ -262,6 +522,8 @@ def wait_for_job(self, job: ModelInstallJob, timeout: int = 0) -> ModelInstallJo 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 @@ -278,6 +540,76 @@ def cancel_job(self, job: ModelInstallJob) -> None: 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.""" @@ -319,16 +651,17 @@ def _migrate_yaml(self) -> None: model_path = self._app_config.models_path / model_path model_path = model_path.resolve() - config: dict[str, Any] = {} - config["name"] = model_name - config["description"] = stanza.get("description") + 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) + 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}") @@ -359,9 +692,20 @@ def delete(self, key: str) -> None: # noqa D102 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) @@ -385,22 +729,47 @@ def download_and_cache_model( 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) - job = self._multifile_download( - dest=model_path, - remote_files=remote_files, - subfolder=model_source.subfolder if isinstance(model_source, HFModelSource) 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) + # Serialize concurrent downloads of the same source. Parallel multi-GPU sessions can each + # request the same remote model (e.g. the LaMa infill model) at once; without this lock they + # both download into the same cache directory and collide on the final rename, which fails on + # Windows with "WinError 32: the file is being used by another process". The other waiters + # find the completed download on the post-lock re-check below and skip downloading. + with self._download_cache_lock(str(source)): + if model_path.exists(): + contents = 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 _download_cache_lock(self, source: str) -> threading.Lock: + """Return the lock that serializes downloads for a given source, creating it on first use.""" + with self._download_cache_locks_guard: + lock = self._download_cache_locks.get(source) + if lock is None: + lock = threading.Lock() + self._download_cache_locks[source] = lock + return lock def _remote_files_from_source( self, source: ModelSource @@ -409,10 +778,13 @@ def _remote_files_from_source( 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, + subfolder=source.subfolder if len(subfolders) <= 1 else None, + subfolders=subfolders if len(subfolders) > 1 else None, session=self._session, ), metadata, @@ -428,7 +800,7 @@ def _remote_files_from_source( except ValueError: pass - return [RemoteModelFile(url=source.url, path=Path("."), size=0)], None + return [RemoteModelFile(url=self._normalize_huggingface_blob_url(source.url), path=Path("."), size=0)], None raise Exception(f"No files associated with {source}") @@ -437,9 +809,16 @@ def _guess_source(self, source: str) -> ModelSource: variants = "|".join(ModelRepoVariant.__members__.values()) hf_repoid_re = f"^([^/:]+/[^/:]+)(?::({variants})?(?::/?([^:]+))?)?$" source_obj: Optional[StringLikeSource] = None - - if Path(source).exists(): # A local file or directory - source_obj = LocalModelSource(path=Path(source)) + 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), @@ -462,6 +841,39 @@ def _start_installer_thread(self) -> None: 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: @@ -491,21 +903,27 @@ def _install_next_item(self) -> None: finally: # if this is an install of a remote file, then clean up the temporary directory if job._install_tmpdir is not None: - rmtree(job._install_tmpdir) + 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__] + 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 + 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) @@ -514,13 +932,78 @@ def _register_or_install(self, job: ModelInstallJob) -> None: 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( - InvalidModelConfigException( + 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." ) ) @@ -535,13 +1018,22 @@ 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}*"): - self._logger.info(f"Removing dangling temporary directory {tmpdir}") - rmtree(tmpdir) + 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 @@ -583,68 +1075,36 @@ def on_model_found(model_path: Path) -> bool: found_models = search.search(self._app_config.models_path) self._logger.info(f"{len(found_models)} new models registered") - def sync_model_path(self, key: str) -> AnyModelConfig: - """ - Move model into the location indicated by its basetype, type and name. - - Call this after updating a model's attributes in order to move - the model's path into the location indicated by its basetype, type and - name. Applies only to models whose paths are within the root `models_dir` - directory. + 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() - May raise an UnknownModelException. - """ - model = self.record_store.get_model(key) - models_dir = self.app_config.models_path - old_path = self.app_config.models_path / model.path - - if not old_path.is_relative_to(models_dir): - # The model is not in the models directory - we don't need to move it. - return model - - new_path = models_dir / model.base.value / model.type.value / old_path.name - - if old_path == new_path or new_path.exists() and old_path == new_path.resolve(): - return model - - self._logger.info(f"Moving {model.name} to {new_path}.") - new_path = self._move_model(old_path, new_path) - model.path = new_path.relative_to(models_dir).as_posix() - self.record_store.update_model(key, ModelRecordChanges(path=model.path)) - return model - - def _copy_model(self, old_path: Path, new_path: Path) -> Path: - if old_path == new_path: - return old_path - new_path.parent.mkdir(parents=True, exist_ok=True) - if old_path.is_dir(): - copytree(old_path, new_path) - else: - copyfile(old_path, new_path) - return new_path - - def _move_model(self, old_path: Path, new_path: Path) -> Path: - if old_path == new_path: - return old_path + 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, + ) - new_path.parent.mkdir(parents=True, exist_ok=True) + 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}") - # if path already exists then we jigger the name to make it unique - counter: int = 1 - while new_path.exists(): - path = new_path.with_stem(new_path.stem + f"_{counter:02d}") - if not path.exists(): - new_path = path - counter += 1 - move(old_path, new_path) - return new_path + return result.config def _register( - self, model_path: Path, config: Optional[Dict[str, Any]] = None, info: Optional[AnyModelConfig] = None + self, model_path: Path, config: Optional[ModelRecordChanges] = None, info: Optional[AnyModelConfig] = None ) -> str: - config = config or {} + config = config or ModelRecordChanges() - info = info or ModelProbe.probe(model_path, config, hash_algo=self._app_config.hashing_algorithm) + 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() @@ -654,7 +1114,7 @@ def _register( info.path = model_path.as_posix() - if isinstance(info, CheckpointConfigBase): + 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 @@ -675,11 +1135,13 @@ def _guess_variant(self) -> Optional[ModelRepoVariant]: precision = TorchDevice.choose_torch_dtype() return ModelRepoVariant.FP16 if precision == torch.float16 else None - def _import_local_model(self, source: LocalModelSource, config: Optional[Dict[str, Any]]) -> ModelInstallJob: + def _import_local_model( + self, source: LocalModelSource, config: Optional[ModelRecordChanges] = None + ) -> ModelInstallJob: return ModelInstallJob( id=self._next_id(), source=source, - config_in=config or {}, + config_in=config or ModelRecordChanges(), local_path=Path(source.path), inplace=source.inplace or False, ) @@ -687,11 +1149,11 @@ def _import_local_model(self, source: LocalModelSource, config: Optional[Dict[st def _import_from_hf( self, source: HFModelSource, - config: Optional[Dict[str, Any]] = None, + config: Optional[ModelRecordChanges] = None, ) -> ModelInstallJob: # Add user's cached access token to HuggingFace requests if source.access_token is None: - source.access_token = HfFolder.get_token() + source.access_token = hf_get_token() remote_files, metadata = self._remote_files_from_source(source) return self._import_remote_model( source=source, @@ -703,7 +1165,7 @@ def _import_from_hf( def _import_from_url( self, source: URLModelSource, - config: Optional[Dict[str, Any]], + config: Optional[ModelRecordChanges] = None, ) -> ModelInstallJob: remote_files, metadata = self._remote_files_from_source(source) return self._import_remote_model( @@ -713,25 +1175,40 @@ def _import_from_url( 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[Dict[str, Any]], + config: Optional[ModelRecordChanges], ) -> ModelInstallJob: if len(remote_files) == 0: raise ValueError(f"{source}: No downloadable files found") - destdir = Path( - mkdtemp( - dir=self._app_config.models_path, - prefix=TMPDIR_PREFIX, + 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 {}, + 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, @@ -739,23 +1216,81 @@ def _import_remote_model( ) # remember the temporary directory for later removal install_job._install_tmpdir = destdir - install_job.total_bytes = sum((x.size or 0) for x in remote_files) + + # 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=source.subfolder if isinstance(source, HFModelSource) else None, + 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 ) - self._download_cache[multifile_job.id] = install_job - install_job._multifile_job = multifile_job - + 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 install_job + return job def _stat_size(self, path: Path) -> int: size = 0 @@ -771,6 +1306,7 @@ def _multifile_download( 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: @@ -778,23 +1314,61 @@ def _multifile_download( # 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. - if subfolder: + # + # 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.parts[-1] # sdxl-turbo/vae/ - path_to_add = Path(f"{top}_{subfolder}") - else: - path_to_remove = Path(".") - path_to_add = Path(".") - - parts: List[RemoteModelFile] = [] - for model_file in remote_files: - assert model_file.size is not None - parts.append( - RemoteModelFile( - url=model_file.url, # if a subfolder, then sdxl-turbo_vae/config.json - path=path_to_add / model_file.path.relative_to(path_to_remove), + 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, @@ -821,7 +1395,9 @@ def _download_started_callback(self, download_job: MultiFileDownloadJob) -> None 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) - install_job.total_bytes = download_job.total_bytes + 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: @@ -832,7 +1408,9 @@ def _download_progress_callback(self, download_job: MultiFileDownloadJob) -> Non else: # update sizes install_job.bytes = sum(x.bytes for x in download_job.download_parts) - install_job.total_bytes = sum(x.total_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: @@ -848,8 +1426,10 @@ def _download_error_callback(self, download_job: MultiFileDownloadJob, excp: Opt with self._lock: if install_job := self._download_cache.pop(download_job.id, None): assert excp is not None - install_job.set_error(excp) + 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() @@ -858,9 +1438,19 @@ def _download_cancelled_callback(self, download_job: MultiFileDownloadJob) -> No 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: + 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() @@ -871,6 +1461,7 @@ def _download_cancelled_callback(self, download_job: MultiFileDownloadJob) -> No 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) @@ -880,6 +1471,7 @@ def _signal_job_download_started(self, job: ModelInstallJob) -> 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: @@ -891,6 +1483,7 @@ def _signal_job_downloading(self, job: ModelInstallJob) -> None: 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) @@ -899,6 +1492,8 @@ def _signal_job_completed(self, job: ModelInstallJob) -> None: 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 @@ -906,6 +1501,8 @@ def _signal_job_completed(self, job: ModelInstallJob) -> None: 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 @@ -913,6 +1510,8 @@ def _signal_job_errored(self, job: ModelInstallJob) -> None: 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) @@ -927,3 +1526,15 @@ def get_fetcher_from_url(url: str) -> Type[ModelMetadataFetchBase]: 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 index b4a86e9348d..4c7e40c8c76 100644 --- a/invokeai/app/services/model_load/__init__.py +++ b/invokeai/app/services/model_load/__init__.py @@ -1,6 +1,6 @@ """Initialization file for model load service module.""" -from .model_load_base import ModelLoadServiceBase -from .model_load_default import ModelLoadService +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 index da567721956..8fc9823328d 100644 --- a/invokeai/app/services/model_load/model_load_base.py +++ b/invokeai/app/services/model_load/model_load_base.py @@ -5,10 +5,10 @@ from pathlib import Path from typing import Callable, Optional -from invokeai.backend.model_manager import AnyModel, AnyModelConfig, SubModelType +from invokeai.backend.model_manager.configs.factory import AnyModelConfig from invokeai.backend.model_manager.load import LoadedModel, LoadedModelWithoutConfig -from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase -from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase +from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache +from invokeai.backend.model_manager.taxonomy import AnyModel, SubModelType class ModelLoadServiceBase(ABC): @@ -25,13 +25,22 @@ def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubMo @property @abstractmethod - def ram_cache(self) -> ModelCacheBase[AnyModel]: - """Return the RAM cache used by this loader.""" + def ram_cache(self) -> ModelCache: + """Return the RAM cache for the current thread's execution device. + + In multi-GPU mode, each session-processor worker is pinned to a device and gets its own + cache; this resolves to the calling thread's cache. Outside a worker (e.g. API threads), + it resolves to the default device's cache. + """ @property @abstractmethod - def convert_cache(self) -> ModelConvertCacheBase: - """Return the checkpoint convert cache used by this loader.""" + def ram_caches(self) -> dict[str, ModelCache]: + """Return all per-device RAM caches, keyed by normalized device string. + + Use this for maintenance operations that must apply to every device (clear cache, drop a + model from all devices, shutdown). + """ @abstractmethod def load_model_from_path( diff --git a/invokeai/app/services/model_load/model_load_default.py b/invokeai/app/services/model_load/model_load_default.py index 70674813785..33c7ef6108c 100644 --- a/invokeai/app/services/model_load/model_load_default.py +++ b/invokeai/app/services/model_load/model_load_default.py @@ -10,21 +10,20 @@ from invokeai.app.services.config import InvokeAIAppConfig from invokeai.app.services.invoker import Invoker -from invokeai.backend.model_manager import AnyModel, AnyModelConfig, SubModelType +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.convert_cache import ModelConvertCacheBase -from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase +from invokeai.backend.model_manager.load.model_cache.model_cache import MODEL_LOAD_LOCK, 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 -from .model_load_base import ModelLoadServiceBase - class ModelLoadService(ModelLoadServiceBase): """Wrapper around ModelLoaderRegistry.""" @@ -32,31 +31,46 @@ class ModelLoadService(ModelLoadServiceBase): def __init__( self, app_config: InvokeAIAppConfig, - ram_cache: ModelCacheBase[AnyModel], - convert_cache: ModelConvertCacheBase, + ram_cache: ModelCache, registry: Optional[Type[ModelLoaderRegistryBase]] = ModelLoaderRegistry, + ram_caches: Optional[dict[str, ModelCache]] = None, ): - """Initialize the model load service.""" + """Initialize the model load service. + + Args: + ram_cache: The default RAM cache, used when no per-device cache matches the calling + thread (e.g. single-device installs, or API threads). + ram_caches: Optional map of normalized device string -> ModelCache for multi-GPU mode. + One cache per generation device. The default `ram_cache` is always included. + """ 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._convert_cache = convert_cache + self._default_ram_cache = ram_cache + # Map normalized device string -> cache. Always includes the default cache so that callers + # without a pinned device (API threads) resolve to a valid cache. + self._ram_caches: dict[str, ModelCache] = dict(ram_caches) if ram_caches else {} + self._ram_caches.setdefault(str(TorchDevice.normalize(ram_cache.execution_device)), ram_cache) self._registry = registry def start(self, invoker: Invoker) -> None: self._invoker = invoker @property - def ram_cache(self) -> ModelCacheBase[AnyModel]: - """Return the RAM cache used by this loader.""" - return self._ram_cache + def ram_cache(self) -> ModelCache: + """Return the RAM cache for the calling thread's execution device. + + `choose_torch_device()` is thread-local-aware: a session-processor worker pinned to a GPU + gets that GPU's cache; everything else falls back to the default cache. + """ + key = str(TorchDevice.choose_torch_device()) + return self._ram_caches.get(key, self._default_ram_cache) @property - def convert_cache(self) -> ModelConvertCacheBase: - """Return the checkpoint convert cache used by this loader.""" - return self._convert_cache + def ram_caches(self) -> dict[str, ModelCache]: + """Return all per-device RAM caches, keyed by normalized device string.""" + return dict(self._ram_caches) def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ @@ -75,8 +89,7 @@ def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubMo loaded_model: LoadedModel = implementation( app_config=self._app_config, logger=self._logger, - ram_cache=self._ram_cache, - convert_cache=self._convert_cache, + ram_cache=self.ram_cache, ).load_model(model_config, submodel_type) if hasattr(self, "_invoker"): @@ -87,17 +100,33 @@ def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubMo def load_model_from_path( self, model_path: Path, loader: Optional[Callable[[Path], AnyModel]] = None ) -> LoadedModelWithoutConfig: - cache_key = str(model_path) + # Resolve the calling thread's cache once so the whole load uses a single device's cache. ram_cache = self.ram_cache + cache_key = str(model_path) try: - return LoadedModelWithoutConfig(_locker=ram_cache.get(key=cache_key)) + return LoadedModelWithoutConfig(cache_record=ram_cache.get(key=cache_key), cache=ram_cache) except IndexError: pass def torch_load_file(checkpoint: Path) -> AnyModel: scan_result = scan_file_path(checkpoint) if scan_result.infected_files != 0: - raise Exception("The model at {checkpoint} is potentially infected by malware. Aborting load.") + 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 @@ -105,7 +134,7 @@ def diffusers_load_directory(directory: Path) -> AnyModel: load_class = GenericDiffusersLoader( app_config=self._app_config, logger=self._logger, - ram_cache=self._ram_cache, + ram_cache=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()) @@ -118,6 +147,15 @@ def diffusers_load_directory(directory: Path) -> AnyModel: else lambda path: safetensors_load_file(path, device="cpu") ) assert loader is not None - raw_model = loader(model_path) - ram_cache.put(key=cache_key, model=raw_model) - return LoadedModelWithoutConfig(_locker=ram_cache.get(key=cache_key)) + # Serialize construction (see MODEL_LOAD_LOCK): the diffusers loader path uses the same + # process-global, non-thread-safe monkey-patches as the main loader, so it takes the write + # lock to exclude concurrent VRAM moves. Re-check the cache after acquiring the lock in case + # a worker sharing this cache built it while we waited. + with MODEL_LOAD_LOCK.write_lock(): + try: + return LoadedModelWithoutConfig(cache_record=ram_cache.get(key=cache_key), cache=ram_cache) + except IndexError: + pass + raw_model = loader(model_path) + ram_cache.put(key=cache_key, model=raw_model) + return LoadedModelWithoutConfig(cache_record=ram_cache.get(key=cache_key), cache=ram_cache) diff --git a/invokeai/app/services/model_manager/__init__.py b/invokeai/app/services/model_manager/__init__.py index 5455577266a..e703d4f1ffc 100644 --- a/invokeai/app/services/model_manager/__init__.py +++ b/invokeai/app/services/model_manager/__init__.py @@ -1,17 +1,10 @@ """Initialization file for model manager service.""" -from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelType, SubModelType +from invokeai.app.services.model_manager.model_manager_default import ModelManagerService, ModelManagerServiceBase from invokeai.backend.model_manager.load import LoadedModel -from .model_manager_default import ModelManagerService, ModelManagerServiceBase - __all__ = [ "ModelManagerServiceBase", "ModelManagerService", - "AnyModel", - "AnyModelConfig", - "BaseModelType", - "ModelType", - "SubModelType", "LoadedModel", ] diff --git a/invokeai/app/services/model_manager/model_manager_base.py b/invokeai/app/services/model_manager/model_manager_base.py index af1b68e1ec3..a906076b163 100644 --- a/invokeai/app/services/model_manager/model_manager_base.py +++ b/invokeai/app/services/model_manager/model_manager_base.py @@ -5,14 +5,13 @@ 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 ..config import InvokeAIAppConfig -from ..download import DownloadQueueServiceBase -from ..events.events_base import EventServiceBase -from ..model_install import ModelInstallServiceBase -from ..model_load import ModelLoadServiceBase -from ..model_records import ModelRecordServiceBase +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): diff --git a/invokeai/app/services/model_manager/model_manager_default.py b/invokeai/app/services/model_manager/model_manager_default.py index 1a2b9a34022..176b61ddcab 100644 --- a/invokeai/app/services/model_manager/model_manager_default.py +++ b/invokeai/app/services/model_manager/model_manager_default.py @@ -6,19 +6,23 @@ 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.backend.model_manager.load import ModelCache, ModelConvertCache, ModelLoaderRegistry +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_cache.ram_budget import RamBudget +from invokeai.backend.model_manager.load.model_cache.shared_cpu_weights import SharedCpuWeightsStore +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 -from ..config import InvokeAIAppConfig -from ..download import DownloadQueueServiceBase -from ..events.events_base import EventServiceBase -from ..model_install import ModelInstallService, ModelInstallServiceBase -from ..model_load import ModelLoadService, ModelLoadServiceBase -from ..model_records import ModelRecordServiceBase -from .model_manager_base import ModelManagerServiceBase - class ModelManagerService(ModelManagerServiceBase): """ @@ -58,6 +62,11 @@ def start(self, invoker: Invoker) -> None: service.start(invoker) def stop(self, invoker: Invoker) -> None: + # Shutdown every per-device model cache to cancel any pending keep-alive timers. + if hasattr(self._load, "ram_caches"): + for cache in self._load.ram_caches.values(): + cache.shutdown() + for service in [self._store, self._install, self._load]: if hasattr(service, "stop"): service.stop(invoker) @@ -79,19 +88,76 @@ def build_model_manager( logger = InvokeAILogger.get_logger(cls.__name__) logger.setLevel(app_config.log_level.upper()) - ram_cache = ModelCache( - max_cache_size=app_config.ram, - max_vram_cache_size=app_config.vram, - lazy_offloading=app_config.lazy_offload, - logger=logger, - execution_device=execution_device or TorchDevice.choose_torch_device(), + # One store + budget shared by every per-device cache. The store deduplicates each model's CPU + # weights to a single copy across GPUs (see SharedCpuWeightsStore); the budget is the single + # system-wide RAM authority so per-device caches stop double-counting shared weights when they + # decide what to evict (see RamBudget). + shared_store = SharedCpuWeightsStore() + + def build_cache(device: torch.device) -> ModelCache: + return 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=device, + storage_device="cpu", + log_memory_usage=app_config.log_memory_usage, + logger=logger, + keep_alive_minutes=app_config.model_cache_keep_alive_min, + shared_cpu_weights=shared_store, + ) + + # The default cache for callers without a pinned device (API threads, single-device installs). + default_device = execution_device or TorchDevice.choose_torch_device() + ram_cache = build_cache(default_device) + + # In multi-GPU mode, build one independent cache per generation device. Each session-processor + # worker is pinned to a device (see TorchDevice.set_session_device) and resolves to its own + # cache. The default cache is always included by ModelLoadService. + ram_caches: dict[str, ModelCache] = {str(TorchDevice.normalize(default_device)): ram_cache} + for device in TorchDevice.get_generation_devices(app_config.generation_devices): + key = str(device) + if key not in ram_caches: + ram_caches[key] = build_cache(device) + + # Attach the single global RAM budget. The cap is the user's max_cache_ram_gb interpreted as a + # true system-wide limit; when unset, it is the sum of the caches' individually-calculated + # sizes, so each device keeps its effective capacity and weight deduplication becomes headroom. + # That sum is then clamped to a safe fraction of system RAM: each per-device heuristic already + # allows up to ~half of system RAM, so summing across N GPUs would otherwise claim ~N× that and + # leave nothing for the OS, causing swap thrashing. The clamp leaves real headroom; shared-weight + # dedup means the true footprint usually stays well under the cap regardless. + gb = 2**30 + distinct_caches = list(dict.fromkeys(ram_caches.values())) + # Cross-device weight adoption (and its per-model meta-shell capture) only pays off with more + # than one device cache; disable the capture cost otherwise. + shared_store.enable_shell_capture = len(distinct_caches) > 1 + if app_config.max_cache_ram_gb is not None: + global_ram_budget_bytes = int(app_config.max_cache_ram_gb * gb) + else: + summed_cache_bytes = sum(c.local_ram_cache_size_bytes for c in distinct_caches) + system_ram_headroom_bytes = ModelCache.calc_system_ram_headroom_bytes() + global_ram_budget_bytes = min(summed_cache_bytes, system_ram_headroom_bytes) + if global_ram_budget_bytes < summed_cache_bytes: + logger.info( + f"Capping model cache RAM budget at {global_ram_budget_bytes / gb:.2f} GB to leave system " + f"headroom (sum of per-device caches was {summed_cache_bytes / gb:.2f} GB)." + ) + ram_budget = RamBudget(max_bytes=global_ram_budget_bytes, shared_store=shared_store) + for cache in distinct_caches: + cache.set_ram_budget(ram_budget) + logger.info( + f"Model cache global RAM budget: {global_ram_budget_bytes / gb:.2f} GB " + f"across {len(distinct_caches)} device cache(s)." ) - convert_cache = ModelConvertCache(cache_path=app_config.convert_cache_path, max_size=app_config.convert_cache) + loader = ModelLoadService( app_config=app_config, ram_cache=ram_cache, - convert_cache=convert_cache, registry=ModelLoaderRegistry, + ram_caches=ram_caches, ) installer = ModelInstallService( app_config=app_config, diff --git a/invokeai/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py index 57531cf3c19..e06f8f2df91 100644 --- a/invokeai/app/services/model_records/model_records_base.py +++ b/invokeai/app/services/model_records/model_records_base.py @@ -6,21 +6,34 @@ from abc import ABC, abstractmethod from enum import Enum from pathlib import Path -from typing import List, Optional, Set, Union +from typing import Any, List, Optional, Set, Union -from pydantic import BaseModel, Field +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.config import ( - AnyModelConfig, +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, - ControlAdapterDefaultSettings, - MainModelDefaultSettings, + ClipVariantType, + Flux2VariantType, + FluxVariantType, ModelFormat, + ModelSourceType, ModelType, ModelVariantType, + Qwen3VariantType, + QwenImageVariantType, SchedulerPredictionType, + ZImageVariantType, ) @@ -48,6 +61,10 @@ class ModelRecordOrderBy(str, Enum): Base = "base" Name = "name" Format = "format" + Size = "size" + DateAdded = "created_at" + DateModified = "updated_at" + Path = "path" class ModelSummary(BaseModel): @@ -66,18 +83,59 @@ 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 | ControlAdapterDefaultSettings] = Field( - description="Default settings 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] = Field(description="The variant of the model.", default=None) + 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 ) @@ -113,12 +171,26 @@ def del_model(self, key: str) -> None: pass @abstractmethod - def update_model(self, key: str, changes: ModelRecordChanges) -> AnyModelConfig: + 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 @@ -146,7 +218,11 @@ def get_model_by_hash(self, hash: str) -> AnyModelConfig: @abstractmethod def list_models( - self, page: int = 0, per_page: int = 10, order_by: ModelRecordOrderBy = ModelRecordOrderBy.Default + 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 @@ -183,6 +259,8 @@ def search_by_attr( 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. diff --git a/invokeai/app/services/model_records/model_records_sql.py b/invokeai/app/services/model_records/model_records_sql.py index 16abf4c523c..3452ba44316 100644 --- a/invokeai/app/services/model_records/model_records_sql.py +++ b/invokeai/app/services/model_records/model_records_sql.py @@ -40,22 +40,16 @@ """ import json +import logging import sqlite3 from math import ceil from pathlib import Path from typing import List, Optional, Union -from invokeai.app.services.shared.pagination import PaginatedResults -from invokeai.backend.model_manager.config import ( - AnyModelConfig, - BaseModelType, - ModelConfigFactory, - ModelFormat, - ModelType, -) +import pydantic +from pydantic import ValidationError -from ..shared.sqlite.sqlite_database import SqliteDatabase -from .model_records_base import ( +from invokeai.app.services.model_records.model_records_base import ( DuplicateModelException, ModelRecordChanges, ModelRecordOrderBy, @@ -63,12 +57,42 @@ 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): + def __init__(self, db: SqliteDatabase, logger: logging.Logger): """ Initialize a new object from preexisting sqlite3 connection and threading lock objects. @@ -76,12 +100,7 @@ def __init__(self, db: SqliteDatabase): """ super().__init__() self._db = db - self._cursor = db.conn.cursor() - - @property - def db(self) -> SqliteDatabase: - """Return the underlying database.""" - return self._db + self._logger = logger def add_model(self, config: AnyModelConfig) -> AnyModelConfig: """ @@ -93,14 +112,14 @@ def add_model(self, config: AnyModelConfig) -> AnyModelConfig: Can raise DuplicateModelException and InvalidModelConfigException exceptions. """ - with self._db.lock: + with self._db.transaction() as cursor: try: - self._cursor.execute( + cursor.execute( """--sql INSERT INTO models ( - id, - config - ) + id, + config + ) VALUES (?,?); """, ( @@ -108,10 +127,8 @@ def add_model(self, config: AnyModelConfig) -> AnyModelConfig: config.model_dump_json(), ), ) - self._db.conn.commit() except sqlite3.IntegrityError as e: - self._db.conn.rollback() if "UNIQUE constraint failed" in str(e): if "models.path" in str(e): msg = f"A model with path '{config.path}' is already installed" @@ -122,9 +139,6 @@ def add_model(self, config: AnyModelConfig) -> AnyModelConfig: raise DuplicateModelException(msg) from e else: raise e - except sqlite3.Error as e: - self._db.conn.rollback() - raise e return self.get_model(config.key) @@ -136,49 +150,93 @@ def del_model(self, key: str) -> None: Can raise an UnknownModelException """ - with self._db.lock: - try: - self._cursor.execute( - """--sql - DELETE FROM models - WHERE id=?; - """, - (key,), - ) - if self._cursor.rowcount == 0: - raise UnknownModelException("model not found") - self._db.conn.commit() - except sqlite3.Error as e: - self._db.conn.rollback() - raise e - - def update_model(self, key: str, changes: ModelRecordChanges) -> AnyModelConfig: - record = self.get_model(key) - - # Model configs use pydantic's `validate_assignment`, so each change is validated by pydantic. - for field_name in changes.model_fields_set: - setattr(record, field_name, getattr(changes, field_name)) + with self._db.transaction() as cursor: + cursor.execute( + """--sql + DELETE FROM models + WHERE id=?; + """, + (key,), + ) + if cursor.rowcount == 0: + raise UnknownModelException("model not found") - json_serialized = record.model_dump_json() + 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") - with self._db.lock: - try: - self._cursor.execute( - """--sql - UPDATE models - SET - config=? - WHERE id=?; - """, - (json_serialized, key), - ) - if self._cursor.rowcount == 0: - raise UnknownModelException("model not found") - self._db.conn.commit() - except sqlite3.Error as e: - self._db.conn.rollback() - raise e + 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: @@ -189,33 +247,33 @@ def get_model(self, key: str) -> AnyModelConfig: Exceptions: UnknownModelException """ - with self._db.lock: - self._cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql - SELECT config, strftime('%s',updated_at) FROM models + SELECT config FROM models WHERE id=?; """, (key,), ) - rows = self._cursor.fetchone() - if not rows: - raise UnknownModelException("model not found") - model = ModelConfigFactory.make_config(json.loads(rows[0]), timestamp=rows[1]) + 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.lock: - self._cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql - SELECT config, strftime('%s',updated_at) FROM models + SELECT config FROM models WHERE hash=?; """, (hash,), ) - rows = self._cursor.fetchone() - if not rows: - raise UnknownModelException("model not found") - model = ModelConfigFactory.make_config(json.loads(rows[0]), timestamp=rows[1]) + 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: @@ -224,16 +282,15 @@ def exists(self, key: str) -> bool: :param key: Unique key for the model to be deleted """ - count = 0 - with self._db.lock: - self._cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql select count(*) FROM models WHERE id=?; """, (key,), ) - count = self._cursor.fetchone()[0] + count = cursor.fetchone()[0] return count > 0 def search_by_attr( @@ -243,6 +300,7 @@ def search_by_attr( 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. @@ -252,111 +310,141 @@ def search_by_attr( :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. """ - - assert isinstance(order_by, ModelRecordOrderBy) - ordering = { - ModelRecordOrderBy.Default: "type, base, name, format", - ModelRecordOrderBy.Type: "type", - ModelRecordOrderBy.Base: "base", - ModelRecordOrderBy.Name: "name", - ModelRecordOrderBy.Format: "format", - } - - 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 "" - with self._db.lock: - self._cursor.execute( + 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, strftime('%s',updated_at) + SELECT config FROM models {where} - ORDER BY {ordering[order_by]} -- using ? to bind doesn't work here for some reason; + ORDER BY {ordering[order_by]} {order_dir} -- using ? to bind doesn't work here for some reason; """, tuple(bindings), ) - result = self._cursor.fetchall() - results = [ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in result] + 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.""" - results = [] - with self._db.lock: - self._cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql - SELECT config, strftime('%s',updated_at) FROM models + SELECT config FROM models WHERE path=?; """, (str(path),), ) - results = [ - ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in self._cursor.fetchall() - ] + 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.""" - results = [] - with self._db.lock: - self._cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql - SELECT config, strftime('%s',updated_at) FROM models + SELECT config FROM models WHERE hash=?; """, (hash,), ) - results = [ - ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in self._cursor.fetchall() - ] + 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 + 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.""" - assert isinstance(order_by, ModelRecordOrderBy) - ordering = { - ModelRecordOrderBy.Default: "type, base, name, format", - ModelRecordOrderBy.Type: "type", - ModelRecordOrderBy.Base: "base", - ModelRecordOrderBy.Name: "name", - ModelRecordOrderBy.Format: "format", - } - - # Lock so that the database isn't updated while we're doing the two queries. - with self._db.lock: + 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 - self._cursor.execute( + cursor.execute( """--sql select count(*) from models; """, (), ) - total = int(self._cursor.fetchone()[0]) + total = int(cursor.fetchone()[0]) # query2: fetch key fields - self._cursor.execute( + cursor.execute( f"""--sql SELECT config FROM models - ORDER BY {ordering[order_by]} -- using ? to bind doesn't work here for some reason + ORDER BY {ordering[order_by]} {order_dir} -- using ? to bind doesn't work here for some reason LIMIT ? OFFSET ?; """, @@ -365,8 +453,6 @@ def list_models( page * per_page, ), ) - rows = self._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 - ) + 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/names_default.py b/invokeai/app/services/names/names_default.py index 104268c8bdd..5804a937d6a 100644 --- a/invokeai/app/services/names/names_default.py +++ b/invokeai/app/services/names/names_default.py @@ -1,7 +1,6 @@ +from invokeai.app.services.names.names_base import NameServiceBase from invokeai.app.util.misc import uuid_string -from .names_base import NameServiceBase - class SimpleNameService(NameServiceBase): """Creates image names from UUIDs.""" diff --git a/invokeai/app/services/object_serializer/object_serializer_disk.py b/invokeai/app/services/object_serializer/object_serializer_disk.py index 8edd29e1505..bbd3f785507 100644 --- a/invokeai/app/services/object_serializer/object_serializer_disk.py +++ b/invokeai/app/services/object_serializer/object_serializer_disk.py @@ -21,10 +21,16 @@ 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, ephemeral: bool = False): + 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 @@ -42,6 +48,8 @@ def __init__(self, output_dir: Path, ephemeral: bool = False): 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: diff --git a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py index b361259a4b1..ae00173e422 100644 --- a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py +++ b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py @@ -1,4 +1,5 @@ from queue import Queue +from threading import Lock from typing import TYPE_CHECKING, Optional, TypeVar from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase @@ -21,6 +22,9 @@ def __init__(self, underlying_storage: ObjectSerializerBase[T], max_cache_size: self._cache: dict[str, T] = {} self._cache_ids = Queue[str]() self._max_cache_size = max_cache_size + # Guards the in-memory cache so concurrent session-processor workers (multi-GPU) can't race + # the check-then-evict in `_set_cache` (which could otherwise raise KeyError on eviction). + self._cache_lock = Lock() def start(self, invoker: "Invoker") -> None: self._invoker = invoker @@ -50,16 +54,19 @@ def save(self, obj: T) -> str: def delete(self, name: str) -> None: self._underlying_storage.delete(name) - if name in self._cache: - del self._cache[name] + with self._cache_lock: + 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] + with self._cache_lock: + 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()) + with self._cache_lock: + 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/session_processor_common.py b/invokeai/app/services/session_processor/session_processor_common.py index 0ca51de517c..346f12d8bbc 100644 --- a/invokeai/app/services/session_processor/session_processor_common.py +++ b/invokeai/app/services/session_processor/session_processor_common.py @@ -1,5 +1,8 @@ +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") @@ -15,6 +18,16 @@ class CanceledException(Exception): class ProgressImage(BaseModel): """The progress image sent intermittently during processing""" - width: int = Field(description="The effective width of the image in pixels") - height: int = Field(description="The effective height of the image in pixels") + 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 index 3f348fb239d..e776dc79614 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -1,8 +1,11 @@ +import gc import traceback -from contextlib import suppress +from contextlib import contextmanager, suppress from threading import BoundedSemaphore, Thread from threading import Event as ThreadEvent -from typing import Optional +from typing import Iterator, Optional + +import torch from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput from invokeai.app.services.events.events_common import ( @@ -13,23 +16,25 @@ 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 +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 - -from ..invoker import Invoker -from .session_processor_base import InvocationServices, SessionProcessorBase, SessionRunnerBase -from .session_processor_common import SessionProcessorStatus +from invokeai.backend.util.device_pool import GENERATION_DEVICE_POOL +from invokeai.backend.util.devices import TorchDevice class DefaultSessionRunner(SessionRunnerBase): @@ -125,16 +130,14 @@ def run_node(self, invocation: BaseInvocation, queue_item: SessionQueueItem): is_canceled=self._is_canceled, ) - # Invoke the node - output = invocation.invoke_internal(context=context, services=self._services) + # Invoke the node, optionally on a borrowed idle GPU (text encoders only). + with self._maybe_offload_to_idle_gpu(invocation): + 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 KeyboardInterrupt: - # TODO(psyche): This is expected to be caught in the main thread. Do we need to catch this here? - pass 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 @@ -155,6 +158,45 @@ def run_node(self, invocation: BaseInvocation, queue_item: SessionQueueItem): error_traceback=error_traceback, ) + @contextmanager + def _maybe_offload_to_idle_gpu(self, invocation: BaseInvocation) -> Iterator[None]: + """Temporarily re-pin this worker thread to an idle GPU for a text-encoder node. + + When ``offload_text_encoders_to_idle_gpus`` is enabled and an idle generation GPU can be + borrowed, the encoder model loads into that GPU's cache and its forward runs there (all + device-selecting code resolves to the pinned device), keeping the busy GPU's denoise model + resident. The conditioning output is stored on the CPU, so the denoiser picks it up on the + worker's own GPU after the pin is restored. + + The borrow holds the idle device's exclusive-use lock for the whole node, so a native + session on that GPU can never run concurrently against the same cached encoder (which would + corrupt it). If no idle GPU is free, the node runs on the worker's own GPU unchanged. + """ + native_device = TorchDevice.get_session_device() + if ( + native_device is None + or native_device.type != "cuda" + or not invocation.idle_gpu_offloadable + or not self._services.configuration.offload_text_encoders_to_idle_gpus + ): + yield + return + + borrowed_device = GENERATION_DEVICE_POOL.try_borrow(exclude=native_device) + if borrowed_device is None: + yield + return + + self._services.logger.debug( + f"Running {invocation.get_type()} on idle device {borrowed_device} (session device {native_device})." + ) + TorchDevice.set_session_device(borrowed_device) + try: + yield + finally: + TorchDevice.set_session_device(native_device) + GENERATION_DEVICE_POOL.release_borrow(borrowed_device) + def _on_before_run_session(self, queue_item: SessionQueueItem) -> None: """Called before a session is run. @@ -210,7 +252,7 @@ def _on_after_run_session(self, queue_item: SessionQueueItem) -> None: # 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() + self._services.performance_statistics.reset_stats(queue_item.session.id) for callback in self._on_after_run_session_callbacks: callback(queue_item=queue_item) @@ -307,6 +349,26 @@ def _on_node_error( ) +class _SessionWorker: + """A single generation worker: one thread, optionally pinned to one device. + + In single-device (legacy) mode there is exactly one worker with `device=None`. In multi-GPU + mode there is one worker per configured device, each with its own session runner and cancel + event so concurrent sessions can be canceled independently. + """ + + def __init__(self, device: Optional[torch.device], runner: SessionRunnerBase) -> None: + self.device = device + self.runner = runner + self.cancel_event = ThreadEvent() + self.queue_item: Optional[SessionQueueItem] = None + self.thread: Optional[Thread] = None + + @property + def label(self) -> str: + return str(self.device) if self.device is not None else "default device" + + class DefaultSessionProcessor(SessionProcessorBase): def __init__( self, @@ -321,74 +383,149 @@ def __init__( self._on_non_fatal_processor_error_callbacks = on_non_fatal_processor_error_callbacks or [] self._thread_limit = thread_limit self._polling_interval = polling_interval + self._workers: list[_SessionWorker] = [] + + def _resolve_devices(self) -> list[Optional[torch.device]]: + """Determine the per-worker devices from config. + + Resolves `generation_devices` (which defaults to `"auto"` — every available GPU) into one + normalized device per worker. Returns a single `None` (legacy single-worker, device chosen by + the global config) only if the resolution is empty (e.g. `generation_devices` set to an empty + list). + """ + generation_devices = self._invoker.services.configuration.generation_devices + devices = TorchDevice.get_generation_devices(generation_devices) + if not devices: + return [None] + return list(devices) + + def _clone_session_runner(self, template: SessionRunnerBase) -> SessionRunnerBase: + """Create an independent runner for an additional worker. + + Each worker needs its own runner because the runner stores its session's cancel event. + We carry over the template's callbacks so all workers behave identically. + """ + if isinstance(template, DefaultSessionRunner): + return DefaultSessionRunner( + on_before_run_session_callbacks=list(template._on_before_run_session_callbacks), + on_before_run_node_callbacks=list(template._on_before_run_node_callbacks), + on_after_run_node_callbacks=list(template._on_after_run_node_callbacks), + on_node_error_callbacks=list(template._on_node_error_callbacks), + on_after_run_session_callbacks=list(template._on_after_run_session_callbacks), + ) + # Unknown runner implementation — only safe to reuse in single-worker mode. + return template 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) + devices = self._resolve_devices() + + # Register the generation devices so the model loader can discover idle GPUs to host text + # encoders on (see offload_text_encoders_to_idle_gpus). None means legacy single-device mode. + GENERATION_DEVICE_POOL.set_generation_devices([d for d in devices if d is not None]) # 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. + # the profiler will create a new profile for each session. Profiling uses a process-global cProfile, which + # cannot cleanly attribute work when multiple sessions run concurrently, so it is disabled in multi-GPU mode. + profiler_enabled = self._invoker.services.configuration.profile_graphs + if profiler_enabled and len(devices) > 1: + self._invoker.services.logger.warning( + "Graph profiling is disabled because multiple generation devices are configured." + ) + profiler_enabled = False 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 + if profiler_enabled 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, - 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() + self._thread_semaphore = BoundedSemaphore(len(devices)) + + # Start in the running (resumed) state. + self._stop_event.clear() + self._resume_event.set() + + self._workers = [] + for index, device in enumerate(devices): + runner = self.session_runner if index == 0 else self._clone_session_runner(self.session_runner) + worker = _SessionWorker(device=device, runner=runner) + runner.start(services=invoker.services, cancel_event=worker.cancel_event, profiler=self._profiler) + self._workers.append(worker) + + if len(self._workers) > 1: + self._invoker.services.logger.info( + f"Starting session processor with {len(self._workers)} parallel workers on devices: " + f"{', '.join(w.label for w in self._workers)}" + ) + + for index, worker in enumerate(self._workers): + worker.thread = Thread( + name=f"session_processor_{index}", + target=self._process, + daemon=True, + kwargs={ + "worker": worker, + "stop_event": self._stop_event, + "poll_now_event": self._poll_now_event, + "resume_event": self._resume_event, + }, + ) + worker.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, a 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"). + for worker in self._workers: + worker.cancel_event.set() + # Wake any worker 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() + # Cancel every worker currently running an item from the cleared queue. + canceled = False + for worker in self._workers: + if worker.queue_item and worker.queue_item.queue_id == event[1].queue_id: + worker.cancel_event.set() + canceled = True + if canceled: 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: - 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() + # Find the worker (if any) currently running the item whose status changed. + for worker in self._workers: + if worker.queue_item and worker.queue_item.item_id == event[1].item_id: + if event[1].status in ["completed", "failed", "canceled"]: + # When the queue item is canceled via HTTP, the status is set to "canceled" and this event is + # emitted. We respond by setting that worker's cancel event, which its session runner checks + # between invocations (and which denoise_latents' step callback checks mid-node, raising + # CanceledException to stop immediately). + if event[1].status == "canceled": + worker.cancel_event.set() + self._poll_now() + return def resume(self) -> SessionProcessorStatus: if not self._resume_event.is_set(): @@ -403,22 +540,41 @@ def pause(self) -> SessionProcessorStatus: def get_status(self) -> SessionProcessorStatus: return SessionProcessorStatus( is_started=self._resume_event.is_set(), - is_processing=self._queue_item is not None, + is_processing=any(worker.queue_item is not None for worker in self._workers), ) + def _is_queue_item_terminal(self, item_id: int) -> bool: + """Return True if the queue item is already finished (canceled/failed/completed) or gone. + + Checked right after a worker claims an item to catch a cancellation that raced the claim and + so never reached this worker's cancel_event — e.g. the status-changed handler ran before the + worker recorded `queue_item` and so couldn't match a worker to signal. + """ + try: + status = self._invoker.services.session_queue.get_queue_item(item_id).status + except SessionQueueItemNotFoundError: + return True + return status in ("canceled", "failed", "completed") + def _process( self, + worker: _SessionWorker, 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. + # Any unhandled exception in this block is a fatal processor error and will stop this worker. self._thread_semaphore.acquire() - stop_event.clear() - resume_event.set() - cancel_event.clear() + + # Pin this worker thread to its device so all device-selecting code (TorchDevice.choose_torch_device, + # which nodes and the model loader consult) resolves to this GPU. CUDA's current device is per-thread. + if worker.device is not None: + TorchDevice.set_session_device(worker.device) + if worker.device.type == "cuda": + torch.cuda.set_device(worker.device) + + worker.cancel_event.clear() while not stop_event.is_set(): poll_now_event.clear() @@ -427,27 +583,67 @@ def _process( # 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 stop_event.is_set(): + break + + # Clear any stale cancel signal from the previous item BEFORE claiming the next + # one. Clearing it after dequeue (as before) could wipe a cancel that arrived for + # the item we just claimed — e.g. during the gc.collect() below — silently losing + # the cancellation. Any cancel that arrives after this point for the claimed item + # stays set and is caught by the runner's _is_canceled() check. + worker.cancel_event.clear() + + # Get the next session to process. dequeue() atomically claims the item, so concurrent + # workers never receive the same item. Pass this worker's device so the item is + # tagged with the GPU that ran it (None in single-device/legacy mode). + worker.queue_item = self._invoker.services.session_queue.dequeue( + device=str(worker.device) if worker.device is not None else None + ) - if self._queue_item is None: + if worker.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 - self._invoker.services.logger.debug(f"Executing queue item {self._queue_item.item_id}") - cancel_event.clear() + # A cancellation can race the claim: it may have marked the row terminal before + # this worker recorded `queue_item`, so _on_queue_item_status_changed couldn't set + # our cancel_event. Re-check (cancel_event + a fresh DB status read) and skip + # running if the item is already finished, so the cancel is never lost. + if worker.cancel_event.is_set() or self._is_queue_item_terminal(worker.queue_item.item_id): + self._invoker.services.logger.debug( + f"Queue item {worker.queue_item.item_id} was canceled before it started; skipping." + ) + 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 {worker.queue_item.item_id}, session {worker.queue_item.session_id} " + f"on {worker.label}" + ) - # Run the graph - self.session_runner.run(queue_item=self._queue_item) + # Run the graph. Hold this GPU's exclusive-use lock for the whole session so no + # other worker can borrow it for text-encoder offload while we're running on it + # (a borrow + concurrent native session on one GPU would corrupt the shared + # cached encoder). Acquired here, after dequeue, so an idle worker doesn't hold + # the lock and block borrows while waiting for work. + GENERATION_DEVICE_POOL.acquire_session(worker.device) + try: + worker.runner.run(queue_item=worker.queue_item) + finally: + GENERATION_DEVICE_POOL.release_session(worker.device) 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, + queue_item=worker.queue_item, error_type=error_type, error_message=error_message, error_traceback=error_traceback, @@ -456,7 +652,7 @@ def _process( poll_now_event.wait(self._polling_interval) continue except Exception as e: - # Fatal error in processor, log and pass - we're done here + # Fatal error in this worker, log and pass - we're done here error_type = e.__class__.__name__ error_message = str(e) error_traceback = traceback.format_exc() @@ -464,9 +660,9 @@ def _process( self._invoker.services.logger.error(error_traceback) pass finally: - stop_event.clear() - poll_now_event.clear() - self._queue_item = None + worker.queue_item = None + if worker.device is not None: + TorchDevice.clear_session_device() self._thread_semaphore.release() def _on_non_fatal_processor_error( diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py index 341e0344874..4ed4f1a62c1 100644 --- a/invokeai/app/services/session_queue/session_queue_base.py +++ b/invokeai/app/services/session_queue/session_queue_base.py @@ -1,36 +1,45 @@ from abc import ABC, abstractmethod -from typing import Optional +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, - SessionQueueItemDTO, 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.""" + def dequeue(self, device: Optional[str] = None) -> Optional[SessionQueueItem]: + """Dequeues the next session queue item, recording the processing device (e.g. 'cuda:1') if given.""" pass @abstractmethod - def enqueue_batch(self, queue_id: str, batch: Batch, prepend: bool) -> EnqueueBatchResult: - """Enqueues all permutations of a batch for execution.""" + 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 @@ -44,13 +53,13 @@ def get_next(self, queue_id: str) -> Optional[SessionQueueItem]: pass @abstractmethod - def clear(self, queue_id: str) -> ClearResult: - """Deletes all session queue items""" + 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) -> PruneResult: - """Deletes all completed and errored session queue items""" + 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 @@ -64,13 +73,36 @@ def is_full(self, queue_id: str) -> IsFullResult: pass @abstractmethod - def get_queue_status(self, queue_id: str) -> SessionQueueStatus: - """Gets the status of the queue""" + 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. + + Aggregate counts (pending/in_progress/.../total) are always global across all users. + If user_id is provided, the requesting user's own counts are additionally returned in + the user_pending/user_in_progress fields (left None otherwise). + + 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_batch_status(self, queue_id: str, batch_id: str) -> BatchStatus: - """Gets the status of a batch""" + 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 @@ -83,6 +115,11 @@ 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 @@ -91,8 +128,24 @@ def fail_queue_item( pass @abstractmethod - def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBatchIDsResult: - """Cancels all queue items with matching batch IDs""" + 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 @@ -100,6 +153,16 @@ 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, @@ -108,16 +171,41 @@ def list_queue_items( priority: int, cursor: Optional[int] = None, status: Optional[QUEUE_ITEM_STATUS] = None, - ) -> CursorPaginatedResults[SessionQueueItemDTO]: - """Gets a page of session queue items""" + 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""" + """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 index 7f4601eba73..63324337aa1 100644 --- a/invokeai/app/services/session_queue/session_queue_common.py +++ b/invokeai/app/services/session_queue/session_queue_common.py @@ -1,7 +1,7 @@ import datetime import json from itertools import chain, product -from typing import Generator, Iterable, Literal, NamedTuple, Optional, TypeAlias, Union, cast +from typing import Generator, Literal, Optional, TypeAlias, Union from pydantic import ( AliasChoices, @@ -15,7 +15,7 @@ ) from pydantic_core import to_jsonable_python -from invokeai.app.invocations.baseinvocation import BaseInvocation +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, @@ -51,11 +51,7 @@ class SessionQueueItemNotFoundError(ValueError): # region Batch -BatchDataType = Union[ - StrictStr, - float, - int, -] +BatchDataType = Union[StrictStr, float, int, ImageField] class NodeFieldValue(BaseModel): @@ -77,6 +73,14 @@ class BatchDatum(BaseModel): 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( @@ -103,8 +107,16 @@ def validate_types(cls, v: Optional[BatchDataCollection]): 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]) if datum.items else None + 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") @@ -124,20 +136,18 @@ def validate_unique_field_mappings(cls, v: Optional[BatchDataCollection]): return v @model_validator(mode="after") - def validate_batch_nodes_and_edges(cls, values): - batch_data_collection = cast(Optional[BatchDataCollection], values.data) - if batch_data_collection is None: - return values - graph = cast(Graph, values.graph) - for batch_data_list in batch_data_collection: + 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 = cast(BaseInvocation, graph.get_node(batch_data.node_path)) + 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 node.model_fields: + 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 values + return self @field_validator("graph") def validate_graph(cls, v: Graph): @@ -160,9 +170,18 @@ def validate_graph(cls, v: Graph): # 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]) @@ -188,13 +207,33 @@ def get_workflow(queue_item_dict: dict) -> Optional[WorkflowWithoutID]: return None -class SessionQueueItemWithoutGraph(BaseModel): +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." ) @@ -210,38 +249,23 @@ class SessionQueueItemWithoutGraph(BaseModel): 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" ) - - @classmethod - def queue_item_dto_from_dict(cls, queue_item_dict: dict) -> "SessionQueueItemDTO": - # must parse these manually - queue_item_dict["field_values"] = get_field_values(queue_item_dict) - return SessionQueueItemDTO(**queue_item_dict) - - model_config = ConfigDict( - json_schema_extra={ - "required": [ - "item_id", - "status", - "batch_id", - "queue_id", - "session_id", - "priority", - "session_id", - "created_at", - "updated_at", - ] - } + retried_from_item_id: Optional[int] = Field( + default=None, description="The item_id of the queue item that this item was retried from" + ) + device: Optional[str] = Field( + default=None, + description="The device that processed this queue item, e.g. 'cuda:1' (set only when running on a CUDA GPU)", ) - - -class SessionQueueItemDTO(SessionQueueItemWithoutGraph): - pass - - -class SessionQueueItem(SessionQueueItemWithoutGraph): 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" @@ -289,11 +313,32 @@ class SessionQueueStatus(BaseModel): 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") + user_pending: Optional[int] = Field( + default=None, + description="Number of the requesting user's queue items with status 'pending' (None for admins/global callers)", + ) + user_in_progress: Optional[int] = Field( + default=None, + description="Number of the requesting user's queue items with status 'in_progress' (None for admins/global callers)", + ) + + +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'") @@ -308,6 +353,12 @@ class EnqueueBatchResult(BaseModel): 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): @@ -328,12 +379,36 @@ class CancelByBatchIDsResult(BaseModel): 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""" @@ -352,61 +427,143 @@ class IsFullResult(BaseModel): # region Util -def populate_graph(graph: Graph, node_field_values: Iterable[NodeFieldValue]) -> Graph: +def create_session_nfv_tuples(batch: Batch, maximum: int) -> Generator[tuple[str, str, str], None, None]: """ - Populates the given graph with the given batch data items. - """ - graph_clone = graph.model_copy(deep=True) - for item in node_field_values: - node = graph_clone.get_node(item.node_path) - if node is None: - continue - setattr(node, item.field_name, item.value) - graph_clone.update_node(item.node_path, node) - return graph_clone - - -def create_session_nfv_tuples( - batch: Batch, maximum: int -) -> Generator[tuple[GraphExecutionState, list[NodeFieldValue], Optional[WorkflowWithoutID]], None, None]: - """ - Create all graph permutations from the given batch data and graph. Yields tuples - of the form (graph, batch_data_items) where batch_data_items is the list of BatchDataItems - that was applied to the graph. + 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[NodeFieldValue]]] = [] + 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: - # each batch_datum_list needs to be convered to NodeFieldValues and then zipped - node_field_values_to_zip: list[list[NodeFieldValue]] = [] + 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 = [ - NodeFieldValue(node_path=batch_datum.node_path, field_name=batch_datum.field_name, value=item) + # 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] - # create generator to yield session,nfv tuples + # 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)) - graph = populate_graph(batch.graph, flat_node_field_values) - yield (GraphExecutionState(graph=graph), flat_node_field_values, batch.workflow) + + # 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 generating them. Adapted from `create_sessions(). + 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: @@ -422,37 +579,82 @@ def calc_session_count(batch: Batch) -> int: return len(data_product) * batch.runs -class SessionQueueValueToInsert(NamedTuple): - """A tuple of values to insert into the session_queue table""" +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 + """ - # Careful with the ordering of this - it must match the insert statement - queue_id: str # queue_id - session: str # session json - session_id: str # session_id - batch_id: str # batch_id - field_values: Optional[str] # field_values json - priority: int # priority - workflow: Optional[str] # workflow json + # 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] = [] -ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert] + # 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 -def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new_queue_items: int) -> ValuesToInsert: - values_to_insert: ValuesToInsert = [] - for session, field_values, workflow in create_session_nfv_tuples(batch, max_new_queue_items): - # sessions must have unique id - session.id = uuid_string() + for session_id, session_json, field_values_json in create_session_nfv_tuples(batch, max_new_queue_items): values_to_insert.append( - SessionQueueValueToInsert( - queue_id, # queue_id - session.model_dump_json(warnings=False, exclude_none=True), # session (json) - session.id, # session_id - batch.batch_id, # batch_id - # must use pydantic_encoder bc field_values is a list of models - json.dumps(field_values, default=to_jsonable_python) if field_values else None, # field_values (json) - priority, # priority - json.dumps(workflow, default=to_jsonable_python) if workflow else None, # workflow (json) + ( + 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 diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py index a3a7004c947..e6d8229860d 100644 --- a/invokeai/app/services/session_queue/session_queue_sqlite.py +++ b/invokeai/app/services/session_queue/session_queue_sqlite.py @@ -1,6 +1,10 @@ +import asyncio +import json import sqlite3 import threading -from typing import Optional, Union, cast +from typing import Any, 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 @@ -9,218 +13,282 @@ QUEUE_ITEM_STATUS, Batch, BatchStatus, + CancelAllExceptCurrentResult, CancelByBatchIDsResult, + CancelByDestinationResult, CancelByQueueIDResult, ClearResult, + DeleteAllExceptCurrentResult, + DeleteByDestinationResult, EnqueueBatchResult, IsEmptyResult, IsFullResult, + ItemIdsResult, PruneResult, + RetryItemsResult, + SessionQueueCountsByDestination, SessionQueueItem, - SessionQueueItemDTO, 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 - __conn: sqlite3.Connection - __cursor: sqlite3.Cursor - __lock: threading.RLock + + # Serializes the select-candidate-then-claim sequence in `dequeue()`. The DB connection's + # RLock serializes individual statements, but the gap between selecting the next pending item + # and marking it 'in_progress' is a race: with multiple session-processor workers (multi-GPU), + # two workers could select the same item. Holding this lock across the whole claim prevents it. + _dequeue_lock = threading.Lock() def start(self, invoker: Invoker) -> None: self.__invoker = invoker self._set_in_progress_to_canceled() - if self.__invoker.services.configuration.clear_queue_on_startup: + 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") - else: - prune_result = self.prune(DEFAULT_QUEUE_ID) - if prune_result.deleted > 0: - self.__invoker.services.logger.info(f"Pruned {prune_result.deleted} finished 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.__lock = db.lock - self.__conn = db.conn - self.__cursor = self.__conn.cursor() + 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. """ - try: - self.__lock.acquire() - self.__cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql UPDATE session_queue - SET status = 'canceled' + SET status = 'canceled', + status_sequence = COALESCE(status_sequence, 0) + 1 WHERE status = 'in_progress'; """ ) - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + + 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""" - self.__cursor.execute( - """--sql - SELECT count(*) - FROM session_queue - WHERE - queue_id = ? - AND status = 'pending' - """, - (queue_id,), - ) - return cast(int, self.__cursor.fetchone()[0]) + 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""" - self.__cursor.execute( - """--sql - SELECT MAX(priority) - FROM session_queue - WHERE - queue_id = ? - AND status = 'pending' - """, - (queue_id,), - ) - return cast(Union[int, None], self.__cursor.fetchone()[0]) or 0 - - def enqueue_batch(self, queue_id: str, batch: Batch, prepend: bool) -> EnqueueBatchResult: - try: - self.__lock.acquire() - - # TODO: how does this work in a multi-user scenario? - 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 + 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 - priority = 0 - if prepend: - priority = self._get_highest_priority(queue_id) + 1 + 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 - requested_count = calc_session_count(batch) - values_to_insert = prepare_values_to_insert( - queue_id=queue_id, - batch=batch, - priority=priority, - max_new_queue_items=max_new_queue_items, - ) - enqueued_count = len(values_to_insert) + priority = 0 + if prepend: + priority = self._get_highest_priority(queue_id) + 1 - if requested_count > enqueued_count: - values_to_insert = values_to_insert[:max_new_queue_items] + 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) - self.__cursor.executemany( + with self._db.transaction() as cursor: + cursor.executemany( """--sql - INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, + 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, ) - self.__conn.commit() - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + 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) + self.__invoker.services.events.emit_batch_enqueued(enqueue_result, user_id=user_id) return enqueue_result - def dequeue(self) -> Optional[SessionQueueItem]: - try: - self.__lock.acquire() - self.__cursor.execute( - """--sql - SELECT * - FROM session_queue - WHERE status = 'pending' - ORDER BY - priority DESC, - item_id ASC - LIMIT 1 - """ - ) - result = cast(Union[sqlite3.Row, None], self.__cursor.fetchone()) - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() - 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") + def dequeue(self, device: Optional[str] = None) -> Optional[SessionQueueItem]: + # Hold the dequeue lock across the select-then-claim so concurrent workers (multi-GPU) + # cannot select and claim the same pending item. `_set_queue_item_status` already no-ops + # if the item was concurrently moved to a terminal state (e.g. canceled), so we only need + # to guard against two dequeues racing for the same pending row. + with self._dequeue_lock: + 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)) + # Record the claiming worker's device so the UI can label the item by GPU. + queue_item = self._set_queue_item_status(item_id=queue_item.item_id, status="in_progress", device=device) return queue_item def get_next(self, queue_id: str) -> Optional[SessionQueueItem]: - try: - self.__lock.acquire() - self.__cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql - SELECT * - FROM session_queue + 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 - queue_id = ? - AND status = 'pending' + sq.queue_id = ? + AND sq.status = 'pending' ORDER BY - priority DESC, - created_at ASC + sq.priority DESC, + sq.created_at ASC LIMIT 1 """, (queue_id,), ) - result = cast(Union[sqlite3.Row, None], self.__cursor.fetchone()) - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + 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]: - try: - self.__lock.acquire() - self.__cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql - SELECT * - FROM session_queue + 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 - queue_id = ? - AND status = 'in_progress' + sq.queue_id = ? + AND sq.status = 'in_progress' LIMIT 1 """, (queue_id,), ) - result = cast(Union[sqlite3.Row, None], self.__cursor.fetchone()) - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + result = cast(Union[sqlite3.Row, None], cursor.fetchone()) if result is None: return None return SessionQueueItem.queue_item_from_dict(dict(result)) @@ -232,33 +300,50 @@ def _set_queue_item_status( error_type: Optional[str] = None, error_message: Optional[str] = None, error_traceback: Optional[str] = None, + device: Optional[str] = None, ) -> SessionQueueItem: - try: - self.__lock.acquire() - self.__cursor.execute( + 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 = ?, error_type = ?, error_message = ?, error_traceback = ? + SET status = ?, status_sequence = COALESCE(status_sequence, 0) + 1, error_type = ?, error_message = ?, error_traceback = ?, device = COALESCE(?, device) WHERE item_id = ? """, - (status, error_type, error_message, error_traceback, item_id), + (status, error_type, error_message, error_traceback, device, item_id), ) - self.__conn.commit() - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + 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) - queue_status = self.get_queue_status(queue_id=queue_item.queue_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: - try: - self.__lock.acquire() - self.__cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql SELECT count(*) FROM session_queue @@ -266,18 +351,12 @@ def is_empty(self, queue_id: str) -> IsEmptyResult: """, (queue_id,), ) - is_empty = cast(int, self.__cursor.fetchone()[0]) == 0 - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + is_empty = cast(int, cursor.fetchone()[0]) == 0 return IsEmptyResult(is_empty=is_empty) def is_full(self, queue_id: str) -> IsFullResult: - try: - self.__lock.acquire() - self.__cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql SELECT count(*) FROM session_queue @@ -286,84 +365,96 @@ def is_full(self, queue_id: str) -> IsFullResult: (queue_id,), ) max_queue_size = self.__invoker.services.configuration.max_queue_size - is_full = cast(int, self.__cursor.fetchone()[0]) >= max_queue_size - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + is_full = cast(int, cursor.fetchone()[0]) >= max_queue_size return IsFullResult(is_full=is_full) - def clear(self, queue_id: str) -> ClearResult: - try: - self.__lock.acquire() - self.__cursor.execute( - """--sql + 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 queue_id = ? + {where} """, - (queue_id,), + tuple(params), ) - count = self.__cursor.fetchone()[0] - self.__cursor.execute( - """--sql + count = cursor.fetchone()[0] + cursor.execute( + f"""--sql DELETE FROM session_queue - WHERE queue_id = ? + {where} """, - (queue_id,), + tuple(params), ) - self.__conn.commit() - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() self.__invoker.services.events.emit_queue_cleared(queue_id) return ClearResult(deleted=count) - def prune(self, queue_id: str) -> PruneResult: - try: - where = """--sql + 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 ( + queue_id = ? + AND ( status = 'completed' OR status = 'failed' OR status = 'canceled' - ) + ) + {user_filter} """ - self.__lock.acquire() - self.__cursor.execute( + params = [queue_id] + if user_id is not None: + params.append(user_id) + + cursor.execute( f"""--sql SELECT COUNT(*) FROM session_queue {where}; """, - (queue_id,), + tuple(params), ) - count = self.__cursor.fetchone()[0] - self.__cursor.execute( + count = cursor.fetchone()[0] + cursor.execute( f"""--sql DELETE FROM session_queue {where}; """, - (queue_id,), + tuple(params), ) - self.__conn.commit() - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() 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 @@ -384,21 +475,62 @@ def fail_queue_item( ) return queue_item - def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBatchIDsResult: - try: - current_queue_item = self.get_current(queue_id) - self.__lock.acquire() - placeholders = ", ".join(["?" for _ in batch_ids]) + def _cancel_in_progress_matching(self, match_filter: str, params: list[Any]) -> int: + """Cancel every in-progress item matching `match_filter`, emitting a cancel event for each. + + The bulk-cancel methods exclude in-progress items from their single UPDATE statement, because + a running item must be canceled via `_set_queue_item_status()` so that its + `QueueItemStatusChangedEvent` is emitted — the session processor responds to that event by + setting the cancel event of the worker running that exact item_id. With multiple workers + (multi-GPU) more than one item can be in_progress at once, so each matching item is canceled + individually here rather than relying on a single `get_current()` (which returns only one). + + `match_filter` is a WHERE fragment without the leading WHERE (e.g. + "queue_id == ? AND batch_id IN (?, ?)"); `params` are its bound values. + + Returns the number of in-progress items actually canceled. + """ + with self._db.transaction() as cursor: + cursor.execute( + f"""--sql + SELECT item_id + FROM session_queue + WHERE status == 'in_progress' AND {match_filter}; + """, + tuple(params), + ) + item_ids = [row[0] for row in cursor.fetchall()] + + canceled = 0 + for item_id in item_ids: + # _set_queue_item_status no-ops (and returns the existing item) if the item finished + # between the SELECT and now, so count only the ones we actually moved to 'canceled'. + if self._set_queue_item_status(item_id, "canceled").status == "canceled": + canceled += 1 + return canceled + + def cancel_by_batch_ids( + self, queue_id: str, batch_ids: list[str], user_id: Optional[str] = None + ) -> CancelByBatchIDsResult: + placeholders = ", ".join(["?" for _ in batch_ids]) + # Build the match filter (with optional user_id filter) shared by the bulk update and the + # in-progress cancellation below. + user_filter = "AND user_id = ?" if user_id is not None else "" + match_filter = f"queue_id == ? AND batch_id IN ({placeholders}) {user_filter}" + params: list[Any] = [queue_id] + batch_ids + if user_id is not None: + params.append(user_id) + + with self._db.transaction() as cursor: where = f"""--sql - WHERE - queue_id == ? - AND batch_id IN ({placeholders}) + WHERE {match_filter} AND status != 'canceled' AND status != 'completed' AND status != 'failed' + -- In-progress items are canceled individually below so each worker is signaled. + AND status != 'in_progress' """ - params = [queue_id] + batch_ids - self.__cursor.execute( + cursor.execute( f"""--sql SELECT COUNT(*) FROM session_queue @@ -406,42 +538,149 @@ def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBa """, tuple(params), ) - count = self.__cursor.fetchone()[0] - self.__cursor.execute( + count = cursor.fetchone()[0] + cursor.execute( f"""--sql UPDATE session_queue - SET status = 'canceled' + SET status = 'canceled', + status_sequence = COALESCE(status_sequence, 0) + 1 {where}; """, tuple(params), ) - self.__conn.commit() - if current_queue_item is not None and current_queue_item.batch_id in batch_ids: - batch_status = self.get_batch_status(queue_id=queue_id, batch_id=current_queue_item.batch_id) - queue_status = self.get_queue_status(queue_id=queue_id) - self.__invoker.services.events.emit_queue_item_status_changed( - current_queue_item, batch_status, queue_status - ) - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + + # Cancel every in-progress item matching the same filter (multi-GPU: possibly several at once). + count += self._cancel_in_progress_matching(match_filter, params) return CancelByBatchIDsResult(canceled=count) - def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult: - try: - current_queue_item = self.get_current(queue_id) - self.__lock.acquire() - where = """--sql - WHERE - queue_id is ? + def cancel_by_destination( + self, queue_id: str, destination: str, user_id: Optional[str] = None + ) -> CancelByDestinationResult: + user_filter = "AND user_id = ?" if user_id is not None else "" + match_filter = f"queue_id == ? AND destination == ? {user_filter}" + params: list[Any] = [queue_id, destination] + if user_id is not None: + params.append(user_id) + + with self._db.transaction() as cursor: + where = f"""--sql + WHERE {match_filter} AND status != 'canceled' AND status != 'completed' AND status != 'failed' + -- In-progress items are canceled individually below so each worker is signaled. + AND status != 'in_progress' + """ + 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), + ) + + # Cancel every in-progress item matching the same filter (multi-GPU: possibly several at once). + count += self._cancel_in_progress_matching(match_filter, params) + return CancelByDestinationResult(canceled=count) + + def delete_by_destination( + self, queue_id: str, destination: str, user_id: Optional[str] = None + ) -> DeleteByDestinationResult: + user_filter = "AND user_id = ?" if user_id is not None else "" + match_filter = f"queue_id == ? AND destination == ? {user_filter}" + params: list[Any] = [queue_id, destination] + if user_id is not None: + params.append(user_id) + + # Cancel every in-progress item first so each running worker is signaled to stop before we + # delete its row. With multiple workers (multi-GPU) more than one item can be in_progress; + # canceling only get_current() would leave the others running (and then failing to update a + # deleted row). See _cancel_in_progress_matching. + self._cancel_in_progress_matching(match_filter, params) + + with self._db.transaction() as cursor: + 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] - self.__cursor.execute( + 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: + match_filter = "queue_id == ?" + params: list[Any] = [queue_id] + + with self._db.transaction() as cursor: + where = f"""--sql + WHERE {match_filter} + AND status != 'canceled' + AND status != 'completed' + AND status != 'failed' + -- In-progress items are canceled individually below so each worker is signaled. + AND status != 'in_progress' + """ + cursor.execute( f"""--sql SELECT COUNT(*) FROM session_queue @@ -449,58 +688,81 @@ def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult: """, tuple(params), ) - count = self.__cursor.fetchone()[0] - self.__cursor.execute( + count = cursor.fetchone()[0] + cursor.execute( f"""--sql UPDATE session_queue - SET status = 'canceled' + SET status = 'canceled', + status_sequence = COALESCE(status_sequence, 0) + 1 {where}; """, tuple(params), ) - self.__conn.commit() - if current_queue_item is not None and current_queue_item.queue_id == queue_id: - batch_status = self.get_batch_status(queue_id=queue_id, batch_id=current_queue_item.batch_id) - queue_status = self.get_queue_status(queue_id=queue_id) - self.__invoker.services.events.emit_queue_item_status_changed( - current_queue_item, batch_status, queue_status - ) - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + + # Cancel every in-progress item in the queue (multi-GPU: possibly several at once). + count += self._cancel_in_progress_matching(match_filter, params) 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: - try: - self.__lock.acquire() - self.__cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql - SELECT * FROM session_queue - WHERE - item_id = ? + 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], self.__cursor.fetchone()) - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + 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: - try: + 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) - self.__lock.acquire() - self.__cursor.execute( + cursor.execute( """--sql UPDATE session_queue SET session = ? @@ -508,12 +770,6 @@ def set_queue_item_session(self, item_id: int, session: GraphExecutionState) -> """, (session_json, item_id), ) - self.__conn.commit() - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() return self.get_queue_item(item_id) def list_queue_items( @@ -523,25 +779,12 @@ def list_queue_items( priority: int, cursor: Optional[int] = None, status: Optional[QUEUE_ITEM_STATUS] = None, - ) -> CursorPaginatedResults[SessionQueueItemDTO]: - try: + destination: Optional[str] = None, + ) -> CursorPaginatedResults[SessionQueueItem]: + with self._db.transaction() as cursor_: item_id = cursor - self.__lock.acquire() query = """--sql - SELECT item_id, - status, - priority, - field_values, - error_type, - error_message, - error_traceback, - created_at, - updated_at, - completed_at, - started_at, - session_id, - batch_id, - queue_id + SELECT * FROM session_queue WHERE queue_id = ? """ @@ -553,6 +796,12 @@ def list_queue_items( """ 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 > ?) @@ -561,30 +810,93 @@ def list_queue_items( query += """--sql ORDER BY - priority DESC, - item_id ASC + priority DESC, + item_id ASC LIMIT ? """ params.append(limit + 1) - self.__cursor.execute(query, params) - results = cast(list[sqlite3.Row], self.__cursor.fetchall()) - items = [SessionQueueItemDTO.queue_item_dto_from_dict(dict(result)) for result in results] - has_more = False - if len(items) > limit: - # remove the extra item - items.pop() - has_more = True - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + 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 get_queue_status(self, queue_id: str) -> SessionQueueStatus: - try: - self.__lock.acquire() - self.__cursor.execute( + 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: + # Aggregate counts are always global (across all users). This lets a non-admin's + # badge show "own / total" — their share of the whole queue — and lets the queue + # list surface (redacted) entries belonging to other users. + cursor.execute( """--sql SELECT status, count(*) FROM session_queue @@ -593,54 +905,84 @@ def get_queue_status(self, queue_id: str) -> SessionQueueStatus: """, (queue_id,), ) - counts_result = cast(list[sqlite3.Row], self.__cursor.fetchall()) - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + counts_result = cast(list[sqlite3.Row], cursor.fetchall()) + + # When user_id is provided, additionally compute that user's own counts so the + # caller can render the per-user portion of the badge. These are returned in the + # separate user_pending/user_in_progress fields and never replace the global counts. + user_counts_result: list[sqlite3.Row] = [] + 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), + ) + user_counts_result = cast(list[sqlite3.Row], cursor.fetchall()) current_item = self.get_current(queue_id=queue_id) - total = sum(row[1] for row in counts_result) + total = sum(row[1] or 0 for row in counts_result) counts: dict[str, int] = {row[0]: row[1] for row in counts_result} + + user_pending: Optional[int] = None + user_in_progress: Optional[int] = None + if user_id is not None: + user_counts: dict[str, int] = {row[0]: row[1] for row in user_counts_result} + user_pending = user_counts.get("pending", 0) + user_in_progress = user_counts.get("in_progress", 0) + + # 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. The aggregate counts stay global; only the current + # item's identifiers are gated. acting_user_id (event path) takes precedence over + # user_id (API path) when deciding the redaction owner; either being None means an + # admin/global caller who may see the current item. + 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 current_item else None, - session_id=current_item.session_id if current_item else None, - batch_id=current_item.batch_id if current_item else None, + 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, + user_pending=user_pending, + user_in_progress=user_in_progress, ) - def get_batch_status(self, queue_id: str, batch_id: str) -> BatchStatus: - try: - self.__lock.acquire() - self.__cursor.execute( - """--sql - SELECT status, count(*) + 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 = ? - GROUP BY status - """, - (queue_id, batch_id), - ) - result = cast(list[sqlite3.Row], self.__cursor.fetchall()) - total = sum(row[1] for row in result) - counts: dict[str, int] = {row[0]: row[1] for row in result} - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() + 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), @@ -649,3 +991,95 @@ def get_batch_status(self, queue_id: str, batch_id: str) -> BatchStatus: 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/graph.py b/invokeai/app/services/shared/graph.py index d745e738233..aa47c3b4bb5 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -2,13 +2,17 @@ import copy import itertools -from typing import Any, Optional, TypeVar, Union, get_args, get_origin, get_type_hints +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, ) @@ -21,16 +25,22 @@ 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") @@ -51,19 +61,784 @@ 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 get_output_field(node: BaseInvocation, field: str) -> Any: - node_type = type(node) - node_outputs = get_type_hints(node_type.get_output_annotation()) - node_output_field = node_outputs.get(field) or None - return node_output_field + 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 get_input_field(node: BaseInvocation, field: str) -> Any: - node_type = type(node) - node_inputs = get_type_hints(node_type) - node_input_field = node_inputs.get(field) or None - return node_input_field + 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): @@ -93,45 +868,58 @@ def is_list_or_contains_list(t): 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: - return False - if not to_type: + if not from_type or not to_type: return False - # TODO: this is pretty forgiving on generic types. Clean that up (need to handle optionals and such) - if from_type and to_type: - # Ports are compatible - if ( - from_type == to_type - or from_type == Any - or to_type == Any - or Any in get_args(from_type) - or Any in get_args(to_type) - ): - return True + # 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 from_type in get_args(to_type): + return True - if to_type in get_args(from_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, 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 + # 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 - # if not issubclass(from_type, to_type): - if not is_union_subtype(from_type, to_type): - return False - else: - return False + # 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 - return True + # Union-to-Union (or Union-to-non-Union) handling + return is_union_subtype(from_type, to_type) def are_connections_compatible( @@ -140,10 +928,10 @@ def are_connections_compatible( """Determines if a connection between fields of two nodes is compatible.""" # TODO: handle iterators and collectors - from_node_field = get_output_field(from_node, from_field) - to_node_field = get_input_field(to_node, to_field) + 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_node_field, to_node_field) + return are_connection_types_compatible(from_type, to_type) T = TypeVar("T") @@ -258,7 +1046,7 @@ class CollectInvocationOutput(BaseInvocationOutput): ) -@invocation("collect", version="1.0.0") +@invocation("collect", version="1.1.0") class CollectInvocation(BaseInvocation): """Collects values into a collection""" @@ -270,7 +1058,10 @@ class CollectInvocation(BaseInvocation): input=Input.Connection, ) collection: list[Any] = InputField( - description="The collection, will be provided on execution", default=[], ui_hidden=True + description="An optional collection to append to", + default=[], + ui_type=UIType._Collection, + input=Input.Connection, ) def invoke(self, context: InvocationContext) -> CollectInvocationOutput: @@ -282,7 +1073,7 @@ 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 BaseInvocation.get_typeadapter().validate_python(v) + return InvocationRegistry.get_invocation_typeadapter().validate_python(v) return core_schema.no_info_plain_validator_function(validate_invocation) @@ -293,7 +1084,7 @@ def __get_pydantic_json_schema__( # 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 BaseInvocation.get_invocations()] + 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} @@ -303,7 +1094,7 @@ class AnyInvocationOutput(BaseInvocationOutput): @classmethod def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler): def validate_invocation_output(v: Any) -> "AnyInvocationOutput": - return BaseInvocationOutput.get_typeadapter().validate_python(v) + return InvocationRegistry.get_output_typeadapter().validate_python(v) return core_schema.no_info_plain_validator_function(validate_invocation_output) @@ -315,13 +1106,15 @@ def __get_pydantic_json_schema__( # 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 BaseInvocationOutput.get_outputs()] + 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) @@ -377,35 +1170,22 @@ def delete_edge(self, edge: Edge) -> None: try: self.edges.remove(edge) - except KeyError: + except ValueError: pass - def validate_self(self) -> None: - """ - Validates the graph. - - Raises an exception if the graph is invalid: - - `DuplicateNodeIdError` - - `NodeIdMismatchError` - - `InvalidSubGraphError` - - `NodeNotFoundError` - - `NodeFieldNotFoundError` - - `CyclicalGraphError` - - `InvalidEdgeError` - """ - - # Validate that all node ids are unique + def _validate_unique_node_ids(self) -> None: node_ids = [n.id for n in self.nodes.values()] - duplicate_node_ids = {node_id for node_id in node_ids if node_ids.count(node_id) >= 2} + 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}") - # Validate that all node ids match the keys in the nodes dict - for k, v in self.nodes.items(): - if k != v.id: - raise NodeIdMismatchError(f"Node ids must match, got {k} and {v.id}") + 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}") - # Validate that all edges match nodes and fields in the graph + 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: @@ -415,24 +1195,22 @@ def validate_self(self) -> None: if destination_node is None: raise NodeNotFoundError(f"Edge destination node {edge.destination.node_id} does not exist in the graph") - # output fields are not on the node object directly, they are on the output type 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}" ) - # input fields are on the node - if edge.destination.field not in destination_node.model_fields: + 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}" ) - # Validate there are no cycles - g = self.nx_graph_flat() - if not nx.is_directed_acyclic_graph(g): + 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") - # Validate all edge connections are valid + def _validate_edge_type_compatibility(self) -> None: for edge in self.edges: if not are_connections_compatible( self.get_node(edge.source.node_id), @@ -440,18 +1218,40 @@ def validate_self(self) -> None: self.get_node(edge.destination.node_id), edge.destination.field, ): - raise InvalidEdgeError( - f"Invalid edge from {edge.source.node_id}.{edge.source.field} to {edge.destination.node_id}.{edge.destination.field}" - ) + raise InvalidEdgeError(f"Edge source and target types do not match ({edge})") - # Validate all iterators & collectors + 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) and not self._is_iterator_connection_valid(node.id): - raise InvalidEdgeError(f"Invalid iterator node {node.id}") - if isinstance(node, CollectInvocation) and not self._is_collector_connection_valid(node.id): - raise InvalidEdgeError(f"Invalid collector node {node.id}") + 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: @@ -477,81 +1277,78 @@ def is_valid(self) -> bool: 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(self.get_node(edge.destination.node_id), edge.destination.field) == 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(self.get_node(edge.destination.node_id), edge.destination.field) == list[Any] + return get_input_field_type(self.get_node(edge.destination.node_id), edge.destination.field) == list[Any] - def _validate_edge(self, edge: Edge): - """Validates that a new edge doesn't create a cycle in the graph""" - - # Validate that the nodes exist + def _get_edge_nodes(self, edge: Edge) -> tuple[BaseInvocation, BaseInvocation]: try: - from_node = self.get_node(edge.source.node_id) - to_node = self.get_node(edge.destination.node_id) + return self.get_node(edge.source.node_id), self.get_node(edge.destination.node_id) except NodeNotFoundError: - raise InvalidEdgeError("One or both nodes don't exist: {edge.source.node_id} -> {edge.destination.node_id}") + raise InvalidEdgeError(f"One or both nodes don't exist ({edge})") - # Validate that an edge to this node+field doesn't already exist + 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(to_node, CollectInvocation): - raise InvalidEdgeError( - f"Edge to node {edge.destination.node_id} field {edge.destination.field} already exists" - ) - - # Validate that no cycles would be created - g = self.nx_graph_flat() - g.add_edge(edge.source.node_id, edge.destination.node_id) - if not nx.is_directed_acyclic_graph(g): - raise InvalidEdgeError( - f"Edge creates a cycle in the graph: {edge.source.node_id} -> {edge.destination.node_id}" - ) - - # Validate that the field types are compatible - if not are_connections_compatible(from_node, edge.source.field, to_node, edge.destination.field): - raise InvalidEdgeError( - f"Fields are incompatible: cannot connect {edge.source.node_id}.{edge.source.field} to {edge.destination.node_id}.{edge.destination.field}" - ) - - # Validate if iterator output type matches iterator input type (if this edge results in both being set) - if isinstance(to_node, IterateInvocation) and edge.destination.field == "collection": - if not self._is_iterator_connection_valid(edge.destination.node_id, new_input=edge.source): - raise InvalidEdgeError( - f"Iterator input type does not match iterator output type: {edge.source.node_id}.{edge.source.field} to {edge.destination.node_id}.{edge.destination.field}" - ) - - # Validate if iterator input type matches output type (if this edge results in both being set) - if isinstance(from_node, IterateInvocation) and edge.source.field == "item": - if not self._is_iterator_connection_valid(edge.source.node_id, new_output=edge.destination): - raise InvalidEdgeError( - f"Iterator output type does not match iterator input type:, {edge.source.node_id}.{edge.source.field} to {edge.destination.node_id}.{edge.destination.field}" - ) - - # Validate if collector input type matches output type (if this edge results in both being set) - if isinstance(to_node, CollectInvocation) and edge.destination.field == "item": - if not self._is_collector_connection_valid(edge.destination.node_id, new_input=edge.source): - raise InvalidEdgeError( - f"Collector output type does not match collector input type: {edge.source.node_id}.{edge.source.field} to {edge.destination.node_id}.{edge.destination.field}" - ) - - # Validate that we are not connecting collector to iterator (currently unsupported) - if isinstance(from_node, CollectInvocation) and isinstance(to_node, IterateInvocation): - raise InvalidEdgeError( - f"Cannot connect collector to iterator: {edge.source.node_id}.{edge.source.field} to {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}") - # Validate if collector output type matches input type (if this edge results in both being set) - skip if the destination field is not Any or list[Any] if ( - isinstance(from_node, CollectInvocation) - and edge.source.field == "collection" + 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) ): - if not self._is_collector_connection_valid(edge.source.node_id, new_output=edge.destination): - raise InvalidEdgeError( - f"Collector input type does not match collector output type: {edge.source.node_id}.{edge.source.field} to {edge.destination.node_id}.{edge.destination.field}" - ) + 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.""" @@ -634,81 +1431,264 @@ def _is_iterator_connection_valid( node_id: str, new_input: Optional[EdgeConnection] = None, new_output: Optional[EdgeConnection] = None, - ) -> bool: - inputs = [e.source for e in self._get_input_edges(node_id, "collection")] - outputs = [e.destination for e in self._get_output_edges(node_id, "item")] + ) -> 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) - # Only one input is allowed for iterators + 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 False + return "Iterator may only have one input edge" + return None - # Get input and output fields (the fields linked to the iterator's input/output) - input_field = get_output_field(self.get_node(inputs[0].node_id), inputs[0].field) - output_fields = [get_input_field(self.get_node(e.node_id), e.field) for e in outputs] + 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] - # Input type must be a list - if get_origin(input_field) != list: - return False + 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 - # Validate that all outputs match the input type - input_field_item_type = get_args(input_field)[0] - if not all((are_connection_types_compatible(input_field_item_type, f) for f in output_fields)): - return False + 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 - return True + def _validate_iterator_collector_input( + self, input_node: BaseInvocation, output_field_types: list[Any] + ) -> str | None: + if not isinstance(input_node, CollectInvocation): + return None - def _is_collector_connection_valid( + 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, - ) -> bool: - inputs = [e.source for e in self._get_input_edges(node_id, "item")] - outputs = [e.destination for e in self._get_output_edges(node_id, "collection")] + ) -> 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: - inputs.append(new_input) + 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) - # Get input and output fields (the fields linked to the iterator's input/output) - input_fields = [get_output_field(self.get_node(e.node_id), e.field) for e in inputs] - output_fields = [get_input_field(self.get_node(e.node_id), e.field) for e in outputs] - - # Validate that all inputs are derived from or match a single type - input_field_types = { - t - for input_field in input_fields - for t in ([input_field] if get_origin(input_field) is None else get_args(input_field)) - if t != NoneType - } # Get unique types - type_tree = nx.DiGraph() - type_tree.add_nodes_from(input_field_types) - type_tree.add_edges_from([e for e in itertools.permutations(input_field_types, 2) if issubclass(e[1], e[0])]) - type_degrees = type_tree.in_degree(type_tree.nodes) - if sum((t[1] == 0 for t in type_degrees)) != 1: # type: ignore - return False # There is more than one root type + return item_inputs, collection_inputs, outputs - # Get the input root type - input_root_type = next(t[0] for t in type_degrees if t[1] == 0) # type: ignore + 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 - # Verify that all outputs are lists - if not all(is_list_or_contains_list(f) for f in output_fields): - return False + 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" - # Verify that all outputs match the input type (are a base class or the same class) - if not all( - is_union_subtype(input_root_type, get_args(f)[0]) or issubclass(input_root_type, get_args(f)[0]) - for f in output_fields - ): - return False + return None - return True + 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""" @@ -718,29 +1698,20 @@ def nx_graph(self) -> nx.DiGraph: g.add_edges_from({(e.source.node_id, e.destination.node_id) for e in self.edges}) return g - def nx_graph_with_data(self) -> nx.DiGraph: - """Returns a NetworkX DiGraph representing the data and layout of this graph""" - g = nx.DiGraph() - g.add_nodes_from(list(self.nodes.items())) - 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() if not isinstance(n, IterateInvocation)]) - - # TODO: figure out if iteration nodes need to be expanded + 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([(e[0], e[1]) for e in unique_edges]) + g.add_edges_from(unique_edges) return g class GraphExecutionState(BaseModel): - """Tracks the state of a graph execution""" + """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? @@ -776,6 +1747,182 @@ class GraphExecutionState(BaseModel): 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): @@ -792,12 +1939,7 @@ def next(self) -> Optional[BaseInvocation]: # If there are no prepared nodes, prepare some nodes next_node = self._get_next_node() if next_node is None: - prepared_id = self._prepare() - - # Prepare as many nodes as we can - while prepared_id is not None: - prepared_id = self._prepare() - next_node = self._get_next_node() + next_node = self._prepare_until_node_ready() # Get values from edges if next_node is not None: @@ -811,21 +1953,7 @@ def next(self) -> Optional[BaseInvocation]: def complete(self, node_id: str, output: BaseInvocationOutput) -> None: """Marks a node as complete""" - - if node_id not in self.execution_graph.nodes: - return # TODO: log error? - - # Mark node as executed - self.executed.add(node_id) - self.results[node_id] = output - - # Check if source node is complete (all prepared nodes are complete) - source_node = self.prepared_source_mapping[node_id] - prepared_nodes = self.source_prepared_mapping[source_node] - - if all(n in self.executed for n in prepared_nodes): - self.executed.add(source_node) - self.executed_history.append(source_node) + self._scheduler().complete(node_id, output) def set_node_error(self, node_id: str, error: str): """Marks a node as errored""" @@ -841,160 +1969,16 @@ def has_error(self) -> bool: return len(self.errors) > 0 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.graph.get_node(node_id) - - self_iteration_count = -1 - - # If this is an iterator node, we must create a copy for each iteration - if isinstance(node, IterateInvocation): - # Get input collection edge (should error if there are no inputs) - input_collection_edge = next(iter(self.graph._get_input_edges(node_id, "collection"))) - input_collection_prepared_node_id = next( - n[1] for n in iteration_node_map if n[0] == input_collection_edge.source.node_id - ) - input_collection_prepared_node_output = self.results[input_collection_prepared_node_id] - input_collection = getattr(input_collection_prepared_node_output, input_collection_edge.source.field) - self_iteration_count = len(input_collection) - - new_nodes: list[str] = [] - if self_iteration_count == 0: - # TODO: should this raise a warning? It might just happen if an empty collection is input, and should be valid. - return new_nodes - - # Get all input edges - input_edges = self.graph._get_input_edges(node_id) - - # Create new edges for this iteration - # For collect nodes, this may contain multiple inputs to the same field - new_edges: list[Edge] = [] - for edge in input_edges: - for input_node_id in (n[1] for n in iteration_node_map if n[0] == edge.source.node_id): - new_edge = Edge( - source=EdgeConnection(node_id=input_node_id, field=edge.source.field), - destination=EdgeConnection(node_id="", field=edge.destination.field), - ) - new_edges.append(new_edge) - - # Create a new node (or one for each iteration of this iterator) - for i in range(self_iteration_count) if self_iteration_count > 0 else [-1]: - # Create a new node - new_node = copy.deepcopy(node) - - # Create the node id (use a random uuid) - new_node.id = uuid_string() - - # Set the iteration index for iteration invocations - if isinstance(new_node, IterateInvocation): - new_node.index = i - - # Add to execution graph - self.execution_graph.add_node(new_node) - self.prepared_source_mapping[new_node.id] = node_id - if node_id not in self.source_prepared_mapping: - self.source_prepared_mapping[node_id] = set() - self.source_prepared_mapping[node_id].add(new_node.id) - - # Add new edges to execution graph - for edge in new_edges: - new_edge = Edge( - source=edge.source, - destination=EdgeConnection(node_id=new_node.id, field=edge.destination.field), - ) - self.execution_graph.add_edge(new_edge) - - new_nodes.append(new_node.id) + return self._materializer().create_execution_node(node_id, iteration_node_map) - return new_nodes + def _iterator_graph(self, base: Optional[nx.DiGraph] = None) -> nx.DiGraph: + return self._materializer().iterator_graph(base) - def _iterator_graph(self) -> nx.DiGraph: - """Gets a DiGraph with edges to collectors removed so an ancestor search produces all active iterators for any node""" - g = self.graph.nx_graph_flat() - collectors = (n for n in self.graph.nodes if isinstance(self.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) -> list[str]: - """Gets iterators for a node""" - g = self._iterator_graph() - iterators = [n for n in nx.ancestors(g, node_id) if isinstance(self.graph.get_node(n), IterateInvocation)] - return iterators - - def _prepare(self) -> Optional[str]: - # Get flattened source graph - g = self.graph.nx_graph_flat() - - # Find next node that: - # - was not already prepared - # - is not an iterate node whose inputs have not been executed - # - does not have an unexecuted iterate ancestor - sorted_nodes = nx.topological_sort(g) - next_node_id = next( - ( - n - for n in sorted_nodes - # exclude nodes that have already been prepared - if n not in self.source_prepared_mapping - # exclude iterate nodes whose inputs have not been executed - and not ( - isinstance(self.graph.get_node(n), IterateInvocation) # `n` is an iterate node... - and not all((e[0] in self.executed for e in g.in_edges(n))) # ...that has unexecuted inputs - ) - # exclude nodes who have unexecuted iterate ancestors - and not any( - ( - isinstance(self.graph.get_node(a), IterateInvocation) # `a` is an iterate ancestor of `n`... - and a not in self.executed # ...that is not executed - for a in nx.ancestors(g, n) # for all ancestors `a` of node `n` - ) - ) - ), - None, - ) - - if next_node_id is None: - return None - - # Get all parents of the next node - next_node_parents = [e[0] for e in g.in_edges(next_node_id)] - - # Create execution nodes - next_node = self.graph.get_node(next_node_id) - new_node_ids = [] - if isinstance(next_node, CollectInvocation): - # Collapse all iterator input mappings and create a single execution node for the collect invocation - all_iteration_mappings = list( - itertools.chain(*(((s, p) for p in self.source_prepared_mapping[s]) for s in next_node_parents)) - ) - # all_iteration_mappings = list(set(itertools.chain(*prepared_parent_mappings))) - create_results = self._create_execution_node(next_node_id, all_iteration_mappings) - if create_results is not None: - new_node_ids.extend(create_results) - else: # Iterators or normal nodes - # Get all iterator combinations for this node - # Will produce a list of lists of prepared iterator nodes, from which results can be iterated - iterator_nodes = self._get_node_iterators(next_node_id) - iterator_nodes_prepared = [list(self.source_prepared_mapping[n]) for n in iterator_nodes] - iterator_node_prepared_combinations = list(itertools.product(*iterator_nodes_prepared)) - - # Select the correct prepared parents for each iteration - # For every iterator, the parent must either not be a child of that iterator, or must match the prepared iteration for that iterator - # TODO: Handle a node mapping to none - eg = self.execution_graph.nx_graph_flat() - prepared_parent_mappings = [ - [(n, self._get_iteration_node(n, g, eg, it)) for n in next_node_parents] - for it in iterator_node_prepared_combinations - ] # type: ignore - - # Create execution node for each iteration - for iteration_mappings in prepared_parent_mappings: - create_results = self._create_execution_node(next_node_id, iteration_mappings) # type: ignore - if create_results is not None: - new_node_ids.extend(create_results) + 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) - return next(iter(new_node_ids), None) + def _prepare(self, base_g: Optional[nx.DiGraph] = None) -> Optional[str]: + return self._materializer().prepare(base_g) def _get_iteration_node( self, @@ -1003,74 +1987,13 @@ def _get_iteration_node( execution_graph: nx.DiGraph, prepared_iterator_nodes: list[str], ) -> Optional[str]: - """Gets the prepared version of the specified source node that matches every iteration specified""" - prepared_nodes = self.source_prepared_mapping[source_node_id] - if len(prepared_nodes) == 1: - return next(iter(prepared_nodes)) - - # Check if the requested node is an iterator - prepared_iterator = next((n for n in prepared_nodes if n in prepared_iterator_nodes), None) - if prepared_iterator is not None: - return prepared_iterator - - # Filter to only iterator nodes that are a parent of the specified node, in tuple format (prepared, source) - iterator_source_node_mapping = [(n, self.prepared_source_mapping[n]) for n in prepared_iterator_nodes] - parent_iterators = [itn for itn in iterator_source_node_mapping if nx.has_path(graph, itn[1], source_node_id)] - - return next( - (n for n in prepared_nodes if all(nx.has_path(execution_graph, pit[0], n) for pit in parent_iterators)), - None, - ) + return self._materializer().get_iteration_node(source_node_id, graph, execution_graph, prepared_iterator_nodes) def _get_next_node(self) -> Optional[BaseInvocation]: - """Gets the deepest node that is ready to be executed""" - g = self.execution_graph.nx_graph() - - # Perform a topological sort using depth-first search - topo_order = list(nx.dfs_postorder_nodes(g)) - - # Get all IterateInvocation nodes - iterate_nodes = [n for n in topo_order if isinstance(self.execution_graph.nodes[n], IterateInvocation)] - - # Sort the IterateInvocation nodes based on their index attribute - iterate_nodes.sort(key=lambda x: self.execution_graph.nodes[x].index) - - # Prioritize IterateInvocation nodes and their children - for iterate_node in iterate_nodes: - if iterate_node not in self.executed and all((e[0] in self.executed for e in g.in_edges(iterate_node))): - return self.execution_graph.nodes[iterate_node] - - # Check the children of the IterateInvocation node - for child_node in nx.dfs_postorder_nodes(g, iterate_node): - if child_node not in self.executed and all((e[0] in self.executed for e in g.in_edges(child_node))): - return self.execution_graph.nodes[child_node] - - # If no IterateInvocation node or its children are ready, return the first ready node in the topological order - for node in topo_order: - if node not in self.executed and all((e[0] in self.executed for e in g.in_edges(node))): - return self.execution_graph.nodes[node] - - # If no node is found, return None - return None + return self._scheduler().get_next_node() def _prepare_inputs(self, node: BaseInvocation): - input_edges = [e for e in self.execution_graph.edges if e.destination.node_id == node.id] - # Inputs must be deep-copied, else if a node mutates the object, other nodes that get the same input - # will see the mutation. - if isinstance(node, CollectInvocation): - output_collection = [ - copydeep(getattr(self.results[edge.source.node_id], edge.source.field)) - for edge in input_edges - if edge.destination.field == "item" - ] - node.collection = output_collection - else: - for edge in input_edges: - setattr( - node, - edge.destination.field, - copydeep(getattr(self.results[edge.source.node_id], edge.source.field)), - ) + 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: diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 01662335e46..e38766d5ba2 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -1,3 +1,4 @@ +from copy import deepcopy from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Callable, Optional, Union @@ -8,22 +9,20 @@ 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.util.step_callback import stable_diffusion_step_callback -from invokeai.backend.model_manager.config import ( - AnyModel, - AnyModelConfig, - BaseModelType, - ModelFormat, - ModelType, - SubModelType, -) +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 @@ -73,7 +72,7 @@ def __init__(self, services: InvocationServices, data: InvocationContextData) -> class BoardsInterface(InvocationContextInterface): def create(self, board_name: str) -> BoardDTO: - """Creates a board. + """Creates a board for the current user. Args: board_name: The name of the board to create. @@ -81,7 +80,8 @@ def create(self, board_name: str) -> BoardDTO: Returns: The created board DTO. """ - return self._services.boards.create(board_name) + 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. @@ -95,12 +95,15 @@ def get_dto(self, board_id: str) -> BoardDTO: return self._services.boards.get_dto(board_id) def get_all(self) -> list[BoardDTO]: - """Gets all boards. + """Gets all boards accessible to the current user. Returns: - A list of all boards. + A list of all boards accessible to the current user. """ - return self._services.boards.get_all() + 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. @@ -120,7 +123,11 @@ def get_all_image_names_for_board(self, board_id: str) -> list[str]: Returns: A list of all image names for the board. """ - return self._services.board_images.get_all_board_image_names_for_board(board_id) + return self._services.board_images.get_all_board_image_names_for_board( + board_id, + categories=None, + is_intermediate=None, + ) class LoggerInterface(InvocationContextInterface): @@ -158,6 +165,10 @@ def error(self, message: str) -> None: 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, @@ -184,6 +195,8 @@ def save( 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: @@ -217,10 +230,11 @@ def save( 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. + """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. @@ -232,11 +246,15 @@ def get_pil(self, image_name: str, mode: IMAGE_MODES | None = None) -> Image: 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]: @@ -271,7 +289,7 @@ def get_path(self, image_name: str, thumbnail: bool = False) -> Path: Returns: The local path of the image or thumbnail. """ - return self._services.images.get_path(image_name, thumbnail) + return Path(self._services.images.get_path(image_name, thumbnail)) class TensorsInterface(InvocationContextInterface): @@ -289,15 +307,15 @@ def save(self, tensor: Tensor) -> str: return name def load(self, name: str) -> Tensor: - """Loads a tensor by name. + """Loads a tensor by name. This method returns a copy of the tensor. Args: name: The name of the tensor to load. Returns: - The loaded tensor. + The tensor. """ - return self._services.tensors.load(name) + return self._services.tensors.load(name).clone() class ConditioningInterface(InvocationContextInterface): @@ -315,21 +333,25 @@ def save(self, conditioning_data: ConditioningFieldData) -> str: return name def load(self, name: str) -> ConditioningFieldData: - """Loads conditioning data by name. + """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 loaded conditioning data. + The conditioning data. """ - return self._services.conditioning.load(name) + 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. @@ -362,11 +384,17 @@ def load( if isinstance(identifier, str): model = self._services.model_manager.store.get_model(identifier) - return self._services.model_manager.load.load_model(model, submodel_type) else: - _submodel_type = submodel_type or identifier.submodel_type + submodel_type = submodel_type or identifier.submodel_type model = self._services.model_manager.store.get_model(identifier.key) - return self._services.model_manager.load.load_model(model, _submodel_type) + + 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 @@ -391,8 +419,18 @@ def load_by_attrs( 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. @@ -461,6 +499,7 @@ def download_and_cache_model( 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( @@ -483,6 +522,8 @@ def load_local_model( 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( @@ -508,8 +549,34 @@ def load_remote_model( 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: @@ -549,14 +616,108 @@ def sd_step_callback(self, intermediate_state: PipelineIntermediateState, base_m base_model: The base model for the current denoising step. """ - stable_diffusion_step_callback( - context_data=self._data, + diffusion_step_callback( + signal_progress=self.signal_progress, intermediate_state=intermediate_state, base_model=base_model, - events=self._services.events, 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. @@ -623,12 +784,12 @@ def build_invocation_context( """ logger = LoggerInterface(services=services, data=data) - images = ImagesInterface(services=services, data=data) tensors = TensorsInterface(services=services, data=data) - models = ModelsInterface(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( diff --git a/invokeai/app/services/shared/sqlite/sqlite_database.py b/invokeai/app/services/shared/sqlite/sqlite_database.py index e860160044e..e67aab0ea58 100644 --- a/invokeai/app/services/shared/sqlite/sqlite_database.py +++ b/invokeai/app/services/shared/sqlite/sqlite_database.py @@ -1,5 +1,7 @@ import sqlite3 import threading +from collections.abc import Generator +from contextlib import contextmanager from logging import Logger from pathlib import Path @@ -27,41 +29,65 @@ class SqliteDatabase: 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._logger = logger + self._db_path = db_path + self._verbose = verbose + self._lock = threading.RLock() - if not self.db_path: + 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._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.lock = threading.RLock() - self.conn.row_factory = sqlite3.Row + 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) + if self._verbose: + self._conn.set_trace_callback(self._logger.debug) - self.conn.execute("PRAGMA foreign_keys = ON;") + # 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: + if not self._db_path: return - with self.lock: - try: - initial_db_size = Path(self.db_path).stat().st_size - self.conn.execute("VACUUM;") - self.conn.commit() - final_db_size = Path(self.db_path).stat().st_size + 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}") + 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 index 3b5f4473066..14b6c61a85a 100644 --- a/invokeai/app/services/shared/sqlite/sqlite_util.py +++ b/invokeai/app/services/shared/sqlite/sqlite_util.py @@ -14,6 +14,28 @@ 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.migrations.migration_32 import build_migration_32 +from invokeai.app.services.shared.sqlite_migrator.migrations.migration_33 import build_migration_33 from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator @@ -45,6 +67,28 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto 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.register_migration(build_migration_32()) + migrator.register_migration(build_migration_33()) migrator.run_migrations() return db diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_11.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_11.py index f66374e0b1e..17e61334f09 100644 --- a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_11.py +++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_11.py @@ -35,7 +35,7 @@ def __call__(self, cursor: sqlite3.Cursor) -> None: def _remove_convert_cache(self) -> None: """Rename models/.cache to models/.convert_cache.""" - self._logger.info("Removing .cache directory. Converted models will now be cached in .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) 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_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_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_32.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py new file mode 100644 index 00000000000..5e06d634e76 --- /dev/null +++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_32.py @@ -0,0 +1,91 @@ +"""Migration 32: Repair model_relationships foreign keys. + +Migration 22 rebuilt the `models` table by renaming it to `models_old`, creating a +fresh `models` table, copying the data over, and dropping `models_old`. Because +modern SQLite (with `legacy_alter_table` off) rewrites foreign-key references in +*other* tables when a table is renamed, the foreign keys in `model_relationships` +were silently repointed at `models_old` -- which was then dropped. + +This left the related-models links referencing a table that no longer exists, +breaking `ON DELETE CASCADE` and foreign-key integrity for related models. + +This migration rebuilds `model_relationships` so its foreign keys reference +`models(id)` again, preserving existing links and dropping any orphaned rows whose +model keys no longer exist (those would violate the restored foreign keys). +""" + +import sqlite3 + +from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration + + +class Migration32Callback: + """Migration to repair the broken foreign keys on the model_relationships table.""" + + def __call__(self, cursor: sqlite3.Cursor) -> None: + self._repair_model_relationships_fks(cursor) + + def _repair_model_relationships_fks(self, cursor: sqlite3.Cursor) -> None: + cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='model_relationships';") + row = cursor.fetchone() + if row is None: + # Table does not exist (fresh db will create it correctly), nothing to repair. + return + + existing_sql: str = row[0] + if "models_old" not in existing_sql: + # Foreign keys already point at the correct table, nothing to repair. + return + + # Rebuild the table with the correct foreign keys referencing models(id). + cursor.execute("ALTER TABLE model_relationships RENAME TO model_relationships_old;") + cursor.execute( + """ + -- many-to-many relationship table for models + CREATE TABLE 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 + ); + """ + ) + + # Copy over the existing links, dropping any orphaned rows whose model keys no + # longer exist -- these would violate the restored foreign keys. + cursor.execute( + """ + INSERT INTO model_relationships (model_key_1, model_key_2, created_at) + SELECT model_key_1, model_key_2, created_at + FROM model_relationships_old + WHERE model_key_1 IN (SELECT id FROM models) + AND model_key_2 IN (SELECT id FROM models); + """ + ) + + # Drop the old table first so its index name is freed before we recreate it. + cursor.execute("DROP TABLE model_relationships_old;") + 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_32() -> Migration: + """Builds the migration object for migrating from version 31 to version 32. + + This migration repairs the foreign keys on the model_relationships table, which were + broken by migration 22 rebuilding the models table. + """ + return Migration( + from_version=31, + to_version=32, + callback=Migration32Callback(), + ) diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_33.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_33.py new file mode 100644 index 00000000000..d99e1897137 --- /dev/null +++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_33.py @@ -0,0 +1,36 @@ +"""Migration 33: Add device column to session_queue table. + +This records which device (e.g. 'cuda:1') processed a queue item, so the UI can show a per-item +GPU number in the Session Queue. Existing rows get NULL (unknown device). +""" + +import sqlite3 + +from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration + + +class Migration33Callback: + """Migration to add a device column to the session_queue table.""" + + 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 "device" not in columns: + cursor.execute("ALTER TABLE session_queue ADD COLUMN device TEXT;") + + +def build_migration_33() -> Migration: + """Builds the migration object for migrating from version 32 to version 33. + + This migration adds a device column to the session_queue table. + """ + return Migration( + from_version=32, + to_version=33, + callback=Migration33Callback(), + ) diff --git a/invokeai/app/services/shared/sqlite_migrator/sqlite_migrator_impl.py b/invokeai/app/services/shared/sqlite_migrator/sqlite_migrator_impl.py index 5d78d55818c..310abf05200 100644 --- a/invokeai/app/services/shared/sqlite_migrator/sqlite_migrator_impl.py +++ b/invokeai/app/services/shared/sqlite_migrator/sqlite_migrator_impl.py @@ -32,7 +32,7 @@ class SqliteMigrator: def __init__(self, db: SqliteDatabase) -> None: self._db = db - self._logger = db.logger + self._logger = db._logger self._migration_set = MigrationSet() self._backup_path: Optional[Path] = None @@ -43,46 +43,45 @@ def register_migration(self, migration: Migration) -> None: def run_migrations(self) -> bool: """Migrates the database to the latest version.""" - with self._db.lock: - # 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 + # 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.lock, self._db.conn as conn: + with self._db._conn as conn: cursor = conn.cursor() if self._get_current_version(cursor) != migration.from_version: raise MigrationError( @@ -108,27 +107,26 @@ def _run_migration(self, migration: Migration) -> None: def _create_migrations_table(self, cursor: sqlite3.Cursor) -> None: """Creates the migrations table for the database, if one does not already exist.""" - with self._db.lock: - 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 + 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: 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/tests/test_model_probe/sd-1/main/dreamshaper-8-inpainting/unet/diffusion_pytorch_model.fp16.safetensors b/invokeai/app/services/style_preset_images/default_style_preset_images/__init__.py similarity index 100% rename from tests/test_model_probe/sd-1/main/dreamshaper-8-inpainting/unet/diffusion_pytorch_model.fp16.safetensors rename to invokeai/app/services/style_preset_images/default_style_preset_images/__init__.py 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/tests/test_model_probe/vae/taesdxl-fp16/diffusion_pytorch_model.fp16.safetensors b/invokeai/app/services/style_preset_records/__init__.py similarity index 100% rename from tests/test_model_probe/vae/taesdxl-fp16/diffusion_pytorch_model.fp16.safetensors rename to invokeai/app/services/style_preset_records/__init__.py 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/urls_base.py b/invokeai/app/services/urls/urls_base.py index 477ef046240..a5602abb3b4 100644 --- a/invokeai/app/services/urls/urls_base.py +++ b/invokeai/app/services/urls/urls_base.py @@ -13,3 +13,13 @@ def get_image_url(self, image_name: str, thumbnail: bool = False) -> str: 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 index ff5071333f6..2e4f36d9d51 100644 --- a/invokeai/app/services/urls/urls_default.py +++ b/invokeai/app/services/urls/urls_default.py @@ -1,6 +1,6 @@ import os -from .urls_base import UrlServiceBase +from invokeai.app.services.urls.urls_base import UrlServiceBase class LocalUrlService(UrlServiceBase): @@ -19,3 +19,9 @@ def get_image_url(self, image_name: str, thumbnail: bool = False) -> str: 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/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 index dd98eca18f3..8589b301836 100644 --- 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 @@ -1,10 +1,11 @@ { - "name": "ESRGAN Upscaling with Canny ControlNet", + "id": "default_686bb1d0-d086-4c70-9fa3-2f600b922023", + "name": "Upscaler - SD1.5, ESRGAN", "author": "InvokeAI", - "description": "Sample workflow for using Upscaling with ControlNet with SD1.5", - "version": "2.0.0", + "description": "Sample workflow for using ESRGAN to upscale with ControlNet with SD1.5", + "version": "2.1.0", "contact": "invoke@invoke.ai", - "tags": "upscale, controlnet, default", + "tags": "sd1.5, upscaling, control", "notes": "", "exposedFields": [ { @@ -36,14 +37,13 @@ "version": "3.0.0", "category": "default" }, - "id": "0e71a27e-a22b-4a9b-b20a-6d789abff2bc", "nodes": [ { - "id": "e8bf67fe-67de-4227-87eb-79e86afdfc74", + "id": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b", "type": "invocation", "data": { - "id": "e8bf67fe-67de-4227-87eb-79e86afdfc74", - "version": "1.1.1", + "id": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b", + "version": "1.2.0", "nodePack": "invokeai", "label": "", "notes": "", @@ -57,6 +57,10 @@ "clip": { "name": "clip", "label": "" + }, + "mask": { + "name": "mask", + "label": "" } }, "isOpen": true, @@ -65,101 +69,148 @@ }, "position": { "x": 1250, - "y": 1500 + "y": 1200 } }, { - "id": "d8ace142-c05f-4f1d-8982-88dc7473958d", + "id": "5ca498a4-c8c8-4580-a396-0c984317205d", "type": "invocation", "data": { - "id": "d8ace142-c05f-4f1d-8982-88dc7473958d", - "version": "1.0.2", + "id": "5ca498a4-c8c8-4580-a396-0c984317205d", + "version": "1.1.0", "nodePack": "invokeai", "label": "", "notes": "", - "type": "main_model_loader", + "type": "i2l", "inputs": { - "model": { - "name": "model", + "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": { - "key": "5cd43ca0-dd0a-418d-9f7e-35b2b9d5e106", - "hash": "blake3:6987f323017f597213cc3264250edf57056d21a40a0a85d83a1a33a7d44dc41a", - "name": "Deliberate_v5", - "base": "sd-1", - "type": "main" - } + "value": false } }, - "isOpen": true, + "isOpen": false, "isIntermediate": true, "useCache": true }, "position": { - "x": 700, - "y": 1375 + "x": 1650, + "y": 1675 } }, { - "id": "771bdf6a-0813-4099-a5d8-921a138754d4", + "id": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0", "type": "invocation", "data": { - "id": "771bdf6a-0813-4099-a5d8-921a138754d4", - "version": "1.0.2", + "id": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0", + "version": "1.3.0", "nodePack": "invokeai", "label": "", "notes": "", - "type": "image", + "type": "l2i", "inputs": { - "image": { - "name": "image", - "label": "Image To Upscale", - "value": { - "image_name": "d2e42ba6-d420-496b-82db-91c9b75956c1.png" - } + "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, + "isIntermediate": false, "useCache": true }, "position": { - "x": 344.5593065887157, - "y": 1698.161491368619 + "x": 2559.4751127537957, + "y": 1246.6000376741406 } }, { - "id": "f7564dd2-9539-47f2-ac13-190804461f4e", + "id": "ca1d020c-89a8-4958-880a-016d28775cfa", "type": "invocation", "data": { - "id": "f7564dd2-9539-47f2-ac13-190804461f4e", - "version": "1.3.2", + "id": "ca1d020c-89a8-4958-880a-016d28775cfa", + "version": "1.1.2", "nodePack": "invokeai", "label": "", "notes": "", - "type": "esrgan", + "type": "controlnet", "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" + "control_model": { + "name": "control_model", + "label": "Control Model (select Canny)" }, - "tile_size": { - "name": "tile_size", + "control_weight": { + "name": "control_weight", "label": "", - "value": 400 + "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, @@ -167,8 +218,8 @@ "useCache": true }, "position": { - "x": 717.3863693661265, - "y": 1721.9215053134815 + "x": 1624.7980608333519, + "y": 1902.9649340196056 } }, { @@ -176,7 +227,7 @@ "type": "invocation", "data": { "id": "1d887701-df21-4966-ae6e-a7d82307d7bd", - "version": "1.3.2", + "version": "1.3.3", "nodePack": "invokeai", "label": "", "notes": "", @@ -225,55 +276,121 @@ } }, { - "id": "ca1d020c-89a8-4958-880a-016d28775cfa", + "id": "d8ace142-c05f-4f1d-8982-88dc7473958d", "type": "invocation", "data": { - "id": "ca1d020c-89a8-4958-880a-016d28775cfa", - "version": "1.1.1", + "id": "d8ace142-c05f-4f1d-8982-88dc7473958d", + "version": "1.0.3", "nodePack": "invokeai", "label": "", "notes": "", - "type": "controlnet", + "type": "main_model_loader", "inputs": { - "image": { - "name": "image", + "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": "" }, - "control_model": { - "name": "control_model", - "label": "Control Model (select Canny)", - "value": { - "key": "a7b9c76f-4bc5-42aa-b918-c1c458a5bb24", - "hash": "blake3:260c7f8e10aefea9868cfc68d89970e91033bd37132b14b903e70ee05ebf530e", - "name": "sd-controlnet-canny", - "base": "sd-1", - "type": "controlnet" - } + "clip": { + "name": "clip", + "label": "" }, - "control_weight": { - "name": "control_weight", - "label": "", - "value": 0.95 + "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": "" }, - "begin_step_percent": { - "name": "begin_step_percent", - "label": "", - "value": 0.1 + "metadata": { + "name": "metadata", + "label": "" }, - "end_step_percent": { - "name": "end_step_percent", - "label": "", - "value": 0.9 + "image": { + "name": "image", + "label": "" }, - "control_mode": { - "name": "control_mode", - "label": "", - "value": "balanced" + "model_name": { + "name": "model_name", + "label": "Upscaler Model", + "value": "RealESRGAN_x2plus.pth" }, - "resize_mode": { - "name": "resize_mode", + "tile_size": { + "name": "tile_size", "label": "", - "value": "just_resize" + "value": 400 } }, "isOpen": true, @@ -281,8 +398,8 @@ "useCache": true }, "position": { - "x": 1624.7980608333519, - "y": 1902.9649340196056 + "x": 717.3863693661265, + "y": 1721.9215053134815 } }, { @@ -413,122 +530,6 @@ "y": 1232.6219060454753 } }, - { - "id": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0", - "type": "invocation", - "data": { - "id": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0", - "version": "1.2.2", - "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 - }, - "fp32": { - "name": "fp32", - "label": "", - "value": false - } - }, - "isOpen": true, - "isIntermediate": false, - "useCache": true - }, - "position": { - "x": 2559.4751127537957, - "y": 1246.6000376741406 - } - }, - { - "id": "5ca498a4-c8c8-4580-a396-0c984317205d", - "type": "invocation", - "data": { - "id": "5ca498a4-c8c8-4580-a396-0c984317205d", - "version": "1.0.2", - "nodePack": "invokeai", - "label": "", - "notes": "", - "type": "i2l", - "inputs": { - "image": { - "name": "image", - "label": "" - }, - "vae": { - "name": "vae", - "label": "" - }, - "tiled": { - "name": "tiled", - "label": "", - "value": false - }, - "fp32": { - "name": "fp32", - "label": "", - "value": false - } - }, - "isOpen": false, - "isIntermediate": true, - "useCache": true - }, - "position": { - "x": 1650, - "y": 1675 - } - }, - { - "id": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b", - "type": "invocation", - "data": { - "id": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b", - "version": "1.1.1", - "nodePack": "invokeai", - "label": "", - "notes": "", - "type": "compel", - "inputs": { - "prompt": { - "name": "prompt", - "label": "", - "value": "" - }, - "clip": { - "name": "clip", - "label": "" - } - }, - "isOpen": true, - "isIntermediate": true, - "useCache": true - }, - "position": { - "x": 1250, - "y": 1200 - } - }, { "id": "eb8f6f8a-c7b1-4914-806e-045ee2717a35", "type": "invocation", @@ -834,4 +835,4 @@ "targetHandle": "image_resolution" } ] -} \ No newline at end of file +} 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 index 8c7dcee30c8..1e7753ea2c3 100644 --- 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 @@ -1,10 +1,11 @@ { - "name": "Face Detailer with IP-Adapter & Canny (See Note in Details)", + "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.0.0", + "version": "2.1.0", "contact": "invoke@invoke.ai", - "tags": "face detailer, IP-Adapter, Canny", + "tags": "sd1.5, reference image, control", "notes": "Set this image as the blur mask: https://i.imgur.com/Gxi61zP.png", "exposedFields": [ { @@ -37,16 +38,335 @@ } ], "meta": { - "category": "default", - "version": "3.0.0" + "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.1.1", + "version": "1.2.0", "nodePack": "invokeai", "label": "", "notes": "", @@ -60,6 +380,10 @@ "clip": { "name": "clip", "label": "" + }, + "mask": { + "name": "mask", + "label": "" } }, "isOpen": true, @@ -235,50 +559,11 @@ "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": "de8b1a48-a2e4-42ca-90bb-66058bffd534", - "type": "invocation", - "data": { - "id": "de8b1a48-a2e4-42ca-90bb-66058bffd534", - "version": "1.0.2", - "nodePack": "invokeai", - "label": "", - "notes": "", - "type": "i2l", - "inputs": { - "image": { - "name": "image", - "label": "" - }, - "vae": { - "name": "vae", - "label": "" - }, - "tiled": { - "name": "tiled", - "label": "", - "value": false - }, - "fp32": { - "name": "fp32", + }, + "resample_mode": { + "name": "resample_mode", "label": "", - "value": true + "value": "lanczos" } }, "isOpen": false, @@ -286,8 +571,8 @@ "useCache": true }, "position": { - "x": 3100, - "y": -275 + "x": 3000, + "y": 0 } }, { @@ -418,53 +703,6 @@ "y": -175 } }, - { - "id": "2224ed72-2453-4252-bd89-3085240e0b6f", - "type": "invocation", - "data": { - "id": "2224ed72-2453-4252-bd89-3085240e0b6f", - "version": "1.2.2", - "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 - }, - "fp32": { - "name": "fp32", - "label": "", - "value": true - } - }, - "isOpen": true, - "isIntermediate": false, - "useCache": true - }, - "position": { - "x": 4980.1395106966565, - "y": -255.9158921745602 - } - }, { "id": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323", "type": "invocation", @@ -692,201 +930,6 @@ "y": -275 } }, - { - "id": "f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65", - "type": "invocation", - "data": { - "id": "f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65", - "version": "1.1.1", - "nodePack": "invokeai", - "label": "", - "notes": "", - "type": "compel", - "inputs": { - "prompt": { - "name": "prompt", - "label": "", - "value": "" - }, - "clip": { - "name": "clip", - "label": "" - } - }, - "isOpen": true, - "isIntermediate": true, - "useCache": true - }, - "position": { - "x": 2550, - "y": -525 - } - }, - { - "id": "22b750db-b85e-486b-b278-ac983e329813", - "type": "invocation", - "data": { - "id": "22b750db-b85e-486b-b278-ac983e329813", - "version": "1.2.2", - "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)", - "value": { - "key": "1cc210bb-4d0a-4312-b36c-b5d46c43768e", - "hash": "blake3:3d669dffa7471b357b4df088b99ffb6bf4d4383d5e0ef1de5ec1c89728a3d5a5", - "name": "ip_adapter_sd15", - "base": "sd-1", - "type": "ip_adapter" - } - }, - "weight": { - "name": "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.8 - } - }, - "isOpen": true, - "isIntermediate": true, - "useCache": true - }, - "position": { - "x": 3575, - "y": -200 - } - }, - { - "id": "f60b6161-8f26-42f6-89ff-545e6011e501", - "type": "invocation", - "data": { - "id": "f60b6161-8f26-42f6-89ff-545e6011e501", - "version": "1.1.1", - "nodePack": "invokeai", - "label": "", - "notes": "", - "type": "controlnet", - "inputs": { - "image": { - "name": "image", - "label": "" - }, - "control_model": { - "name": "control_model", - "label": "Control Model (select canny)", - "value": { - "key": "5bdaacf7-a7a3-4fb8-b394-cc0ffbb8941d", - "hash": "blake3:260c7f8e10aefea9868cfc68d89970e91033bd37132b14b903e70ee05ebf530e", - "name": "sd-controlnet-canny", - "base": "sd-1", - "type": "controlnet" - } - }, - "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": "8fe598c6-d447-44fa-a165-4975af77d080", - "type": "invocation", - "data": { - "id": "8fe598c6-d447-44fa-a165-4975af77d080", - "version": "1.3.2", - "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": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a", "type": "invocation", @@ -1035,30 +1078,6 @@ "x": 2578.2364832140506, "y": 78.7948456497351 } - }, - { - "id": "c6359181-6479-40ec-bf3a-b7e8451683b8", - "type": "invocation", - "data": { - "id": "c6359181-6479-40ec-bf3a-b7e8451683b8", - "version": "1.0.2", - "label": "", - "notes": "", - "type": "main_model_loader", - "inputs": { - "model": { - "name": "model", - "label": "" - } - }, - "isOpen": true, - "isIntermediate": true, - "useCache": true - }, - "position": { - "x": 2031.5518710051792, - "y": -492.1742944307074 - } } ], "edges": [ @@ -1413,4 +1432,4 @@ "targetHandle": "vae" } ] -} \ No newline at end of file +} 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 index d859094216e..1c91f4a7b0c 100644 --- 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 @@ -1,10 +1,11 @@ { - "name": "Multi ControlNet (Canny & Depth)", + "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.0.0", + "version": "2.1.0", "contact": "invoke@invoke.ai", - "tags": "ControlNet, canny, depth", + "tags": "sd1.5, control, text to image", "notes": "", "exposedFields": [ { @@ -37,24 +38,104 @@ } ], "meta": { - "category": "default", - "version": "3.0.0" + "version": "3.0.0", + "category": "default" }, "nodes": [ { - "id": "8e860e51-5045-456e-bf04-9a62a2a5c49e", + "id": "9db25398-c869-4a63-8815-c6559341ef12", "type": "invocation", "data": { - "id": "8e860e51-5045-456e-bf04-9a62a2a5c49e", - "version": "1.0.2", + "id": "9db25398-c869-4a63-8815-c6559341ef12", + "version": "1.3.0", "nodePack": "invokeai", "label": "", "notes": "", - "type": "image", + "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": "Depth Input 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, @@ -62,16 +143,69 @@ "useCache": true }, "position": { - "x": 3666.135718057363, - "y": 186.66887319822808 + "x": 4095.757337055795, + "y": -455.63440891935863 } }, { - "id": "a33199c2-8340-401e-b8a2-42ffa875fc1c", + "id": "018b1214-c2af-43a7-9910-fb687c6726d7", "type": "invocation", "data": { - "id": "a33199c2-8340-401e-b8a2-42ffa875fc1c", - "version": "1.1.1", + "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": "", @@ -83,14 +217,7 @@ }, "control_model": { "name": "control_model", - "label": "Control Model (select depth)", - "value": { - "key": "87e8855c-671f-4c9e-bbbb-8ed47ccb4aac", - "hash": "blake3:2550bf22a53942dfa28ab2fed9d10d80851112531f44d977168992edf9d0534c", - "name": "control_v11f1p_sd15_depth", - "base": "sd-1", - "type": "controlnet" - } + "label": "Control Model (select canny)" }, "control_weight": { "name": "control_weight", @@ -123,16 +250,16 @@ "useCache": true }, "position": { - "x": 4477.604342844504, - "y": -49.39005411272677 + "x": 4479.68542130465, + "y": -618.4221638099414 } }, { - "id": "273e3f96-49ea-4dc5-9d5b-9660390f14e1", + "id": "7ce68934-3419-42d4-ac70-82cfc9397306", "type": "invocation", "data": { - "id": "273e3f96-49ea-4dc5-9d5b-9660390f14e1", - "version": "1.1.1", + "id": "7ce68934-3419-42d4-ac70-82cfc9397306", + "version": "1.2.0", "nodePack": "invokeai", "label": "", "notes": "", @@ -140,12 +267,16 @@ "inputs": { "prompt": { "name": "prompt", - "label": "Negative Prompt", + "label": "Positive Prompt", "value": "" }, "clip": { "name": "clip", "label": "" + }, + "mask": { + "name": "mask", + "label": "" } }, "isOpen": true, @@ -154,7 +285,7 @@ }, "position": { "x": 4075, - "y": -825 + "y": -1125 } }, { @@ -162,7 +293,7 @@ "type": "invocation", "data": { "id": "54486974-835b-4d81-8f82-05f9f32ce9e9", - "version": "1.0.2", + "version": "1.0.3", "nodePack": "invokeai", "label": "", "notes": "", @@ -183,11 +314,11 @@ } }, { - "id": "7ce68934-3419-42d4-ac70-82cfc9397306", + "id": "273e3f96-49ea-4dc5-9d5b-9660390f14e1", "type": "invocation", "data": { - "id": "7ce68934-3419-42d4-ac70-82cfc9397306", - "version": "1.1.1", + "id": "273e3f96-49ea-4dc5-9d5b-9660390f14e1", + "version": "1.2.0", "nodePack": "invokeai", "label": "", "notes": "", @@ -195,12 +326,16 @@ "inputs": { "prompt": { "name": "prompt", - "label": "Positive Prompt", + "label": "Negative Prompt", "value": "" }, "clip": { "name": "clip", "label": "" + }, + "mask": { + "name": "mask", + "label": "" } }, "isOpen": true, @@ -209,15 +344,15 @@ }, "position": { "x": 4075, - "y": -1125 + "y": -825 } }, { - "id": "d204d184-f209-4fae-a0a1-d152800844e1", + "id": "a33199c2-8340-401e-b8a2-42ffa875fc1c", "type": "invocation", "data": { - "id": "d204d184-f209-4fae-a0a1-d152800844e1", - "version": "1.1.1", + "id": "a33199c2-8340-401e-b8a2-42ffa875fc1c", + "version": "1.1.2", "nodePack": "invokeai", "label": "", "notes": "", @@ -229,14 +364,7 @@ }, "control_model": { "name": "control_model", - "label": "Control Model (select canny)", - "value": { - "key": "5bdaacf7-a7a3-4fb8-b394-cc0ffbb8941d", - "hash": "blake3:260c7f8e10aefea9868cfc68d89970e91033bd37132b14b903e70ee05ebf530e", - "name": "sd-controlnet-canny", - "base": "sd-1", - "type": "controlnet" - } + "label": "Control Model (select depth)" }, "control_weight": { "name": "control_weight", @@ -269,15 +397,15 @@ "useCache": true }, "position": { - "x": 4479.68542130465, - "y": -618.4221638099414 + "x": 4477.604342844504, + "y": -49.39005411272677 } }, { - "id": "c4b23e64-7986-40c4-9cad-46327b12e204", + "id": "8e860e51-5045-456e-bf04-9a62a2a5c49e", "type": "invocation", "data": { - "id": "c4b23e64-7986-40c4-9cad-46327b12e204", + "id": "8e860e51-5045-456e-bf04-9a62a2a5c49e", "version": "1.0.2", "nodePack": "invokeai", "label": "", @@ -286,31 +414,7 @@ "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": "" + "label": "Depth Input Image" } }, "isOpen": true, @@ -318,52 +422,24 @@ "useCache": true }, "position": { - "x": 4875, - "y": -575 + "x": 3666.135718057363, + "y": 186.66887319822808 } }, { - "id": "018b1214-c2af-43a7-9910-fb687c6726d7", + "id": "c4b23e64-7986-40c4-9cad-46327b12e204", "type": "invocation", "data": { - "id": "018b1214-c2af-43a7-9910-fb687c6726d7", - "version": "1.2.3", + "id": "c4b23e64-7986-40c4-9cad-46327b12e204", + "version": "1.0.2", "nodePack": "invokeai", "label": "", "notes": "", - "type": "midas_depth_image_processor", + "type": "image", "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 + "label": "Canny Input Image" } }, "isOpen": true, @@ -371,52 +447,23 @@ "useCache": true }, "position": { - "x": 4082.783145980783, - "y": 0.01629251229994111 + "x": 3625, + "y": -425 } }, { - "id": "c826ba5e-9676-4475-b260-07b85e88753c", + "id": "ca4d5059-8bfb-447f-b415-da0faba5a143", "type": "invocation", "data": { - "id": "c826ba5e-9676-4475-b260-07b85e88753c", - "version": "1.3.2", - "nodePack": "invokeai", - "label": "", + "id": "ca4d5059-8bfb-447f-b415-da0faba5a143", + "version": "1.0.0", + "label": "ControlNet Collection", "notes": "", - "type": "canny_image_processor", + "type": "collect", "inputs": { - "board": { - "name": "board", - "label": "" - }, - "metadata": { - "name": "metadata", - "label": "" - }, - "image": { - "name": "image", + "item": { + "name": "item", "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, @@ -424,55 +471,8 @@ "useCache": true }, "position": { - "x": 4095.757337055795, - "y": -455.63440891935863 - } - }, - { - "id": "9db25398-c869-4a63-8815-c6559341ef12", - "type": "invocation", - "data": { - "id": "9db25398-c869-4a63-8815-c6559341ef12", - "version": "1.2.2", - "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 - }, - "fp32": { - "name": "fp32", - "label": "", - "value": false - } - }, - "isOpen": true, - "isIntermediate": false, - "useCache": true - }, - "position": { - "x": 5675, - "y": -825 + "x": 4875, + "y": -575 } }, { @@ -1001,4 +1001,4 @@ "targetHandle": "image_resolution" } ] -} \ No newline at end of file +} 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 index 765b236714f..747213e140b 100644 --- a/invokeai/app/services/workflow_records/default_workflows/Prompt from File.json +++ b/invokeai/app/services/workflow_records/default_workflows/Prompt from File.json @@ -1,10 +1,11 @@ { - "name": "Prompt from File", + "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.0.0", + "version": "2.1.0", "contact": "invoke@invoke.ai", - "tags": "text2image, prompt from file, default", + "tags": "sd1.5, text to image", "notes": "", "exposedFields": [ { @@ -37,16 +38,127 @@ } ], "meta": { - "category": "default", - "version": "3.0.0" + "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.1.1", + "version": "1.2.0", "nodePack": "invokeai", "label": "", "notes": "", @@ -60,6 +172,10 @@ "clip": { "name": "clip", "label": "" + }, + "mask": { + "name": "mask", + "label": "" } }, "isOpen": false, @@ -141,61 +257,6 @@ "y": -400 } }, - { - "id": "d6353b7f-b447-4e17-8f2e-80a88c91d426", - "type": "invocation", - "data": { - "id": "d6353b7f-b447-4e17-8f2e-80a88c91d426", - "version": "1.0.2", - "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": "fc9d0e35-a6de-4a19-84e1-c72497c823f6", - "type": "invocation", - "data": { - "id": "fc9d0e35-a6de-4a19-84e1-c72497c823f6", - "version": "1.1.1", - "nodePack": "invokeai", - "label": "", - "notes": "", - "type": "compel", - "inputs": { - "prompt": { - "name": "prompt", - "label": "", - "value": "" - }, - "clip": { - "name": "clip", - "label": "" - } - }, - "isOpen": false, - "isIntermediate": true, - "useCache": true - }, - "position": { - "x": 925, - "y": -275 - } - }, { "id": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77", "type": "invocation", @@ -268,53 +329,6 @@ "y": -50 } }, - { - "id": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1", - "type": "invocation", - "data": { - "id": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1", - "version": "1.2.2", - "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 - }, - "fp32": { - "name": "fp32", - "label": "", - "value": false - } - }, - "isOpen": true, - "isIntermediate": true, - "useCache": true - }, - "position": { - "x": 2037.861329274915, - "y": -329.8393457509562 - } - }, { "id": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e", "type": "invocation", @@ -499,4 +513,4 @@ "targetHandle": "vae" } ] -} \ No newline at end of file +} diff --git a/invokeai/app/services/workflow_records/default_workflows/README.md b/invokeai/app/services/workflow_records/default_workflows/README.md index 3901ead1cd5..a70cc14c879 100644 --- a/invokeai/app/services/workflow_records/default_workflows/README.md +++ b/invokeai/app/services/workflow_records/default_workflows/README.md @@ -3,12 +3,14 @@ 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: 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 index d3d52150bc4..a6b4ddf6dfb 100644 --- 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 @@ -1,10 +1,11 @@ { + "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.0.0", + "version": "2.1.0", "contact": "invoke@invoke.ai", - "tags": "text2image, SD1.5, SD2, default", + "tags": "SD1.5, text to image", "notes": "", "exposedFields": [ { @@ -33,70 +34,85 @@ } ], "meta": { - "category": "default", - "version": "3.0.0" + "version": "3.0.0", + "category": "default" }, "nodes": [ { - "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402", + "id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", "type": "invocation", "data": { - "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402", - "version": "1.1.1", + "id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", + "version": "1.3.0", "nodePack": "invokeai", - "label": "Negative Compel Prompt", + "label": "", "notes": "", - "type": "compel", + "type": "l2i", "inputs": { - "prompt": { - "name": "prompt", - "label": "Negative Prompt", - "value": "" + "board": { + "name": "board", + "label": "" }, - "clip": { - "name": "clip", + "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": true, + "isIntermediate": false, "useCache": true }, "position": { - "x": 1000, - "y": 350 + "x": 1800, + "y": 25 } }, { - "id": "55705012-79b9-4aac-9f26-c0b10309785b", + "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c", "type": "invocation", "data": { - "id": "55705012-79b9-4aac-9f26-c0b10309785b", - "version": "1.0.2", + "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c", + "version": "1.2.0", "nodePack": "invokeai", - "label": "", + "label": "Positive Compel Prompt", "notes": "", - "type": "noise", + "type": "compel", "inputs": { - "seed": { - "name": "seed", - "label": "", - "value": 0 - }, - "width": { - "name": "width", - "label": "", - "value": 512 + "prompt": { + "name": "prompt", + "label": "Positive Prompt", + "value": "Super cute tiger cub, national geographic award-winning photograph" }, - "height": { - "name": "height", - "label": "", - "value": 768 + "clip": { + "name": "clip", + "label": "" }, - "use_cpu": { - "name": "use_cpu", - "label": "", - "value": true + "mask": { + "name": "mask", + "label": "" } }, "isOpen": true, @@ -104,8 +120,8 @@ "useCache": true }, "position": { - "x": 600, - "y": 325 + "x": 1000, + "y": 25 } }, { @@ -113,7 +129,7 @@ "type": "invocation", "data": { "id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8", - "version": "1.0.2", + "version": "1.0.3", "nodePack": "invokeai", "label": "", "notes": "", @@ -134,24 +150,28 @@ } }, { - "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c", + "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402", "type": "invocation", "data": { - "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c", - "version": "1.1.1", + "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402", + "version": "1.2.0", "nodePack": "invokeai", - "label": "Positive Compel Prompt", + "label": "Negative Compel Prompt", "notes": "", "type": "compel", "inputs": { "prompt": { "name": "prompt", - "label": "Positive Prompt", - "value": "Super cute tiger cub, national geographic award-winning photograph" + "label": "Negative Prompt", + "value": "" }, "clip": { "name": "clip", "label": "" + }, + "mask": { + "name": "mask", + "label": "" } }, "isOpen": true, @@ -160,7 +180,48 @@ }, "position": { "x": 1000, - "y": 25 + "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 } }, { @@ -280,53 +341,6 @@ "x": 1400, "y": 25 } - }, - { - "id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", - "type": "invocation", - "data": { - "id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f", - "version": "1.2.2", - "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 - }, - "fp32": { - "name": "fp32", - "label": "", - "value": true - } - }, - "isOpen": true, - "isIntermediate": false, - "useCache": true - }, - "position": { - "x": 1800, - "y": 25 - } } ], "edges": [ @@ -403,4 +417,4 @@ "targetHandle": "vae" } ] -} \ No newline at end of file +} 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 index 1527bbceb17..391ff46e9e5 100644 --- 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 @@ -1,10 +1,11 @@ { + "id": "default_5e8b008d-c697-45d0-8883-085a954c6ace", "name": "Text to Image - SDXL", "author": "InvokeAI", "description": "Sample text to image workflow for SDXL", - "version": "2.0.0", + "version": "2.1.0", "contact": "invoke@invoke.ai", - "tags": "text2image, SDXL, default", + "tags": "SDXL, text to image", "notes": "", "exposedFields": [ { @@ -29,84 +30,105 @@ } ], "meta": { - "category": "default", - "version": "3.0.0" + "version": "3.0.0", + "category": "default" }, "nodes": [ { - "id": "3774ec24-a69e-4254-864c-097d07a6256f", + "id": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8", "type": "invocation", "data": { - "id": "3774ec24-a69e-4254-864c-097d07a6256f", - "version": "1.0.1", - "label": "Positive Style Concat", + "id": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8", + "version": "1.0.3", + "label": "", "notes": "", - "type": "string_join", + "type": "vae_loader", "inputs": { - "string_left": { - "name": "string_left", - "label": "", - "value": "" - }, - "string_right": { - "name": "string_right", - "label": "Positive Style Concat", - "value": "" + "vae_model": { + "name": "vae_model", + "label": "VAE (use the FP16 model)" } }, - "isOpen": false, + "isOpen": true, "isIntermediate": true, "useCache": true }, "position": { - "x": 750, + "x": 375, "y": -225 } }, { - "id": "719dabe8-8297-4749-aea1-37be301cd425", + "id": "63e91020-83b2-4f35-b174-ad9692aabb48", "type": "invocation", "data": { - "id": "719dabe8-8297-4749-aea1-37be301cd425", - "version": "1.0.1", - "label": "Negative Prompt", + "id": "63e91020-83b2-4f35-b174-ad9692aabb48", + "version": "1.3.0", + "nodePack": "invokeai", + "label": "", "notes": "", - "type": "string", + "type": "l2i", "inputs": { - "value": { - "name": "value", - "label": "Negative Prompt", - "value": "photograph" + "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 + "isIntermediate": false, + "useCache": false }, "position": { - "x": 750, - "y": -125 + "x": 1475, + "y": -500 } }, { - "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", + "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", "type": "invocation", "data": { - "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", - "version": "1.1.1", + "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", + "version": "1.2.0", "nodePack": "invokeai", - "label": "SDXL Negative Compel Prompt", + "label": "SDXL Positive Compel Prompt", "notes": "", "type": "sdxl_compel_prompt", "inputs": { "prompt": { "name": "prompt", - "label": "Negative Prompt", + "label": "Positive Prompt", "value": "" }, "style": { "name": "style", - "label": "Negative Style", + "label": "Positive Style", "value": "" }, "original_width": { @@ -146,6 +168,10 @@ "clip2": { "name": "clip2", "label": "" + }, + "mask": { + "name": "mask", + "label": "" } }, "isOpen": false, @@ -154,79 +180,7 @@ }, "position": { "x": 750, - "y": 200 - } - }, - { - "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 + "y": -175 } }, { @@ -234,7 +188,7 @@ "type": "invocation", "data": { "id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22", - "version": "1.0.2", + "version": "1.0.3", "nodePack": "invokeai", "label": "", "notes": "", @@ -242,14 +196,7 @@ "inputs": { "model": { "name": "model", - "label": "", - "value": { - "key": "4a63b226-e8ff-4da4-854e-0b9f04b562ba", - "hash": "blake3:d279309ea6e5ee6e8fd52504275865cc280dac71cbf528c5b07c98b888bddaba", - "name": "dreamshaper-xl-v2-turbo", - "base": "sdxl", - "type": "main" - } + "label": "" } }, "isOpen": true, @@ -262,24 +209,24 @@ } }, { - "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", + "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", "type": "invocation", "data": { - "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08", - "version": "1.1.1", + "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204", + "version": "1.2.0", "nodePack": "invokeai", - "label": "SDXL Positive Compel Prompt", + "label": "SDXL Negative Compel Prompt", "notes": "", "type": "sdxl_compel_prompt", "inputs": { "prompt": { "name": "prompt", - "label": "Positive Prompt", + "label": "Negative Prompt", "value": "" }, "style": { "name": "style", - "label": "Positive Style", + "label": "Negative Style", "value": "" }, "original_width": { @@ -319,6 +266,10 @@ "clip2": { "name": "clip2", "label": "" + }, + "mask": { + "name": "mask", + "label": "" } }, "isOpen": false, @@ -327,54 +278,134 @@ }, "position": { "x": 750, - "y": -175 + "y": 200 } }, { - "id": "63e91020-83b2-4f35-b174-ad9692aabb48", + "id": "3774ec24-a69e-4254-864c-097d07a6256f", "type": "invocation", "data": { - "id": "63e91020-83b2-4f35-b174-ad9692aabb48", - "version": "1.2.2", + "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": "l2i", + "type": "noise", "inputs": { - "board": { - "name": "board", - "label": "" - }, - "metadata": { - "name": "metadata", - "label": "" - }, - "latents": { - "name": "latents", - "label": "" + "seed": { + "name": "seed", + "label": "", + "value": 0 }, - "vae": { - "name": "vae", - "label": "" + "width": { + "name": "width", + "label": "", + "value": 1024 }, - "tiled": { - "name": "tiled", + "height": { + "name": "height", "label": "", - "value": false + "value": 1024 }, - "fp32": { - "name": "fp32", + "use_cpu": { + "name": "use_cpu", "label": "", - "value": false + "value": true } }, "isOpen": true, - "isIntermediate": false, + "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": 1475, - "y": -500 + "x": 375, + "y": -50 } }, { @@ -464,37 +495,6 @@ "y": -500 } }, - { - "id": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8", - "type": "invocation", - "data": { - "id": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8", - "version": "1.0.2", - "label": "", - "notes": "", - "type": "vae_loader", - "inputs": { - "vae_model": { - "name": "vae_model", - "label": "VAE (use the FP16 model)", - "value": { - "key": "f20f9e5c-1bce-4c46-a84d-34ebfa7df069", - "hash": "blake3:9705ab1c31fa96b308734214fb7571a958621c7a9247eed82b7d277145f8d9fa", - "name": "sdxl-vae-fp16-fix", - "base": "sdxl", - "type": "vae" - } - } - }, - "isOpen": true, - "isIntermediate": true, - "useCache": true - }, - "position": { - "x": 375, - "y": -225 - } - }, { "id": "ade2c0d3-0384-4157-b39b-29ce429cfa15", "type": "invocation", @@ -701,4 +701,4 @@ "targetHandle": "style" } ] -} \ No newline at end of file +} 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 index 6df02b675de..ca1b0bc8793 100644 --- 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 @@ -1,10 +1,11 @@ { - "name": "Text to Image with LoRA", + "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.0.0", + "version": "2.1.0", "contact": "invoke@invoke.ai", - "tags": "text to image, lora, default", + "tags": "sd1.5, text to image, lora", "notes": "", "exposedFields": [ { @@ -37,51 +38,82 @@ } ], "meta": { - "category": "default", - "version": "3.0.0" + "version": "3.0.0", + "category": "default" }, "nodes": [ { - "id": "85b77bb2-c67a-416a-b3e8-291abe746c44", + "id": "a9683c0a-6b1f-4a5e-8187-c57e764b3400", "type": "invocation", "data": { - "id": "85b77bb2-c67a-416a-b3e8-291abe746c44", - "version": "1.1.1", + "id": "a9683c0a-6b1f-4a5e-8187-c57e764b3400", + "version": "1.3.0", "label": "", "notes": "", - "type": "compel", + "type": "l2i", "inputs": { - "prompt": { - "name": "prompt", - "label": "Negative Prompt", - "value": "" + "board": { + "name": "board", + "label": "" }, - "clip": { - "name": "clip", + "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, + "isIntermediate": false, "useCache": true }, "position": { - "x": 3425, - "y": -300 + "x": 4450, + "y": -550 } }, { - "id": "24e9d7ed-4836-4ec4-8f9e-e747721f9818", + "id": "c3fa6872-2599-4a82-a596-b3446a66cf8b", "type": "invocation", "data": { - "id": "24e9d7ed-4836-4ec4-8f9e-e747721f9818", - "version": "1.0.2", + "id": "c3fa6872-2599-4a82-a596-b3446a66cf8b", + "version": "1.2.0", "label": "", "notes": "", - "type": "main_model_loader", + "type": "compel", "inputs": { - "model": { - "name": "model", + "prompt": { + "name": "prompt", + "label": "Positive Prompt", + "value": "super cute tiger cub" + }, + "clip": { + "name": "clip", + "label": "" + }, + "mask": { + "name": "mask", "label": "" } }, @@ -90,8 +122,8 @@ "useCache": true }, "position": { - "x": 2500, - "y": -600 + "x": 3425, + "y": -575 } }, { @@ -99,7 +131,7 @@ "type": "invocation", "data": { "id": "c41e705b-f2e3-4d1a-83c4-e34bb9344966", - "version": "1.0.2", + "version": "1.0.3", "label": "", "notes": "", "type": "lora_loader", @@ -132,23 +164,51 @@ } }, { - "id": "c3fa6872-2599-4a82-a596-b3446a66cf8b", + "id": "24e9d7ed-4836-4ec4-8f9e-e747721f9818", "type": "invocation", "data": { - "id": "c3fa6872-2599-4a82-a596-b3446a66cf8b", - "version": "1.1.1", + "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": "Positive Prompt", - "value": "super cute tiger cub" + "label": "Negative Prompt", + "value": "" }, "clip": { "name": "clip", "label": "" + }, + "mask": { + "name": "mask", + "label": "" } }, "isOpen": true, @@ -157,7 +217,7 @@ }, "position": { "x": 3425, - "y": -575 + "y": -300 } }, { @@ -315,52 +375,6 @@ "x": 3425, "y": 0 } - }, - { - "id": "a9683c0a-6b1f-4a5e-8187-c57e764b3400", - "type": "invocation", - "data": { - "id": "a9683c0a-6b1f-4a5e-8187-c57e764b3400", - "version": "1.2.2", - "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 - }, - "fp32": { - "name": "fp32", - "label": "", - "value": false - } - }, - "isOpen": true, - "isIntermediate": false, - "useCache": true - }, - "position": { - "x": 4450, - "y": -550 - } } ], "edges": [ 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 index bb0e9062e48..7bc96cd911f 100644 --- a/invokeai/app/services/workflow_records/default_workflows/Tiled Upscaling (Beta).json +++ b/invokeai/app/services/workflow_records/default_workflows/Tiled Upscaling (Beta).json @@ -1,10 +1,11 @@ { - "name": "Tiled Upscaling (Beta)", + "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.0.0", + "version": "2.1.0", "contact": "invoke@invoke.ai", - "tags": "tiled, upscaling, sd1.5", + "tags": "sd1.5, upscaling", "notes": "", "exposedFields": [ { @@ -41,93 +42,184 @@ } ], "meta": { - "category": "default", - "version": "3.0.0" + "version": "3.0.0", + "category": "default" }, "nodes": [ { - "id": "b875cae6-d8a3-4fdc-b969-4d53cbd03f9a", + "id": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5", "type": "invocation", "data": { - "id": "b875cae6-d8a3-4fdc-b969-4d53cbd03f9a", - "version": "1.0.1", - "label": "Creativity Input", + "id": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5", + "version": "1.0.3", + "label": "", "notes": "", - "type": "float_math", + "type": "main_model_loader", "inputs": { - "operation": { - "name": "operation", + "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": "DIV" + "value": "ViT-H" }, - "a": { - "name": "a", - "label": "Creativity", - "value": 0.3 + "weight": { + "name": "weight", + "label": "", + "value": 0.2 }, - "b": { - "name": "b", + "method": { + "name": "method", "label": "", - "value": 3.3 + "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": false, + "isOpen": true, "isIntermediate": true, "useCache": true }, "position": { - "x": -4007.507843708216, - "y": -621.6878478530825 + "x": -2855.8555540799207, + "y": -183.58854843775742 } }, { - "id": "7dbb756b-7d79-431c-a46d-d8f7b082c127", + "id": "b76fe66f-7884-43ad-b72c-fadc81d7a73c", "type": "invocation", "data": { - "id": "7dbb756b-7d79-431c-a46d-d8f7b082c127", - "version": "1.0.1", + "id": "b76fe66f-7884-43ad-b72c-fadc81d7a73c", + "version": "1.3.0", "label": "", "notes": "", - "type": "float_to_int", + "type": "l2i", "inputs": { - "value": { - "name": "value", + "board": { + "name": "board", + "label": "" + }, + "metadata": { + "name": "metadata", + "label": "" + }, + "latents": { + "name": "latents", + "label": "" + }, + "vae": { + "name": "vae", + "label": "" + }, + "tiled": { + "name": "tiled", "label": "", - "value": 0 + "value": false }, - "multiple": { - "name": "multiple", + "tile_size": { + "name": "tile_size", "label": "", - "value": 8 + "value": 0 }, - "method": { - "name": "method", + "fp32": { + "name": "fp32", "label": "", - "value": "Floor" + "value": false } }, - "isOpen": false, + "isOpen": true, "isIntermediate": true, "useCache": true }, "position": { - "x": -4470.518114882552, - "y": -246.9687512362472 + "x": -1999.770193862987, + "y": -1075 } }, { - "id": "5ca87ace-edf9-49c7-a424-cd42416b86a7", + "id": "d334f2da-016a-4524-9911-bdab85546888", "type": "invocation", "data": { - "id": "5ca87ace-edf9-49c7-a424-cd42416b86a7", - "version": "1.0.2", + "id": "d334f2da-016a-4524-9911-bdab85546888", + "version": "1.1.2", "label": "", "notes": "", - "type": "image", + "type": "controlnet", "inputs": { "image": { "name": "image", - "label": "Image to Upscale" + "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, @@ -135,41 +227,42 @@ "useCache": true }, "position": { - "x": -4485.384246996007, - "y": -977.6662925348955 + "x": -2481.9569385477016, + "y": -181.06590482739782 } }, { - "id": "fad15012-0787-43a8-99dd-27f1518b5bc7", + "id": "338b883c-3728-4f18-b3a6-6e7190c2f850", "type": "invocation", "data": { - "id": "fad15012-0787-43a8-99dd-27f1518b5bc7", - "version": "1.2.2", + "id": "338b883c-3728-4f18-b3a6-6e7190c2f850", + "version": "1.1.0", "label": "", "notes": "", - "type": "img_scale", + "type": "i2l", "inputs": { - "board": { - "name": "board", + "image": { + "name": "image", "label": "" }, - "metadata": { - "name": "metadata", + "vae": { + "name": "vae", "label": "" }, - "image": { - "name": "image", - "label": "" + "tiled": { + "name": "tiled", + "label": "", + "value": false }, - "scale_factor": { - "name": "scale_factor", + "tile_size": { + "name": "tile_size", "label": "", - "value": 3 + "value": 0 }, - "resample_mode": { - "name": "resample_mode", + "fp32": { + "name": "fp32", "label": "", - "value": "lanczos" + "value": false } }, "isOpen": false, @@ -177,8 +270,41 @@ "useCache": true }, "position": { - "x": -4478.200192078582, - "y": 3.422855503409039 + "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 } }, { @@ -186,19 +312,117 @@ "type": "invocation", "data": { "id": "9b2d8c58-ce8f-4162-a5a1-48de854040d6", - "version": "1.1.1", + "version": "1.2.0", "label": "", "notes": "", "type": "compel", "inputs": { - "prompt": { - "name": "prompt", - "label": "Positive Prompt", - "value": "" - }, - "clip": { - "name": "clip", - "label": "" + "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, @@ -206,37 +430,50 @@ "useCache": true }, "position": { - "x": -4014.4136788915944, - "y": -1243.5677253775948 + "x": -4485.384246996007, + "y": -977.6662925348955 } }, { - "id": "947c3f88-0305-4695-8355-df4abac64b1c", + "id": "fad15012-0787-43a8-99dd-27f1518b5bc7", "type": "invocation", "data": { - "id": "947c3f88-0305-4695-8355-df4abac64b1c", - "version": "1.1.1", + "id": "fad15012-0787-43a8-99dd-27f1518b5bc7", + "version": "1.2.2", "label": "", "notes": "", - "type": "compel", + "type": "img_scale", "inputs": { - "prompt": { - "name": "prompt", - "label": "", - "value": "" + "board": { + "name": "board", + "label": "" }, - "clip": { - "name": "clip", + "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": true, + "isOpen": false, "isIntermediate": true, "useCache": true }, "position": { - "x": -4014.4136788915944, - "y": -968.5677253775948 + "x": -4478.200192078582, + "y": 3.422855503409039 } }, { @@ -379,104 +616,6 @@ "y": -29.08699277598673 } }, - { - "id": "338b883c-3728-4f18-b3a6-6e7190c2f850", - "type": "invocation", - "data": { - "id": "338b883c-3728-4f18-b3a6-6e7190c2f850", - "version": "1.0.2", - "label": "", - "notes": "", - "type": "i2l", - "inputs": { - "image": { - "name": "image", - "label": "" - }, - "vae": { - "name": "vae", - "label": "" - }, - "tiled": { - "name": "tiled", - "label": "", - "value": false - }, - "fp32": { - "name": "fp32", - "label": "", - "value": false - } - }, - "isOpen": false, - "isIntermediate": true, - "useCache": true - }, - "position": { - "x": -2908.4791167517287, - "y": -408.87504820159086 - } - }, - { - "id": "d334f2da-016a-4524-9911-bdab85546888", - "type": "invocation", - "data": { - "id": "d334f2da-016a-4524-9911-bdab85546888", - "version": "1.1.1", - "label": "", - "notes": "", - "type": "controlnet", - "inputs": { - "image": { - "name": "image", - "label": "" - }, - "control_model": { - "name": "control_model", - "label": "Control Model (select contro_v11f1e_sd15_tile)", - "value": { - "key": "773843c8-db1f-4502-8f65-59782efa7960", - "hash": "blake3:f0812e13758f91baf4e54b7dbb707b70642937d3b2098cd2b94cc36d3eba308e", - "name": "control_v11f1e_sd15_tile", - "base": "sd-1", - "type": "controlnet" - } - }, - "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": "1011539e-85de-4e02-a003-0b22358491b8", "type": "invocation", @@ -563,52 +702,6 @@ "y": -1006.415909408244 } }, - { - "id": "b76fe66f-7884-43ad-b72c-fadc81d7a73c", - "type": "invocation", - "data": { - "id": "b76fe66f-7884-43ad-b72c-fadc81d7a73c", - "version": "1.2.2", - "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 - }, - "fp32": { - "name": "fp32", - "label": "", - "value": false - } - }, - "isOpen": true, - "isIntermediate": true, - "useCache": true - }, - "position": { - "x": -1999.770193862987, - "y": -1075 - } - }, { "id": "ab6f5dda-4b60-4ddf-99f2-f61fb5937527", "type": "invocation", @@ -779,56 +872,6 @@ "y": -78.2819050861178 } }, - { - "id": "287f134f-da8d-41d1-884e-5940e8f7b816", - "type": "invocation", - "data": { - "id": "287f134f-da8d-41d1-884e-5940e8f7b816", - "version": "1.2.2", - "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)", - "value": { - "key": "1cc210bb-4d0a-4312-b36c-b5d46c43768e", - "hash": "blake3:3d669dffa7471b357b4df088b99ffb6bf4d4383d5e0ef1de5ec1c89728a3d5a5", - "name": "ip_adapter_sd15", - "base": "sd-1", - "type": "ip_adapter" - } - }, - "weight": { - "name": "weight", - "label": "", - "value": 0.2 - }, - "begin_step_percent": { - "name": "begin_step_percent", - "label": "", - "value": 0 - }, - "end_step_percent": { - "name": "end_step_percent", - "label": "", - "value": 1 - } - }, - "isOpen": true, - "isIntermediate": true, - "useCache": true - }, - "position": { - "x": -2855.8555540799207, - "y": -183.58854843775742 - } - }, { "id": "1f86c8bf-06f9-4e28-abee-02f46f445ac4", "type": "invocation", @@ -899,30 +942,6 @@ "y": -41.810810454906914 } }, - { - "id": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5", - "type": "invocation", - "data": { - "id": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5", - "version": "1.0.2", - "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": "f5d9bf3b-2646-4b17-9894-20fd2b4218ea", "type": "invocation", @@ -1783,4 +1802,4 @@ "targetHandle": "unet" } ] -} \ No newline at end of file +} diff --git a/invokeai/app/services/workflow_records/workflow_records_base.py b/invokeai/app/services/workflow_records/workflow_records_base.py index 499b0f005d1..c07daa2662e 100644 --- a/invokeai/app/services/workflow_records/workflow_records_base.py +++ b/invokeai/app/services/workflow_records/workflow_records_base.py @@ -4,6 +4,7 @@ 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, @@ -22,29 +23,81 @@ def get(self, workflow_id: str) -> WorkflowRecordDTO: pass @abstractmethod - def create(self, workflow: WorkflowWithoutID) -> WorkflowRecordDTO: + 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) -> WorkflowRecordDTO: - """Updates a workflow.""" + 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) -> None: - """Deletes a workflow.""" + 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, - page: int, - per_page: int, order_by: WorkflowRecordOrderBy, direction: SQLiteDirection, - category: WorkflowCategory, + 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 index e02600a0c35..9c505530c90 100644 --- a/invokeai/app/services/workflow_records/workflow_records_common.py +++ b/invokeai/app/services/workflow_records/workflow_records_common.py @@ -1,6 +1,6 @@ import datetime from enum import Enum -from typing import Any, Union +from typing import Any, Optional, Union import semver from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter, field_validator @@ -9,6 +9,9 @@ __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 @@ -26,19 +29,17 @@ class WorkflowRecordOrderBy(str, Enum, metaclass=MetaEnum): UpdatedAt = "updated_at" OpenedAt = "opened_at" Name = "name" + IsPublic = "is_public" class WorkflowCategory(str, Enum, metaclass=MetaEnum): User = "user" Default = "default" - Project = "project" class WorkflowMeta(BaseModel): version: str = Field(description="The version of the workflow schema.") - category: WorkflowCategory = Field( - default=WorkflowCategory.User, description="The category of the workflow (user or default)." - ) + category: WorkflowCategory = Field(description="The category of the workflow (user or default).") @field_validator("version") def validate_version(cls, version: str): @@ -62,9 +63,13 @@ class WorkflowWithoutID(BaseModel): 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: nodes and edges are very loosely typed + # 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") @@ -96,7 +101,11 @@ class WorkflowRecordDTOBase(BaseModel): 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: Union[datetime.datetime, str] = Field(description="The opened 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): @@ -114,6 +123,15 @@ def from_dict(cls, data: dict[str, Any]) -> "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 index af0bad8260f..a62dbb9dfa8 100644 --- a/invokeai/app/services/workflow_records/workflow_records_sqlite.py +++ b/invokeai/app/services/workflow_records/workflow_records_sqlite.py @@ -7,6 +7,7 @@ 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, @@ -14,18 +15,18 @@ WorkflowRecordListItemDTO, WorkflowRecordListItemDTOValidator, WorkflowRecordOrderBy, + WorkflowValidator, WorkflowWithoutID, - WorkflowWithoutIDValidator, ) 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._lock = db.lock - self._conn = db.conn - self._cursor = self._conn.cursor() + self._db = db def start(self, invoker: Invoker) -> None: self._invoker = invoker @@ -33,156 +34,467 @@ def start(self, invoker: Invoker) -> None: def get(self, workflow_id: str) -> WorkflowRecordDTO: """Gets a workflow by ID. Updates the opened_at column.""" - try: - self._lock.acquire() - self._cursor.execute( + with self._db.transaction() as cursor: + cursor.execute( """--sql - UPDATE workflow_library - SET opened_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') - WHERE workflow_id = ?; - """, - (workflow_id,), - ) - self._conn.commit() - self._cursor.execute( - """--sql - SELECT workflow_id, workflow, name, created_at, updated_at, opened_at + SELECT workflow_id, workflow, name, created_at, updated_at, opened_at, user_id, is_public FROM workflow_library WHERE workflow_id = ?; """, (workflow_id,), ) - row = self._cursor.fetchone() - if row is None: - raise WorkflowNotFoundError(f"Workflow with id {workflow_id} not found") - return WorkflowRecordDTO.from_dict(dict(row)) - except Exception: - self._conn.rollback() - raise - finally: - self._lock.release() - - def create(self, workflow: WorkflowWithoutID) -> WorkflowRecordDTO: - try: - # Only user workflows may be created by this method - assert workflow.meta.category is WorkflowCategory.User + 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()) - self._lock.acquire() - self._cursor.execute( + cursor.execute( """--sql INSERT OR IGNORE INTO workflow_library ( workflow_id, - workflow + workflow, + user_id, + is_public ) - VALUES (?, ?); + VALUES (?, ?, ?, ?); """, - (workflow_with_id.id, workflow_with_id.model_dump_json()), + (workflow_with_id.id, workflow_with_id.model_dump_json(), user_id, is_public), ) - self._conn.commit() - except Exception: - self._conn.rollback() - raise - finally: - self._lock.release() return self.get(workflow_with_id.id) - def update(self, workflow: Workflow) -> WorkflowRecordDTO: - try: - self._lock.acquire() - self._cursor.execute( - """--sql - UPDATE workflow_library - SET workflow = ? - WHERE workflow_id = ? AND category = 'user'; - """, - (workflow.model_dump_json(), workflow.id), - ) - self._conn.commit() - except Exception: - self._conn.rollback() - raise - finally: - self._lock.release() + 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) -> None: - try: - self._lock.acquire() - self._cursor.execute( - """--sql - DELETE from workflow_library - WHERE workflow_id = ? AND category = 'user'; - """, - (workflow_id,), - ) - self._conn.commit() - except Exception: - self._conn.rollback() - raise - finally: - self._lock.release() + 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, - page: int, - per_page: int, order_by: WorkflowRecordOrderBy, direction: SQLiteDirection, - category: WorkflowCategory, + 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]: - try: - self._lock.acquire() + with self._db.transaction() as cursor: # sanitize! assert order_by in WorkflowRecordOrderBy assert direction in SQLiteDirection - assert category in WorkflowCategory - count_query = "SELECT COUNT(*) FROM workflow_library WHERE category = ?" + + # 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 - FROM workflow_library - WHERE category = ? - """ - main_params: list[int | str] = [category.value] - count_params: list[int | str] = [category.value] + 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 + "%" - main_query += " AND name LIKE ? OR description LIKE ? " - count_query += " AND name LIKE ? OR description LIKE ?;" - main_params.extend([wildcard_query, wildcard_query]) - count_params.extend([wildcard_query, wildcard_query]) - - main_query += f" ORDER BY {order_by.value} {direction.value} LIMIT ? OFFSET ?;" - main_params.extend([per_page, page * per_page]) - self._cursor.execute(main_query, main_params) - rows = self._cursor.fetchall() + 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] - self._cursor.execute(count_query, count_params) - total = self._cursor.fetchone()[0] + 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, - pages=pages, - total=total, - ) - except Exception: - self._conn.rollback() - raise - finally: - self._lock.release() + 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.""" @@ -197,28 +509,68 @@ def _sync_default_workflows(self) -> None: meaningless, as they are overwritten every time the server starts. """ - try: - self._lock.acquire() - workflows: list[Workflow] = [] + 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_without_id = WorkflowWithoutIDValidator.validate_json(bytes_) - workflow = Workflow(**workflow_without_id.model_dump(), id=uuid_string()) - workflows.append(workflow) - # Only default workflows may be managed by this method - assert all(w.meta.category is WorkflowCategory.Default for w in workflows) - self._cursor.execute( - """--sql - DELETE FROM workflow_library - WHERE category = 'default'; - """ - ) - for w in workflows: - self._cursor.execute( + 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 OR REPLACE INTO workflow_library ( + INSERT INTO workflow_library ( workflow_id, workflow ) @@ -226,9 +578,14 @@ def _sync_default_workflows(self) -> None: """, (w.id, w.model_dump_json()), ) - self._conn.commit() - except Exception: - self._conn.rollback() - raise - finally: - self._lock.release() + + 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/util/controlnet_utils.py b/invokeai/app/util/controlnet_utils.py index fde8d52ee64..0f14ed7bfb3 100644 --- a/invokeai/app/util/controlnet_utils.py +++ b/invokeai/app/util/controlnet_utils.py @@ -230,6 +230,86 @@ def heuristic_resize(np_img: np.ndarray[Any, Any], size: tuple[int, int]) -> np. 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 @@ -244,7 +324,7 @@ def np_img_resize( np_img = normalize_image_channel_count(np_img) if resize_mode == "just_resize": # RESIZE - np_img = heuristic_resize(np_img, (w, h)) + 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 @@ -265,7 +345,7 @@ def safeint(x: Union[int, float]) -> int: # 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(np_img, (safeint(old_w * k), safeint(old_h * k))) + 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) @@ -275,7 +355,7 @@ def safeint(x: Union[int, float]) -> int: 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(np_img, (safeint(old_w * k), safeint(old_h * k))) + 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) @@ -289,7 +369,7 @@ def prepare_control_image( width: int, height: int, num_channels: int = 3, - device: str = "cuda", + device: str | torch.device = "cuda", dtype: torch.dtype = torch.float16, control_mode: CONTROLNET_MODE_VALUES = "balanced", resize_mode: CONTROLNET_RESIZE_VALUES = "just_resize_simple", @@ -304,7 +384,7 @@ def prepare_control_image( 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, optional): The target device for the output image. Defaults to "cuda". + 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. diff --git a/invokeai/app/util/custom_openapi.py b/invokeai/app/util/custom_openapi.py index 50259c12ccc..f674fa76218 100644 --- a/invokeai/app/util/custom_openapi.py +++ b/invokeai/app/util/custom_openapi.py @@ -4,11 +4,18 @@ from fastapi.openapi.utils import get_openapi from pydantic.json_schema import models_json_schema -from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, UIConfigBase +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: @@ -23,6 +30,23 @@ def move_defs_to_top_level(openapi_schema: dict[str, Any], component_schema: dic 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]]: @@ -56,14 +80,18 @@ def openapi() -> 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 BaseInvocationOutput.get_outputs(): + 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 BaseInvocation.get_invocations(): + for invocation in InvocationRegistry.get_invocation_classes(): json_schema = invocation.model_json_schema( mode="serialization", ref_template="#/components/schemas/{model}" ) @@ -81,8 +109,8 @@ def openapi() -> dict[str, Any]: # Add the output map to the schema openapi_schema["components"]["schemas"]["InvocationOutputMap"] = { "type": "object", - "properties": invocation_output_map_properties, - "required": invocation_output_map_required, + "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. @@ -105,9 +133,18 @@ def openapi() -> dict[str, Any]: # 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 diff --git a/invokeai/app/util/misc.py b/invokeai/app/util/misc.py index da431929dbe..f75683539ac 100644 --- a/invokeai/app/util/misc.py +++ b/invokeai/app/util/misc.py @@ -10,7 +10,7 @@ def get_timestamp() -> int: def get_iso_timestamp() -> str: - return datetime.datetime.utcnow().isoformat() + return datetime.datetime.now(datetime.timezone.utc).isoformat() def get_datetime_from_iso_timestamp(iso_timestamp: str) -> datetime.datetime: 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 index 8992e59ace9..08dc9a2265c 100644 --- a/invokeai/app/util/step_callback.py +++ b/invokeai/app/util/step_callback.py @@ -1,17 +1,14 @@ -from typing import TYPE_CHECKING, Callable, Optional +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, ProgressImage -from invokeai.backend.model_manager.config import BaseModelType +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 -from ...backend.stable_diffusion import PipelineIntermediateState -from ...backend.util.util import image_to_dataURL - -if TYPE_CHECKING: - from invokeai.app.services.events.events_base import EventServiceBase - from invokeai.app.services.shared.invocation_context import InvocationContextData +# See scripts/generate_vae_linear_approximation.py for generating these factors. # fast latents preview matrix for sdxl # generated by @StAlKeR7779 @@ -39,11 +36,162 @@ [-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 + samples: torch.Tensor, + latent_rgb_factors: torch.Tensor, + smooth_matrix: Optional[torch.Tensor] = None, + latent_rgb_bias: Optional[torch.Tensor] = None, ): - latent_image = samples[0].permute(1, 2, 0) @ latent_rgb_factors + 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) @@ -57,11 +205,32 @@ def sample_to_lowres_estimated_image( return Image.fromarray(latents_ubyte.numpy()) -def stable_diffusion_step_callback( - context_data: "InvocationContextData", +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, - events: "EventServiceBase", is_canceled: Callable[[], bool], ) -> None: if is_canceled(): @@ -75,23 +244,51 @@ def stable_diffusion_step_callback( else: sample = intermediate_state.latents - if base_model in [BaseModelType.StableDiffusionXL, BaseModelType.StableDiffusionXLRefiner]: - sdxl_latent_rgb_factors = torch.tensor(SDXL_LATENT_RGB_FACTORS, dtype=sample.dtype, device=sample.device) - sdxl_smooth_matrix = torch.tensor(SDXL_SMOOTH_MATRIX, dtype=sample.dtype, device=sample.device) - image = sample_to_lowres_estimated_image(sample, sdxl_latent_rgb_factors, sdxl_smooth_matrix) + 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: - v1_5_latent_rgb_factors = torch.tensor(SD1_5_LATENT_RGB_FACTORS, dtype=sample.dtype, device=sample.device) - image = sample_to_lowres_estimated_image(sample, v1_5_latent_rgb_factors) + raise ValueError(f"Unsupported base model: {base_model}") - (width, height) = image.size - width *= 8 - height *= 8 + 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, + ) - dataURL = image_to_dataURL(image, image_format="JPEG") + width = image.width * 8 + height = image.height * 8 + percentage = calc_percentage(intermediate_state) - events.emit_invocation_denoise_progress( - context_data.queue_item, - context_data.invocation, - intermediate_state, - ProgressImage(dataURL=dataURL, width=width, height=height), - ) + signal_progress("Denoising", percentage, image, (width, height)) 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/ti_utils.py b/invokeai/app/util/ti_utils.py index 34669fe64ef..8f18b14d66b 100644 --- a/invokeai/app/util/ti_utils.py +++ b/invokeai/app/util/ti_utils.py @@ -4,7 +4,7 @@ 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.config import BaseModelType, ModelType +from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType from invokeai.backend.textual_inversion import TextualInversionModelRaw 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/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..7b29a58d44f --- /dev/null +++ b/invokeai/backend/flux/denoise.py @@ -0,0 +1,412 @@ +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 +from invokeai.backend.util.devices import TorchDevice + + +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=f"Denoising{TorchDevice.get_session_device_label()}") + 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))), + desc=f"Denoising{TorchDevice.get_session_device_label()}", + ): + # 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..561a844bfb8 --- /dev/null +++ b/invokeai/backend/flux2/denoise.py @@ -0,0 +1,310 @@ +"""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 +from invokeai.backend.util.devices import TorchDevice + + +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=f"Denoising{TorchDevice.get_session_device_label()}") + 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 + ) + # 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=2, + total_steps=total_steps, + timestep=int(t_curr * 1000), + latents=callback_latents, + ), + ) + 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))), + desc=f"Denoising{TorchDevice.get_session_device_label()}", + ): + 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..368f3c4452f --- /dev/null +++ b/invokeai/backend/flux2/ref_image_extension.py @@ -0,0 +1,310 @@ +"""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) + + # The FLUX.2 VAE encoder's mid-block self-attention scales quadratically with the + # input's spatial size (and on ROCm, SDPA falls back to a *materialized* attention + # matrix), so encoding a reference image at full size OOMs VRAM — ~15GB at 1024px, + # hundreds of GB at the 2024px reference cap. Tile the encode to bound peak memory + # regardless of reference resolution. The VAE's default tile size equals its + # sample_size (1024), which still OOMs per tile, so force a smaller 512px tile. + # Save/restore the tiling config because this VAE is a shared, cached instance (e.g. + # the final image decode must not inherit these settings). + downsample = 2 ** (len(vae.config.block_out_channels) - 1) + prev_tiling = (vae.use_tiling, vae.tile_sample_min_size, vae.tile_latent_min_size) + vae.use_tiling = True + vae.tile_sample_min_size = 512 + vae.tile_latent_min_size = 512 // downsample + try: + # 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 + finally: + vae.use_tiling, vae.tile_sample_min_size, vae.tile_latent_min_size = prev_tiling + + 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 index f45af9feb47..bc5eed7ddd7 100644 --- a/invokeai/backend/image_util/__init__.py +++ b/invokeai/backend/image_util/__init__.py @@ -2,6 +2,11 @@ Initialization file for invokeai.backend.image_util methods. """ -from .infill_methods.patchmatch import PatchMatch # noqa: F401 -from .pngwriter import PngWriter, PromptFormatter, retrieve_metadata, write_metadata # noqa: F401 -from .util import InitImageResizer, make_grid # noqa: F401 +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/rrdbnet_arch.py b/invokeai/backend/image_util/basicsr/rrdbnet_arch.py index cdb77f3c215..a99a6971236 100644 --- a/invokeai/backend/image_util/basicsr/rrdbnet_arch.py +++ b/invokeai/backend/image_util/basicsr/rrdbnet_arch.py @@ -2,7 +2,7 @@ from torch import nn as nn from torch.nn import functional as F -from .arch_util import default_init_weights, make_layer, pixel_unshuffle +from invokeai.backend.image_util.basicsr.arch_util import default_init_weights, make_layer, pixel_unshuffle class ResidualDenseBlock(nn.Module): 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/__init__.py b/invokeai/backend/image_util/depth_anything/__init__.py deleted file mode 100644 index 1adcc6b2029..00000000000 --- a/invokeai/backend/image_util/depth_anything/__init__.py +++ /dev/null @@ -1,90 +0,0 @@ -from pathlib import Path -from typing import Literal - -import cv2 -import numpy as np -import torch -import torch.nn.functional as F -from einops import repeat -from PIL import Image -from torchvision.transforms import Compose - -from invokeai.app.services.config.config_default import get_config -from invokeai.backend.image_util.depth_anything.model.dpt import DPT_DINOv2 -from invokeai.backend.image_util.depth_anything.utilities.util import NormalizeImage, PrepareForNet, Resize -from invokeai.backend.util.logging import InvokeAILogger - -config = get_config() -logger = InvokeAILogger.get_logger(config=config) - -DEPTH_ANYTHING_MODELS = { - "large": "https://huggingface.co/spaces/LiheYoung/Depth-Anything/resolve/main/checkpoints/depth_anything_vitl14.pth?download=true", - "base": "https://huggingface.co/spaces/LiheYoung/Depth-Anything/resolve/main/checkpoints/depth_anything_vitb14.pth?download=true", - "small": "https://huggingface.co/spaces/LiheYoung/Depth-Anything/resolve/main/checkpoints/depth_anything_vits14.pth?download=true", -} - - -transform = Compose( - [ - Resize( - width=518, - height=518, - resize_target=False, - keep_aspect_ratio=True, - ensure_multiple_of=14, - resize_method="lower_bound", - image_interpolation_method=cv2.INTER_CUBIC, - ), - NormalizeImage(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), - PrepareForNet(), - ] -) - - -class DepthAnythingDetector: - def __init__(self, model: DPT_DINOv2, device: torch.device) -> None: - self.model = model - self.device = device - - @staticmethod - def load_model( - model_path: Path, device: torch.device, model_size: Literal["large", "base", "small"] = "small" - ) -> DPT_DINOv2: - match model_size: - case "small": - model = DPT_DINOv2(encoder="vits", features=64, out_channels=[48, 96, 192, 384]) - case "base": - model = DPT_DINOv2(encoder="vitb", features=128, out_channels=[96, 192, 384, 768]) - case "large": - model = DPT_DINOv2(encoder="vitl", features=256, out_channels=[256, 512, 1024, 1024]) - - model.load_state_dict(torch.load(model_path.as_posix(), map_location="cpu")) - model.eval() - - model.to(device) - return model - - def __call__(self, image: Image.Image, resolution: int = 512) -> Image.Image: - if not self.model: - logger.warn("DepthAnything model was not loaded. Returning original image") - return image - - np_image = np.array(image, dtype=np.uint8) - np_image = np_image[:, :, ::-1] / 255.0 - - image_height, image_width = np_image.shape[:2] - np_image = transform({"image": np_image})["image"] - tensor_image = torch.from_numpy(np_image).unsqueeze(0).to(self.device) - - with torch.no_grad(): - depth = self.model(tensor_image) - depth = F.interpolate(depth[None], (image_height, image_width), mode="bilinear", align_corners=False)[0, 0] - depth = (depth - depth.min()) / (depth.max() - depth.min()) * 255.0 - - depth_map = repeat(depth, "h w -> h w 3").cpu().numpy().astype(np.uint8) - depth_map = Image.fromarray(depth_map) - - new_height = int(image_height * (resolution / image_width)) - depth_map = depth_map.resize((resolution, new_height)) - - return depth_map 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/depth_anything/model/blocks.py b/invokeai/backend/image_util/depth_anything/model/blocks.py deleted file mode 100644 index 4534f522373..00000000000 --- a/invokeai/backend/image_util/depth_anything/model/blocks.py +++ /dev/null @@ -1,145 +0,0 @@ -import torch.nn as nn - - -def _make_scratch(in_shape, out_shape, groups=1, expand=False): - scratch = nn.Module() - - out_shape1 = out_shape - out_shape2 = out_shape - out_shape3 = out_shape - if len(in_shape) >= 4: - out_shape4 = out_shape - - if expand: - out_shape1 = out_shape - out_shape2 = out_shape * 2 - out_shape3 = out_shape * 4 - if len(in_shape) >= 4: - out_shape4 = out_shape * 8 - - scratch.layer1_rn = nn.Conv2d( - in_shape[0], out_shape1, kernel_size=3, stride=1, padding=1, bias=False, groups=groups - ) - scratch.layer2_rn = nn.Conv2d( - in_shape[1], out_shape2, kernel_size=3, stride=1, padding=1, bias=False, groups=groups - ) - scratch.layer3_rn = nn.Conv2d( - in_shape[2], out_shape3, kernel_size=3, stride=1, padding=1, bias=False, groups=groups - ) - if len(in_shape) >= 4: - scratch.layer4_rn = nn.Conv2d( - in_shape[3], out_shape4, kernel_size=3, stride=1, padding=1, bias=False, groups=groups - ) - - return scratch - - -class ResidualConvUnit(nn.Module): - """Residual convolution module.""" - - def __init__(self, features, activation, bn): - """Init. - - Args: - features (int): number of features - """ - super().__init__() - - self.bn = bn - - self.groups = 1 - - self.conv1 = nn.Conv2d(features, features, kernel_size=3, stride=1, padding=1, bias=True, groups=self.groups) - - self.conv2 = nn.Conv2d(features, features, kernel_size=3, stride=1, padding=1, bias=True, groups=self.groups) - - if self.bn: - self.bn1 = nn.BatchNorm2d(features) - self.bn2 = nn.BatchNorm2d(features) - - self.activation = activation - - self.skip_add = nn.quantized.FloatFunctional() - - def forward(self, x): - """Forward pass. - - Args: - x (tensor): input - - Returns: - tensor: output - """ - - out = self.activation(x) - out = self.conv1(out) - if self.bn: - out = self.bn1(out) - - out = self.activation(out) - out = self.conv2(out) - if self.bn: - out = self.bn2(out) - - if self.groups > 1: - out = self.conv_merge(out) - - return self.skip_add.add(out, x) - - -class FeatureFusionBlock(nn.Module): - """Feature fusion block.""" - - def __init__(self, features, activation, deconv=False, bn=False, expand=False, align_corners=True, size=None): - """Init. - - Args: - features (int): number of features - """ - super(FeatureFusionBlock, self).__init__() - - self.deconv = deconv - self.align_corners = align_corners - - self.groups = 1 - - self.expand = expand - out_features = features - if self.expand: - out_features = features // 2 - - self.out_conv = nn.Conv2d(features, out_features, kernel_size=1, stride=1, padding=0, bias=True, groups=1) - - self.resConfUnit1 = ResidualConvUnit(features, activation, bn) - self.resConfUnit2 = ResidualConvUnit(features, activation, bn) - - self.skip_add = nn.quantized.FloatFunctional() - - self.size = size - - def forward(self, *xs, size=None): - """Forward pass. - - Returns: - tensor: output - """ - output = xs[0] - - if len(xs) == 2: - res = self.resConfUnit1(xs[1]) - output = self.skip_add.add(output, res) - - output = self.resConfUnit2(output) - - if (size is None) and (self.size is None): - modifier = {"scale_factor": 2} - elif size is None: - modifier = {"size": self.size} - else: - modifier = {"size": size} - - output = nn.functional.interpolate(output, **modifier, mode="bilinear", align_corners=self.align_corners) - - output = self.out_conv(output) - - return output diff --git a/invokeai/backend/image_util/depth_anything/model/dpt.py b/invokeai/backend/image_util/depth_anything/model/dpt.py deleted file mode 100644 index e1101b3c39b..00000000000 --- a/invokeai/backend/image_util/depth_anything/model/dpt.py +++ /dev/null @@ -1,183 +0,0 @@ -from pathlib import Path - -import torch -import torch.nn as nn -import torch.nn.functional as F - -from .blocks import FeatureFusionBlock, _make_scratch - -torchhub_path = Path(__file__).parent.parent / "torchhub" - - -def _make_fusion_block(features, use_bn, size=None): - return FeatureFusionBlock( - features, - nn.ReLU(False), - deconv=False, - bn=use_bn, - expand=False, - align_corners=True, - size=size, - ) - - -class DPTHead(nn.Module): - def __init__(self, nclass, in_channels, features, out_channels, use_bn=False, use_clstoken=False): - super(DPTHead, self).__init__() - - self.nclass = nclass - self.use_clstoken = use_clstoken - - self.projects = nn.ModuleList( - [ - nn.Conv2d( - in_channels=in_channels, - out_channels=out_channel, - kernel_size=1, - stride=1, - padding=0, - ) - for out_channel in out_channels - ] - ) - - self.resize_layers = nn.ModuleList( - [ - nn.ConvTranspose2d( - in_channels=out_channels[0], out_channels=out_channels[0], kernel_size=4, stride=4, padding=0 - ), - nn.ConvTranspose2d( - in_channels=out_channels[1], out_channels=out_channels[1], kernel_size=2, stride=2, padding=0 - ), - nn.Identity(), - nn.Conv2d( - in_channels=out_channels[3], out_channels=out_channels[3], kernel_size=3, stride=2, padding=1 - ), - ] - ) - - if use_clstoken: - self.readout_projects = nn.ModuleList() - for _ in range(len(self.projects)): - self.readout_projects.append(nn.Sequential(nn.Linear(2 * in_channels, in_channels), nn.GELU())) - - self.scratch = _make_scratch( - out_channels, - features, - groups=1, - expand=False, - ) - - self.scratch.stem_transpose = None - - self.scratch.refinenet1 = _make_fusion_block(features, use_bn) - self.scratch.refinenet2 = _make_fusion_block(features, use_bn) - self.scratch.refinenet3 = _make_fusion_block(features, use_bn) - self.scratch.refinenet4 = _make_fusion_block(features, use_bn) - - head_features_1 = features - head_features_2 = 32 - - if nclass > 1: - self.scratch.output_conv = nn.Sequential( - nn.Conv2d(head_features_1, head_features_1, kernel_size=3, stride=1, padding=1), - nn.ReLU(True), - nn.Conv2d(head_features_1, nclass, kernel_size=1, stride=1, padding=0), - ) - else: - self.scratch.output_conv1 = nn.Conv2d( - head_features_1, head_features_1 // 2, kernel_size=3, stride=1, padding=1 - ) - - self.scratch.output_conv2 = nn.Sequential( - nn.Conv2d(head_features_1 // 2, head_features_2, kernel_size=3, stride=1, padding=1), - nn.ReLU(True), - nn.Conv2d(head_features_2, 1, kernel_size=1, stride=1, padding=0), - nn.ReLU(True), - nn.Identity(), - ) - - def forward(self, out_features, patch_h, patch_w): - out = [] - for i, x in enumerate(out_features): - if self.use_clstoken: - x, cls_token = x[0], x[1] - readout = cls_token.unsqueeze(1).expand_as(x) - x = self.readout_projects[i](torch.cat((x, readout), -1)) - else: - x = x[0] - - x = x.permute(0, 2, 1).reshape((x.shape[0], x.shape[-1], patch_h, patch_w)) - - x = self.projects[i](x) - x = self.resize_layers[i](x) - - out.append(x) - - layer_1, layer_2, layer_3, layer_4 = out - - layer_1_rn = self.scratch.layer1_rn(layer_1) - layer_2_rn = self.scratch.layer2_rn(layer_2) - layer_3_rn = self.scratch.layer3_rn(layer_3) - layer_4_rn = self.scratch.layer4_rn(layer_4) - - path_4 = self.scratch.refinenet4(layer_4_rn, size=layer_3_rn.shape[2:]) - path_3 = self.scratch.refinenet3(path_4, layer_3_rn, size=layer_2_rn.shape[2:]) - path_2 = self.scratch.refinenet2(path_3, layer_2_rn, size=layer_1_rn.shape[2:]) - path_1 = self.scratch.refinenet1(path_2, layer_1_rn) - - out = self.scratch.output_conv1(path_1) - out = F.interpolate(out, (int(patch_h * 14), int(patch_w * 14)), mode="bilinear", align_corners=True) - out = self.scratch.output_conv2(out) - - return out - - -class DPT_DINOv2(nn.Module): - def __init__( - self, - features, - out_channels, - encoder="vitl", - use_bn=False, - use_clstoken=False, - ): - super(DPT_DINOv2, self).__init__() - - assert encoder in ["vits", "vitb", "vitl"] - - # # in case the Internet connection is not stable, please load the DINOv2 locally - # if use_local: - # self.pretrained = torch.hub.load( - # torchhub_path / "facebookresearch_dinov2_main", - # "dinov2_{:}14".format(encoder), - # source="local", - # pretrained=False, - # ) - # else: - # self.pretrained = torch.hub.load( - # "facebookresearch/dinov2", - # "dinov2_{:}14".format(encoder), - # ) - - self.pretrained = torch.hub.load( - "facebookresearch/dinov2", - "dinov2_{:}14".format(encoder), - ) - - dim = self.pretrained.blocks[0].attn.qkv.in_features - - self.depth_head = DPTHead(1, dim, features, out_channels=out_channels, use_bn=use_bn, use_clstoken=use_clstoken) - - def forward(self, x): - h, w = x.shape[-2:] - - features = self.pretrained.get_intermediate_layers(x, 4, return_class_token=True) - - patch_h, patch_w = h // 14, w // 14 - - depth = self.depth_head(features, patch_h, patch_w) - depth = F.interpolate(depth, size=(h, w), mode="bilinear", align_corners=True) - depth = F.relu(depth) - - return depth.squeeze(1) diff --git a/invokeai/backend/image_util/depth_anything/utilities/util.py b/invokeai/backend/image_util/depth_anything/utilities/util.py deleted file mode 100644 index 5362ef6c3e0..00000000000 --- a/invokeai/backend/image_util/depth_anything/utilities/util.py +++ /dev/null @@ -1,227 +0,0 @@ -import math - -import cv2 -import numpy as np -import torch -import torch.nn.functional as F - - -def apply_min_size(sample, size, image_interpolation_method=cv2.INTER_AREA): - """Rezise the sample to ensure the given size. Keeps aspect ratio. - - Args: - sample (dict): sample - size (tuple): image size - - Returns: - tuple: new size - """ - shape = list(sample["disparity"].shape) - - if shape[0] >= size[0] and shape[1] >= size[1]: - return sample - - scale = [0, 0] - scale[0] = size[0] / shape[0] - scale[1] = size[1] / shape[1] - - scale = max(scale) - - shape[0] = math.ceil(scale * shape[0]) - shape[1] = math.ceil(scale * shape[1]) - - # resize - sample["image"] = cv2.resize(sample["image"], tuple(shape[::-1]), interpolation=image_interpolation_method) - - sample["disparity"] = cv2.resize(sample["disparity"], tuple(shape[::-1]), interpolation=cv2.INTER_NEAREST) - sample["mask"] = cv2.resize( - sample["mask"].astype(np.float32), - tuple(shape[::-1]), - interpolation=cv2.INTER_NEAREST, - ) - sample["mask"] = sample["mask"].astype(bool) - - return tuple(shape) - - -class Resize(object): - """Resize sample to given size (width, height).""" - - def __init__( - self, - width, - height, - resize_target=True, - keep_aspect_ratio=False, - ensure_multiple_of=1, - resize_method="lower_bound", - image_interpolation_method=cv2.INTER_AREA, - ): - """Init. - - Args: - width (int): desired output width - height (int): desired output height - resize_target (bool, optional): - True: Resize the full sample (image, mask, target). - False: Resize image only. - Defaults to True. - keep_aspect_ratio (bool, optional): - True: Keep the aspect ratio of the input sample. - Output sample might not have the given width and height, and - resize behaviour depends on the parameter 'resize_method'. - Defaults to False. - ensure_multiple_of (int, optional): - Output width and height is constrained to be multiple of this parameter. - Defaults to 1. - resize_method (str, optional): - "lower_bound": Output will be at least as large as the given size. - "upper_bound": Output will be at max as large as the given size. (Output size might be smaller - than given size.) - "minimal": Scale as least as possible. (Output size might be smaller than given size.) - Defaults to "lower_bound". - """ - self.__width = width - self.__height = height - - self.__resize_target = resize_target - self.__keep_aspect_ratio = keep_aspect_ratio - self.__multiple_of = ensure_multiple_of - self.__resize_method = resize_method - self.__image_interpolation_method = image_interpolation_method - - def constrain_to_multiple_of(self, x, min_val=0, max_val=None): - y = (np.round(x / self.__multiple_of) * self.__multiple_of).astype(int) - - if max_val is not None and y > max_val: - y = (np.floor(x / self.__multiple_of) * self.__multiple_of).astype(int) - - if y < min_val: - y = (np.ceil(x / self.__multiple_of) * self.__multiple_of).astype(int) - - return y - - def get_size(self, width, height): - # determine new height and width - scale_height = self.__height / height - scale_width = self.__width / width - - if self.__keep_aspect_ratio: - if self.__resize_method == "lower_bound": - # scale such that output size is lower bound - if scale_width > scale_height: - # fit width - scale_height = scale_width - else: - # fit height - scale_width = scale_height - elif self.__resize_method == "upper_bound": - # scale such that output size is upper bound - if scale_width < scale_height: - # fit width - scale_height = scale_width - else: - # fit height - scale_width = scale_height - elif self.__resize_method == "minimal": - # scale as least as possbile - if abs(1 - scale_width) < abs(1 - scale_height): - # fit width - scale_height = scale_width - else: - # fit height - scale_width = scale_height - else: - raise ValueError(f"resize_method {self.__resize_method} not implemented") - - if self.__resize_method == "lower_bound": - new_height = self.constrain_to_multiple_of(scale_height * height, min_val=self.__height) - new_width = self.constrain_to_multiple_of(scale_width * width, min_val=self.__width) - elif self.__resize_method == "upper_bound": - new_height = self.constrain_to_multiple_of(scale_height * height, max_val=self.__height) - new_width = self.constrain_to_multiple_of(scale_width * width, max_val=self.__width) - elif self.__resize_method == "minimal": - new_height = self.constrain_to_multiple_of(scale_height * height) - new_width = self.constrain_to_multiple_of(scale_width * width) - else: - raise ValueError(f"resize_method {self.__resize_method} not implemented") - - return (new_width, new_height) - - def __call__(self, sample): - width, height = self.get_size(sample["image"].shape[1], sample["image"].shape[0]) - - # resize sample - sample["image"] = cv2.resize( - sample["image"], - (width, height), - interpolation=self.__image_interpolation_method, - ) - - if self.__resize_target: - if "disparity" in sample: - sample["disparity"] = cv2.resize( - sample["disparity"], - (width, height), - interpolation=cv2.INTER_NEAREST, - ) - - if "depth" in sample: - sample["depth"] = cv2.resize(sample["depth"], (width, height), interpolation=cv2.INTER_NEAREST) - - if "semseg_mask" in sample: - # sample["semseg_mask"] = cv2.resize( - # sample["semseg_mask"], (width, height), interpolation=cv2.INTER_NEAREST - # ) - sample["semseg_mask"] = F.interpolate( - torch.from_numpy(sample["semseg_mask"]).float()[None, None, ...], (height, width), mode="nearest" - ).numpy()[0, 0] - - if "mask" in sample: - sample["mask"] = cv2.resize( - sample["mask"].astype(np.float32), - (width, height), - interpolation=cv2.INTER_NEAREST, - ) - # sample["mask"] = sample["mask"].astype(bool) - - # print(sample['image'].shape, sample['depth'].shape) - return sample - - -class NormalizeImage(object): - """Normlize image by given mean and std.""" - - def __init__(self, mean, std): - self.__mean = mean - self.__std = std - - def __call__(self, sample): - sample["image"] = (sample["image"] - self.__mean) / self.__std - - return sample - - -class PrepareForNet(object): - """Prepare sample for usage as network input.""" - - def __init__(self): - pass - - def __call__(self, sample): - image = np.transpose(sample["image"], (2, 0, 1)) - sample["image"] = np.ascontiguousarray(image).astype(np.float32) - - if "mask" in sample: - sample["mask"] = sample["mask"].astype(np.float32) - sample["mask"] = np.ascontiguousarray(sample["mask"]) - - if "depth" in sample: - depth = sample["depth"].astype(np.float32) - sample["depth"] = np.ascontiguousarray(depth) - - if "semseg_mask" in sample: - sample["semseg_mask"] = sample["semseg_mask"].astype(np.float32) - sample["semseg_mask"] = np.ascontiguousarray(sample["semseg_mask"]) - - return sample diff --git a/invokeai/backend/image_util/dw_openpose/__init__.py b/invokeai/backend/image_util/dw_openpose/__init__.py index cfd3ea4b0da..dafee63adec 100644 --- a/invokeai/backend/image_util/dw_openpose/__init__.py +++ b/invokeai/backend/image_util/dw_openpose/__init__.py @@ -1,78 +1,83 @@ from pathlib import Path from typing import Dict +import huggingface_hub import numpy as np +import onnxruntime as ort import torch -from controlnet_aux.util import resize_image 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.dw_openpose.wholebody import Wholebody +from invokeai.backend.image_util.util import np_to_pil +from invokeai.backend.util.devices import TorchDevice -DWPOSE_MODELS = { - "yolox_l.onnx": "https://huggingface.co/yzd-v/DWPose/resolve/main/yolox_l.onnx?download=true", - "dw-ll_ucoco_384.onnx": "https://huggingface.co/yzd-v/DWPose/resolve/main/dw-ll_ucoco_384.onnx?download=true", -} +class DWOpenposeDetector: + """ + Code from the original implementation of the DW Openpose Detector. + Credits: https://github.com/IDEA-Research/DWPose + """ -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, - resolution: int = 512, -) -> Image.Image: - bodies = pose["bodies"] - faces = pose["faces"] - hands = pose["hands"] - - assert isinstance(bodies, dict) - candidate = bodies["candidate"] + hf_repo_id = "yzd-v/DWPose" + hf_filename_onnx_det = "yolox_l.onnx" + hf_filename_onnx_pose = "dw-ll_ucoco_384.onnx" - assert isinstance(bodies, dict) - subset = bodies["subset"] + @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) - canvas = np.zeros(shape=(H, W, 3), dtype=np.uint8) + @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) - if draw_body: - canvas = draw_bodypose(canvas, candidate, subset) + @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.""" - if draw_hands: - assert isinstance(hands, np.ndarray) - canvas = draw_handpose(canvas, hands) + device = TorchDevice.choose_torch_device() + providers = ["CUDAExecutionProvider"] if device.type == "cuda" else ["CPUExecutionProvider"] + return ort.InferenceSession(path_or_bytes=model_path, providers=providers) - if draw_face: - assert isinstance(hands, np.ndarray) - canvas = draw_facepose(canvas, faces) # type: ignore + def __init__(self, session_det: ort.InferenceSession, session_pose: ort.InferenceSession): + self.session_det = session_det + self.session_pose = session_pose - dwpose_image: Image.Image = resize_image( - canvas, - resolution, - ) - dwpose_image = Image.fromarray(dwpose_image) + def pose_estimation(self, np_image: np.ndarray): + """Does the pose estimation on the given image and returns the keypoints and scores.""" - return dwpose_image + 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 -class DWOpenposeDetector: - """ - Code from the original implementation of the DW Openpose Detector. - Credits: https://github.com/IDEA-Research/DWPose - """ + keypoints, scores = keypoints_info[..., :2], keypoints_info[..., 2] - def __init__(self, onnx_det: Path, onnx_pose: Path) -> None: - self.pose_estimation = Wholebody(onnx_det=onnx_det, onnx_pose=onnx_pose) + return keypoints, scores - def __call__( + def run( self, image: Image.Image, draw_face: bool = False, draw_body: bool = True, draw_hands: bool = False, - resolution: int = 512, ) -> 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 @@ -104,9 +109,44 @@ def __call__( bodies = {"candidate": body, "subset": score} pose = {"bodies": bodies, "hands": hands, "faces": faces} - return draw_pose( - pose, H, W, draw_face=draw_face, draw_hands=draw_hands, draw_body=draw_body, resolution=resolution + 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) -__all__ = ["DWPOSE_MODELS", "DWOpenposeDetector"] + return dwpose_image diff --git a/invokeai/backend/image_util/dw_openpose/utils.py b/invokeai/backend/image_util/dw_openpose/utils.py index dc142dfa71c..060f6857c73 100644 --- a/invokeai/backend/image_util/dw_openpose/utils.py +++ b/invokeai/backend/image_util/dw_openpose/utils.py @@ -3,7 +3,6 @@ import math import cv2 -import matplotlib import numpy as np import numpy.typing as npt @@ -127,11 +126,13 @@ def draw_handpose(canvas: NDArrayInt, all_hand_peaks: NDArrayInt) -> NDArrayInt: 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), - matplotlib.colors.hsv_to_rgb([ie / float(len(edges)), 1.0, 1.0]) * 255, + rgb_color.tolist(), thickness=2, ) diff --git a/invokeai/backend/image_util/dw_openpose/wholebody.py b/invokeai/backend/image_util/dw_openpose/wholebody.py deleted file mode 100644 index 3f77f20b9c2..00000000000 --- a/invokeai/backend/image_util/dw_openpose/wholebody.py +++ /dev/null @@ -1,45 +0,0 @@ -# Code from the original DWPose Implementation: https://github.com/IDEA-Research/DWPose -# Modified pathing to suit Invoke - - -from pathlib import Path - -import numpy as np -import onnxruntime as ort - -from invokeai.app.services.config.config_default import get_config -from invokeai.backend.util.devices import TorchDevice - -from .onnxdet import inference_detector -from .onnxpose import inference_pose - -config = get_config() - - -class Wholebody: - def __init__(self, onnx_det: Path, onnx_pose: Path): - device = TorchDevice.choose_torch_device() - - providers = ["CUDAExecutionProvider"] if device.type == "cuda" else ["CPUExecutionProvider"] - - self.session_det = ort.InferenceSession(path_or_bytes=onnx_det, providers=providers) - self.session_pose = ort.InferenceSession(path_or_bytes=onnx_pose, providers=providers) - - def __call__(self, oriImg): - det_result = inference_detector(self.session_det, oriImg) - keypoints, scores = inference_pose(self.session_pose, det_result, oriImg) - - 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 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 index 97706df8b98..a2d3449f650 100644 --- a/invokeai/backend/image_util/hed.py +++ b/invokeai/backend/image_util/hed.py @@ -1,6 +1,9 @@ -"""Adapted from https://github.com/huggingface/controlnet_aux (Apache-2.0 license).""" +# 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 @@ -15,6 +18,7 @@ resize_image_to_resolution, safe_step, ) +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device class DoubleConvBlock(torch.nn.Module): @@ -106,7 +110,7 @@ def run( Returns: The detected edges. """ - device = next(iter(self.network.parameters())).device + 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) @@ -140,3 +144,74 @@ def run( 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/lama.py b/invokeai/backend/image_util/infill_methods/lama.py index cd5838d1f2b..5b3b6857099 100644 --- a/invokeai/backend/image_util/infill_methods/lama.py +++ b/invokeai/backend/image_util/infill_methods/lama.py @@ -6,7 +6,8 @@ from PIL import Image import invokeai.backend.util.logging as logger -from invokeai.backend.model_manager.config import AnyModel +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): @@ -31,7 +32,7 @@ def __call__(self, input_image: Image.Image, *args: Any, **kwds: Any) -> Any: mask = norm_img(mask) mask = (mask > 0) * 1 - device = next(self._model.buffers()).device + device = get_effective_device(self._model) image = torch.from_numpy(image).unsqueeze(0).to(device) mask = torch.from_numpy(mask).unsqueeze(0).to(device) diff --git a/invokeai/backend/image_util/invisible_watermark.py b/invokeai/backend/image_util/invisible_watermark.py index 84342e442fc..95c483848cc 100644 --- a/invokeai/backend/image_util/invisible_watermark.py +++ b/invokeai/backend/image_util/invisible_watermark.py @@ -6,13 +6,10 @@ import cv2 import numpy as np -from imwatermark import WatermarkEncoder from PIL import Image import invokeai.backend.util.logging as logger -from invokeai.app.services.config.config_default import get_config - -config = get_config() +from invokeai.backend.image_util.imwatermark.vendor import WatermarkDecoder, WatermarkEncoder class InvisibleWatermark: @@ -28,3 +25,25 @@ def add_watermark(cls, image: Image.Image, watermark_text: str) -> Image.Image: 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 index 3d19262822e..bfef6f6da08 100644 --- a/invokeai/backend/image_util/lineart.py +++ b/invokeai/backend/image_util/lineart.py @@ -1,6 +1,9 @@ """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 @@ -14,6 +17,7 @@ pil_to_np, resize_image_to_resolution, ) +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device class ResidualBlock(nn.Module): @@ -127,7 +131,7 @@ def run( Returns: The detected lineart. """ - device = next(iter(self.model.parameters())).device + device = get_effective_device(self.model) np_image = pil_to_np(input_image) np_image = normalize_image_channel_count(np_image) @@ -156,3 +160,69 @@ def run( 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 index 5185d92c512..fa406cf1d4b 100644 --- a/invokeai/backend/image_util/lineart_anime.py +++ b/invokeai/backend/image_util/lineart_anime.py @@ -1,9 +1,11 @@ """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 @@ -17,6 +19,7 @@ pil_to_np, resize_image_to_resolution, ) +from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device class UnetGenerator(nn.Module): @@ -98,7 +101,7 @@ def __init__( """ super(UnetSkipConnectionBlock, self).__init__() self.outermost = outermost - if type(norm_layer) == functools.partial: + if isinstance(norm_layer, functools.partial): use_bias = norm_layer.func == nn.InstanceNorm2d else: use_bias = norm_layer == nn.InstanceNorm2d @@ -169,7 +172,7 @@ def run(self, input_image: Image.Image, detect_resolution: int = 512, image_reso Returns: The detected lineart. """ - device = next(iter(self.model.parameters())).device + device = get_effective_device(self.model) np_image = pil_to_np(input_image) np_image = normalize_image_channel_count(np_image) @@ -201,3 +204,71 @@ def run(self, input_image: Image.Image, detect_resolution: int = 512, image_reso 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 index f537b4681c9..1f4b42fe217 100644 --- a/invokeai/backend/image_util/pngwriter.py +++ b/invokeai/backend/image_util/pngwriter.py @@ -91,10 +91,10 @@ def normalize_prompt(self): 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"-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}') @@ -109,7 +109,7 @@ def normalize_prompt(self): 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])}') + 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: diff --git a/invokeai/backend/image_util/realesrgan/realesrgan.py b/invokeai/backend/image_util/realesrgan/realesrgan.py index c5fe3fa598c..37853401c21 100644 --- a/invokeai/backend/image_util/realesrgan/realesrgan.py +++ b/invokeai/backend/image_util/realesrgan/realesrgan.py @@ -10,7 +10,7 @@ from tqdm import tqdm from invokeai.backend.image_util.basicsr.rrdbnet_arch import RRDBNet -from invokeai.backend.model_manager.config import AnyModel +from invokeai.backend.model_manager.taxonomy import AnyModel from invokeai.backend.util.devices import TorchDevice """ 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 index 5b2116975f9..1e7aad4eb45 100644 --- a/invokeai/backend/image_util/util.py +++ b/invokeai/backend/image_util/util.py @@ -86,12 +86,20 @@ def np_to_pil(image: np.ndarray) -> Image.Image: def pil_to_cv2(image: Image.Image) -> np.ndarray: """Converts a PIL image to a CV2 image.""" - return cv2.cvtColor(np.array(image, dtype=np.uint8), cv2.COLOR_RGB2BGR) + + 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.""" - return Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + + 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: @@ -217,3 +225,23 @@ def safe_step(x: np.ndarray, step: int = 2) -> np.ndarray: 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 index c85acae4982..7ac845e5346 100644 --- a/invokeai/backend/ip_adapter/README.md +++ b/invokeai/backend/ip_adapter/README.md @@ -42,4 +42,5 @@ IP-Adapters: - [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) \ No newline at end of file +- [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/ip_adapter.py b/invokeai/backend/ip_adapter/ip_adapter.py index c33cb3f4ab4..4607ddd0120 100644 --- a/invokeai/backend/ip_adapter/ip_adapter.py +++ b/invokeai/backend/ip_adapter/ip_adapter.py @@ -11,9 +11,8 @@ from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection from invokeai.backend.ip_adapter.ip_attention_weights import IPAttentionWeights - -from ..raw_model import RawModel -from .resampler import Resampler +from invokeai.backend.ip_adapter.resampler import Resampler +from invokeai.backend.raw_model import RawModel class IPAdapterStateDict(TypedDict): @@ -125,22 +124,20 @@ def __init__( self.device, dtype=self.dtype ) - def to( - self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None, non_blocking: bool = False - ): + 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, non_blocking=non_blocking) - self.attn_weights.to(device=self.device, dtype=self.dtype, non_blocking=non_blocking) + 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): - # workaround for circular import - from invokeai.backend.model_manager.load.model_util import calc_model_size_by_data + 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_model_size_by_data(self._image_proj_model) + calc_model_size_by_data(self.attn_weights) + 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] @@ -210,15 +207,24 @@ def _init_image_proj_model(self, state_dict: dict[str, torch.Tensor]): def load_ip_adapter_tensors(ip_adapter_ckpt_path: pathlib.Path, device: str) -> IPAdapterStateDict: - state_dict: IPAdapterStateDict = {"ip_adapter": {}, "image_proj": {}} + 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("image_proj."): - state_dict["image_proj"][key.replace("image_proj.", "")] = model[key] - elif key.startswith("ip_adapter."): + 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: 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/lora.py b/invokeai/backend/lora.py deleted file mode 100644 index f7c3863a6ad..00000000000 --- a/invokeai/backend/lora.py +++ /dev/null @@ -1,631 +0,0 @@ -# Copyright (c) 2024 The InvokeAI Development team -"""LoRA model support.""" - -import bisect -from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union - -import torch -from safetensors.torch import load_file -from typing_extensions import Self - -from invokeai.backend.model_manager import BaseModelType - -from .raw_model import RawModel - - -class LoRALayerBase: - # rank: Optional[int] - # alpha: Optional[float] - # bias: Optional[torch.Tensor] - # layer_key: str - - # @property - # def scale(self): - # return self.alpha / self.rank if (self.alpha and self.rank) else 1.0 - - def __init__( - self, - layer_key: str, - values: Dict[str, torch.Tensor], - ): - if "alpha" in values: - self.alpha = values["alpha"].item() - else: - self.alpha = None - - if "bias_indices" in values and "bias_values" in values and "bias_size" in values: - self.bias: Optional[torch.Tensor] = torch.sparse_coo_tensor( - values["bias_indices"], - values["bias_values"], - tuple(values["bias_size"]), - ) - - else: - self.bias = None - - self.rank = None # set in layer implementation - self.layer_key = layer_key - - def get_weight(self, orig_weight: Optional[torch.Tensor]) -> torch.Tensor: - raise NotImplementedError() - - def calc_size(self) -> int: - model_size = 0 - for val in [self.bias]: - if val is not None: - model_size += val.nelement() * val.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - non_blocking: bool = False, - ) -> None: - if self.bias is not None: - self.bias = self.bias.to(device=device, dtype=dtype, non_blocking=non_blocking) - - -# TODO: find and debug lora/locon with bias -class LoRALayer(LoRALayerBase): - # up: torch.Tensor - # mid: Optional[torch.Tensor] - # down: torch.Tensor - - def __init__( - self, - layer_key: str, - values: Dict[str, torch.Tensor], - ): - super().__init__(layer_key, values) - - self.up = values["lora_up.weight"] - self.down = values["lora_down.weight"] - if "lora_mid.weight" in values: - self.mid: Optional[torch.Tensor] = values["lora_mid.weight"] - else: - self.mid = None - - self.rank = self.down.shape[0] - - def get_weight(self, orig_weight: Optional[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: - weight = self.up.reshape(self.up.shape[0], -1) @ self.down.reshape(self.down.shape[0], -1) - - return weight - - def calc_size(self) -> int: - model_size = super().calc_size() - for val in [self.up, self.mid, self.down]: - if val is not None: - model_size += val.nelement() * val.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - non_blocking: bool = False, - ) -> None: - super().to(device=device, dtype=dtype, non_blocking=non_blocking) - - self.up = self.up.to(device=device, dtype=dtype, non_blocking=non_blocking) - self.down = self.down.to(device=device, dtype=dtype, non_blocking=non_blocking) - - if self.mid is not None: - self.mid = self.mid.to(device=device, dtype=dtype, non_blocking=non_blocking) - - -class LoHALayer(LoRALayerBase): - # w1_a: torch.Tensor - # w1_b: torch.Tensor - # w2_a: torch.Tensor - # w2_b: torch.Tensor - # t1: Optional[torch.Tensor] = None - # t2: Optional[torch.Tensor] = None - - def __init__(self, layer_key: str, values: Dict[str, torch.Tensor]): - super().__init__(layer_key, values) - - self.w1_a = values["hada_w1_a"] - self.w1_b = values["hada_w1_b"] - self.w2_a = values["hada_w2_a"] - self.w2_b = values["hada_w2_b"] - - if "hada_t1" in values: - self.t1: Optional[torch.Tensor] = values["hada_t1"] - else: - self.t1 = None - - if "hada_t2" in values: - self.t2: Optional[torch.Tensor] = values["hada_t2"] - else: - self.t2 = None - - self.rank = self.w1_b.shape[0] - - def get_weight(self, orig_weight: Optional[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 calc_size(self) -> int: - model_size = super().calc_size() - for val in [self.w1_a, self.w1_b, self.w2_a, self.w2_b, self.t1, self.t2]: - if val is not None: - model_size += val.nelement() * val.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - non_blocking: bool = False, - ) -> None: - super().to(device=device, dtype=dtype) - - self.w1_a = self.w1_a.to(device=device, dtype=dtype, non_blocking=non_blocking) - self.w1_b = self.w1_b.to(device=device, dtype=dtype, non_blocking=non_blocking) - if self.t1 is not None: - self.t1 = self.t1.to(device=device, dtype=dtype, non_blocking=non_blocking) - - self.w2_a = self.w2_a.to(device=device, dtype=dtype, non_blocking=non_blocking) - self.w2_b = self.w2_b.to(device=device, dtype=dtype, non_blocking=non_blocking) - if self.t2 is not None: - self.t2 = self.t2.to(device=device, dtype=dtype, non_blocking=non_blocking) - - -class LoKRLayer(LoRALayerBase): - # w1: Optional[torch.Tensor] = None - # w1_a: Optional[torch.Tensor] = None - # w1_b: Optional[torch.Tensor] = None - # w2: Optional[torch.Tensor] = None - # w2_a: Optional[torch.Tensor] = None - # w2_b: Optional[torch.Tensor] = None - # t2: Optional[torch.Tensor] = None - - def __init__( - self, - layer_key: str, - values: Dict[str, torch.Tensor], - ): - super().__init__(layer_key, values) - - if "lokr_w1" in values: - self.w1: Optional[torch.Tensor] = values["lokr_w1"] - self.w1_a = None - self.w1_b = None - else: - self.w1 = None - self.w1_a = values["lokr_w1_a"] - self.w1_b = values["lokr_w1_b"] - - if "lokr_w2" in values: - self.w2: Optional[torch.Tensor] = values["lokr_w2"] - self.w2_a = None - self.w2_b = None - else: - self.w2 = None - self.w2_a = values["lokr_w2_a"] - self.w2_b = values["lokr_w2_b"] - - if "lokr_t2" in values: - self.t2: Optional[torch.Tensor] = values["lokr_t2"] - else: - self.t2 = None - - if "lokr_w1_b" in values: - self.rank = values["lokr_w1_b"].shape[0] - elif "lokr_w2_b" in values: - self.rank = values["lokr_w2_b"].shape[0] - else: - self.rank = None # unscaled - - def get_weight(self, orig_weight: Optional[torch.Tensor]) -> torch.Tensor: - w1: Optional[torch.Tensor] = 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() - assert w1 is not None - assert w2 is not None - weight = torch.kron(w1, w2) - - return weight - - def calc_size(self) -> int: - model_size = super().calc_size() - for val in [self.w1, self.w1_a, self.w1_b, self.w2, self.w2_a, self.w2_b, self.t2]: - if val is not None: - model_size += val.nelement() * val.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - non_blocking: bool = False, - ) -> None: - super().to(device=device, dtype=dtype) - - if self.w1 is not None: - self.w1 = self.w1.to(device=device, dtype=dtype) - else: - assert self.w1_a is not None - assert self.w1_b is not None - self.w1_a = self.w1_a.to(device=device, dtype=dtype, non_blocking=non_blocking) - self.w1_b = self.w1_b.to(device=device, dtype=dtype, non_blocking=non_blocking) - - if self.w2 is not None: - self.w2 = self.w2.to(device=device, dtype=dtype, non_blocking=non_blocking) - else: - assert self.w2_a is not None - assert self.w2_b is not None - self.w2_a = self.w2_a.to(device=device, dtype=dtype, non_blocking=non_blocking) - self.w2_b = self.w2_b.to(device=device, dtype=dtype, non_blocking=non_blocking) - - if self.t2 is not None: - self.t2 = self.t2.to(device=device, dtype=dtype, non_blocking=non_blocking) - - -class FullLayer(LoRALayerBase): - # weight: torch.Tensor - - def __init__( - self, - layer_key: str, - values: Dict[str, torch.Tensor], - ): - super().__init__(layer_key, values) - - self.weight = values["diff"] - - if len(values.keys()) > 1: - _keys = list(values.keys()) - _keys.remove("diff") - raise NotImplementedError(f"Unexpected keys in lora diff layer: {_keys}") - - self.rank = None # unscaled - - def get_weight(self, orig_weight: Optional[torch.Tensor]) -> torch.Tensor: - return self.weight - - def calc_size(self) -> int: - model_size = super().calc_size() - model_size += self.weight.nelement() * self.weight.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - non_blocking: bool = False, - ) -> None: - super().to(device=device, dtype=dtype) - - self.weight = self.weight.to(device=device, dtype=dtype, non_blocking=non_blocking) - - -class IA3Layer(LoRALayerBase): - # weight: torch.Tensor - # on_input: torch.Tensor - - def __init__( - self, - layer_key: str, - values: Dict[str, torch.Tensor], - ): - super().__init__(layer_key, values) - - self.weight = values["weight"] - self.on_input = values["on_input"] - - self.rank = None # unscaled - - def get_weight(self, orig_weight: Optional[torch.Tensor]) -> torch.Tensor: - weight = self.weight - if not self.on_input: - weight = weight.reshape(-1, 1) - assert orig_weight is not None - return orig_weight * weight - - def calc_size(self) -> int: - model_size = super().calc_size() - model_size += self.weight.nelement() * self.weight.element_size() - model_size += self.on_input.nelement() * self.on_input.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - non_blocking: bool = False, - ): - super().to(device=device, dtype=dtype) - - self.weight = self.weight.to(device=device, dtype=dtype, non_blocking=non_blocking) - self.on_input = self.on_input.to(device=device, dtype=dtype, non_blocking=non_blocking) - - -AnyLoRALayer = Union[LoRALayer, LoHALayer, LoKRLayer, FullLayer, IA3Layer] - - -class LoRAModelRaw(RawModel): # (torch.nn.Module): - _name: str - layers: Dict[str, AnyLoRALayer] - - def __init__( - self, - name: str, - layers: Dict[str, AnyLoRALayer], - ): - self._name = name - self.layers = layers - - @property - def name(self) -> str: - return self._name - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - non_blocking: bool = False, - ) -> None: - # TODO: try revert if exception? - for _key, layer in self.layers.items(): - layer.to(device=device, dtype=dtype, non_blocking=non_blocking) - - def calc_size(self) -> int: - model_size = 0 - for _, layer in self.layers.items(): - model_size += layer.calc_size() - return model_size - - @classmethod - def _convert_sdxl_keys_to_diffusers_format(cls, state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: - """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 = {} - 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 - - @classmethod - def from_checkpoint( - cls, - file_path: Union[str, Path], - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - base_model: Optional[BaseModelType] = None, - ) -> Self: - device = device or torch.device("cpu") - dtype = dtype or torch.float32 - - if isinstance(file_path, str): - file_path = Path(file_path) - - model = cls( - name=file_path.stem, - layers={}, - ) - - if file_path.suffix == ".safetensors": - sd = load_file(file_path.absolute().as_posix(), device="cpu") - else: - sd = torch.load(file_path, map_location="cpu") - - state_dict = cls._group_state(sd) - - if base_model == BaseModelType.StableDiffusionXL: - state_dict = cls._convert_sdxl_keys_to_diffusers_format(state_dict) - - for layer_key, values in state_dict.items(): - # lora and locon - if "lora_down.weight" in values: - layer: AnyLoRALayer = LoRALayer(layer_key, values) - - # loha - elif "hada_w1_b" in values: - layer = LoHALayer(layer_key, values) - - # lokr - elif "lokr_w1_b" in values or "lokr_w1" in values: - layer = LoKRLayer(layer_key, values) - - # diff - elif "diff" in values: - layer = FullLayer(layer_key, values) - - # ia3 - elif "weight" in values and "on_input" in values: - layer = IA3Layer(layer_key, values) - - else: - print(f">> Encountered unknown lora layer module in {model.name}: {layer_key} - {list(values.keys())}") - raise Exception("Unknown lora format!") - - # lower memory consumption by removing already parsed layer values - state_dict[layer_key].clear() - - layer.to(device=device, dtype=dtype, non_blocking=True) - model.layers[layer_key] = layer - - return model - - @staticmethod - 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 - - -# 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 = [] - - 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 = [] - 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/model_hash/hash_validator.py b/invokeai/backend/model_hash/hash_validator.py index 8c387885147..622cdbbddfb 100644 --- a/invokeai/backend/model_hash/hash_validator.py +++ b/invokeai/backend/model_hash/hash_validator.py @@ -12,7 +12,9 @@ def validate_hash(hash: str): map = json.loads(b64decode(enc_hash)) if alg in map: if hash_ == map[alg]: - raise Exception("Unrecoverable Model Error") + 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] = [ 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 index 98cc5054c73..e69de29bb2d 100644 --- a/invokeai/backend/model_manager/__init__.py +++ b/invokeai/backend/model_manager/__init__.py @@ -1,35 +0,0 @@ -"""Re-export frequently-used symbols from the Model Manager backend.""" - -from .config import ( - AnyModel, - AnyModelConfig, - BaseModelType, - InvalidModelConfigException, - ModelConfigFactory, - ModelFormat, - ModelRepoVariant, - ModelType, - ModelVariantType, - SchedulerPredictionType, - SubModelType, -) -from .load import LoadedModel -from .probe import ModelProbe -from .search import ModelSearch - -__all__ = [ - "AnyModel", - "AnyModelConfig", - "BaseModelType", - "ModelRepoVariant", - "InvalidModelConfigException", - "LoadedModel", - "ModelConfigFactory", - "ModelFormat", - "ModelProbe", - "ModelSearch", - "ModelType", - "ModelVariantType", - "SchedulerPredictionType", - "SubModelType", -] diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py deleted file mode 100644 index 7ed12a7674d..00000000000 --- a/invokeai/backend/model_manager/config.py +++ /dev/null @@ -1,454 +0,0 @@ -# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Development Team -""" -Configuration definitions for image generation models. - -Typical usage: - - from invokeai.backend.model_manager import ModelConfigFactory - raw = dict(path='models/sd-1/main/foo.ckpt', - name='foo', - base='sd-1', - type='main', - config='configs/stable-diffusion/v1-inference.yaml', - variant='normal', - format='checkpoint' - ) - config = ModelConfigFactory.make_config(raw) - print(config.name) - -Validation errors will raise an InvalidModelConfigException error. - -""" - -import time -from enum import Enum -from typing import Literal, Optional, Type, TypeAlias, Union - -import torch -from diffusers.models.modeling_utils import ModelMixin -from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag, TypeAdapter -from typing_extensions import Annotated, Any, Dict - -from invokeai.app.invocations.constants import SCHEDULER_NAME_VALUES -from invokeai.app.util.misc import uuid_string -from invokeai.backend.model_hash.hash_validator import validate_hash - -from ..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 = Union[ModelMixin, RawModel, torch.nn.Module, Dict[str, torch.Tensor]] - - -class InvalidModelConfigException(Exception): - """Exception for when config parser doesn't recognized this combination of model type and format.""" - - -class BaseModelType(str, Enum): - """Base model type.""" - - Any = "any" - StableDiffusion1 = "sd-1" - StableDiffusion2 = "sd-2" - StableDiffusionXL = "sdxl" - StableDiffusionXLRefiner = "sdxl-refiner" - # Kandinsky2_1 = "kandinsky-2.1" - - -class ModelType(str, Enum): - """Model type.""" - - ONNX = "onnx" - Main = "main" - VAE = "vae" - LoRA = "lora" - ControlNet = "controlnet" # used by model_probe - TextualInversion = "embedding" - IPAdapter = "ip_adapter" - CLIPVision = "clip_vision" - T2IAdapter = "t2i_adapter" - - -class SubModelType(str, Enum): - """Submodel type.""" - - UNet = "unet" - TextEncoder = "text_encoder" - TextEncoder2 = "text_encoder_2" - Tokenizer = "tokenizer" - Tokenizer2 = "tokenizer_2" - VAE = "vae" - VAEDecoder = "vae_decoder" - VAEEncoder = "vae_encoder" - Scheduler = "scheduler" - SafetyChecker = "safety_checker" - - -class ModelVariantType(str, Enum): - """Variant type.""" - - Normal = "normal" - Inpaint = "inpaint" - Depth = "depth" - - -class ModelFormat(str, Enum): - """Storage format of model.""" - - Diffusers = "diffusers" - Checkpoint = "checkpoint" - LyCORIS = "lycoris" - ONNX = "onnx" - Olive = "olive" - EmbeddingFile = "embedding_file" - EmbeddingFolder = "embedding_folder" - InvokeAI = "invokeai" - - -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" - - -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") - - model_config = ConfigDict(extra="forbid") - - -class ControlAdapterDefaultSettings(BaseModel): - # This could be narrowed to controlnet processor nodes, but they change. Leaving this a string is safer. - preprocessor: str | None - - model_config = ConfigDict(extra="forbid") - - -class ModelConfigBase(BaseModel): - """Base class for model configuration information.""" - - key: str = Field(description="A unique key for this model.", default_factory=uuid_string) - 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." - ) - name: str = Field(description="Name of the model.") - base: BaseModelType = Field(description="The base model.") - description: Optional[str] = Field(description="Model description", default=None) - 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: Optional[str] = Field( - description="The original API response from the source, as stringified JSON.", default=None - ) - cover_image: Optional[str] = Field(description="Url for image to preview model", default=None) - - @staticmethod - def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseModel]) -> None: - schema["required"].extend(["key", "type", "format"]) - - model_config = ConfigDict(validate_assignment=True, json_schema_extra=json_schema_extra) - - -class CheckpointConfigBase(ModelConfigBase): - """Model config for checkpoint-style models.""" - - format: Literal[ModelFormat.Checkpoint] = ModelFormat.Checkpoint - config_path: str = Field(description="path to the checkpoint model config file") - converted_at: Optional[float] = Field( - description="When this model was last converted to diffusers", default_factory=time.time - ) - - -class DiffusersConfigBase(ModelConfigBase): - """Model config for diffusers-style models.""" - - format: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers - repo_variant: Optional[ModelRepoVariant] = ModelRepoVariant.Default - - -class LoRAConfigBase(ModelConfigBase): - type: Literal[ModelType.LoRA] = ModelType.LoRA - trigger_phrases: Optional[set[str]] = Field(description="Set of trigger phrases for this model", default=None) - - -class LoRALyCORISConfig(LoRAConfigBase): - """Model config for LoRA/Lycoris models.""" - - format: Literal[ModelFormat.LyCORIS] = ModelFormat.LyCORIS - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.LoRA.value}.{ModelFormat.LyCORIS.value}") - - -class LoRADiffusersConfig(LoRAConfigBase): - """Model config for LoRA/Diffusers models.""" - - format: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.LoRA.value}.{ModelFormat.Diffusers.value}") - - -class VAECheckpointConfig(CheckpointConfigBase): - """Model config for standalone VAE models.""" - - type: Literal[ModelType.VAE] = ModelType.VAE - format: Literal[ModelFormat.Checkpoint] = ModelFormat.Checkpoint - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.VAE.value}.{ModelFormat.Checkpoint.value}") - - -class VAEDiffusersConfig(ModelConfigBase): - """Model config for standalone VAE models (diffusers version).""" - - type: Literal[ModelType.VAE] = ModelType.VAE - format: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.VAE.value}.{ModelFormat.Diffusers.value}") - - -class ControlAdapterConfigBase(BaseModel): - default_settings: Optional[ControlAdapterDefaultSettings] = Field( - description="Default settings for this model", default=None - ) - - -class ControlNetDiffusersConfig(DiffusersConfigBase, ControlAdapterConfigBase): - """Model config for ControlNet models (diffusers version).""" - - type: Literal[ModelType.ControlNet] = ModelType.ControlNet - format: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.ControlNet.value}.{ModelFormat.Diffusers.value}") - - -class ControlNetCheckpointConfig(CheckpointConfigBase, ControlAdapterConfigBase): - """Model config for ControlNet models (diffusers version).""" - - type: Literal[ModelType.ControlNet] = ModelType.ControlNet - format: Literal[ModelFormat.Checkpoint] = ModelFormat.Checkpoint - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.ControlNet.value}.{ModelFormat.Checkpoint.value}") - - -class TextualInversionFileConfig(ModelConfigBase): - """Model config for textual inversion embeddings.""" - - type: Literal[ModelType.TextualInversion] = ModelType.TextualInversion - format: Literal[ModelFormat.EmbeddingFile] = ModelFormat.EmbeddingFile - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.TextualInversion.value}.{ModelFormat.EmbeddingFile.value}") - - -class TextualInversionFolderConfig(ModelConfigBase): - """Model config for textual inversion embeddings.""" - - type: Literal[ModelType.TextualInversion] = ModelType.TextualInversion - format: Literal[ModelFormat.EmbeddingFolder] = ModelFormat.EmbeddingFolder - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.TextualInversion.value}.{ModelFormat.EmbeddingFolder.value}") - - -class MainConfigBase(ModelConfigBase): - type: Literal[ModelType.Main] = ModelType.Main - trigger_phrases: Optional[set[str]] = Field(description="Set of trigger phrases for this model", default=None) - default_settings: Optional[MainModelDefaultSettings] = Field( - description="Default settings for this model", default=None - ) - variant: ModelVariantType = ModelVariantType.Normal - - -class MainCheckpointConfig(CheckpointConfigBase, MainConfigBase): - """Model config for main checkpoint models.""" - - prediction_type: SchedulerPredictionType = SchedulerPredictionType.Epsilon - upcast_attention: bool = False - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.Main.value}.{ModelFormat.Checkpoint.value}") - - -class MainDiffusersConfig(DiffusersConfigBase, MainConfigBase): - """Model config for main diffusers models.""" - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.Main.value}.{ModelFormat.Diffusers.value}") - - -class IPAdapterBaseConfig(ModelConfigBase): - type: Literal[ModelType.IPAdapter] = ModelType.IPAdapter - - -class IPAdapterInvokeAIConfig(IPAdapterBaseConfig): - """Model config for IP Adapter diffusers format models.""" - - image_encoder_model_id: str - format: Literal[ModelFormat.InvokeAI] - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.IPAdapter.value}.{ModelFormat.InvokeAI.value}") - - -class IPAdapterCheckpointConfig(IPAdapterBaseConfig): - """Model config for IP Adapter checkpoint format models.""" - - format: Literal[ModelFormat.Checkpoint] - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.IPAdapter.value}.{ModelFormat.Checkpoint.value}") - - -class CLIPVisionDiffusersConfig(DiffusersConfigBase): - """Model config for CLIPVision.""" - - type: Literal[ModelType.CLIPVision] = ModelType.CLIPVision - format: Literal[ModelFormat.Diffusers] - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.CLIPVision.value}.{ModelFormat.Diffusers.value}") - - -class T2IAdapterConfig(DiffusersConfigBase, ControlAdapterConfigBase): - """Model config for T2I.""" - - type: Literal[ModelType.T2IAdapter] = ModelType.T2IAdapter - format: Literal[ModelFormat.Diffusers] - - @staticmethod - def get_tag() -> Tag: - return Tag(f"{ModelType.T2IAdapter.value}.{ModelFormat.Diffusers.value}") - - -def get_model_discriminator_value(v: Any) -> str: - """ - Computes the discriminator value for a model config. - https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions-with-callable-discriminator - """ - format_ = None - type_ = None - if isinstance(v, dict): - format_ = v.get("format") - if isinstance(format_, Enum): - format_ = format_.value - type_ = v.get("type") - if isinstance(type_, Enum): - type_ = type_.value - else: - format_ = v.format.value - type_ = v.type.value - v = f"{type_}.{format_}" - return v - - -AnyModelConfig = Annotated[ - Union[ - Annotated[MainDiffusersConfig, MainDiffusersConfig.get_tag()], - Annotated[MainCheckpointConfig, MainCheckpointConfig.get_tag()], - Annotated[VAEDiffusersConfig, VAEDiffusersConfig.get_tag()], - Annotated[VAECheckpointConfig, VAECheckpointConfig.get_tag()], - Annotated[ControlNetDiffusersConfig, ControlNetDiffusersConfig.get_tag()], - Annotated[ControlNetCheckpointConfig, ControlNetCheckpointConfig.get_tag()], - Annotated[LoRALyCORISConfig, LoRALyCORISConfig.get_tag()], - Annotated[LoRADiffusersConfig, LoRADiffusersConfig.get_tag()], - Annotated[TextualInversionFileConfig, TextualInversionFileConfig.get_tag()], - Annotated[TextualInversionFolderConfig, TextualInversionFolderConfig.get_tag()], - Annotated[IPAdapterInvokeAIConfig, IPAdapterInvokeAIConfig.get_tag()], - Annotated[IPAdapterCheckpointConfig, IPAdapterCheckpointConfig.get_tag()], - Annotated[T2IAdapterConfig, T2IAdapterConfig.get_tag()], - Annotated[CLIPVisionDiffusersConfig, CLIPVisionDiffusersConfig.get_tag()], - ], - Discriminator(get_model_discriminator_value), -] - -AnyModelConfigValidator = TypeAdapter(AnyModelConfig) -AnyDefaultSettings: TypeAlias = Union[MainModelDefaultSettings, ControlAdapterDefaultSettings] - - -class ModelConfigFactory(object): - """Class for parsing config dicts into StableDiffusion Config obects.""" - - @classmethod - def make_config( - cls, - model_data: Union[Dict[str, Any], AnyModelConfig], - key: Optional[str] = None, - dest_class: Optional[Type[ModelConfigBase]] = None, - timestamp: Optional[float] = None, - ) -> AnyModelConfig: - """ - Return the appropriate config object from raw dict values. - - :param model_data: A raw dict corresponding the obect fields to be - parsed into a ModelConfigBase obect (or descendent), or a ModelConfigBase - object, which will be passed through unchanged. - :param dest_class: The config class to be returned. If not provided, will - be selected automatically. - """ - model: Optional[ModelConfigBase] = None - if isinstance(model_data, ModelConfigBase): - model = model_data - elif dest_class: - model = dest_class.model_validate(model_data) - else: - # mypy doesn't typecheck TypeAdapters well? - model = AnyModelConfigValidator.validate_python(model_data) # type: ignore - assert model is not None - if key: - model.key = key - if isinstance(model, CheckpointConfigBase) and timestamp is not None: - model.converted_at = timestamp - if model: - validate_hash(model.hash) - return model # type: ignore 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..b176a6ff0b2 --- /dev/null +++ b/invokeai/backend/model_manager/configs/factory.py @@ -0,0 +1,585 @@ +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_QwenImage_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_QwenImage_Config, Main_Checkpoint_QwenImage_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..10835b389fc --- /dev/null +++ b/invokeai/backend/model_manager/configs/main.py @@ -0,0 +1,1466 @@ +import re +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 ( + 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 + + +# ComfyUI single-file checkpoints prefix every transformer key with one of these. +# The loaders strip them before instantiating the model (see `_strip_comfyui_prefix` +# in the qwen_image loader); detection must strip them too so the two paths agree. +_COMFYUI_KEY_PREFIXES = ("model.diffusion_model.", "diffusion_model.") + + +def _strip_comfyui_key_prefix(key: str) -> str: + """Strip a leading ComfyUI `model.diffusion_model.` / `diffusion_model.` prefix from a key.""" + for prefix in _COMFYUI_KEY_PREFIXES: + if key.startswith(prefix): + return key[len(prefix) :] + return key + + +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. ComfyUI-style prefixes are + stripped first so prefixed checkpoints are detected and reach the loader. + """ + keys = [_strip_comfyui_key_prefix(k) for k in state_dict.keys() if isinstance(k, str)] + has_txt_in = any(k.startswith("txt_in.") for k in keys) + has_txt_norm = any(k.startswith("txt_norm.") for k in keys) + has_img_in = any(k.startswith("img_in.") for k in keys) + # Must NOT have context_embedder (which would indicate FLUX) + has_context_embedder = any("context_embedder" in k for k in keys) + return has_txt_in and has_txt_norm and has_img_in and not has_context_embedder + + +# Matches "edit" as a standalone token (delimited by start/end or any non-alphanumeric +# separator), so `qwen_image_edit_2509` matches but `credited` / `edited` / `unedited` do not. +_EDIT_TOKEN_RE = re.compile(r"(?:^|[^a-z0-9])edit(?:[^a-z0-9]|$)") + + +def _infer_qwen_image_variant(sd: dict[str | int, Any], path: Path) -> QwenImageVariantType: + """Infer Qwen Image variant from state dict marker or filename heuristic. + + Edit-variant models include an `__index_timestep_zero__` tensor used by the + `zero_cond_t` dual-modulation path. Falls back to a filename "edit" token check + for converters that don't emit the marker. + """ + marker = "__index_timestep_zero__" + if marker in sd or any(isinstance(k, str) and _strip_comfyui_key_prefix(k) == marker for k in sd): + return QwenImageVariantType.Edit + if _EDIT_TOKEN_RE.search(path.stem.lower()): + return QwenImageVariantType.Edit + return QwenImageVariantType.Generate + + +class Main_Checkpoint_QwenImage_Config(Checkpoint_Config_Base, Main_Config_Base, Config_Base): + """Model config for Qwen Image single-file checkpoint models (safetensors, etc). + + Covers both raw bf16/fp16 checkpoints and ComfyUI-style fp8_scaled checkpoints. + The loader dequantizes fp8 weights back to bf16 at load time; the + `default_settings.fp8_storage` toggle can then optionally re-cast to fp8 for + VRAM savings. + """ + + base: Literal[BaseModelType.QwenImage] = Field(default=BaseModelType.QwenImage) + format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint) + 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 model") + + if _has_ggml_tensors(sd): + raise NotAMatchError("state dict looks like GGUF quantized") + + explicit_variant = override_fields.pop("variant", None) or _infer_qwen_image_variant(sd, mod.path) + + return cls(**override_fields, variant=explicit_variant) + + +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") + + explicit_variant = override_fields.pop("variant", None) or _infer_qwen_image_variant(sd, mod.path) + + 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..b026c03db2f --- /dev/null +++ b/invokeai/backend/model_manager/configs/qwen3_encoder.py @@ -0,0 +1,311 @@ +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 _has_qwen_vl_visual_tower(state_dict: dict[str | int, Any]) -> bool: + """Check if state dict bundles a Qwen2.5-VL / Qwen2-VL vision tower. + + Qwen-VL encoders ship the visual tower (`visual.blocks.*`, `visual.patch_embed.*`) + alongside the language model, whereas a text-only Qwen3 encoder never does. A Qwen-VL + file otherwise satisfies the Qwen3 key heuristic (it has `model.layers.*` / + `model.embed_tokens.weight` too), so without this check it matches *both* the Qwen3 and + the QwenVLEncoder configs and the tiebreak can misroute it to Qwen3. We use it to keep + the two mutually exclusive. + """ + for key in state_dict.keys(): + if isinstance(key, str) and (key.startswith("visual.blocks.") or key.startswith("visual.patch_embed.")): + return True + return False + + +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: + state_dict = mod.load_state_dict() + if not _has_qwen3_keys(state_dict): + raise NotAMatchError("state dict does not look like a Qwen3 model") + # Reject Qwen2.5-VL / Qwen2-VL encoders: they carry a visual tower and must be + # classified as QwenVLEncoder (text-only Qwen3 encoders never have one). + if _has_qwen_vl_visual_tower(state_dict): + raise NotAMatchError( + "state dict bundles a Qwen-VL visual tower; this is a Qwen-VL encoder, not a text-only Qwen3 encoder" + ) + + @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: + state_dict = mod.load_state_dict() + if not _has_qwen3_keys(state_dict): + raise NotAMatchError("state dict does not look like a Qwen3 model") + # Reject Qwen2.5-VL / Qwen2-VL encoders: they carry a visual tower and must be + # classified as QwenVLEncoder (text-only Qwen3 encoders never have one). + if _has_qwen_vl_visual_tower(state_dict): + raise NotAMatchError( + "state dict bundles a Qwen-VL visual tower; this is a Qwen-VL encoder, not a text-only Qwen3 encoder" + ) + + @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/convert_ckpt_to_diffusers.py b/invokeai/backend/model_manager/convert_ckpt_to_diffusers.py deleted file mode 100644 index 450e69cf38a..00000000000 --- a/invokeai/backend/model_manager/convert_ckpt_to_diffusers.py +++ /dev/null @@ -1,83 +0,0 @@ -# Adapted for use in InvokeAI by Lincoln Stein, July 2023 -# -"""Conversion script for the Stable Diffusion checkpoints.""" - -from pathlib import Path -from typing import Optional - -import torch -from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL -from diffusers.pipelines.stable_diffusion.convert_from_ckpt import ( - convert_ldm_vae_checkpoint, - create_vae_diffusers_config, - download_controlnet_from_original_ckpt, - download_from_original_stable_diffusion_ckpt, -) -from omegaconf import DictConfig - -from . import AnyModel - - -def convert_ldm_vae_to_diffusers( - checkpoint: torch.Tensor | dict[str, torch.Tensor], - vae_config: DictConfig, - image_size: int, - dump_path: Optional[Path] = None, - precision: torch.dtype = torch.float16, -) -> AutoencoderKL: - """Convert a checkpoint-style VAE into a Diffusers VAE""" - vae_config = create_vae_diffusers_config(vae_config, image_size=image_size) - converted_vae_checkpoint = convert_ldm_vae_checkpoint(checkpoint, vae_config) - - vae = AutoencoderKL(**vae_config) - vae.load_state_dict(converted_vae_checkpoint) - vae.to(precision) - - if dump_path: - vae.save_pretrained(dump_path, safe_serialization=True) - - return vae - - -def convert_ckpt_to_diffusers( - checkpoint_path: str | Path, - dump_path: Optional[str | Path] = None, - precision: torch.dtype = torch.float16, - use_safetensors: bool = True, - **kwargs, -) -> AnyModel: - """ - Takes all the arguments of download_from_original_stable_diffusion_ckpt(), - and in addition a path-like object indicating the location of the desired diffusers - model to be written. - """ - pipe = download_from_original_stable_diffusion_ckpt(Path(checkpoint_path).as_posix(), **kwargs) - pipe = pipe.to(precision) - - # TO DO: save correct repo variant - if dump_path: - pipe.save_pretrained( - dump_path, - safe_serialization=use_safetensors, - ) - return pipe - - -def convert_controlnet_to_diffusers( - checkpoint_path: Path, - dump_path: Optional[Path] = None, - precision: torch.dtype = torch.float16, - **kwargs, -) -> AnyModel: - """ - Takes all the arguments of download_controlnet_from_original_ckpt(), - and in addition a path-like object indicating the location of the desired diffusers - model to be written. - """ - pipe = download_controlnet_from_original_ckpt(checkpoint_path.as_posix(), **kwargs) - pipe = pipe.to(precision) - - # TO DO: save correct repo variant - if dump_path: - pipe.save_pretrained(dump_path, safe_serialization=True) - return pipe diff --git a/invokeai/backend/model_manager/libc_util.py b/invokeai/backend/model_manager/libc_util.py deleted file mode 100644 index 1fbcae0a93c..00000000000 --- a/invokeai/backend/model_manager/libc_util.py +++ /dev/null @@ -1,75 +0,0 @@ -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): - 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): - 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 - return mallinfo2() diff --git a/invokeai/backend/model_manager/load/__init__.py b/invokeai/backend/model_manager/load/__init__.py index 25125f43fb0..eba7bd16a32 100644 --- a/invokeai/backend/model_manager/load/__init__.py +++ b/invokeai/backend/model_manager/load/__init__.py @@ -6,11 +6,10 @@ from importlib import import_module from pathlib import Path -from .convert_cache.convert_cache_default import ModelConvertCache -from .load_base import LoadedModel, LoadedModelWithoutConfig, ModelLoaderBase -from .load_default import ModelLoader -from .model_cache.model_cache_default import ModelCache -from .model_loader_registry import ModelLoaderRegistry, ModelLoaderRegistryBase +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__"] @@ -21,7 +20,6 @@ "LoadedModel", "LoadedModelWithoutConfig", "ModelCache", - "ModelConvertCache", "ModelLoaderBase", "ModelLoader", "ModelLoaderRegistryBase", diff --git a/invokeai/backend/model_manager/load/convert_cache/__init__.py b/invokeai/backend/model_manager/load/convert_cache/__init__.py deleted file mode 100644 index 5be56d2d584..00000000000 --- a/invokeai/backend/model_manager/load/convert_cache/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .convert_cache_base import ModelConvertCacheBase -from .convert_cache_default import ModelConvertCache - -__all__ = ["ModelConvertCacheBase", "ModelConvertCache"] diff --git a/invokeai/backend/model_manager/load/convert_cache/convert_cache_base.py b/invokeai/backend/model_manager/load/convert_cache/convert_cache_base.py deleted file mode 100644 index ef363cc7f46..00000000000 --- a/invokeai/backend/model_manager/load/convert_cache/convert_cache_base.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Disk-based converted model cache. -""" - -from abc import ABC, abstractmethod -from pathlib import Path - - -class ModelConvertCacheBase(ABC): - @property - @abstractmethod - def max_size(self) -> float: - """Return the maximum size of this cache directory.""" - pass - - @abstractmethod - def make_room(self, size: float) -> None: - """ - Make sufficient room in the cache directory for a model of max_size. - - :param size: Size required (GB) - """ - pass - - @abstractmethod - def cache_path(self, key: str) -> Path: - """Return the path for a model with the indicated key.""" - pass diff --git a/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py b/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py deleted file mode 100644 index cf6448c0568..00000000000 --- a/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Placeholder for convert cache implementation. -""" - -import shutil -from pathlib import Path - -from invokeai.backend.util import GIG, directory_size -from invokeai.backend.util.logging import InvokeAILogger -from invokeai.backend.util.util import safe_filename - -from .convert_cache_base import ModelConvertCacheBase - - -class ModelConvertCache(ModelConvertCacheBase): - def __init__(self, cache_path: Path, max_size: float = 10.0): - """Initialize the convert cache with the base directory and a limit on its maximum size (in GBs).""" - if not cache_path.exists(): - cache_path.mkdir(parents=True) - self._cache_path = cache_path - self._max_size = max_size - - # adjust cache size at startup in case it has been changed - if self._cache_path.exists(): - self.make_room(0.0) - - @property - def max_size(self) -> float: - """Return the maximum size of this cache directory (GB).""" - return self._max_size - - @max_size.setter - def max_size(self, value: float) -> None: - """Set the maximum size of this cache directory (GB).""" - self._max_size = value - - def cache_path(self, key: str) -> Path: - """Return the path for a model with the indicated key.""" - key = safe_filename(self._cache_path, key) - return self._cache_path / key - - def make_room(self, size: float) -> None: - """ - Make sufficient room in the cache directory for a model of max_size. - - :param size: Size required (GB) - """ - size_needed = directory_size(self._cache_path) + size - max_size = int(self.max_size) * GIG - logger = InvokeAILogger.get_logger() - - if size_needed <= max_size: - return - - logger.debug( - f"Convert cache has gotten too large {(size_needed / GIG):4.2f} > {(max_size / GIG):4.2f}G.. Trimming." - ) - - # For this to work, we make the assumption that the directory contains - # a 'model_index.json', 'unet/config.json' file, or a 'config.json' file at top level. - # This should be true for any diffusers model. - def by_atime(path: Path) -> float: - for config in ["model_index.json", "unet/config.json", "config.json"]: - sentinel = path / config - if sentinel.exists(): - return sentinel.stat().st_atime - - # no sentinel file found! - pick the most recent file in the directory - try: - atimes = sorted([x.stat().st_atime for x in path.iterdir() if x.is_file()], reverse=True) - return atimes[0] - except IndexError: - return 0.0 - - # sort by last access time - least accessed files will be at the end - lru_models = sorted(self._cache_path.iterdir(), key=by_atime, reverse=True) - logger.debug(f"cached models in descending atime order: {lru_models}") - while size_needed > max_size and len(lru_models) > 0: - next_victim = lru_models.pop() - victim_size = directory_size(next_victim) - logger.debug(f"Removing cached converted model {next_victim} to free {victim_size / GIG} GB") - shutil.rmtree(next_victim) - size_needed -= victim_size diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py index 6748e85dca1..984362f185d 100644 --- a/invokeai/backend/model_manager/load/load_base.py +++ b/invokeai/backend/model_manager/load/load_base.py @@ -5,7 +5,6 @@ from abc import ABC, abstractmethod from contextlib import contextmanager -from dataclasses import dataclass from logging import Logger from pathlib import Path from typing import Any, Dict, Generator, Optional, Tuple @@ -13,25 +12,22 @@ import torch from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.model_manager.config import ( - AnyModel, - AnyModelConfig, - SubModelType, +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.convert_cache.convert_cache_base import ModelConvertCacheBase -from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase, ModelLockerBase +from invokeai.backend.model_manager.load.model_cache.model_cache import MODEL_LOAD_LOCK, ModelCache +from invokeai.backend.model_manager.taxonomy import AnyModel, SubModelType -@dataclass class LoadedModelWithoutConfig: - """ - Context manager object that mediates transfer from RAM<->VRAM. + """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 + 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: ``` @@ -41,13 +37,9 @@ class LoadedModelWithoutConfig: ``` 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. + 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: ``` @@ -56,51 +48,73 @@ class LoadedModelWithoutConfig: 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. + 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. """ - _locker: ModelLockerBase + def __init__(self, cache_record: CacheRecord, cache: ModelCache): + self._cache_record = cache_record + self._cache = cache def __enter__(self) -> AnyModel: - """Context entry.""" - self._locker.lock() - return self.model + # Hold the MODEL_LOAD_LOCK read lock across the VRAM load (lock() runs + # load_state_dict(assign=True), which calls register_parameter) so it can't overlap a + # concurrent model construction that has the global register_parameter -> meta patch active. + # Acquired before the cache's own lock to keep a consistent lock order (see MODEL_LOAD_LOCK). + with MODEL_LOAD_LOCK.read_lock(): + 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: - """Context exit.""" - self._locker.unlock() + self._cache.unlock(self._cache_record) @contextmanager - def model_on_device(self) -> 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.""" - locked_model = self._locker.lock() + 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. + """ + # See __enter__ for why the VRAM load is wrapped in the read lock. + with MODEL_LOAD_LOCK.read_lock(): + self._cache.lock(self._cache_record, working_mem_bytes) try: - state_dict = self._locker.get_state_dict() - yield (state_dict, locked_model) + self.repair_required_tensors_on_device() + yield (self._cache_record.cached_model.get_cpu_state_dict(), self._cache_record.cached_model.model) finally: - self._locker.unlock() + self._cache.unlock(self._cache_record) @property def model(self) -> AnyModel: """Return the model without locking it.""" - return self._locker.model + 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 + # Repair runs load_state_dict(assign=True) -> register_parameter, so it must hold the read + # lock to avoid being hijacked onto the `meta` device by a concurrent construction. This is + # also called directly (outside __enter__/model_on_device) by some text-encoder invocations, + # so the guard lives here rather than only at the call sites. + with MODEL_LOAD_LOCK.read_lock(): + return cached_model.repair_required_tensors_on_compute_device() -@dataclass class LoadedModel(LoadedModelWithoutConfig): """Context manager object that mediates transfer from RAM<->VRAM.""" - config: Optional[AnyModelConfig] = None - - -# TODO(MM2): -# Some "intermediary" subclasses in the ModelLoaderBase class hierarchy define methods that their subclasses don't -# know about. I think the problem may be related to this class being an ABC. -# -# For example, GenericDiffusersLoader defines `get_hf_load_class()`, and StableDiffusionDiffusersModel attempts to -# call it. However, the method is not defined in the ABC, so it is not guaranteed to be implemented. + 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): @@ -111,8 +125,7 @@ def __init__( self, app_config: InvokeAIAppConfig, logger: Logger, - ram_cache: ModelCacheBase[AnyModel], - convert_cache: ModelConvertCacheBase, + ram_cache: ModelCache, ): """Initialize the loader.""" pass @@ -140,12 +153,6 @@ def get_size_fs( @property @abstractmethod - def convert_cache(self) -> ModelConvertCacheBase: - """Return the convert cache associated with this loader.""" - pass - - @property - @abstractmethod - def ram_cache(self) -> ModelCacheBase[AnyModel]: + 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 index a63cc66a86c..de87c797e8e 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -1,27 +1,66 @@ # Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team """Default implementation of model loading in InvokeAI.""" +import copy +import itertools +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 import ( - AnyModel, - AnyModelConfig, - InvalidModelConfigException, - SubModelType, -) -from invokeai.backend.model_manager.config import DiffusersConfigBase, ModelType -from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase +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.model_cache_base import ModelCacheBase, ModelLockerBase +from invokeai.backend.model_manager.load.model_cache.cache_record import CacheRecord +from invokeai.backend.model_manager.load.model_cache.model_cache import ( + MODEL_LOAD_LOCK, + 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, +) -# TO DO: The loader is not thread safe! +# 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$", +) + + +# The construction path is not thread-safe on its own; it monkey-patches process-global torch state +# (see MODEL_LOAD_LOCK). Concurrent callers must hold the MODEL_LOAD_LOCK write lock (see +# _load_and_cache). class ModelLoader(ModelLoaderBase): """Default implementation of ModelLoaderBase.""" @@ -29,15 +68,14 @@ def __init__( self, app_config: InvokeAIAppConfig, logger: Logger, - ram_cache: ModelCacheBase[AnyModel], - convert_cache: ModelConvertCacheBase, + ram_cache: ModelCache, ): """Initialize the loader.""" self._app_config = app_config self._logger = logger self._ram_cache = ram_cache - self._convert_cache = convert_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: """ @@ -50,25 +88,16 @@ def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubMo :param submodel_type: an ModelType enum indicating the portion of the model to retrieve (e.g. ModelType.Vae) """ - if model_config.type is ModelType.Main and not submodel_type: - raise InvalidModelConfigException("submodel_type is required when loading a main model") - model_path = self._get_model_path(model_config) if not model_path.exists(): - raise InvalidModelConfigException(f"Files for model '{model_config.name}' not found at {model_path}") + raise FileNotFoundError(f"Files for model '{model_config.name}' not found at {model_path}") - with skip_torch_weight_init(): - locker = self._convert_and_load(model_config, model_path, submodel_type) - return LoadedModel(config=model_config, _locker=locker) - - @property - def convert_cache(self) -> ModelConvertCacheBase: - """Return the convert cache associated with this loader.""" - return self._convert_cache + 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) -> ModelCacheBase[AnyModel]: + def ram_cache(self) -> ModelCache: """Return the ram cache associated with this loader.""" return self._ram_cache @@ -76,32 +105,96 @@ def _get_model_path(self, config: AnyModelConfig) -> Path: model_base = self._app_config.models_path return (model_base / config.path).resolve() - def _convert_and_load( - self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None - ) -> ModelLockerBase: + 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 "")]) + cache_key = get_model_cache_key(config.key, submodel_type) try: - return self._ram_cache.get(config.key, submodel_type) + return self._ram_cache.get(key=cache_key, stats_name=stats_name) except IndexError: pass - cache_path: Path = self._convert_cache.cache_path(str(model_path)) - if self._needs_conversion(config, model_path, cache_path): - loaded_model = self._do_convert(config, model_path, cache_path, submodel_type) - else: - config.path = str(cache_path) if cache_path.exists() else str(self._get_model_path(config)) - loaded_model = self._load_model(config, submodel_type) + # Cache miss: construct the model from disk. This path holds the MODEL_LOAD_LOCK *write* + # lock because it relies on process-global, non-thread-safe monkey-patches + # (skip_torch_weight_init and, inside the loaders, accelerate.init_empty_weights / diffusers + # low_cpu_mem_usage). The write lock excludes both other constructions AND concurrent VRAM + # load/unload on other workers (which take the read lock); without that, a concurrent move's + # load_state_dict(assign=True) -> register_parameter gets hijacked onto the `meta` device. + # See MODEL_LOAD_LOCK for the full explanation. + # + # Lock-ordering: the write lock is acquired before any ModelCache._lock taken below + # (get/make_room/put), matching the readers' order, so there is no AB-BA deadlock. + with MODEL_LOAD_LOCK.write_lock(): + # Double-checked locking: another worker sharing this cache may have loaded the same + # entry while we waited for the mutex. (Workers on other devices use a different cache, + # so they will still miss here and construct their own copy — which is intended.) + try: + return self._ram_cache.get(key=cache_key, stats_name=stats_name) + except IndexError: + pass - self._ram_cache.put( - config.key, - submodel_type=submodel_type, - model=loaded_model, - ) + config.path = str(self._get_model_path(config)) - return self._ram_cache.get( - key=config.key, - submodel_type=submodel_type, - stats_name=":".join([config.base, config.type, config.name, (submodel_type or "")]), - ) + # Fast path (multi-GPU): if another device already loaded this exact model, its canonical + # CPU weights are still resident in the shared store along with an empty (meta-weight) + # clone of the built module. Adopt those weights instead of re-reading the model from + # disk — this avoids both the redundant disk read and the large transient second copy + # that would otherwise spike RAM (and, on a RAM-constrained box, drive the system into + # swap). Any failure falls back to a normal load, so it can never change the result. + loaded_model = self._try_adopt_shared_weights(cache_key) + + shell_to_register: Optional[torch.nn.Module] = None + if loaded_model is None: + self._ram_cache.make_room(self.get_size_fs(config, Path(config.path), submodel_type)) + with skip_torch_weight_init(): + loaded_model = self._load_model(config, submodel_type) + # Snapshot a meta-weight clone now — before put() applies custom layers or any VRAM + # move — so the next device to load this model can adopt these weights (see above). + # Skipped in single-device setups, where no other cache will ever adopt it. + shared_store = self._ram_cache.shared_cpu_weights + if shared_store is not None and shared_store.enable_shell_capture: + shell_to_register = self._build_meta_shell(loaded_model) + + # Determine execution device from model config, considering submodel type + execution_device = self._get_execution_device(config, submodel_type) + + self._ram_cache.put( + cache_key, + model=loaded_model, + execution_device=execution_device, + ) + + # Register the shell only after put() has created the shared entry (via the wrapper's + # acquire); it is dropped automatically when that entry's last reference is released. + if shell_to_register is not None: + shared_store = self._ram_cache.shared_cpu_weights + if shared_store is not None: + shared_store.set_shell(cache_key, shell_to_register) + + return self._ram_cache.get(key=cache_key, stats_name=stats_name) def get_size_fs( self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None @@ -110,30 +203,237 @@ def get_size_fs( 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, DiffusersConfigBase) else None, + variant=config.repo_variant if isinstance(config, Diffusers_Config_Base) else None, ) - def _do_convert( - self, config: AnyModelConfig, model_path: Path, cache_path: Path, submodel_type: Optional[SubModelType] = None - ) -> AnyModel: - self.convert_cache.make_room(calc_model_size_by_fs(model_path)) - pipeline = self._convert_model(config, model_path, cache_path if self.convert_cache.max_size > 0 else None) - if submodel_type: - # Proactively load the various submodels into the RAM cache so that we don't have to re-convert - # 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._ram_cache.put(config.key, submodel_type=subtype, model=submodel) - return getattr(pipeline, submodel_type.value) if submodel_type else pipeline - - def _needs_conversion(self, config: AnyModelConfig, model_path: Path, dest_path: Path) -> bool: + 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 - # This needs to be implemented in subclasses that handle checkpoints - def _convert_model(self, config: AnyModelConfig, model_path: Path, output_path: Optional[Path] = None) -> AnyModel: - raise NotImplementedError + 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) + + def _try_adopt_shared_weights(self, cache_key: str) -> Optional[AnyModel]: + """Build this model by adopting another device's already-resident CPU weights, skipping the + disk read entirely. + + Returns the constructed model, or None if adoption is unavailable or fails for any reason (in + which case the caller loads the model from disk normally). Loader-agnostic: it deep-copies the + meta-weight shell that the first device registered (`_build_meta_shell`) and assigns the + shared canonical weights into the copy — no per-loader architecture knowledge required, and + fp8 cast hooks carried by the shell are preserved automatically. + + Must be called while holding the MODEL_LOAD_LOCK write lock (as `_load_and_cache` does), so + the peeked canonical weights and shell cannot be evicted between the peek and the adopt. + """ + shared_store = self._ram_cache.shared_cpu_weights + if shared_store is None: + return None + canonical = shared_store.peek(cache_key) + shell = shared_store.get_shell(cache_key) + if canonical is None or shell is None: + return None + + try: + # Independent module per device (its params will be moved to its own GPU); deep-copying an + # all-meta shell is cheap (no weight data). assign=True then re-points the copy's + # parameters at the shared canonical tensors with no allocation. + model = copy.deepcopy(shell) + model.load_state_dict(canonical, assign=True) + # Safety net: if anything is left on the meta device (e.g. a persistent buffer somehow + # missing from the canonical state dict) the model would silently produce wrong results. + for tensor in itertools.chain(model.parameters(), model.buffers()): + if tensor.is_meta: + raise RuntimeError("adopted model has tensors left on the meta device") + except Exception as e: + # Adoption is best-effort; never let it break a load. Fall back to a normal disk load. + self._logger.warning( + f"Could not adopt shared CPU weights for '{cache_key}' ({e!r}); loading from disk instead." + ) + return None + + self._logger.info( + f"Adopted shared CPU weights for '{cache_key}' from another device's cache (skipped disk load)." + ) + return model + + @staticmethod + def _build_meta_shell(model: AnyModel) -> Optional[torch.nn.Module]: + """Return an empty, meta-weight structural clone of `model`, or None if it can't be cloned. + + The clone has the identical module structure, registered hooks (e.g. the fp8 layerwise-cast + hooks), and non-persistent buffers as `model`, but every parameter and persistent buffer is + replaced by a 0-byte tensor on the `meta` device. A second device adopts it by deep-copying + and assigning the shared canonical weights — so this works for every model family (diffusers, + single-file checkpoint, GGUF, transformers) without any per-loader code. + + Best-effort: returns None on any failure (the model then simply isn't adoptable, and the next + device loads it from disk as before). + """ + if not isinstance(model, torch.nn.Module): + return None + try: + # Persistent buffers come from the canonical state dict on adoption, so they (like params) + # are replaced by meta placeholders. Non-persistent buffers are NOT in the state dict, so + # they must be carried over with real data (deepcopy copies them); they are typically + # small (e.g. rotary-embedding tables, attention masks). + persistent_names = set(model.state_dict().keys()) + persistent_buffer_ids = {id(b) for n, b in model.named_buffers() if n in persistent_names} + + memo: dict[int, object] = {} + for param in model.parameters(recurse=True): + memo[id(param)] = torch.nn.Parameter( + torch.empty_like(param, device="meta"), requires_grad=param.requires_grad + ) + for buffer in model.buffers(recurse=True): + if id(buffer) in persistent_buffer_ids: + memo[id(buffer)] = torch.empty_like(buffer, device="meta") + + return copy.deepcopy(model, memo) + except Exception: + return None # This needs to be implemented in the subclass def _load_model( diff --git a/invokeai/backend/model_manager/load/memory_snapshot.py b/invokeai/backend/model_manager/load/memory_snapshot.py index 195e39361b4..7b693bf8318 100644 --- a/invokeai/backend/model_manager/load/memory_snapshot.py +++ b/invokeai/backend/model_manager/load/memory_snapshot.py @@ -5,7 +5,7 @@ import torch from typing_extensions import Self -from ..util.libc_util import LibcUtil, Struct_mallinfo2 +from invokeai.backend.model_manager.util.libc_util import LibcUtil, Struct_mallinfo2 GB = 2**30 # 1 GB @@ -70,7 +70,7 @@ def get_pretty_snapshot_diff(snapshot_1: Optional[MemorySnapshot], snapshot_2: O 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" + return f"{prefix: <30} ({(diff / GB):+5.3f}): {(val1 / GB):5.3f}GB -> {(val2 / GB):5.3f}GB\n" msg = "" diff --git a/invokeai/backend/model_manager/load/model_cache/__init__.py b/invokeai/backend/model_manager/load/model_cache/__init__.py index 32c682d0424..e69de29bb2d 100644 --- a/invokeai/backend/model_manager/load/model_cache/__init__.py +++ b/invokeai/backend/model_manager/load/model_cache/__init__.py @@ -1,6 +0,0 @@ -"""Init file for ModelCache.""" - -from .model_cache_base import ModelCacheBase, CacheStats # noqa F401 -from .model_cache_default import ModelCache # noqa F401 - -_all__ = ["ModelCacheBase", "ModelCache", "CacheStats"] 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..243a00015d6 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_only_full_load.py @@ -0,0 +1,173 @@ +from typing import Any + +import torch + +from invokeai.backend.model_manager.load.model_cache.shared_cpu_weights import SharedCpuWeightsStore +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, + shared_store: SharedCpuWeightsStore | None = None, + cache_key: str | None = None, + ): + """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). + shared_store (SharedCpuWeightsStore | None): If provided (along with cache_key), share a single canonical + CPU copy of the weights across per-device caches instead of one copy per device. + cache_key (str | None): The model cache key used to identify shared weights in `shared_store`. + """ + # 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") + # When set, this model's CPU weights are a shared canonical copy owned by `shared_store` + # under `cache_key`; `release_shared_weights()` must be called exactly once on eviction. + self._shared_store: SharedCpuWeightsStore | None = None + self._shared_key: str | None = None + + # 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: + cpu_state_dict = model.state_dict() + # In multi-GPU mode, share one canonical CPU copy across the per-device caches (see + # SharedCpuWeightsStore). If another device already registered this key, re-point our + # module at the shared tensors and drop our duplicate so the weights live once in RAM. + if shared_store is not None and cache_key is not None: + canonical = shared_store.acquire(cache_key, cpu_state_dict) + self._shared_store = shared_store + self._shared_key = cache_key + try: + if canonical is not cpu_state_dict: + model.load_state_dict(canonical, assign=True) + cpu_state_dict = canonical + except Exception: + # The re-point failed after acquiring a reference; release it so the shared + # entry's refcount isn't leaked (this wrapper will never enter the cache). + self.release_shared_weights() + raise + self._cpu_state_dict = cpu_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 + + @property + def uses_shared_weights(self) -> bool: + """True if this model's CPU weights are deduplicated in a SharedCpuWeightsStore. + + When True, its RAM is accounted by the store (counted once across devices); when False, its + RAM is per-instance and must be counted by the RamBudget's non-shared total. + """ + return self._shared_store is not None + + def release_shared_weights(self) -> None: + """Release this model's reference to its shared canonical CPU weights, if any. + + Must be called exactly once when the cache entry is evicted. Idempotent: a second call is a + no-op. After release, the shared store frees the canonical tensors once the last device that + held this key releases it. + """ + if self._shared_store is not None and self._shared_key is not None: + self._shared_store.release(self._shared_key) + self._shared_store = None + self._shared_key = None + + 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..2a1d83cb011 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/cached_model/cached_model_with_partial_load.py @@ -0,0 +1,421 @@ +import torch + +from invokeai.backend.model_manager.load.model_cache.shared_cpu_weights import SharedCpuWeightsStore +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, + shared_store: SharedCpuWeightsStore | None = None, + cache_key: str | None = None, + ): + self._model = model + self._compute_device = compute_device + # When set, this model's CPU weights are a shared canonical copy owned by `shared_store` + # under `cache_key`; `release_shared_weights()` must be called exactly once on eviction. + self._shared_store: SharedCpuWeightsStore | None = None + self._shared_key: str | None = None + + 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. + 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. + # + # Note: these are computed from the model's own state dict *before* the shared-weights re-point + # below. The re-point only swaps tensor storage; keys, shapes and dtypes are unchanged, so the + # metadata is identical either way. Computing it first keeps the acquire the last (and only + # failure-prone) step, so a failure there can release the reference cleanly without leaking it. + 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) + + # In multi-GPU mode, share a single canonical CPU copy of the weights across the per-device + # caches instead of keeping one copy per device (see SharedCpuWeightsStore). If another + # device already registered this key, re-point our module's params at the shared tensors and + # drop our freshly-built duplicate so the weights live once in RAM. + if cpu_state_dict is not None and shared_store is not None and cache_key is not None: + canonical = shared_store.acquire(cache_key, cpu_state_dict) + self._shared_store = shared_store + self._shared_key = cache_key + try: + if canonical is not cpu_state_dict: + self._model.load_state_dict(canonical, assign=True) + cpu_state_dict = canonical + except Exception: + # The re-point failed after acquiring a reference; release it so the shared entry's + # refcount isn't leaked (this wrapper will never be inserted into the cache). + self.release_shared_weights() + raise + + self._cpu_state_dict: dict[str, torch.Tensor] | None = cpu_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 + + @property + def uses_shared_weights(self) -> bool: + """True if this model's CPU weights are deduplicated in a SharedCpuWeightsStore. + + When True, its RAM is accounted by the store (counted once across devices); when False, its + RAM is per-instance and must be counted by the RamBudget's non-shared total. + """ + return self._shared_store is not None + + def release_shared_weights(self) -> None: + """Release this model's reference to its shared canonical CPU weights, if any. + + Must be called exactly once when the cache entry is evicted. Idempotent: a second call is a + no-op. After release, the shared store frees the canonical tensors once the last device that + held this key releases it. + """ + if self._shared_store is not None and self._shared_key is not None: + self._shared_store.release(self._shared_key) + self._shared_store = None + self._shared_key = None + + 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..7bc6931e5a3 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/model_cache.py @@ -0,0 +1,1141 @@ +import gc +import logging +import threading +import time +from contextlib import contextmanager +from dataclasses import dataclass +from functools import wraps +from logging import Logger +from typing import Any, Callable, Dict, Generator, 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.ram_budget import RamBudget +from invokeai.backend.model_manager.load.model_cache.shared_cpu_weights import ( + SHARED_CPU_WEIGHTS, + SharedCpuWeightsStore, +) +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 + +# Default RAM-cache sizing constants. These are used both by the per-device heuristic +# (_calc_ram_available_to_model_cache) and by the multi-GPU global budget cap +# (ModelManagerService.build_model_manager), so the two stay consistent. +# +# - RAM_CACHE_SYSTEM_FRACTION: fraction of total system RAM the model cache may use by default. +# - RAM_CACHE_BASELINE_BYTES: assumed non-model RAM used by InvokeAI itself, reserved before sizing. +# - MIN_RAM_CACHE_BYTES: absolute floor so the cache is never sized uselessly small. +RAM_CACHE_SYSTEM_FRACTION = 0.5 +RAM_CACHE_BASELINE_BYTES = 2 * GB +MIN_RAM_CACHE_BYTES = 4 * GB + + +class _ModelLoadReadWriteLock: + """A write-preferring readers-writer lock that serializes model construction against VRAM moves. + + The model load machinery depends on PROCESS-GLOBAL monkey-patches that are not thread-safe: + model CONSTRUCTION (diffusers `from_pretrained` / `accelerate.init_empty_weights`) temporarily + replaces `torch.nn.Module.register_parameter` so that every newly-registered parameter is routed + to the `meta` device. While that patch is installed, ANY `register_parameter` call in ANY thread + is hijacked onto `meta`. VRAM load/unload uses `nn.Module.load_state_dict(assign=True)`, which + assigns `Parameter`s via `__setattr__` -> `register_parameter` — so if it runs concurrently with + a construction on another worker thread, its real weights get stranded on `meta`. That surfaces + later as "Cannot copy out of meta tensor; no data!" or "unrecognized device meta". + + - Construction takes the WRITE lock (exclusive — no reader and no other writer may run). + - VRAM load/unload takes the READ lock (shared, so concurrent moves on different GPUs still + overlap each other; they only block while a construction holds the write lock). + + Write-preferring: once a construction is waiting, new readers queue behind it, so a steady stream + of VRAM moves from busy workers can't starve a pending load. + + Lock-ordering contract: callers MUST acquire this lock *before* any `ModelCache._lock`, never + after. Readers do so by taking the read lock around the outer `ModelCache.lock()` call (see + `LoadedModelWithoutConfig`), and writers around the whole construction (see + `ModelLoader._load_and_cache`). Acquiring it in the other order — cache lock first, then this + lock — would risk an AB-BA deadlock with a writer that takes a cache lock during `put()`. + """ + + def __init__(self) -> None: + self._cond = threading.Condition(threading.Lock()) + self._readers = 0 + self._writers_waiting = 0 + self._writer_active = False + + @contextmanager + def read_lock(self) -> Generator[None, None, None]: + with self._cond: + # Defer to any active or waiting writer (write-preferring). + while self._writer_active or self._writers_waiting > 0: + self._cond.wait() + self._readers += 1 + try: + yield + finally: + with self._cond: + self._readers -= 1 + if self._readers == 0: + self._cond.notify_all() + + @contextmanager + def write_lock(self) -> Generator[None, None, None]: + with self._cond: + self._writers_waiting += 1 + while self._writer_active or self._readers > 0: + self._cond.wait() + self._writers_waiting -= 1 + self._writer_active = True + try: + yield + finally: + with self._cond: + self._writer_active = False + self._cond.notify_all() + + +# Process-global lock guarding the non-thread-safe model load machinery. See _ModelLoadReadWriteLock. +MODEL_LOAD_LOCK = _ModelLoadReadWriteLock() + + +# 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, + shared_cpu_weights: SharedCpuWeightsStore | None = SHARED_CPU_WEIGHTS, + ram_budget: RamBudget | None = None, + ): + """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. + :param shared_cpu_weights: Process-global store that lets per-device caches share a single CPU copy of each + model's weights (see SharedCpuWeightsStore). Defaults to the global store so that, in multi-GPU mode, a + model loaded on multiple GPUs occupies RAM only once. Pass None to disable sharing for this cache. + :param ram_budget: Optional shared RamBudget used as the single global RAM authority across all per-device + caches. When provided, eviction decisions are made against the deduplicated, system-wide RAM total rather + than this cache's local (double-counted) sum. When None, the cache uses its own local RAM accounting. + """ + self._shared_cpu_weights = shared_cpu_weights + self._ram_budget = ram_budget + 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 + def execution_device(self) -> torch.device: + """Return the default execution device this cache loads models onto.""" + return self._execution_device + + @property + def shared_cpu_weights(self) -> SharedCpuWeightsStore | None: + """The process-global store this cache deduplicates CPU weights into, or None if disabled. + + Exposed so the loader can check (via `peek`) whether another device already holds a model's + canonical CPU weights and adopt them at construction time instead of re-reading from disk. + """ + return self._shared_cpu_weights + + def set_ram_budget(self, ram_budget: RamBudget) -> None: + """Attach the shared global RamBudget after construction. + + Used by the model manager once all per-device caches exist and the global cap has been + computed from their individual sizes (see ModelManagerService.build_model_manager). + """ + self._ram_budget = ram_budget + + @property + def local_ram_cache_size_bytes(self) -> int: + """The RAM cache size this cache computed for itself (from max_cache_ram_gb or the heuristic). + + Used by the model manager to seed the global RamBudget cap when no explicit limit is set. + """ + return self._ram_cache_size_bytes + + @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. Prefer the global budget cap + # (the real system-wide limit) when one is attached. + if self._stats is not None: + self._stats.cache_size = ( + self._ram_budget.max_bytes if self._ram_budget is not None else 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, + shared_store=self._shared_cpu_weights, + cache_key=key, + ) + else: + wrapped_model = CachedModelOnlyFullLoad( + model, + effective_execution_device, + size, + keep_ram_copy=self._keep_ram_copy_of_weights, + shared_store=self._shared_cpu_weights, + cache_key=key, + ) + + cache_record = CacheRecord(key=key, cached_model=wrapped_model) + self._cached_models[key] = cache_record + self._cache_stack.append(key) + # Account this model's RAM in the global budget. Shared weights are tracked once by the + # SharedCpuWeightsStore; only non-deduplicated models are added to the budget's non-shared + # total (a non-shared model resident on N devices correctly counts N times). + if self._ram_budget is not None and not wrapped_model.uses_shared_weights: + self._ram_budget.add_non_shared(wrapped_model.total_bytes()) + 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 + if model_device.type == "cuda": + device_label = f"cuda device #{model_device.index}" if model_device.index is not None else "cuda device" + else: + device_label = f"{model_device.type} device" + self._logger.info( + f"Loaded model '{cache_entry.key}' ({cache_entry.cached_model.model.__class__.__name__}) onto " + f"{device_label} 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": + # Must be queried for THIS cache's execution device, not the process-current device. In + # multi-GPU mode each worker calls torch.cuda.set_device for its own GPU, so the current + # device flips between workers; querying without the device argument can read a different + # (e.g. idle) GPU's allocation. That breaks the cancellation in _get_vram_available + # (which adds vram_allocated(execution_device)), inflating "available" toward total VRAM + # so the cache never offloads — causing VRAM OOMs that ignore device_working_mem_gb. + return torch.cuda.memory_allocated(self._execution_device) + 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 = RAM_CACHE_BASELINE_BYTES + ram_available_to_model_cache = int( + total_system_ram_bytes * RAM_CACHE_SYSTEM_FRACTION - 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 < MIN_RAM_CACHE_BYTES: + heuristics_applied.append(3) + ram_available_to_model_cache = MIN_RAM_CACHE_BYTES + + 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 + + @staticmethod + def calc_system_ram_headroom_bytes() -> int: + """The default system-wide cap on TOTAL model-cache RAM, leaving headroom for the OS. + + This is the maximum RAM the model caches should collectively use when the user has not set an + explicit `max_cache_ram_gb`. It mirrors heuristic 1 of `_calc_ram_available_to_model_cache` + (a fraction of system RAM, less InvokeAI's baseline) with the same minimum floor. + + In multi-GPU mode there is one cache per device, and each device's heuristic independently + allows up to this fraction of system RAM; summed across N devices that would claim ~N× as + much RAM and cause the system to swap. The model manager uses this value to cap that sum so a + safe amount of RAM is always left for the OS and other processes. + """ + total_system_ram_bytes = psutil.virtual_memory().total + return max( + int(total_system_ram_bytes * RAM_CACHE_SYSTEM_FRACTION) - RAM_CACHE_BASELINE_BYTES, + MIN_RAM_CACHE_BYTES, + ) + + def _get_ram_in_use(self) -> int: + """Get the amount of RAM currently in use. + + With a shared RamBudget attached, this returns the deduplicated, system-wide total across all + per-device caches (shared model weights counted once). Without one, it returns this cache's + local sum. + """ + if self._ram_budget is not None: + return self._ram_budget.total_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.""" + if self._ram_budget is not None: + return self._ram_budget.available() + 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(): + # Query this cache's execution device (not the process-current one) for correct + # per-device numbers in multi-GPU mode. See _get_vram_in_use. + allocated = ( + torch.cuda.memory_allocated(self._execution_device) if self._execution_device.type == "cuda" else 0 + ) + log += " {:<30} {:.1f} MB\n".format("CUDA Memory Allocated:", 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 pos < len(self._cache_stack): + # Stop once there is enough room. With a shared RamBudget, re-check the global, + # deduplicated availability each iteration: evicting a model that other devices still + # hold frees no RAM (its shared weights stay live until the last reference is released), + # so a fixed "bytes freed" tally would be wrong. Without a budget, the local tally is + # exact, so the original cheaper check is kept. + if self._ram_budget is not None: + if bytes_needed <= self._get_ram_available(): + break + elif ram_bytes_freed >= ram_bytes_to_free: + break + + 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.""" + was_present = cache_entry.key in self._cached_models + self._cache_stack = [key for key in self._cache_stack if key != cache_entry.key] + self._cached_models.pop(cache_entry.key, None) + # Drop this device's reference to the shared canonical CPU weights so they can be freed once + # the last device releases them. Guard on was_present so a double-delete doesn't + # double-release (release_shared_weights is itself idempotent, but a re-added entry under the + # same key must not be released by a stale delete). + if was_present: + uses_shared = cache_entry.cached_model.uses_shared_weights + total_bytes = cache_entry.cached_model.total_bytes() + cache_entry.cached_model.release_shared_weights() + # Drop the matching non-shared contribution from the global budget (shared weights are + # released via the store above). Captured before release_shared_weights() flips the flag. + if self._ram_budget is not None and not uses_shared: + self._ram_budget.remove_non_shared(total_bytes) + + @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/model_cache_base.py b/invokeai/backend/model_manager/load/model_cache/model_cache_base.py deleted file mode 100644 index 012fd42d556..00000000000 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_base.py +++ /dev/null @@ -1,213 +0,0 @@ -# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development team -# TODO: Add Stalker's proper name to copyright -""" -Manage a RAM cache of diffusion/transformer models for fast switching. -They are moved between GPU VRAM and CPU RAM as necessary. If the cache -grows larger than a preset maximum, then the least recently used -model will be cleared and (re)loaded from disk when next needed. -""" - -from abc import ABC, abstractmethod -from dataclasses import dataclass, field -from logging import Logger -from typing import Dict, Generic, Optional, TypeVar - -import torch - -from invokeai.backend.model_manager.config import AnyModel, SubModelType - - -class ModelLockerBase(ABC): - """Base class for the model locker used by the loader.""" - - @abstractmethod - def lock(self) -> AnyModel: - """Lock the contained model and move it into VRAM.""" - pass - - @abstractmethod - def unlock(self) -> None: - """Unlock the contained model, and remove it from VRAM.""" - pass - - @abstractmethod - def get_state_dict(self) -> Optional[Dict[str, torch.Tensor]]: - """Return the state dict (if any) for the cached model.""" - pass - - @property - @abstractmethod - def model(self) -> AnyModel: - """Return the model.""" - pass - - -T = TypeVar("T") - - -@dataclass -class CacheRecord(Generic[T]): - """ - Elements of the cache: - - key: Unique key for each model, same as used in the models database. - model: Model in memory. - state_dict: A read-only copy of the model's state dict in RAM. It will be - used as a template for creating a copy in the VRAM. - size: Size of the model - loaded: True if the model's state dict is currently in VRAM - - Before a model is executed, the state_dict template is copied into VRAM, - and then injected into the model. When the model is finished, the VRAM - copy of the state dict is deleted, and the RAM version is reinjected - into the model. - - The state_dict should be treated as a read-only attribute. Do not attempt - to patch or otherwise modify it. Instead, patch the copy of the state_dict - after it is loaded into the execution device (e.g. CUDA) using the `LoadedModel` - context manager call `model_on_device()`. - """ - - key: str - model: T - device: torch.device - state_dict: Optional[Dict[str, torch.Tensor]] - size: int - loaded: bool = False - _locks: int = 0 - - 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 locked(self) -> bool: - """Return true if record is locked.""" - return self._locks > 0 - - -@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) - - -class ModelCacheBase(ABC, Generic[T]): - """Virtual base class for RAM model cache.""" - - @property - @abstractmethod - def storage_device(self) -> torch.device: - """Return the storage device (e.g. "CPU" for RAM).""" - pass - - @property - @abstractmethod - def execution_device(self) -> torch.device: - """Return the exection device (e.g. "cuda" for VRAM).""" - pass - - @property - @abstractmethod - def lazy_offloading(self) -> bool: - """Return true if the cache is configured to lazily offload models in VRAM.""" - pass - - @property - @abstractmethod - def max_cache_size(self) -> float: - """Return true if the cache is configured to lazily offload models in VRAM.""" - pass - - @abstractmethod - def offload_unlocked_models(self, size_required: int) -> None: - """Offload from VRAM any models not actively in use.""" - pass - - @abstractmethod - def move_model_to_device(self, cache_entry: CacheRecord[AnyModel], target_device: torch.device) -> None: - """Move model into the indicated device.""" - pass - - @property - @abstractmethod - def stats(self) -> Optional[CacheStats]: - """Return collected CacheStats object.""" - pass - - @stats.setter - @abstractmethod - def stats(self, stats: CacheStats) -> None: - """Set the CacheStats object for collectin cache statistics.""" - pass - - @property - @abstractmethod - def logger(self) -> Logger: - """Return the logger used by the cache.""" - pass - - @abstractmethod - def make_room(self, size: int) -> None: - """Make enough room in the cache to accommodate a new model of indicated size.""" - pass - - @abstractmethod - def put( - self, - key: str, - model: T, - submodel_type: Optional[SubModelType] = None, - ) -> None: - """Store model under key and optional submodel_type.""" - pass - - @abstractmethod - def get( - self, - key: str, - submodel_type: Optional[SubModelType] = None, - stats_name: Optional[str] = None, - ) -> ModelLockerBase: - """ - Retrieve model using key and optional submodel_type. - - :param key: Opaque model key - :param submodel_type: Type of the submodel to fetch - :param stats_name: A human-readable id for the model for the purposes of - stats reporting. - - This may raise an IndexError if the model is not in the cache. - """ - pass - - @abstractmethod - def exists( - self, - key: str, - submodel_type: Optional[SubModelType] = None, - ) -> bool: - """Return true if the model identified by key and submodel_type is in the cache.""" - pass - - @abstractmethod - def cache_size(self) -> int: - """Get the total size of the models currently cached.""" - pass - - @abstractmethod - def print_cuda_stats(self) -> None: - """Log debugging information on CUDA usage.""" - pass diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py deleted file mode 100644 index d48e45426e3..00000000000 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py +++ /dev/null @@ -1,409 +0,0 @@ -# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development team -# TODO: Add Stalker's proper name to copyright -""" -Manage a RAM cache of diffusion/transformer models for fast switching. -They are moved between GPU VRAM and CPU RAM as necessary. If the cache -grows larger than a preset maximum, then the least recently used -model will be cleared and (re)loaded from disk when next needed. - -The cache returns context manager generators designed to load the -model into the GPU within the context, and unload outside the -context. Use like this: - - cache = ModelCache(max_cache_size=7.5) - with cache.get_model('runwayml/stable-diffusion-1-5') as SD1, - cache.get_model('stabilityai/stable-diffusion-2') as SD2: - do_something_in_GPU(SD1,SD2) - - -""" - -import gc -import math -import time -from contextlib import suppress -from logging import Logger -from typing import Dict, List, Optional - -import torch - -from invokeai.backend.model_manager import AnyModel, SubModelType -from invokeai.backend.model_manager.load.memory_snapshot import MemorySnapshot, get_pretty_snapshot_diff -from invokeai.backend.model_manager.load.model_util import calc_model_size_by_data -from invokeai.backend.util.devices import TorchDevice -from invokeai.backend.util.logging import InvokeAILogger - -from .model_cache_base import CacheRecord, CacheStats, ModelCacheBase, ModelLockerBase -from .model_locker import ModelLocker - -# Maximum size of the cache, in gigs -# Default is roughly enough to hold three fp16 diffusers models in RAM simultaneously -DEFAULT_MAX_CACHE_SIZE = 6.0 - -# amount of GPU memory to hold in reserve for use by generations (GB) -DEFAULT_MAX_VRAM_CACHE_SIZE = 2.75 - -# actual size of a gig -GIG = 1073741824 - -# Size of a MB in bytes. -MB = 2**20 - - -class ModelCache(ModelCacheBase[AnyModel]): - """Implementation of ModelCacheBase.""" - - def __init__( - self, - max_cache_size: float = DEFAULT_MAX_CACHE_SIZE, - max_vram_cache_size: float = DEFAULT_MAX_VRAM_CACHE_SIZE, - execution_device: torch.device = torch.device("cuda"), - storage_device: torch.device = torch.device("cpu"), - precision: torch.dtype = torch.float16, - sequential_offload: bool = False, - lazy_offloading: bool = True, - sha_chunksize: int = 16777216, - log_memory_usage: bool = False, - logger: Optional[Logger] = None, - ): - """ - Initialize the model RAM cache. - - :param max_cache_size: Maximum size of the RAM cache [6.0 GB] - :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 precision: Precision for loaded models [torch.float16] - :param lazy_offloading: Keep model in VRAM until another model needs to be loaded - :param sequential_offload: Conserve VRAM by loading and unloading each stage of the pipeline sequentially - :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. - """ - # allow lazy offloading only when vram cache enabled - self._lazy_offloading = lazy_offloading and max_vram_cache_size > 0 - self._precision: torch.dtype = precision - self._max_cache_size: float = max_cache_size - self._max_vram_cache_size: float = max_vram_cache_size - self._execution_device: torch.device = execution_device - self._storage_device: torch.device = storage_device - self._logger = logger or InvokeAILogger.get_logger(self.__class__.__name__) - self._log_memory_usage = log_memory_usage - self._stats: Optional[CacheStats] = None - - self._cached_models: Dict[str, CacheRecord[AnyModel]] = {} - self._cache_stack: List[str] = [] - - @property - def logger(self) -> Logger: - """Return the logger used by the cache.""" - return self._logger - - @property - def lazy_offloading(self) -> bool: - """Return true if the cache is configured to lazily offload models in VRAM.""" - return self._lazy_offloading - - @property - def storage_device(self) -> torch.device: - """Return the storage device (e.g. "CPU" for RAM).""" - return self._storage_device - - @property - def execution_device(self) -> torch.device: - """Return the exection device (e.g. "cuda" for VRAM).""" - return self._execution_device - - @property - def max_cache_size(self) -> float: - """Return the cap on cache size.""" - return self._max_cache_size - - @max_cache_size.setter - def max_cache_size(self, value: float) -> None: - """Set the cap on cache size.""" - self._max_cache_size = value - - @property - def stats(self) -> Optional[CacheStats]: - """Return collected CacheStats object.""" - return self._stats - - @stats.setter - def stats(self, stats: CacheStats) -> None: - """Set the CacheStats object for collectin cache statistics.""" - self._stats = stats - - def cache_size(self) -> int: - """Get the total size of the models currently cached.""" - total = 0 - for cache_record in self._cached_models.values(): - total += cache_record.size - return total - - def exists( - self, - key: str, - submodel_type: Optional[SubModelType] = None, - ) -> bool: - """Return true if the model identified by key and submodel_type is in the cache.""" - key = self._make_cache_key(key, submodel_type) - return key in self._cached_models - - def put( - self, - key: str, - model: AnyModel, - submodel_type: Optional[SubModelType] = None, - ) -> None: - """Store model under key and optional submodel_type.""" - key = self._make_cache_key(key, submodel_type) - if key in self._cached_models: - return - size = calc_model_size_by_data(model) - self.make_room(size) - - state_dict = model.state_dict() if isinstance(model, torch.nn.Module) else None - cache_record = CacheRecord(key=key, model=model, device=self.storage_device, state_dict=state_dict, size=size) - self._cached_models[key] = cache_record - self._cache_stack.append(key) - - def get( - self, - key: str, - submodel_type: Optional[SubModelType] = None, - stats_name: Optional[str] = None, - ) -> ModelLockerBase: - """ - Retrieve model using key and optional submodel_type. - - :param key: Opaque model key - :param submodel_type: Type of the submodel to fetch - :param stats_name: A human-readable id for the model for the purposes of - stats reporting. - - This may raise an IndexError if the model is not in the cache. - """ - key = self._make_cache_key(key, submodel_type) - if key in self._cached_models: - if self.stats: - self.stats.hits += 1 - else: - if self.stats: - self.stats.misses += 1 - 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.cache_size = int(self._max_cache_size * GIG) - self.stats.high_watermark = max(self.stats.high_watermark, self.cache_size()) - 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.size - ) - - # this moves the entry to the top (right end) of the stack - with suppress(Exception): - self._cache_stack.remove(key) - self._cache_stack.append(key) - return ModelLocker( - cache=self, - cache_entry=cache_entry, - ) - - def _capture_memory_snapshot(self) -> Optional[MemorySnapshot]: - if self._log_memory_usage: - return MemorySnapshot.capture() - return None - - def _make_cache_key(self, model_key: str, submodel_type: Optional[SubModelType] = None) -> str: - if submodel_type: - return f"{model_key}:{submodel_type.value}" - else: - return model_key - - def offload_unlocked_models(self, size_required: int) -> None: - """Move any unused models from VRAM.""" - reserved = self._max_vram_cache_size * GIG - vram_in_use = torch.cuda.memory_allocated() + size_required - self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM needed for models; max allowed={(reserved/GIG):.2f}GB") - for _, cache_entry in sorted(self._cached_models.items(), key=lambda x: x[1].size): - if vram_in_use <= reserved: - break - if not cache_entry.loaded: - continue - if not cache_entry.locked: - self.move_model_to_device(cache_entry, self.storage_device) - cache_entry.loaded = False - vram_in_use = torch.cuda.memory_allocated() + size_required - self.logger.debug( - f"Removing {cache_entry.key} from VRAM to free {(cache_entry.size/GIG):.2f}GB; vram free = {(torch.cuda.memory_allocated()/GIG):.2f}GB" - ) - - TorchDevice.empty_cache() - - def move_model_to_device(self, cache_entry: CacheRecord[AnyModel], target_device: torch.device) -> None: - """Move model into the indicated device. - - :param cache_entry: The CacheRecord for the model - :param target_device: The torch.device to move the model into - - May raise a torch.cuda.OutOfMemoryError - """ - self.logger.debug(f"Called to move {cache_entry.key} to {target_device}") - source_device = cache_entry.device - - # Note: We compare device types only so that 'cuda' == 'cuda:0'. - # This would need to be revised to support multi-GPU. - if torch.device(source_device).type == torch.device(target_device).type: - return - - # Some models don't have a `to` method, in which case they run in RAM/CPU. - if not hasattr(cache_entry.model, "to"): - return - - # This roundabout method for moving the model around is done to avoid - # the cost of moving the model from RAM to VRAM and then back from VRAM to RAM. - # When moving to VRAM, we copy (not move) each element of the state dict from - # RAM to a new state dict in VRAM, and then inject it into the model. - # This operation is slightly faster than running `to()` on the whole model. - # - # When the model needs to be removed from VRAM we simply delete the copy - # of the state dict in VRAM, and reinject the state dict that is cached - # in RAM into the model. So this operation is very fast. - start_model_to_time = time.time() - snapshot_before = self._capture_memory_snapshot() - - try: - if cache_entry.state_dict is not None: - assert hasattr(cache_entry.model, "load_state_dict") - if target_device == self.storage_device: - cache_entry.model.load_state_dict(cache_entry.state_dict, assign=True) - else: - new_dict: Dict[str, torch.Tensor] = {} - for k, v in cache_entry.state_dict.items(): - new_dict[k] = v.to(torch.device(target_device), copy=True, non_blocking=True) - cache_entry.model.load_state_dict(new_dict, assign=True) - cache_entry.model.to(target_device, non_blocking=True) - cache_entry.device = target_device - except Exception as e: # blow away cache entry - self._delete_cache_entry(cache_entry) - raise e - - snapshot_after = self._capture_memory_snapshot() - end_model_to_time = time.time() - self.logger.debug( - f"Moved model '{cache_entry.key}' from {source_device} to" - f" {target_device} in {(end_model_to_time-start_model_to_time):.2f}s." - f"Estimated model size: {(cache_entry.size/GIG):.3f} GB." - f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}" - ) - - if ( - snapshot_before is not None - and snapshot_after is not None - and snapshot_before.vram is not None - and snapshot_after.vram is not None - ): - vram_change = abs(snapshot_before.vram - snapshot_after.vram) - - # If the estimated model size does not match the change in VRAM, log a warning. - if not math.isclose( - vram_change, - cache_entry.size, - rel_tol=0.1, - abs_tol=10 * MB, - ): - self.logger.debug( - f"Moving model '{cache_entry.key}' from {source_device} to" - f" {target_device} caused an unexpected change in VRAM usage. The model's" - " estimated size may be incorrect. Estimated model size:" - f" {(cache_entry.size/GIG):.3f} GB.\n" - f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}" - ) - - def print_cuda_stats(self) -> None: - """Log CUDA diagnostics.""" - vram = "%4.2fG" % (torch.cuda.memory_allocated() / GIG) - ram = "%4.2fG" % (self.cache_size() / GIG) - - in_ram_models = 0 - in_vram_models = 0 - locked_in_vram_models = 0 - for cache_record in self._cached_models.values(): - if hasattr(cache_record.model, "device"): - if cache_record.model.device == self.storage_device: - in_ram_models += 1 - else: - in_vram_models += 1 - if cache_record.locked: - locked_in_vram_models += 1 - - self.logger.debug( - f"Current VRAM/RAM usage: {vram}/{ram}; models_in_ram/models_in_vram(locked) =" - f" {in_ram_models}/{in_vram_models}({locked_in_vram_models})" - ) - - def make_room(self, size: int) -> None: - """Make enough room in the cache to accommodate a new model of indicated size.""" - # calculate how much memory this model will require - # multiplier = 2 if self.precision==torch.float32 else 1 - bytes_needed = size - maximum_size = self.max_cache_size * GIG # stored in GB, convert to bytes - current_size = self.cache_size() - - if current_size + bytes_needed > maximum_size: - self.logger.debug( - f"Max cache size exceeded: {(current_size/GIG):.2f}/{self.max_cache_size:.2f} GB, need an additional" - f" {(bytes_needed/GIG):.2f} GB" - ) - - self.logger.debug(f"Before making_room: cached_models={len(self._cached_models)}") - - pos = 0 - models_cleared = 0 - while current_size + bytes_needed > maximum_size and pos < len(self._cache_stack): - model_key = self._cache_stack[pos] - cache_entry = self._cached_models[model_key] - device = cache_entry.model.device if hasattr(cache_entry.model, "device") else None - self.logger.debug( - f"Model: {model_key}, locks: {cache_entry._locks}, device: {device}, loaded: {cache_entry.loaded}" - ) - - if not cache_entry.locked: - self.logger.debug( - f"Removing {model_key} from RAM cache to free at least {(size/GIG):.2f} GB (-{(cache_entry.size/GIG):.2f} GB)" - ) - current_size -= cache_entry.size - models_cleared += 1 - self._delete_cache_entry(cache_entry) - del cache_entry - - 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 - gc.collect() - - TorchDevice.empty_cache() - self.logger.debug(f"After making room: cached_models={len(self._cached_models)}") - - def _delete_cache_entry(self, cache_entry: CacheRecord[AnyModel]) -> None: - self._cache_stack.remove(cache_entry.key) - del self._cached_models[cache_entry.key] diff --git a/invokeai/backend/model_manager/load/model_cache/model_locker.py b/invokeai/backend/model_manager/load/model_cache/model_locker.py deleted file mode 100644 index 9de17ca5f53..00000000000 --- a/invokeai/backend/model_manager/load/model_cache/model_locker.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Base class and implementation of a class that moves models in and out of VRAM. -""" - -from typing import Dict, Optional - -import torch - -from invokeai.backend.model_manager import AnyModel - -from .model_cache_base import CacheRecord, ModelCacheBase, ModelLockerBase - - -class ModelLocker(ModelLockerBase): - """Internal class that mediates movement in and out of GPU.""" - - def __init__(self, cache: ModelCacheBase[AnyModel], cache_entry: CacheRecord[AnyModel]): - """ - Initialize the model locker. - - :param cache: The ModelCache object - :param cache_entry: The entry in the model cache - """ - self._cache = cache - self._cache_entry = cache_entry - - @property - def model(self) -> AnyModel: - """Return the model without moving it around.""" - return self._cache_entry.model - - def get_state_dict(self) -> Optional[Dict[str, torch.Tensor]]: - """Return the state dict (if any) for the cached model.""" - return self._cache_entry.state_dict - - def lock(self) -> AnyModel: - """Move the model into the execution device (GPU) and lock it.""" - self._cache_entry.lock() - try: - if self._cache.lazy_offloading: - self._cache.offload_unlocked_models(self._cache_entry.size) - self._cache.move_model_to_device(self._cache_entry, self._cache.execution_device) - self._cache_entry.loaded = True - self._cache.logger.debug(f"Locking {self._cache_entry.key} in {self._cache.execution_device}") - self._cache.print_cuda_stats() - except torch.cuda.OutOfMemoryError: - self._cache.logger.warning("Insufficient GPU memory to load model. Aborting") - self._cache_entry.unlock() - raise - except Exception: - self._cache_entry.unlock() - raise - - return self.model - - def unlock(self) -> None: - """Call upon exit from context.""" - self._cache_entry.unlock() - if not self._cache.lazy_offloading: - self._cache.offload_unlocked_models(0) - self._cache.print_cuda_stats() diff --git a/invokeai/backend/model_manager/load/model_cache/ram_budget.py b/invokeai/backend/model_manager/load/model_cache/ram_budget.py new file mode 100644 index 00000000000..6428c646753 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/ram_budget.py @@ -0,0 +1,64 @@ +import threading +from typing import Optional + +from invokeai.backend.model_manager.load.model_cache.shared_cpu_weights import SharedCpuWeightsStore + + +class RamBudget: + """The single global authority for how much RAM the model caches are actually using. + + In multi-GPU mode there is one `ModelCache` per device. Each cache independently sums the + `total_bytes()` of the models it holds, so a model resident on N devices is counted N times — + even though Phase 1/2 made its CPU weights live only ONCE in RAM (see SharedCpuWeightsStore). + That per-cache double-count makes the caches believe RAM is fuller than it is, causing premature + eviction and reload churn, and makes `max_cache_ram_gb` meaningless as a system-wide cap. + + RamBudget fixes the accounting by separating RAM into two non-overlapping parts: + + - Shared weights: model weights that are deduplicated in the SharedCpuWeightsStore. Counted + exactly once via `store.total_bytes_in_use()`, regardless of how many devices hold them. + - Non-shared RAM: models that are NOT deduplicated (keep_ram_copy disabled, or non-Module + models whose single in-RAM copy is per-device). These are tracked here as an explicit running + total; a model resident on N devices contributes N times, which is correct because it really + does occupy N copies of RAM. + + `total_in_use()` is the sum of the two and reflects the true RAM footprint. All per-device caches + share one RamBudget and make their eviction decisions against it. + + Thread-safety / lock ordering: RamBudget guards its own counter with an internal lock and NEVER + acquires a ModelCache lock (it only reads the store, which has its own lock). Callers update it + while holding their cache lock, so the only lock order is cache-lock -> (store-lock | budget-lock), + never the reverse — so it cannot deadlock against the per-device caches. + """ + + def __init__(self, max_bytes: int, shared_store: Optional[SharedCpuWeightsStore]): + self._max_bytes = max_bytes + self._store = shared_store + self._non_shared_bytes = 0 + self._lock = threading.Lock() + + @property + def max_bytes(self) -> int: + """The global cap on actual model-cache RAM, in bytes.""" + return self._max_bytes + + def add_non_shared(self, nbytes: int) -> None: + """Record `nbytes` of newly-resident non-deduplicated model RAM.""" + with self._lock: + self._non_shared_bytes += nbytes + + def remove_non_shared(self, nbytes: int) -> None: + """Record the release of `nbytes` of non-deduplicated model RAM.""" + with self._lock: + self._non_shared_bytes = max(0, self._non_shared_bytes - nbytes) + + def total_in_use(self) -> int: + """The true total RAM used by the model caches: shared weights (counted once) + non-shared.""" + shared = self._store.total_bytes_in_use() if self._store is not None else 0 + with self._lock: + non_shared = self._non_shared_bytes + return shared + non_shared + + def available(self) -> int: + """Bytes remaining under the global cap (may be negative if over budget).""" + return self._max_bytes - self.total_in_use() diff --git a/invokeai/backend/model_manager/load/model_cache/shared_cpu_weights.py b/invokeai/backend/model_manager/load/model_cache/shared_cpu_weights.py new file mode 100644 index 00000000000..4ce456e45b6 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/shared_cpu_weights.py @@ -0,0 +1,152 @@ +import threading +from dataclasses import dataclass, field + +import torch + +from invokeai.backend.util.calc_tensor_size import calc_tensor_size + + +@dataclass +class _SharedWeightsEntry: + """A single canonical CPU state dict shared across per-device caches.""" + + state_dict: dict[str, torch.Tensor] + total_bytes: int + # Number of per-device cached models currently aliasing this entry. The entry is freed + # (its RAM released) when this drops to zero. + refcount: int = 0 + # An empty (meta-weight) structural clone of the first-built module, used so a second device can + # adopt the canonical weights without re-reading the model from disk. None until registered (and + # for entries whose model isn't an nn.Module). Holds ~no real RAM: its weights are on `meta`. + shell: object | None = None + _key_bytes: dict[str, int] = field(default_factory=dict) + + +class SharedCpuWeightsStore: + """Process-global store of canonical CPU weight tensors, shared across per-device model caches. + + In multi-GPU mode there is one `ModelCache` per generation device. Without coordination each + cache keeps its own CPU copy of every model's weights, so a model loaded on N GPUs occupies N + copies in RAM. The cached-model wrappers cannot simply share a single `torch.nn.Module`, because + loading to VRAM mutates a module's parameters in place (`load_state_dict(assign=True)` / `.to`), + and two GPUs running the same model concurrently need their params on two different devices at + once. The CPU weight tensors, however, are read-only and device-agnostic, so they can be shared. + + This store keeps a single canonical CPU `state_dict` per cache key. The first device to load a + key registers its freshly-built state dict as canonical; subsequent devices `acquire()` the + canonical and re-point their own module's CPU parameters at the shared tensors (via + `load_state_dict(assign=True)`), discarding their private duplicate. The result: model weights + live once in RAM regardless of how many GPUs hold the model. + + Lifetime is reference-counted. Each per-device cached model that adopts an entry must call + `release()` exactly once when it is evicted; the canonical tensors are dropped only when the + last device releases them. + + Thread-safety: `acquire()`/`release()` are guarded by an internal lock. Note that model + construction (where `acquire()` is normally called) is already serialized process-globally by + `MODEL_LOAD_LOCK.write_lock()`; the internal lock here additionally protects `release()`, which + runs under a per-cache lock off the global construction lock. + """ + + def __init__(self) -> None: + self._lock = threading.Lock() + self._entries: dict[str, _SharedWeightsEntry] = {} + # Whether to capture per-model meta-weight shells for cross-device adoption. Only useful with + # more than one device cache, so the model manager disables it in single-device setups to + # avoid the (small) per-first-load clone cost. See ModelLoader._build_meta_shell. + self.enable_shell_capture: bool = True + + def acquire(self, key: str, state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + """Adopt the canonical CPU state dict for `key`, registering `state_dict` as canonical if + this is the first acquire. + + Increments the entry's refcount. The caller MUST pair every `acquire()` with exactly one + `release()`. + + Returns: + The canonical state dict. If this call registered the entry, the returned object is the + same `state_dict` that was passed in (the caller keeps using its own tensors). Otherwise + it is the previously-registered canonical dict, and the caller is responsible for + re-pointing its module at these tensors and dropping the `state_dict` it passed in. + """ + with self._lock: + entry = self._entries.get(key) + if entry is None: + entry = _SharedWeightsEntry( + state_dict=state_dict, + total_bytes=sum(calc_tensor_size(v) for v in state_dict.values()), + ) + self._entries[key] = entry + entry.refcount += 1 + return entry.state_dict + + def peek(self, key: str) -> dict[str, torch.Tensor] | None: + """Return the canonical state dict for `key` WITHOUT changing its refcount, or None if absent. + + Used by the loader to adopt already-resident weights at construction time (skipping the disk + read) when another device has already loaded this model. The reference is taken later, in the + cached-model wrapper's `acquire()`, exactly as for a normal load — so this peek must not + itself increment the count. + """ + with self._lock: + entry = self._entries.get(key) + return entry.state_dict if entry is not None else None + + def set_shell(self, key: str, shell: object) -> None: + """Register the empty (meta-weight) structural clone for `key`, if an entry exists and none + is set yet. A no-op when the key has no canonical entry (e.g. keep_ram_copy disabled).""" + with self._lock: + entry = self._entries.get(key) + if entry is not None and entry.shell is None: + entry.shell = shell + + def get_shell(self, key: str) -> object | None: + """Return the registered meta-weight shell for `key`, or None if absent.""" + with self._lock: + entry = self._entries.get(key) + return entry.shell if entry is not None else None + + def release(self, key: str) -> None: + """Release one reference to `key`'s canonical state dict, freeing it when the count hits 0. + + A `release()` for a key that is not present is a no-op (e.g. a cached model that never + acquired shared weights, or a double eviction guard). + """ + with self._lock: + entry = self._entries.get(key) + if entry is None: + return + entry.refcount -= 1 + if entry.refcount <= 0: + del self._entries[key] + + # -- Introspection / accounting (also used by tests) ---------------------------------------- + + def __contains__(self, key: str) -> bool: + with self._lock: + return key in self._entries + + def refcount(self, key: str) -> int: + """Return the current refcount for `key`, or 0 if not present.""" + with self._lock: + entry = self._entries.get(key) + return entry.refcount if entry is not None else 0 + + def total_bytes_in_use(self) -> int: + """Return the total size (in bytes) of all canonical state dicts currently held. + + This counts each shared model's weights exactly once, regardless of how many devices alias + it — i.e. the true RAM footprint of cached weights, not the per-device double-count. + """ + with self._lock: + return sum(entry.total_bytes for entry in self._entries.values()) + + def keys(self) -> list[str]: + with self._lock: + return list(self._entries.keys()) + + +# Process-global default store. Per-device caches share this instance so that the same model loaded +# on multiple GPUs keeps a single CPU copy. Tests may construct isolated `SharedCpuWeightsStore` +# instances instead. +SHARED_CPU_WEIGHTS = SharedCpuWeightsStore() 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..75328441e28 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_invoke_linear_nf4.py @@ -0,0 +1,93 @@ +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. + weight_was_offloaded = self.weight.device.type != x.device.type + 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) + if weight_was_offloaded and x.numel() == x.shape[-1]: + # bitsandbytes routes single-vector inputs through gemv_4bit, which can fail with CPU-stored, + # device-autocasted Params4bit weights on some CUDA/bnb combinations. Use the same dequantized + # matmul path that bnb.matmul_4bit uses for batched inputs. + dequantized_weight = bnb.functional.dequantize_4bit(weight, weight.quant_state).to(x.dtype) + return torch.nn.functional.linear(x, dequantized_weight, bias).to(inp_dtype) + 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 index bb6bd18d7f8..ca9ea56edbe 100644 --- a/invokeai/backend/model_manager/load/model_loader_registry.py +++ b/invokeai/backend/model_manager/load/model_loader_registry.py @@ -18,15 +18,10 @@ from abc import ABC, abstractmethod from typing import Callable, Dict, Optional, Tuple, Type, TypeVar -from ..config import ( - AnyModelConfig, - BaseModelType, - ModelConfigBase, - ModelFormat, - ModelType, - SubModelType, -) -from . import ModelLoaderBase +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): @@ -43,7 +38,7 @@ def register( @abstractmethod def get_implementation( cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] - ) -> Tuple[Type[ModelLoaderBase], ModelConfigBase, Optional[SubModelType]]: + ) -> Tuple[Type[ModelLoaderBase], Config_Base, Optional[SubModelType]]: """ Get subclass of ModelLoaderBase registered to handle base and type. @@ -87,7 +82,7 @@ def decorator(subclass: Type[TModelLoader]) -> Type[TModelLoader]: @classmethod def get_implementation( cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] - ) -> Tuple[Type[ModelLoaderBase], ModelConfigBase, 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 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 index 0b93d8d2cad..e50e45849ab 100644 --- a/invokeai/backend/model_manager/load/model_loaders/controlnet.py +++ b/invokeai/backend/model_manager/load/model_loaders/controlnet.py @@ -1,58 +1,55 @@ # Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team """Class for ControlNet model loading in InvokeAI.""" -from pathlib import Path from typing import Optional -from invokeai.backend.model_manager import ( +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, - AnyModelConfig, BaseModelType, ModelFormat, ModelType, + SubModelType, ) -from invokeai.backend.model_manager.config import CheckpointConfigBase -from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_controlnet_to_diffusers - -from .. import ModelLoaderRegistry -from .generic_diffusers import GenericDiffusersLoader -@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ControlNet, format=ModelFormat.Diffusers) -@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ControlNet, format=ModelFormat.Checkpoint) +@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 _needs_conversion(self, config: AnyModelConfig, model_path: Path, dest_path: Path) -> bool: - if not isinstance(config, CheckpointConfigBase): - return False - elif ( - dest_path.exists() - and (dest_path / "config.json").stat().st_mtime >= (config.converted_at or 0.0) - and (dest_path / "config.json").stat().st_mtime >= model_path.stat().st_mtime - ): - return False - else: - return True - - def _convert_model(self, config: AnyModelConfig, model_path: Path, output_path: Optional[Path] = None) -> AnyModel: - assert isinstance(config, CheckpointConfigBase) - image_size = ( - 512 - if config.base == BaseModelType.StableDiffusion1 - else 768 - if config.base == BaseModelType.StableDiffusion2 - else 1024 - ) - - self._logger.info(f"Converting {model_path} to diffusers format") - with open(self._app_config.legacy_conf_path / config.config_path, "r") as config_stream: - result = convert_controlnet_to_diffusers( - model_path, - output_path, - original_config_file=config_stream, - image_size=image_size, - precision=self._torch_dtype, - from_safetensors=model_path.suffix == ".safetensors", + 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, ) - return result + 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..739ba458888 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/flux.py @@ -0,0 +1,1484 @@ +# 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) + + # Do the multiply in float32 for precision, but store bf16 (FLUX.2's compute dtype) + # immediately so the *whole* model is never materialized in float32. Holding every + # dequantized weight as float32 here doubled RAM transiently (~36GB vs ~17GB for a 9B + # model) and was the dominant cold-load spike, especially with two GPUs. The result is + # identical to the previous code, which cast the same values to bf16 a few steps later. + sd[weight_key] = (weight_float * scale).to(torch.bfloat16) + del weight_float + + # 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 native FP8 tensors straight to bf16 (FLUX.2's compute dtype) rather than float32, + # so a cold load never transiently holds the whole model in float32 (see the scaled path). + sd[key] = sd[key].to(torch.bfloat16) + + 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 index 6320797b8af..7e87869c9e3 100644 --- a/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py +++ b/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py @@ -8,21 +8,19 @@ from diffusers.configuration_utils import ConfigMixin from diffusers.models.modeling_utils import ModelMixin -from invokeai.backend.model_manager import ( +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, - AnyModelConfig, BaseModelType, - InvalidModelConfigException, ModelFormat, ModelType, SubModelType, ) -from invokeai.backend.model_manager.config import DiffusersConfigBase -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.""" @@ -36,17 +34,20 @@ def _load_model( 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, DiffusersConfigBase) else None + 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) + 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) + 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 @@ -59,9 +60,7 @@ def get_hf_load_class(self, model_path: Path, submodel_type: Optional[SubModelTy module, class_name = config[submodel_type.value] result = self._hf_definition_to_type(module=module, class_name=class_name) except KeyError as e: - raise InvalidModelConfigException( - f'The "{submodel_type}" submodel is not available for this model.' - ) from 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") @@ -70,15 +69,20 @@ def get_hf_load_class(self, model_path: Path, submodel_type: Optional[SubModelTy elif class_name := config.get("architectures"): result = self._hf_definition_to_type(module="transformers", class_name=class_name[0]) else: - raise InvalidModelConfigException("Unable to decipher Load Class based on given config.json") + raise RuntimeError("Unable to decipher Load Class based on given config.json") except KeyError as e: - raise InvalidModelConfigException("An expected config.json file is missing from this model.") from 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"]: + 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 diff --git a/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py b/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py index 55eed81fcd5..d133a36498c 100644 --- a/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py +++ b/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py @@ -7,8 +7,9 @@ import torch from invokeai.backend.ip_adapter.ip_adapter import build_ip_adapter -from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType +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 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 index 53814279ecd..15dfa376179 100644 --- a/invokeai/backend/model_manager/load/model_loaders/lora.py +++ b/invokeai/backend/model_manager/load/model_loaders/lora.py @@ -5,24 +5,73 @@ 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.lora import LoRAModelRaw -from invokeai.backend.model_manager import ( +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, - AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType, ) -from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase -from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase - -from .. import ModelLoader, ModelLoaderRegistry +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.""" @@ -31,11 +80,10 @@ def __init__( self, app_config: InvokeAIAppConfig, logger: Logger, - ram_cache: ModelCacheBase[AnyModel], - convert_cache: ModelConvertCacheBase, + ram_cache: ModelCache, ): """Initialize the loader.""" - super().__init__(app_config, logger, ram_cache, convert_cache) + super().__init__(app_config, logger, ram_cache) self._model_base: Optional[BaseModelType] = None def _load_model( @@ -47,14 +95,92 @@ def _load_model( raise ValueError("There are no submodels in a LoRA model.") model_path = Path(config.path) assert self._model_base is not None - model = LoRAModelRaw.from_checkpoint( - file_path=model_path, - dtype=self._torch_dtype, - base_model=self._model_base, - ) + + # 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 - # override 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 diff --git a/invokeai/backend/model_manager/load/model_loaders/onnx.py b/invokeai/backend/model_manager/load/model_loaders/onnx.py index b43e0a1bdfb..6ffab997cf3 100644 --- a/invokeai/backend/model_manager/load/model_loaders/onnx.py +++ b/invokeai/backend/model_manager/load/model_loaders/onnx.py @@ -5,18 +5,17 @@ from pathlib import Path from typing import Optional -from invokeai.backend.model_manager import ( +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, - AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType, ) -from .. import ModelLoaderRegistry -from .generic_diffusers import GenericDiffusersLoader - @ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.ONNX) @ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.Olive) @@ -39,5 +38,6 @@ def _load_model( 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..289f1c92d1e --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/qwen_image.py @@ -0,0 +1,533 @@ +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_Checkpoint_QwenImage_Config, + 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 + + +def _strip_comfyui_prefix(sd: dict) -> dict: + """Strip ComfyUI-style `model.diffusion_model.` / `diffusion_model.` prefixes from keys.""" + 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 is None: + return sd + stripped: dict = {} + for key, value in sd.items(): + if isinstance(key, str) and key.startswith(prefix_to_strip): + stripped[key[len(prefix_to_strip) :]] = value + else: + stripped[key] = value + return stripped + + +def _dequantize_comfyui_fp8(sd: dict, compute_dtype: torch.dtype) -> int: + """Dequantize ComfyUI-style fp8_scaled weights in-place. Returns count of dequantized tensors. + + Weights are dequantized directly to `compute_dtype` (typically bf16) instead of via a + full-precision float32 intermediate. The previous float32 path materialised a complete + 4-byte/param copy of the model before a separate downcast pass, spiking peak RAM to ~2x the + final bf16 size (~80GB for the 20B Qwen-Image transformer). Multiplying in the target dtype + keeps the dict at the bf16 model size plus a single transient tensor. fp8 has only 3 mantissa + bits and bf16 shares float32's exponent range, so the bf16 multiply loses no meaningful + precision here. + + 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)] + 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].to(compute_dtype) + scale = sd[scale_key].to(compute_dtype) + if scale.shape != weight.shape and scale.numel() > 1: + for dim in range(len(weight.shape)): + if dim < len(scale.shape) and scale.shape[dim] != weight.shape[dim]: + block_size = weight.shape[dim] // scale.shape[dim] + if block_size > 1: + scale = scale.repeat_interleave(block_size, dim=dim) + sd[weight_key] = weight * scale + count += 1 + return count + + +def _strip_quantization_metadata(sd: dict) -> None: + """Strip ComfyUI fp8 quantization metadata keys in-place.""" + 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] + + +def _build_qwen_image_transformer_config(sd: dict, is_edit: bool) -> dict: + """Auto-detect Qwen Image transformer architecture parameters from the state dict. + + Works for both GGUF (GGMLTensor) and plain safetensors (torch.Tensor) state dicts. + Mutates nothing. + """ + from diffusers import QwenImageTransformer2DModel + + def _shape(t): + return t.tensor_shape if isinstance(t, GGMLTensor) else t.shape + + 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: + num_layers = max(num_layers, int(parts[1]) + 1) + except ValueError: + pass + + num_attention_heads = 24 + attention_head_dim = 128 + in_channels = 64 + + if "img_in.weight" in sd: + shape = _shape(sd["img_in.weight"]) + hidden_dim = shape[0] + in_channels = shape[1] + num_attention_heads = hidden_dim // attention_head_dim + + joint_attention_dim = 3584 + if "txt_in.weight" in sd: + joint_attention_dim = _shape(sd["txt_in.weight"])[1] + + model_config: dict = { + "patch_size": 2, + "in_channels": in_channels, + "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 enables dual modulation for noisy vs reference patches in edit-variant + # models. Setting it on txt2img models produces garbage. Requires diffusers 0.37+. + import inspect + + if is_edit and "zero_cond_t" in inspect.signature(QwenImageTransformer2DModel.__init__).parameters: + model_config["zero_cond_t"] = True + + return model_config + + +@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 + + result = self._apply_fp8_layerwise_casting(result, config, submodel_type) + 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) + sd = _strip_comfyui_prefix(sd) + + is_edit = getattr(config, "variant", None) == QwenImageVariantType.Edit + model_config = _build_qwen_image_transformer_config(sd, is_edit=is_edit) + + with accelerate.init_empty_weights(): + model = QwenImageTransformer2DModel(**model_config) + + model.load_state_dict(sd, strict=False, assign=True) + return model + + +@ModelLoaderRegistry.register(base=BaseModelType.QwenImage, type=ModelType.Main, format=ModelFormat.Checkpoint) +class QwenImageCheckpointModel(ModelLoader): + """Loads Qwen Image transformer models from single-file safetensors checkpoints + (e.g. ComfyUI fp8_scaled, plain bf16/fp16). Dequantizes ComfyUI fp8 scaling to + bf16 at load time; the `default_settings.fp8_storage` toggle then optionally + re-casts to fp8 for VRAM savings.""" + + 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) + return self._apply_fp8_layerwise_casting(model, config, submodel_type) + + 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 + from safetensors.torch import load_file + + from invokeai.backend.util.logging import InvokeAILogger + + logger = InvokeAILogger.get_logger(self.__class__.__name__) + + if not isinstance(config, Main_Checkpoint_QwenImage_Config): + raise TypeError(f"Expected Main_Checkpoint_QwenImage_Config, got {type(config).__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)) + sd = _strip_comfyui_prefix(sd) + + dequantized = _dequantize_comfyui_fp8(sd, model_dtype) + if dequantized > 0: + logger.info(f"Dequantized {dequantized} ComfyUI-quantized weights") + _strip_quantization_metadata(sd) + + is_edit = getattr(config, "variant", None) == QwenImageVariantType.Edit + model_config = _build_qwen_image_transformer_config(sd, is_edit=is_edit) + + with accelerate.init_empty_weights(): + model = QwenImageTransformer2DModel(**model_config) + + # Dequantized fp8 weights are already at model_dtype; this only casts any remaining + # non-quantized float weights (e.g. a plain fp16/fp32 checkpoint) to the compute dtype + # so the cache reservation below is sized from the actual post-cast tensors. + for k in list(sd.keys()): + if sd[k].is_floating_point(): + sd[k] = sd[k].to(model_dtype) + + new_sd_size = sum(t.nelement() * t.element_size() for t in sd.values()) + self._ram_cache.make_room(new_sd_size) + + 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, then strip the now-unused quantization + # metadata (`scale_input` is the activation scale ComfyUI's fp8 matmul kernels + # use at runtime — we run the encoder in bf16 after dequantization). + dequantized_count = _dequantize_comfyui_fp8(sd, model_dtype) + if dequantized_count > 0: + logger.info(f"Dequantized {dequantized_count} ComfyUI-quantized weights") + _strip_quantization_metadata(sd) + + # 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 index 3ca7a5b2e4a..d19d6477626 100644 --- a/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py +++ b/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py @@ -4,25 +4,37 @@ from pathlib import Path from typing import Optional -from invokeai.backend.model_manager import ( +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, - AnyModelConfig, BaseModelType, ModelFormat, ModelType, - SchedulerPredictionType, - SubModelType, -) -from invokeai.backend.model_manager.config import ( - CheckpointConfigBase, - DiffusersConfigBase, - MainCheckpointConfig, ModelVariantType, + SubModelType, ) -from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_ckpt_to_diffusers - -from .. import ModelLoaderRegistry -from .generic_diffusers import GenericDiffusersLoader +from invokeai.backend.util.silence_warnings import SilenceWarnings VARIANT_TO_IN_CHANNEL_MAP = { ModelVariantType.Normal: 4, @@ -31,28 +43,36 @@ } -@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.Main, format=ModelFormat.Diffusers) -@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.Main, format=ModelFormat.Checkpoint) +@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.""" - model_base_to_model_type = { - BaseModelType.StableDiffusion1: "FrozenCLIPEmbedder", - BaseModelType.StableDiffusion2: "FrozenOpenCLIPEmbedder", - BaseModelType.StableDiffusionXL: "SDXL", - BaseModelType.StableDiffusionXLRefiner: "SDXL-Refiner", - } - def _load_model( self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None, ) -> AnyModel: - if not submodel_type is not None: + 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, DiffusersConfigBase) else None + 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: @@ -60,57 +80,81 @@ def _load_model( 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) + 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 _needs_conversion(self, config: AnyModelConfig, model_path: Path, dest_path: Path) -> bool: - if not isinstance(config, CheckpointConfigBase): - return False - elif ( - dest_path.exists() - and (dest_path / "model_index.json").stat().st_mtime >= (config.converted_at or 0.0) - and (dest_path / "model_index.json").stat().st_mtime >= model_path.stat().st_mtime - ): - return False - else: - return True + 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 - def _convert_model(self, config: AnyModelConfig, model_path: Path, output_path: Optional[Path] = None) -> AnyModel: - assert isinstance(config, MainCheckpointConfig) - base = config.base + # 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'] - prediction_type = config.prediction_type.value - upcast_attention = config.upcast_attention - image_size = ( - 1024 - if base == BaseModelType.StableDiffusionXL - else 768 - if config.prediction_type == SchedulerPredictionType.VPrediction and base == BaseModelType.StableDiffusion2 - else 512 - ) + with SilenceWarnings(): + pipeline = load_class.from_single_file(config.path, torch_dtype=self._torch_dtype) - self._logger.info(f"Converting {model_path} to diffusers format") + if not submodel_type: + return pipeline - loaded_model = convert_ckpt_to_diffusers( - model_path, - output_path, - model_type=self.model_base_to_model_type[base], - original_config_file=self._app_config.legacy_conf_path / config.config_path, - extract_ema=True, - from_safetensors=model_path.suffix == ".safetensors", - precision=self._torch_dtype, - prediction_type=prediction_type, - image_size=image_size, - upcast_attention=upcast_attention, - load_safety_checker=False, - num_in_channels=VARIANT_TO_IN_CHANNEL_MAP[config.variant], - ) - return loaded_model + # 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 index cfdc689cc84..2d0411a8df2 100644 --- a/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py +++ b/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py @@ -4,9 +4,11 @@ from pathlib import Path from typing import Optional -from invokeai.backend.model_manager import ( +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, - AnyModelConfig, BaseModelType, ModelFormat, ModelType, @@ -14,8 +16,6 @@ ) from invokeai.backend.textual_inversion import TextualInversionModelRaw -from .. import ModelLoader, ModelLoaderRegistry - @ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.TextualInversion, format=ModelFormat.EmbeddingFile) @ModelLoaderRegistry.register( diff --git a/invokeai/backend/model_manager/load/model_loaders/vae.py b/invokeai/backend/model_manager/load/model_loaders/vae.py index f51c551f091..720821f3af8 100644 --- a/invokeai/backend/model_manager/load/model_loaders/vae.py +++ b/invokeai/backend/model_manager/load/model_loaders/vae.py @@ -1,24 +1,25 @@ # Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team """Class for VAE model loading in InvokeAI.""" -from pathlib import Path from typing import Optional -import torch -from omegaconf import DictConfig, OmegaConf -from safetensors.torch import load_file as safetensors_load_file +from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL -from invokeai.backend.model_manager import ( - AnyModelConfig, +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, ) -from invokeai.backend.model_manager.config import AnyModel, CheckpointConfigBase -from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_ldm_vae_to_diffusers - -from .. import ModelLoaderRegistry -from .generic_diffusers import GenericDiffusersLoader @ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.VAE, format=ModelFormat.Diffusers) @@ -26,39 +27,54 @@ class VAELoader(GenericDiffusersLoader): """Class to load VAE models.""" - def _needs_conversion(self, config: AnyModelConfig, model_path: Path, dest_path: Path) -> bool: - if not isinstance(config, CheckpointConfigBase): - return False - elif ( - dest_path.exists() - and (dest_path / "config.json").stat().st_mtime >= (config.converted_at or 0.0) - and (dest_path / "config.json").stat().st_mtime >= model_path.stat().st_mtime - ): - return False + 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 True + return super()._load_model(config, submodel_type) - def _convert_model(self, config: AnyModelConfig, model_path: Path, output_path: Optional[Path] = None) -> AnyModel: - assert isinstance(config, CheckpointConfigBase) - config_file = self._app_config.legacy_conf_path / config.config_path + def _load_qwen_image_vae(self, config: VAE_Checkpoint_QwenImage_Config) -> AnyModel: + """Load a Qwen Image VAE from a single safetensors file. - if model_path.suffix == ".safetensors": - checkpoint = safetensors_load_file(model_path, device="cpu") - else: - checkpoint = torch.load(model_path, map_location="cpu") + 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) - # sometimes weights are hidden under "state_dict", and sometimes not - if "state_dict" in checkpoint: - checkpoint = checkpoint["state_dict"] + with accelerate.init_empty_weights(): + model = AutoencoderKLQwenImage() - ckpt_config = OmegaConf.load(config_file) - assert isinstance(ckpt_config, DictConfig) - self._logger.info(f"Converting {model_path} to diffusers format") - vae_model = convert_ldm_vae_to_diffusers( - checkpoint=checkpoint, - vae_config=ckpt_config, - image_size=512, - precision=self._torch_dtype, - dump_path=output_path, - ) - return vae_model + 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 index c55eee48fa5..c3477fa6603 100644 --- a/invokeai/backend/model_manager/load/model_util.py +++ b/invokeai/backend/model_manager/load/model_util.py @@ -2,25 +2,88 @@ """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 import DiffusionPipeline - -from invokeai.backend.model_manager.config import AnyModel +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(model: AnyModel) -> int: +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_model_by_data(model) + 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 @@ -30,15 +93,15 @@ def _calc_pipeline_by_data(pipeline: DiffusionPipeline) -> int: 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_model_by_data(submodel) + res += calc_module_size(submodel) return res -def _calc_model_by_data(model: torch.nn.Module) -> int: - mem_params = sum([param.nelement() * param.element_size() for param in model.parameters()]) - mem_bufs = sum([buf.nelement() * buf.element_size() for buf in model.buffers()]) - mem: int = mem_params + mem_bufs # in bytes - return mem +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: @@ -97,6 +160,7 @@ def calc_model_size_by_fs(model_path: Path, subfolder: Optional[str] = None, var (".msgpack",), # flax (".ckpt",), # tf (".h5",), # tf2 + (".gguf",), # gguf quantized ] for file_format in formats: diff --git a/invokeai/backend/model_manager/merge.py b/invokeai/backend/model_manager/merge.py deleted file mode 100644 index 125e99be935..00000000000 --- a/invokeai/backend/model_manager/merge.py +++ /dev/null @@ -1,169 +0,0 @@ -""" -invokeai.backend.model_manager.merge exports: -merge_diffusion_models() -- combine multiple models by location and return a pipeline object -merge_diffusion_models_and_commit() -- combine multiple models by ModelManager ID and write to the models tables - -Copyright (c) 2023 Lincoln Stein and the InvokeAI Development Team -""" - -import warnings -from enum import Enum -from pathlib import Path -from typing import Any, List, Optional, Set - -import torch -from diffusers import AutoPipelineForText2Image -from diffusers.utils import logging as dlogging - -from invokeai.app.services.model_install import ModelInstallServiceBase -from invokeai.app.services.model_records.model_records_base import ModelRecordChanges -from invokeai.backend.util.devices import TorchDevice - -from . import ( - AnyModelConfig, - BaseModelType, - ModelType, - ModelVariantType, -) -from .config import MainDiffusersConfig - - -class MergeInterpolationMethod(str, Enum): - WeightedSum = "weighted_sum" - Sigmoid = "sigmoid" - InvSigmoid = "inv_sigmoid" - AddDifference = "add_difference" - - -class ModelMerger(object): - """Wrapper class for model merge function.""" - - def __init__(self, installer: ModelInstallServiceBase): - """ - Initialize a ModelMerger object with the model installer. - """ - self._installer = installer - self._dtype = TorchDevice.choose_torch_dtype() - - def merge_diffusion_models( - self, - model_paths: List[Path], - alpha: float = 0.5, - interp: Optional[MergeInterpolationMethod] = None, - force: bool = False, - variant: Optional[str] = None, - **kwargs: Any, - ) -> Any: # pipe.merge is an untyped function. - """ - :param model_paths: up to three models, designated by their local paths or HuggingFace repo_ids - :param alpha: The interpolation parameter. Ranges from 0 to 1. It affects the ratio in which the checkpoints are merged. A 0.8 alpha - would mean that the first model checkpoints would affect the final result far less than an alpha of 0.2 - :param interp: The interpolation method to use for the merging. Supports "sigmoid", "inv_sigmoid", "add_difference" and None. - Passing None uses the default interpolation which is weighted sum interpolation. For merging three checkpoints, only "add_difference" is supported. - :param force: Whether to ignore mismatch in model_config.json for the current models. Defaults to False. - - **kwargs - the default DiffusionPipeline.get_config_dict kwargs: - cache_dir, resume_download, force_download, proxies, local_files_only, use_auth_token, revision, torch_dtype, device_map - """ - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - verbosity = dlogging.get_verbosity() - dlogging.set_verbosity_error() - dtype = torch.float16 if variant == "fp16" else self._dtype - - # Note that checkpoint_merger will not work with downloaded HuggingFace fp16 models - # until upstream https://github.com/huggingface/diffusers/pull/6670 is merged and released. - pipe = AutoPipelineForText2Image.from_pretrained( - model_paths[0], - custom_pipeline="checkpoint_merger", - torch_dtype=dtype, - variant=variant, - ) # type: ignore - merged_pipe = pipe.merge( - pretrained_model_name_or_path_list=model_paths, - alpha=alpha, - interp=interp.value if interp else None, # diffusers API treats None as "weighted sum" - force=force, - torch_dtype=dtype, - variant=variant, - **kwargs, - ) - dlogging.set_verbosity(verbosity) - return merged_pipe - - def merge_diffusion_models_and_save( - self, - model_keys: List[str], - merged_model_name: str, - alpha: float = 0.5, - force: bool = False, - interp: Optional[MergeInterpolationMethod] = None, - merge_dest_directory: Optional[Path] = None, - variant: Optional[str] = None, - **kwargs: Any, - ) -> AnyModelConfig: - """ - :param models: up to three models, designated by their registered InvokeAI model name - :param merged_model_name: name for new model - :param alpha: The interpolation parameter. Ranges from 0 to 1. It affects the ratio in which the checkpoints are merged. A 0.8 alpha - would mean that the first model checkpoints would affect the final result far less than an alpha of 0.2 - :param interp: The interpolation method to use for the merging. Supports "weighted_average", "sigmoid", "inv_sigmoid", "add_difference" and None. - Passing None uses the default interpolation which is weighted sum interpolation. For merging three checkpoints, only "add_difference" is supported. Add_difference is A+(B-C). - :param force: Whether to ignore mismatch in model_config.json for the current models. Defaults to False. - :param merge_dest_directory: Save the merged model to the designated directory (with 'merged_model_name' appended) - **kwargs - the default DiffusionPipeline.get_config_dict kwargs: - cache_dir, resume_download, force_download, proxies, local_files_only, use_auth_token, revision, torch_dtype, device_map - """ - model_paths: List[Path] = [] - model_names: List[str] = [] - config = self._installer.app_config - store = self._installer.record_store - base_models: Set[BaseModelType] = set() - variant = None if self._installer.app_config.precision == "float32" else "fp16" - - assert ( - len(model_keys) <= 2 or interp == MergeInterpolationMethod.AddDifference - ), "When merging three models, only the 'add_difference' merge method is supported" - - for key in model_keys: - info = store.get_model(key) - model_names.append(info.name) - assert isinstance( - info, MainDiffusersConfig - ), f"{info.name} ({info.key}) is not a diffusers model. It must be optimized before merging" - assert info.variant == ModelVariantType( - "normal" - ), f"{info.name} ({info.key}) is a {info.variant} model, which cannot currently be merged" - - # tally base models used - base_models.add(info.base) - model_paths.extend([config.models_path / info.path]) - - assert len(base_models) == 1, f"All models to merge must have same base model, but found bases {base_models}" - base_model = base_models.pop() - - merge_method = None if interp == "weighted_sum" else MergeInterpolationMethod(interp) - merged_pipe = self.merge_diffusion_models(model_paths, alpha, merge_method, force, variant=variant, **kwargs) - dump_path = ( - Path(merge_dest_directory) - if merge_dest_directory - else config.models_path / base_model.value / ModelType.Main.value - ) - dump_path.mkdir(parents=True, exist_ok=True) - dump_path = dump_path / merged_model_name - - dtype = torch.float16 if variant == "fp16" else self._dtype - merged_pipe.save_pretrained(dump_path.as_posix(), safe_serialization=True, torch_dtype=dtype, variant=variant) - - # register model and get its unique key - key = self._installer.register_path(dump_path) - - # update model's config - model_config = self._installer.record_store.get_model(key) - model_config.name = merged_model_name - model_config.description = f"Merge of models {', '.join(model_names)}" - - self._installer.record_store.update_model( - key, ModelRecordChanges(name=model_config.name, description=model_config.description) - ) - return model_config diff --git a/invokeai/backend/model_manager/metadata/__init__.py b/invokeai/backend/model_manager/metadata/__init__.py index 1fd080b6793..76da268153a 100644 --- a/invokeai/backend/model_manager/metadata/__init__.py +++ b/invokeai/backend/model_manager/metadata/__init__.py @@ -16,8 +16,8 @@ assert isinstance(data, HuggingFaceMetadata) """ -from .fetch import HuggingFaceMetadataFetch, ModelMetadataFetchBase -from .metadata_base import ( +from invokeai.backend.model_manager.metadata.fetch import HuggingFaceMetadataFetch, ModelMetadataFetchBase +from invokeai.backend.model_manager.metadata.metadata_base import ( AnyModelRepoMetadata, AnyModelRepoMetadataValidator, BaseMetadata, diff --git a/invokeai/backend/model_manager/metadata/fetch/__init__.py b/invokeai/backend/model_manager/metadata/fetch/__init__.py index 652a3cf6b77..62b3dc4d540 100644 --- a/invokeai/backend/model_manager/metadata/fetch/__init__.py +++ b/invokeai/backend/model_manager/metadata/fetch/__init__.py @@ -10,7 +10,7 @@ assert isinstance(data, HuggingFaceMetadata) """ -from .fetch_base import ModelMetadataFetchBase -from .huggingface import HuggingFaceMetadataFetch +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 index f84479404e8..1dee3b1a75a 100644 --- a/invokeai/backend/model_manager/metadata/fetch/fetch_base.py +++ b/invokeai/backend/model_manager/metadata/fetch/fetch_base.py @@ -17,9 +17,12 @@ from pydantic.networks import AnyHttpUrl from requests.sessions import Session -from invokeai.backend.model_manager import ModelRepoVariant - -from ..metadata_base import AnyModelRepoMetadata, AnyModelRepoMetadataValidator, BaseMetadata +from invokeai.backend.model_manager.metadata.metadata_base import ( + AnyModelRepoMetadata, + AnyModelRepoMetadataValidator, + BaseMetadata, +) +from invokeai.backend.model_manager.taxonomy import ModelRepoVariant class ModelMetadataFetchBase(ABC): diff --git a/invokeai/backend/model_manager/metadata/fetch/huggingface.py b/invokeai/backend/model_manager/metadata/fetch/huggingface.py index ab78b3e0640..30fe418fe14 100644 --- a/invokeai/backend/model_manager/metadata/fetch/huggingface.py +++ b/invokeai/backend/model_manager/metadata/fetch/huggingface.py @@ -19,20 +19,18 @@ from typing import Optional import requests -from huggingface_hub import HfApi, configure_http_backend, hf_hub_url -from huggingface_hub.utils._errors import RepositoryNotFoundError, RevisionNotFoundError +from huggingface_hub import hf_hub_url from pydantic.networks import AnyHttpUrl from requests.sessions import Session -from invokeai.backend.model_manager.config import ModelRepoVariant - -from ..metadata_base import ( +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 .fetch_base import ModelMetadataFetchBase +from invokeai.backend.model_manager.taxonomy import ModelRepoVariant HF_MODEL_RE = r"https?://huggingface.co/([\w\-.]+/[\w\-.]+)" @@ -48,7 +46,6 @@ def __init__(self, session: Optional[Session] = None): this module without an internet connection. """ self._requests = session or requests.Session() - configure_http_backend(backend_factory=lambda: self._requests) @classmethod def from_json(cls, json: str) -> HuggingFaceMetadata: @@ -56,6 +53,22 @@ def from_json(cls, json: str) -> HuggingFaceMetadata: 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. @@ -63,12 +76,15 @@ def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyMod # 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 = HfApi().model_info(repo_id=id, files_metadata=True, revision=variant) - except RepositoryNotFoundError as excp: - raise UnknownMetadataException(f"'{id}' not found. See trace for details.") from excp - except RevisionNotFoundError: + model_info = self._fetch_model_info(repo_id, variant) + except UnknownMetadataException: + raise + except requests.HTTPError: if variant is None: raise else: @@ -76,17 +92,20 @@ def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyMod files: list[RemoteModelFile] = [] - _, name = id.split("/") + _, name = repo_id.split("/") - for s in model_info.siblings or []: - assert s.rfilename is not None - assert s.size is not None + 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(id, s.rfilename, revision=variant or "main"), - path=Path(name, s.rfilename), - size=s.size, - sha256=s.lfs.get("sha256") if s.lfs else None, + 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, ) ) @@ -113,10 +132,10 @@ def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyMod ) return HuggingFaceMetadata( - id=model_info.id, + id=model_info["id"], name=name, files=files, - api_response=json.dumps(model_info.__dict__, default=str), + api_response=json.dumps(model_info, default=str), is_diffusers=is_diffusers, ckpt_urls=ckpt_urls, ) diff --git a/invokeai/backend/model_manager/metadata/metadata_base.py b/invokeai/backend/model_manager/metadata/metadata_base.py index f9f5335d175..b048144e547 100644 --- a/invokeai/backend/model_manager/metadata/metadata_base.py +++ b/invokeai/backend/model_manager/metadata/metadata_base.py @@ -17,15 +17,14 @@ from pathlib import Path from typing import List, Literal, Optional, Union -from huggingface_hub import configure_http_backend, hf_hub_url +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 import ModelRepoVariant - -from ..util import select_hf_files +from invokeai.backend.model_manager.taxonomy import ModelRepoVariant +from invokeai.backend.model_manager.util.select_hf_files import filter_files class UnknownMetadataException(Exception): @@ -96,13 +95,15 @@ 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, if any. + 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 + :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: @@ -110,14 +111,16 @@ def download_urls( full-precision model is returned. """ session = session or Session() - configure_http_backend(backend_factory=lambda: session) # used in testing - paths = select_hf_files.filter_files( - [x.path for x in self.files], variant, subfolder - ) # all files in the model - prefix = f"{subfolder}/" if subfolder else "" + 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 + # 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) 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/probe.py b/invokeai/backend/model_manager/probe.py deleted file mode 100644 index a19a7727642..00000000000 --- a/invokeai/backend/model_manager/probe.py +++ /dev/null @@ -1,817 +0,0 @@ -import json -import re -from pathlib import Path -from typing import Any, Dict, Literal, Optional, Union - -import safetensors.torch -import torch -from picklescan.scanner import scan_file_path - -import invokeai.backend.util.logging as logger -from invokeai.app.util.misc import uuid_string -from invokeai.backend.model_hash.model_hash import HASHING_ALGORITHMS, ModelHash -from invokeai.backend.util.silence_warnings import SilenceWarnings - -from .config import ( - AnyModelConfig, - BaseModelType, - ControlAdapterDefaultSettings, - InvalidModelConfigException, - MainModelDefaultSettings, - ModelConfigFactory, - ModelFormat, - ModelRepoVariant, - ModelSourceType, - ModelType, - ModelVariantType, - SchedulerPredictionType, -) -from .util.model_util import lora_token_vector_length, read_checkpoint_meta - -CkptType = Dict[str | int, Any] - -LEGACY_CONFIGS: Dict[BaseModelType, Dict[ModelVariantType, Union[str, Dict[SchedulerPredictionType, str]]]] = { - BaseModelType.StableDiffusion1: { - ModelVariantType.Normal: { - SchedulerPredictionType.Epsilon: "v1-inference.yaml", - SchedulerPredictionType.VPrediction: "v1-inference-v.yaml", - }, - ModelVariantType.Inpaint: "v1-inpainting-inference.yaml", - }, - BaseModelType.StableDiffusion2: { - ModelVariantType.Normal: { - SchedulerPredictionType.Epsilon: "v2-inference.yaml", - SchedulerPredictionType.VPrediction: "v2-inference-v.yaml", - }, - ModelVariantType.Inpaint: { - SchedulerPredictionType.Epsilon: "v2-inpainting-inference.yaml", - SchedulerPredictionType.VPrediction: "v2-inpainting-inference-v.yaml", - }, - ModelVariantType.Depth: "v2-midas-inference.yaml", - }, - BaseModelType.StableDiffusionXL: { - ModelVariantType.Normal: "sd_xl_base.yaml", - ModelVariantType.Inpaint: "sd_xl_inpaint.yaml", - }, - BaseModelType.StableDiffusionXLRefiner: { - ModelVariantType.Normal: "sd_xl_refiner.yaml", - }, -} - - -class ProbeBase(object): - """Base class for probes.""" - - def __init__(self, model_path: Path): - self.model_path = model_path - - def get_base_type(self) -> BaseModelType: - """Get model base type.""" - raise NotImplementedError - - def get_format(self) -> ModelFormat: - """Get model file format.""" - raise NotImplementedError - - def get_variant_type(self) -> Optional[ModelVariantType]: - """Get model variant type.""" - return None - - def get_scheduler_prediction_type(self) -> Optional[SchedulerPredictionType]: - """Get model scheduler prediction type.""" - return None - - def get_image_encoder_model_id(self) -> Optional[str]: - """Get image encoder (IP adapters only).""" - return None - - -class ModelProbe(object): - PROBES: Dict[str, Dict[ModelType, type[ProbeBase]]] = { - "diffusers": {}, - "checkpoint": {}, - "onnx": {}, - } - - CLASS2TYPE = { - "StableDiffusionPipeline": ModelType.Main, - "StableDiffusionInpaintPipeline": ModelType.Main, - "StableDiffusionXLPipeline": ModelType.Main, - "StableDiffusionXLImg2ImgPipeline": ModelType.Main, - "StableDiffusionXLInpaintPipeline": ModelType.Main, - "LatentConsistencyModelPipeline": ModelType.Main, - "AutoencoderKL": ModelType.VAE, - "AutoencoderTiny": ModelType.VAE, - "ControlNetModel": ModelType.ControlNet, - "CLIPVisionModelWithProjection": ModelType.CLIPVision, - "T2IAdapter": ModelType.T2IAdapter, - } - - @classmethod - def register_probe( - cls, format: Literal["diffusers", "checkpoint", "onnx"], model_type: ModelType, probe_class: type[ProbeBase] - ) -> None: - cls.PROBES[format][model_type] = probe_class - - @classmethod - def probe( - cls, model_path: Path, fields: Optional[Dict[str, Any]] = None, hash_algo: HASHING_ALGORITHMS = "blake3_single" - ) -> AnyModelConfig: - """ - Probe the model at model_path and return its configuration record. - - :param model_path: Path to the model file (checkpoint) or directory (diffusers). - :param fields: An optional dictionary that can be used to override probed - fields. Typically used for fields that don't probe well, such as prediction_type. - - Returns: The appropriate model configuration derived from ModelConfigBase. - """ - if fields is None: - fields = {} - - model_path = model_path.resolve() - - format_type = ModelFormat.Diffusers if model_path.is_dir() else ModelFormat.Checkpoint - model_info = None - model_type = ModelType(fields["type"]) if "type" in fields and fields["type"] else None - if not model_type: - if format_type is ModelFormat.Diffusers: - model_type = cls.get_model_type_from_folder(model_path) - else: - model_type = cls.get_model_type_from_checkpoint(model_path) - format_type = ModelFormat.ONNX if model_type == ModelType.ONNX else format_type - - probe_class = cls.PROBES[format_type].get(model_type) - if not probe_class: - raise InvalidModelConfigException(f"Unhandled combination of {format_type} and {model_type}") - - probe = probe_class(model_path) - - fields["source_type"] = fields.get("source_type") or ModelSourceType.Path - fields["source"] = fields.get("source") or model_path.as_posix() - fields["key"] = fields.get("key", uuid_string()) - fields["path"] = model_path.as_posix() - fields["type"] = fields.get("type") or model_type - fields["base"] = fields.get("base") or probe.get_base_type() - fields["variant"] = fields.get("variant") or probe.get_variant_type() - fields["prediction_type"] = fields.get("prediction_type") or probe.get_scheduler_prediction_type() - fields["image_encoder_model_id"] = fields.get("image_encoder_model_id") or probe.get_image_encoder_model_id() - fields["name"] = fields.get("name") or cls.get_model_name(model_path) - fields["description"] = ( - fields.get("description") or f"{fields['base'].value} {model_type.value} model {fields['name']}" - ) - fields["format"] = fields.get("format") or probe.get_format() - fields["hash"] = fields.get("hash") or ModelHash(algorithm=hash_algo).hash(model_path) - - fields["default_settings"] = fields.get("default_settings") - - if not fields["default_settings"]: - if fields["type"] in {ModelType.ControlNet, ModelType.T2IAdapter}: - fields["default_settings"] = get_default_settings_controlnet_t2i_adapter(fields["name"]) - elif fields["type"] is ModelType.Main: - fields["default_settings"] = get_default_settings_main(fields["base"]) - - if format_type == ModelFormat.Diffusers and isinstance(probe, FolderProbeBase): - fields["repo_variant"] = fields.get("repo_variant") or probe.get_repo_variant() - - # additional fields needed for main and controlnet models - if ( - fields["type"] in [ModelType.Main, ModelType.ControlNet, ModelType.VAE] - and fields["format"] is ModelFormat.Checkpoint - ): - ckpt_config_path = cls._get_checkpoint_config_path( - model_path, - model_type=fields["type"], - base_type=fields["base"], - variant_type=fields["variant"], - prediction_type=fields["prediction_type"], - ) - fields["config_path"] = str(ckpt_config_path) - - # additional fields needed for main non-checkpoint models - elif fields["type"] == ModelType.Main and fields["format"] in [ - ModelFormat.ONNX, - ModelFormat.Olive, - ModelFormat.Diffusers, - ]: - fields["upcast_attention"] = fields.get("upcast_attention") or ( - fields["base"] == BaseModelType.StableDiffusion2 - and fields["prediction_type"] == SchedulerPredictionType.VPrediction - ) - - model_info = ModelConfigFactory.make_config(fields) # , key=fields.get("key", None)) - return model_info - - @classmethod - def get_model_name(cls, model_path: Path) -> str: - if model_path.suffix in {".safetensors", ".bin", ".pt", ".ckpt"}: - return model_path.stem - else: - return model_path.name - - @classmethod - def get_model_type_from_checkpoint(cls, model_path: Path, checkpoint: Optional[CkptType] = None) -> ModelType: - if model_path.suffix not in (".bin", ".pt", ".ckpt", ".safetensors", ".pth"): - raise InvalidModelConfigException(f"{model_path}: unrecognized suffix") - - if model_path.name == "learned_embeds.bin": - return ModelType.TextualInversion - - ckpt = checkpoint if checkpoint else read_checkpoint_meta(model_path, scan=True) - ckpt = ckpt.get("state_dict", ckpt) - - for key in [str(k) for k in ckpt.keys()]: - if any(key.startswith(v) for v in {"cond_stage_model.", "first_stage_model.", "model.diffusion_model."}): - return ModelType.Main - elif any(key.startswith(v) for v in {"encoder.conv_in", "decoder.conv_in"}): - return ModelType.VAE - elif any(key.startswith(v) for v in {"lora_te_", "lora_unet_"}): - return ModelType.LoRA - elif any(key.endswith(v) for v in {"to_k_lora.up.weight", "to_q_lora.down.weight"}): - return ModelType.LoRA - elif any(key.startswith(v) for v in {"controlnet", "control_model", "input_blocks"}): - return ModelType.ControlNet - elif any(key.startswith(v) for v in {"image_proj.", "ip_adapter."}): - return ModelType.IPAdapter - elif key in {"emb_params", "string_to_param"}: - return ModelType.TextualInversion - else: - # diffusers-ti - if len(ckpt) < 10 and all(isinstance(v, torch.Tensor) for v in ckpt.values()): - return ModelType.TextualInversion - - raise InvalidModelConfigException(f"Unable to determine model type for {model_path}") - - @classmethod - def get_model_type_from_folder(cls, folder_path: Path) -> ModelType: - """Get the model type of a hugging-face style folder.""" - class_name = None - error_hint = None - for suffix in ["bin", "safetensors"]: - if (folder_path / f"learned_embeds.{suffix}").exists(): - return ModelType.TextualInversion - if (folder_path / f"pytorch_lora_weights.{suffix}").exists(): - return ModelType.LoRA - if (folder_path / "unet/model.onnx").exists(): - return ModelType.ONNX - if (folder_path / "image_encoder.txt").exists(): - return ModelType.IPAdapter - - i = folder_path / "model_index.json" - c = folder_path / "config.json" - config_path = i if i.exists() else c if c.exists() else None - - if config_path: - with open(config_path, "r") as file: - conf = json.load(file) - if "_class_name" in conf: - class_name = conf["_class_name"] - elif "architectures" in conf: - class_name = conf["architectures"][0] - else: - class_name = None - else: - error_hint = f"No model_index.json or config.json found in {folder_path}." - - if class_name and (type := cls.CLASS2TYPE.get(class_name)): - return type - else: - error_hint = f"class {class_name} is not one of the supported classes [{', '.join(cls.CLASS2TYPE.keys())}]" - - # give up - raise InvalidModelConfigException( - f"Unable to determine model type for {folder_path}" + (f"; {error_hint}" if error_hint else "") - ) - - @classmethod - def _get_checkpoint_config_path( - cls, - model_path: Path, - model_type: ModelType, - base_type: BaseModelType, - variant_type: ModelVariantType, - prediction_type: SchedulerPredictionType, - ) -> Path: - # look for a YAML file adjacent to the model file first - possible_conf = model_path.with_suffix(".yaml") - if possible_conf.exists(): - return possible_conf.absolute() - - if model_type is ModelType.Main: - config_file = LEGACY_CONFIGS[base_type][variant_type] - if isinstance(config_file, dict): # need another tier for sd-2.x models - config_file = config_file[prediction_type] - config_file = f"stable-diffusion/{config_file}" - elif model_type is ModelType.ControlNet: - config_file = ( - "controlnet/cldm_v15.yaml" - if base_type is BaseModelType.StableDiffusion1 - else "controlnet/cldm_v21.yaml" - ) - elif model_type is ModelType.VAE: - config_file = ( - "stable-diffusion/v1-inference.yaml" - if base_type is BaseModelType.StableDiffusion1 - else "stable-diffusion/v2-inference.yaml" - ) - else: - raise InvalidModelConfigException( - f"{model_path}: Unrecognized combination of model_type={model_type}, base_type={base_type}" - ) - return Path(config_file) - - @classmethod - def _scan_and_load_checkpoint(cls, model_path: Path) -> CkptType: - with SilenceWarnings(): - if model_path.suffix.endswith((".ckpt", ".pt", ".pth", ".bin")): - cls._scan_model(model_path.name, model_path) - model = torch.load(model_path, map_location="cpu") - assert isinstance(model, dict) - return model - else: - return safetensors.torch.load_file(model_path) - - @classmethod - def _scan_model(cls, model_name: str, checkpoint: Path) -> None: - """ - Apply picklescanner to the indicated checkpoint and issue a warning - and option to exit if an infected file is identified. - """ - # scan model - scan_result = scan_file_path(checkpoint) - if scan_result.infected_files != 0: - raise Exception("The model {model_name} is potentially infected by malware. Aborting import.") - - -# Probing utilities -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": "lineart_image_processor", - "lineart_anime": "lineart_anime_image_processor", - "softedge": "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", -} - - -def get_default_settings_controlnet_t2i_adapter(model_name: str) -> Optional[ControlAdapterDefaultSettings]: - for k, v in MODEL_NAME_TO_PREPROCESSOR.items(): - if k in model_name: - return ControlAdapterDefaultSettings(preprocessor=v) - return None - - -def get_default_settings_main(model_base: BaseModelType) -> Optional[MainModelDefaultSettings]: - if model_base is BaseModelType.StableDiffusion1 or model_base is BaseModelType.StableDiffusion2: - return MainModelDefaultSettings(width=512, height=512) - elif model_base is BaseModelType.StableDiffusionXL: - return MainModelDefaultSettings(width=1024, height=1024) - # We don't provide defaults for BaseModelType.StableDiffusionXLRefiner, as they are not standalone models. - return None - - -# ##################################################3 -# Checkpoint probing -# ##################################################3 - - -class CheckpointProbeBase(ProbeBase): - def __init__(self, model_path: Path): - super().__init__(model_path) - self.checkpoint = ModelProbe._scan_and_load_checkpoint(model_path) - - def get_format(self) -> ModelFormat: - return ModelFormat("checkpoint") - - def get_variant_type(self) -> ModelVariantType: - model_type = ModelProbe.get_model_type_from_checkpoint(self.model_path, self.checkpoint) - if model_type != ModelType.Main: - return ModelVariantType.Normal - state_dict = self.checkpoint.get("state_dict") or self.checkpoint - in_channels = state_dict["model.diffusion_model.input_blocks.0.0.weight"].shape[1] - if in_channels == 9: - return ModelVariantType.Inpaint - elif in_channels == 5: - return ModelVariantType.Depth - elif in_channels == 4: - return ModelVariantType.Normal - else: - raise InvalidModelConfigException( - f"Cannot determine variant type (in_channels={in_channels}) at {self.model_path}" - ) - - -class PipelineCheckpointProbe(CheckpointProbeBase): - def get_base_type(self) -> BaseModelType: - checkpoint = self.checkpoint - state_dict = self.checkpoint.get("state_dict") or checkpoint - 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 - else: - raise InvalidModelConfigException("Cannot determine base type") - - def get_scheduler_prediction_type(self) -> SchedulerPredictionType: - """Return model prediction type.""" - type = self.get_base_type() - if type == BaseModelType.StableDiffusion2: - checkpoint = self.checkpoint - state_dict = self.checkpoint.get("state_dict") or checkpoint - 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 checkpoint: - if checkpoint["global_step"] == 220000: - return SchedulerPredictionType.Epsilon - elif checkpoint["global_step"] == 110000: - return SchedulerPredictionType.VPrediction - return SchedulerPredictionType.VPrediction # a guess for sd2 ckpts - - elif type == BaseModelType.StableDiffusion1: - return SchedulerPredictionType.Epsilon # a reasonable guess for sd1 ckpts - else: - return SchedulerPredictionType.Epsilon - - -class VaeCheckpointProbe(CheckpointProbeBase): - def get_base_type(self) -> BaseModelType: - # VAEs of all base types have the same structure, so we wimp out and - # guess using the name. - for regexp, basetype in [ - (r"xl", BaseModelType.StableDiffusionXL), - (r"sd2", BaseModelType.StableDiffusion2), - (r"vae", BaseModelType.StableDiffusion1), - ]: - if re.search(regexp, self.model_path.name, re.IGNORECASE): - return basetype - raise InvalidModelConfigException("Cannot determine base type") - - -class LoRACheckpointProbe(CheckpointProbeBase): - """Class for LoRA checkpoints.""" - - def get_format(self) -> ModelFormat: - return ModelFormat("lycoris") - - def get_base_type(self) -> BaseModelType: - checkpoint = self.checkpoint - token_vector_length = lora_token_vector_length(checkpoint) - - 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 InvalidModelConfigException(f"Unknown LoRA type: {self.model_path}") - - -class TextualInversionCheckpointProbe(CheckpointProbeBase): - """Class for probing embeddings.""" - - def get_format(self) -> ModelFormat: - return ModelFormat.EmbeddingFile - - def get_base_type(self) -> BaseModelType: - checkpoint = self.checkpoint - if "string_to_token" in checkpoint: - token_dim = list(checkpoint["string_to_param"].values())[0].shape[-1] - elif "emb_params" in checkpoint: - token_dim = checkpoint["emb_params"].shape[-1] - elif "clip_g" in checkpoint: - token_dim = checkpoint["clip_g"].shape[-1] - else: - token_dim = list(checkpoint.values())[0].shape[0] - if token_dim == 768: - return BaseModelType.StableDiffusion1 - elif token_dim == 1024: - return BaseModelType.StableDiffusion2 - elif token_dim == 1280: - return BaseModelType.StableDiffusionXL - else: - raise InvalidModelConfigException(f"{self.model_path}: Could not determine base type") - - -class ControlNetCheckpointProbe(CheckpointProbeBase): - """Class for probing controlnets.""" - - def get_base_type(self) -> BaseModelType: - checkpoint = self.checkpoint - for key_name 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_name not in checkpoint: - continue - width = checkpoint[key_name].shape[-1] - if width == 768: - return BaseModelType.StableDiffusion1 - elif width == 1024: - return BaseModelType.StableDiffusion2 - elif width == 2048: - return BaseModelType.StableDiffusionXL - elif width == 1280: - return BaseModelType.StableDiffusionXL - raise InvalidModelConfigException(f"{self.model_path}: Unable to determine base type") - - -class IPAdapterCheckpointProbe(CheckpointProbeBase): - """Class for probing IP Adapters""" - - def get_base_type(self) -> BaseModelType: - checkpoint = self.checkpoint - for key in checkpoint.keys(): - if not key.startswith(("image_proj.", "ip_adapter.")): - continue - cross_attention_dim = checkpoint["ip_adapter.1.to_k_ip.weight"].shape[-1] - if cross_attention_dim == 768: - return BaseModelType.StableDiffusion1 - elif cross_attention_dim == 1024: - return BaseModelType.StableDiffusion2 - elif cross_attention_dim == 2048: - return BaseModelType.StableDiffusionXL - else: - raise InvalidModelConfigException( - f"IP-Adapter had unexpected cross-attention dimension: {cross_attention_dim}." - ) - raise InvalidModelConfigException(f"{self.model_path}: Unable to determine base type") - - -class CLIPVisionCheckpointProbe(CheckpointProbeBase): - def get_base_type(self) -> BaseModelType: - raise NotImplementedError() - - -class T2IAdapterCheckpointProbe(CheckpointProbeBase): - def get_base_type(self) -> BaseModelType: - raise NotImplementedError() - - -######################################################## -# classes for probing folders -####################################################### -class FolderProbeBase(ProbeBase): - def get_variant_type(self) -> ModelVariantType: - return ModelVariantType.Normal - - def get_format(self) -> ModelFormat: - return ModelFormat("diffusers") - - def get_repo_variant(self) -> ModelRepoVariant: - # get all files ending in .bin or .safetensors - weight_files = list(self.model_path.glob("**/*.safetensors")) - weight_files.extend(list(self.model_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 PipelineFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - with open(self.model_path / "unet" / "config.json", "r") as file: - unet_conf = json.load(file) - if unet_conf["cross_attention_dim"] == 768: - return BaseModelType.StableDiffusion1 - elif unet_conf["cross_attention_dim"] == 1024: - return BaseModelType.StableDiffusion2 - elif unet_conf["cross_attention_dim"] == 1280: - return BaseModelType.StableDiffusionXLRefiner - elif unet_conf["cross_attention_dim"] == 2048: - return BaseModelType.StableDiffusionXL - else: - raise InvalidModelConfigException(f"Unknown base model for {self.model_path}") - - def get_scheduler_prediction_type(self) -> SchedulerPredictionType: - with open(self.model_path / "scheduler" / "scheduler_config.json", "r") as file: - scheduler_conf = json.load(file) - if scheduler_conf.get("prediction_type", "epsilon") == "v_prediction": - return SchedulerPredictionType.VPrediction - elif scheduler_conf.get("prediction_type", "epsilon") == "epsilon": - return SchedulerPredictionType.Epsilon - else: - raise InvalidModelConfigException("Unknown scheduler prediction type: {scheduler_conf['prediction_type']}") - - def get_variant_type(self) -> ModelVariantType: - # This only works for pipelines! Any kind of - # exception results in our returning the - # "normal" variant type - try: - config_file = self.model_path / "unet" / "config.json" - with open(config_file, "r") as file: - conf = json.load(file) - - in_channels = conf["in_channels"] - if in_channels == 9: - return ModelVariantType.Inpaint - elif in_channels == 5: - return ModelVariantType.Depth - elif in_channels == 4: - return ModelVariantType.Normal - except Exception: - pass - return ModelVariantType.Normal - - -class VaeFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - if self._config_looks_like_sdxl(): - return BaseModelType.StableDiffusionXL - elif self._name_looks_like_sdxl(): - # but SD and SDXL VAE are the same shape (3-channel RGB to 4-channel float scaled down - # by a factor of 8), we can't necessarily tell them apart by config hyperparameters. - return BaseModelType.StableDiffusionXL - else: - return BaseModelType.StableDiffusion1 - - def _config_looks_like_sdxl(self) -> bool: - # config values that distinguish Stability's SD 1.x VAE from their SDXL VAE. - config_file = self.model_path / "config.json" - if not config_file.exists(): - raise InvalidModelConfigException(f"Cannot determine base type for {self.model_path}") - with open(config_file, "r") as file: - config = json.load(file) - return config.get("scaling_factor", 0) == 0.13025 and config.get("sample_size") in [512, 1024] - - def _name_looks_like_sdxl(self) -> bool: - return bool(re.search(r"xl\b", self._guess_name(), re.IGNORECASE)) - - def _guess_name(self) -> str: - name = self.model_path.name - if name == "vae": - name = self.model_path.parent.name - return name - - -class TextualInversionFolderProbe(FolderProbeBase): - def get_format(self) -> ModelFormat: - return ModelFormat.EmbeddingFolder - - def get_base_type(self) -> BaseModelType: - path = self.model_path / "learned_embeds.bin" - if not path.exists(): - raise InvalidModelConfigException( - f"{self.model_path.as_posix()} does not contain expected 'learned_embeds.bin' file" - ) - return TextualInversionCheckpointProbe(path).get_base_type() - - -class ONNXFolderProbe(PipelineFolderProbe): - def get_base_type(self) -> BaseModelType: - # Due to the way the installer is set up, the configuration file for safetensors - # will come along for the ride if both the onnx and safetensors forms - # share the same directory. We take advantage of this here. - if (self.model_path / "unet" / "config.json").exists(): - return super().get_base_type() - else: - logger.warning('Base type probing is not implemented for ONNX models. Assuming "sd-1"') - return BaseModelType.StableDiffusion1 - - def get_format(self) -> ModelFormat: - return ModelFormat("onnx") - - def get_variant_type(self) -> ModelVariantType: - return ModelVariantType.Normal - - -class ControlNetFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - config_file = self.model_path / "config.json" - if not config_file.exists(): - raise InvalidModelConfigException(f"Cannot determine base type for {self.model_path}") - with open(config_file, "r") as file: - config = json.load(file) - # no obvious way to distinguish between sd2-base and sd2-768 - dimension = config["cross_attention_dim"] - base_model = ( - BaseModelType.StableDiffusion1 - if dimension == 768 - else ( - BaseModelType.StableDiffusion2 - if dimension == 1024 - else BaseModelType.StableDiffusionXL - if dimension == 2048 - else None - ) - ) - if not base_model: - raise InvalidModelConfigException(f"Unable to determine model base for {self.model_path}") - return base_model - - -class LoRAFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - model_file = None - for suffix in ["safetensors", "bin"]: - base_file = self.model_path / f"pytorch_lora_weights.{suffix}" - if base_file.exists(): - model_file = base_file - break - if not model_file: - raise InvalidModelConfigException("Unknown LoRA format encountered") - return LoRACheckpointProbe(model_file).get_base_type() - - -class IPAdapterFolderProbe(FolderProbeBase): - def get_format(self) -> ModelFormat: - return ModelFormat.InvokeAI - - def get_base_type(self) -> BaseModelType: - model_file = self.model_path / "ip_adapter.bin" - if not model_file.exists(): - raise InvalidModelConfigException("Unknown IP-Adapter model format.") - - state_dict = torch.load(model_file, map_location="cpu") - cross_attention_dim = state_dict["ip_adapter"]["1.to_k_ip.weight"].shape[-1] - if cross_attention_dim == 768: - return BaseModelType.StableDiffusion1 - elif cross_attention_dim == 1024: - return BaseModelType.StableDiffusion2 - elif cross_attention_dim == 2048: - return BaseModelType.StableDiffusionXL - else: - raise InvalidModelConfigException( - f"IP-Adapter had unexpected cross-attention dimension: {cross_attention_dim}." - ) - - def get_image_encoder_model_id(self) -> Optional[str]: - encoder_id_path = self.model_path / "image_encoder.txt" - if not encoder_id_path.exists(): - return None - with open(encoder_id_path, "r") as f: - image_encoder_model = f.readline().strip() - return image_encoder_model - - -class CLIPVisionFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - return BaseModelType.Any - - -class T2IAdapterFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - config_file = self.model_path / "config.json" - if not config_file.exists(): - raise InvalidModelConfigException(f"Cannot determine base type for {self.model_path}") - with open(config_file, "r") as file: - config = json.load(file) - - adapter_type = config.get("adapter_type", None) - if adapter_type == "full_adapter_xl": - return BaseModelType.StableDiffusionXL - elif adapter_type == "full_adapter" or "light_adapter": - # I haven't seen any T2I adapter models for SD2, so assume that this is an SD1 adapter. - return BaseModelType.StableDiffusion1 - else: - raise InvalidModelConfigException( - f"Unable to determine base model for '{self.model_path}' (adapter_type = {adapter_type})." - ) - - -# Register probe classes -ModelProbe.register_probe("diffusers", ModelType.Main, PipelineFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.VAE, VaeFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.LoRA, LoRAFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.TextualInversion, TextualInversionFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.ControlNet, ControlNetFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.IPAdapter, IPAdapterFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.CLIPVision, CLIPVisionFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.T2IAdapter, T2IAdapterFolderProbe) - -ModelProbe.register_probe("checkpoint", ModelType.Main, PipelineCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.VAE, VaeCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.LoRA, LoRACheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.TextualInversion, TextualInversionCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.ControlNet, ControlNetCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.IPAdapter, IPAdapterCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.CLIPVision, CLIPVisionCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.T2IAdapter, T2IAdapterCheckpointProbe) - -ModelProbe.register_probe("onnx", ModelType.ONNX, ONNXFolderProbe) diff --git a/invokeai/backend/model_manager/search.py b/invokeai/backend/model_manager/search.py index fe545bfe34b..a0569798804 100644 --- a/invokeai/backend/model_manager/search.py +++ b/invokeai/backend/model_manager/search.py @@ -130,7 +130,7 @@ def _walk_directory(self, path: Path, max_depth: int = 20) -> None: return for n in file_names: - if n.endswith((".ckpt", ".bin", ".pth", ".safetensors", ".pt")): + if n.endswith((".ckpt", ".bin", ".pth", ".safetensors", ".pt", ".gguf")): try: self.model_found(absolute_path / n) except KeyboardInterrupt: 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 index 31b16d9c8a9..9bc58e44269 100644 --- a/invokeai/backend/model_manager/starter_models.py +++ b/invokeai/backend/model_manager/starter_models.py @@ -2,7 +2,20 @@ from pydantic import BaseModel -from invokeai.backend.model_manager.config import BaseModelType, ModelType +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): @@ -11,7 +24,15 @@ class StarterModelWithoutDependencies(BaseModel): 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): @@ -19,372 +40,1773 @@ class StarterModel(StarterModelWithoutDependencies): dependencies: Optional[list[StarterModelWithoutDependencies]] = None -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, +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.StableDiffusion1, + 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.StableDiffusionXL, + 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 -cyberrealistic_negative = StarterModel( - name="CyberRealistic Negative v3", +# 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_Negative/resolve/main/CyberRealistic_Negative_v3.pt", - description="Negative embedding specifically for use with CyberRealistic.", + 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, +) -# 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] = [ - # region: Main - 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], - ), - StarterModel( - name="ReV Animated", - base=BaseModelType.StableDiffusion1, - source="stablediffusionapi/rev-animated", - description="Fantasy and anime style images.", - type=ModelType.Main, - ), - StarterModel( - name="Dreamshaper 8", - base=BaseModelType.StableDiffusion1, - source="Lykon/dreamshaper-8", - description="Popular versatile model.", - type=ModelType.Main, - ), - StarterModel( - name="Dreamshaper 8 (inpainting)", - base=BaseModelType.StableDiffusion1, - source="Lykon/dreamshaper-8-inpainting", - description="Inpainting version of Dreamshaper 8.", - type=ModelType.Main, - ), - 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, - ), - 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, - ), - 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], - ), - 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], - ), - 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 VAE - sdxl_fp16_vae_fix, - # endregion - # region LoRA - 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, - ), - 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 - 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 - StarterModel( - name="IP Adapter", - base=BaseModelType.StableDiffusion1, - source="https://huggingface.co/InvokeAI/ip_adapter_sd15/resolve/main/ip-adapter_sd15.safetensors", - description="IP-Adapter for SD 1.5 models", - type=ModelType.IPAdapter, - dependencies=[ip_adapter_sd_image_encoder], - ), - StarterModel( - name="IP Adapter Plus", - base=BaseModelType.StableDiffusion1, - source="https://huggingface.co/InvokeAI/ip_adapter_plus_sd15/resolve/main/ip-adapter-plus_sd15.safetensors", - description="Refined IP-Adapter for SD 1.5 models", - type=ModelType.IPAdapter, - dependencies=[ip_adapter_sd_image_encoder], - ), - StarterModel( - name="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="Refined IP-Adapter for SD 1.5 models, adapted for faces", - type=ModelType.IPAdapter, - dependencies=[ip_adapter_sd_image_encoder], - ), - StarterModel( - name="IP Adapter SDXL", - base=BaseModelType.StableDiffusionXL, - source="https://huggingface.co/InvokeAI/ip_adapter_sdxl_vit_h/resolve/main/ip-adapter_sdxl_vit-h.safetensors", - description="IP-Adapter for SDXL models", - type=ModelType.IPAdapter, - dependencies=[ip_adapter_sdxl_image_encoder], - ), - # endregion - # region ControlNet - StarterModel( - name="QRCode Monster", - base=BaseModelType.StableDiffusion1, - source="monster-labs/control_v1p_sd15_qrcode_monster", - description="Controlnet model that generates scannable creative QR codes", - type=ModelType.ControlNet, - ), - StarterModel( - name="canny", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11p_sd15_canny", - description="Controlnet weights trained on sd-1.5 with canny conditioning.", - type=ModelType.ControlNet, - ), - StarterModel( - name="inpaint", - 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, - ), - StarterModel( - name="mlsd", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11p_sd15_mlsd", - description="Controlnet weights trained on sd-1.5 with canny conditioning, MLSD version", - type=ModelType.ControlNet, - ), - StarterModel( - name="depth", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11f1p_sd15_depth", - description="Controlnet weights trained on sd-1.5 with depth conditioning", - type=ModelType.ControlNet, - ), - StarterModel( - name="normal_bae", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11p_sd15_normalbae", - description="Controlnet weights trained on sd-1.5 with normalbae image conditioning", - type=ModelType.ControlNet, - ), - StarterModel( - name="seg", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11p_sd15_seg", - description="Controlnet weights trained on sd-1.5 with seg image conditioning", - type=ModelType.ControlNet, - ), - StarterModel( - name="lineart", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11p_sd15_lineart", - description="Controlnet weights trained on sd-1.5 with lineart image conditioning", - type=ModelType.ControlNet, - ), - StarterModel( - name="lineart_anime", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11p_sd15s2_lineart_anime", - description="Controlnet weights trained on sd-1.5 with anime image conditioning", - type=ModelType.ControlNet, - ), - StarterModel( - name="openpose", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11p_sd15_openpose", - description="Controlnet weights trained on sd-1.5 with openpose image conditioning", - type=ModelType.ControlNet, - ), - StarterModel( - name="scribble", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11p_sd15_scribble", - description="Controlnet weights trained on sd-1.5 with scribble image conditioning", - type=ModelType.ControlNet, - ), - StarterModel( - name="softedge", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11p_sd15_softedge", - description="Controlnet weights trained on sd-1.5 with soft edge conditioning", - type=ModelType.ControlNet, +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), + }, ), - StarterModel( - name="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, + 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, ), - StarterModel( - name="tile", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11f1e_sd15_tile", - description="Controlnet weights trained on sd-1.5 with tiled image conditioning", - type=ModelType.ControlNet, + 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, ), - StarterModel( - name="ip2p", - base=BaseModelType.StableDiffusion1, - source="lllyasviel/control_v11e_sd15_ip2p", - description="Controlnet weights trained on sd-1.5 with ip2p conditioning.", - type=ModelType.ControlNet, + 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), + }, ), - StarterModel( - name="canny-sdxl", - base=BaseModelType.StableDiffusionXL, - source="diffusers/controlnet-canny-sdxl-1.0", - description="Controlnet weights trained on sdxl-1.0 with canny conditioning.", - type=ModelType.ControlNet, + 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), + }, ), - StarterModel( - name="depth-sdxl", - base=BaseModelType.StableDiffusionXL, - source="diffusers/controlnet-depth-sdxl-1.0", - description="Controlnet weights trained on sdxl-1.0 with depth conditioning.", - type=ModelType.ControlNet, + 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), + }, ), - StarterModel( - name="softedge-dexined-sdxl", - base=BaseModelType.StableDiffusionXL, - source="SargeZT/controlnet-sd-xl-1.0-softedge-dexined", - description="Controlnet weights trained on sdxl-1.0 with dexined soft edge preprocessing.", - type=ModelType.ControlNet, + 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), + }, ), - StarterModel( - name="depth-16bit-zoe-sdxl", - base=BaseModelType.StableDiffusionXL, - source="SargeZT/controlnet-sd-xl-1.0-depth-16bit-zoe", - description="Controlnet weights trained on sdxl-1.0 with Zoe's preprocessor (16 bits).", - type=ModelType.ControlNet, + 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), + }, ), - StarterModel( - name="depth-zoe-sdxl", - base=BaseModelType.StableDiffusionXL, - source="diffusers/controlnet-zoe-depth-sdxl-1.0", - description="Controlnet weights trained on sdxl-1.0 with Zoe's preprocessor (32 bits).", - type=ModelType.ControlNet, + 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, ), - # endregion - # region T2I Adapter - StarterModel( - name="canny-sd15", - base=BaseModelType.StableDiffusion1, - source="TencentARC/t2iadapter_canny_sd15v2", - description="T2I Adapter weights trained on sd-1.5 with canny conditioning.", - type=ModelType.T2IAdapter, + 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, ), - StarterModel( - name="sketch-sd15", - base=BaseModelType.StableDiffusion1, - source="TencentARC/t2iadapter_sketch_sd15v2", - description="T2I Adapter weights trained on sd-1.5 with sketch conditioning.", - type=ModelType.T2IAdapter, + 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, ), - StarterModel( - name="depth-sd15", - base=BaseModelType.StableDiffusion1, - source="TencentARC/t2iadapter_depth_sd15v2", - description="T2I Adapter weights trained on sd-1.5 with depth conditioning.", - type=ModelType.T2IAdapter, + 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, ), - StarterModel( - name="zoedepth-sd15", - base=BaseModelType.StableDiffusion1, - source="TencentARC/t2iadapter_zoedepth_sd15v1", - description="T2I Adapter weights trained on sd-1.5 with zoe depth conditioning.", - type=ModelType.T2IAdapter, + 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), + }, ), - StarterModel( - name="canny-sdxl", - base=BaseModelType.StableDiffusionXL, - source="TencentARC/t2i-adapter-canny-sdxl-1.0", - description="T2I Adapter weights trained on sdxl-1.0 with canny conditioning.", - type=ModelType.T2IAdapter, + 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, ), - StarterModel( - name="zoedepth-sdxl", - base=BaseModelType.StableDiffusionXL, - source="TencentARC/t2i-adapter-depth-zoe-sdxl-1.0", - description="T2I Adapter weights trained on sdxl-1.0 with zoe depth conditioning.", - type=ModelType.T2IAdapter, + 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, ), - StarterModel( - name="lineart-sdxl", - base=BaseModelType.StableDiffusionXL, - source="TencentARC/t2i-adapter-lineart-sdxl-1.0", - description="T2I Adapter weights trained on sdxl-1.0 with lineart conditioning.", - type=ModelType.T2IAdapter, + 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, ), - StarterModel( - name="sketch-sdxl", - base=BaseModelType.StableDiffusionXL, - source="TencentARC/t2i-adapter-sketch-sdxl-1.0", - description="T2I Adapter weights trained on sdxl-1.0 with sketch conditioning.", - type=ModelType.T2IAdapter, + 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, ), - # endregion + 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 index ef1ac2f8a4b..8d104093085 100644 --- a/invokeai/backend/model_manager/util/libc_util.py +++ b/invokeai/backend/model_manager/util/libc_util.py @@ -37,19 +37,21 @@ class Struct_mallinfo2(ctypes.Structure): 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"{'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"{'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"{'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)" + 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" + 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 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 index 2e448520e56..c153129353b 100644 --- a/invokeai/backend/model_manager/util/model_util.py +++ b/invokeai/backend/model_manager/util/model_util.py @@ -1,12 +1,19 @@ -"""Utilities for parsing model files, used mostly by probe.py""" +"""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 picklescan.scanner import scan_file_path + +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]: @@ -41,7 +48,7 @@ def _fast_safetensors_reader(path: str) -> Dict[str, torch.Tensor]: return checkpoint -def read_checkpoint_meta(path: Union[str, Path], scan: bool = False) -> Dict[str, torch.Tensor]: +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 @@ -49,23 +56,41 @@ def read_checkpoint_meta(path: Union[str, Path], scan: bool = False) -> Dict[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 = scan_file_path(path) + scan_result = pscan.scan_file_path(path) if scan_result.infected_files != 0: - raise Exception(f'The model file "{path}" is potentially infected by malware. Aborting import.') + 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, torch.Tensor]) -> Optional[int]: +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, torch.Tensor]) -> Optional[int]: + 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: @@ -111,6 +136,8 @@ def _get_shape_1(key: str, tensor: torch.Tensor, checkpoint: Dict[str, torch.Ten 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 ( @@ -133,3 +160,51 @@ def _get_shape_1(key: str, tensor: torch.Tensor, checkpoint: Dict[str, torch.Ten 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 index 4a63ab27b77..a8428f4edcd 100644 --- a/invokeai/backend/model_manager/util/select_hf_files.py +++ b/invokeai/backend/model_manager/util/select_hf_files.py @@ -17,19 +17,21 @@ from pathlib import Path from typing import Dict, List, Optional, Set -from ..config import ModelRepoVariant +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. + :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, @@ -37,15 +39,28 @@ def filter_files( """ variant = variant or ModelRepoVariant.Default paths: List[Path] = [] - root = files[0].parts[0] + + 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 subfolder and subfolder.suffix in [".safetensors", ".bin", ".onnx", ".xml", ".pth", ".pt", ".ckpt", ".msgpack"]: - return [root / subfolder] + 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")): + if file.name.endswith((".json", ".txt", ".jinja")): # .jinja for chat templates paths.append(file) elif file.name.endswith( ( @@ -54,6 +69,7 @@ def filter_files( "lora_weights.safetensors", "weights.pb", "onnx_data", + "spiece.model", # Added for `black-forest-labs/FLUX.1-schnell`. ) ): paths.append(file) @@ -62,13 +78,13 @@ def filter_files( # 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): + elif re.search(r"model.*\.(safetensors|bin|onnx|xml|pth|pt|ckpt|msgpack)$", file.name): paths.append(file) - # limit search to subfolder if requested - if subfolder: - subfolder = root / subfolder - paths = [x for x in paths if x.parent == Path(subfolder)] + # 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)) @@ -84,6 +100,7 @@ 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: @@ -97,7 +114,10 @@ def _filter_by_variant(files: List[Path], variant: ModelRepoVariant) -> Set[Path if variant == ModelRepoVariant.Flax: result.add(path) - elif path.suffix in [".json", ".txt"]: + # 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 [ @@ -116,19 +136,27 @@ def _filter_by_variant(files: List[Path], variant: ModelRepoVariant) -> Set[Path # 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 candidate_variant_label == f".{variant}" or ( - not candidate_variant_label and variant in [ModelRepoVariant.FP32, ModelRepoVariant.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: @@ -140,9 +168,35 @@ def _filter_by_variant(files: List[Path], variant: ModelRepoVariant) -> Set[Path 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: - result.add(highest_score_candidate.path) + 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 diff --git a/invokeai/backend/model_patcher.py b/invokeai/backend/model_patcher.py index fdc79539ae7..04f99495609 100644 --- a/invokeai/backend/model_patcher.py +++ b/invokeai/backend/model_patcher.py @@ -5,163 +5,38 @@ import pickle from contextlib import contextmanager -from typing import Any, Dict, Generator, Iterator, List, Optional, Tuple, Union +from typing import Any, Generator, Iterator, List, Optional, Tuple, Type, Union -import numpy as np import torch -from diffusers import OnnxRuntimeModel, UNet2DConditionModel +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 import AnyModel from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init -from invokeai.backend.onnx.onnx_runtime import IAIOnnxRuntimeModel +from invokeai.backend.textual_inversion import TextualInversionManager, TextualInversionModelRaw +from invokeai.backend.util.devices import TorchDevice -from .lora import LoRAModelRaw -from .textual_inversion import TextualInversionManager, TextualInversionModelRaw -""" -loras = [ - (lora_model1, 0.7), - (lora_model2, 0.4), -] -with LoRAHelper.apply_lora_unet(unet, loras): - # unet with applied loras -# unmodified unet - -""" - - -# TODO: rename smth like ModelPatcher and add TI method? class ModelPatcher: @staticmethod - def _resolve_lora_key(model: torch.nn.Module, lora_key: str, prefix: str) -> Tuple[str, torch.nn.Module]: - assert "." not in lora_key - - if not lora_key.startswith(prefix): - raise Exception(f"lora_key with invalid prefix: {lora_key}, {prefix}") - - module = model - module_key = "" - key_parts = lora_key[len(prefix) :].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) - - @classmethod - @contextmanager - def apply_lora_unet( - cls, - unet: UNet2DConditionModel, - loras: Iterator[Tuple[LoRAModelRaw, float]], - model_state_dict: Optional[Dict[str, torch.Tensor]] = None, - ) -> Generator[None, None, None]: - with cls.apply_lora( - unet, - loras=loras, - prefix="lora_unet_", - model_state_dict=model_state_dict, - ): - yield - - @classmethod @contextmanager - def apply_lora_text_encoder( - cls, - text_encoder: CLIPTextModel, - loras: Iterator[Tuple[LoRAModelRaw, float]], - model_state_dict: Optional[Dict[str, torch.Tensor]] = None, - ) -> Generator[None, None, None]: - with cls.apply_lora(text_encoder, loras=loras, prefix="lora_te_", model_state_dict=model_state_dict): - yield + def patch_unet_attention_processor(unet: UNet2DConditionModel, processor_cls: Type[Any]): + """A context manager that patches `unet` with the provided attention processor. - @classmethod - @contextmanager - def apply_lora( - cls, - model: AnyModel, - loras: Iterator[Tuple[LoRAModelRaw, float]], - prefix: str, - model_state_dict: Optional[Dict[str, torch.Tensor]] = None, - ) -> Generator[None, None, None]: + 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(...). """ - Apply one or more LoRAs to a model. + unet_orig_processors = unet.attn_processors - :param model: The model to patch. - :param loras: An iterator that returns the LoRA to patch in and its patch weight. - :param prefix: A string prefix that precedes keys used in the LoRAs weight layers. - :model_state_dict: Read-only copy of the model's state dict in CPU, for unpatching purposes. - """ - original_weights = {} + # 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: - with torch.no_grad(): - for lora, lora_weight in loras: - # assert lora.device.type == "cpu" - for layer_key, layer in lora.layers.items(): - if not layer_key.startswith(prefix): - continue - - # TODO(ryand): A non-negligible amount of time is currently spent resolving LoRA keys. This - # should be improved in the following ways: - # 1. The key mapping could be more-efficiently pre-computed. This would save time every time a - # LoRA model is applied. - # 2. From an API perspective, there's no reason that the `ModelPatcher` should be aware of the - # intricacies of Stable Diffusion key resolution. It should just expect the input LoRA - # weights to have valid keys. - assert isinstance(model, torch.nn.Module) - module_key, module = cls._resolve_lora_key(model, layer_key, prefix) - - # 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.) - device = module.weight.device - dtype = module.weight.dtype - - if module_key not in original_weights: - if model_state_dict is not None: # we were provided with the CPU copy of the state dict - original_weights[module_key] = model_state_dict[module_key + ".weight"] - else: - original_weights[module_key] = module.weight.detach().to(device="cpu", copy=True) - - layer_scale = layer.alpha / layer.rank if (layer.alpha and layer.rank) else 1.0 - - # 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(...)'. - layer.to(device=device, non_blocking=True) - layer.to(dtype=torch.float32, non_blocking=True) - # 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. - layer_weight = layer.get_weight(module.weight) * (lora_weight * layer_scale) - layer.to(device=torch.device("cpu"), non_blocking=True) - - assert isinstance(layer_weight, torch.Tensor) # mypy thinks layer_weight is a float|Any ??! - if module.weight.shape != layer_weight.shape: - # TODO: debug on lycoris - assert hasattr(layer_weight, "reshape") - layer_weight = layer_weight.reshape(module.weight.shape) - - assert isinstance(layer_weight, torch.Tensor) # mypy thinks layer_weight is a float|Any ??! - module.weight += layer_weight.to(dtype=dtype, non_blocking=True) - - yield # wait for context manager exit + unet.set_attn_processor(unet_new_processors) + yield None finally: - assert hasattr(model, "get_submodule") # mypy not picking up fact that torch.nn.Module has get_submodule() - with torch.no_grad(): - for module_key, weight in original_weights.items(): - model.get_submodule(module_key).weight.copy_(weight, non_blocking=True) + unet.set_attn_processor(unet_orig_processors) @classmethod @contextmanager @@ -171,6 +46,10 @@ def apply_ti( 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 @@ -248,7 +127,7 @@ def _get_ti_embedding(model_embeddings: torch.nn.Module, ti: TextualInversionMod ) model_embeddings.weight.data[token_id] = embedding.to( - device=text_encoder.device, dtype=text_encoder.dtype + device=TorchDevice.choose_torch_device(), dtype=text_encoder.dtype ) ti_tokens.append(token_id) @@ -267,7 +146,7 @@ def apply_clip_skip( cls, text_encoder: Union[CLIPTextModel, CLIPTextModelWithProjection], clip_skip: int, - ) -> None: + ) -> Generator[None, Any, Any]: skipped_layers = [] try: for _i in range(clip_skip): @@ -285,7 +164,7 @@ def apply_freeu( cls, unet: UNet2DConditionModel, freeu_config: Optional[FreeUConfig] = None, - ) -> None: + ) -> Generator[None, Any, Any]: did_apply_freeu = False try: assert hasattr(unet, "enable_freeu") # mypy doesn't pick up this attribute? @@ -299,200 +178,3 @@ def apply_freeu( assert hasattr(unet, "disable_freeu") # mypy doesn't pick up this attribute? if did_apply_freeu: unet.disable_freeu() - - -class ONNXModelPatcher: - @classmethod - @contextmanager - def apply_lora_unet( - cls, - unet: OnnxRuntimeModel, - loras: Iterator[Tuple[LoRAModelRaw, float]], - ) -> None: - with cls.apply_lora(unet, loras, "lora_unet_"): - yield - - @classmethod - @contextmanager - def apply_lora_text_encoder( - cls, - text_encoder: OnnxRuntimeModel, - loras: List[Tuple[LoRAModelRaw, float]], - ) -> None: - with cls.apply_lora(text_encoder, loras, "lora_te_"): - yield - - # based on - # https://github.com/ssube/onnx-web/blob/ca2e436f0623e18b4cfe8a0363fcfcf10508acf7/api/onnx_web/convert/diffusion/lora.py#L323 - @classmethod - @contextmanager - def apply_lora( - cls, - model: IAIOnnxRuntimeModel, - loras: List[Tuple[LoRAModelRaw, float]], - prefix: str, - ) -> None: - from .models.base import IAIOnnxRuntimeModel - - if not isinstance(model, IAIOnnxRuntimeModel): - raise Exception("Only IAIOnnxRuntimeModel models supported") - - orig_weights = {} - - try: - blended_loras: Dict[str, torch.Tensor] = {} - - for lora, lora_weight in loras: - for layer_key, layer in lora.layers.items(): - if not layer_key.startswith(prefix): - continue - - layer.to(dtype=torch.float32) - layer_key = layer_key.replace(prefix, "") - # TODO: rewrite to pass original tensor weight(required by ia3) - layer_weight = layer.get_weight(None).detach().cpu().numpy() * lora_weight - if layer_key in blended_loras: - blended_loras[layer_key] += layer_weight - else: - blended_loras[layer_key] = layer_weight - - node_names = {} - for node in model.nodes.values(): - node_names[node.name.replace("/", "_").replace(".", "_").lstrip("_")] = node.name - - for layer_key, lora_weight in blended_loras.items(): - conv_key = layer_key + "_Conv" - gemm_key = layer_key + "_Gemm" - matmul_key = layer_key + "_MatMul" - - if conv_key in node_names or gemm_key in node_names: - if conv_key in node_names: - conv_node = model.nodes[node_names[conv_key]] - else: - conv_node = model.nodes[node_names[gemm_key]] - - weight_name = [n for n in conv_node.input if ".weight" in n][0] - orig_weight = model.tensors[weight_name] - - if orig_weight.shape[-2:] == (1, 1): - if lora_weight.shape[-2:] == (1, 1): - new_weight = orig_weight.squeeze((3, 2)) + lora_weight.squeeze((3, 2)) - else: - new_weight = orig_weight.squeeze((3, 2)) + lora_weight - - new_weight = np.expand_dims(new_weight, (2, 3)) - else: - if orig_weight.shape != lora_weight.shape: - new_weight = orig_weight + lora_weight.reshape(orig_weight.shape) - else: - new_weight = orig_weight + lora_weight - - orig_weights[weight_name] = orig_weight - model.tensors[weight_name] = new_weight.astype(orig_weight.dtype) - - elif matmul_key in node_names: - weight_node = model.nodes[node_names[matmul_key]] - matmul_name = [n for n in weight_node.input if "MatMul" in n][0] - - orig_weight = model.tensors[matmul_name] - new_weight = orig_weight + lora_weight.transpose() - - orig_weights[matmul_name] = orig_weight - model.tensors[matmul_name] = new_weight.astype(orig_weight.dtype) - - else: - # warn? err? - pass - - yield - - finally: - # restore original weights - for name, orig_weight in orig_weights.items(): - model.tensors[name] = orig_weight - - @classmethod - @contextmanager - def apply_ti( - cls, - tokenizer: CLIPTokenizer, - text_encoder: IAIOnnxRuntimeModel, - ti_list: List[Tuple[str, Any]], - ) -> Iterator[Tuple[CLIPTokenizer, TextualInversionManager]]: - from .models.base import IAIOnnxRuntimeModel - - if not isinstance(text_encoder, IAIOnnxRuntimeModel): - raise Exception("Only IAIOnnxRuntimeModel models supported") - - orig_embeddings = None - - 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) - - def _get_trigger(ti_name: str, index: int) -> str: - trigger = ti_name - if index > 0: - trigger += f"-!pad-{i}" - return f"<{trigger}>" - - # modify text_encoder - orig_embeddings = text_encoder.tensors["text_model.embeddings.token_embedding.weight"] - - # modify tokenizer - new_tokens_added = 0 - for ti_name, ti in ti_list: - if ti.embedding_2 is not None: - ti_embedding = ( - ti.embedding_2 if ti.embedding_2.shape[1] == orig_embeddings.shape[0] else ti.embedding - ) - else: - ti_embedding = ti.embedding - - for i in range(ti_embedding.shape[0]): - new_tokens_added += ti_tokenizer.add_tokens(_get_trigger(ti_name, i)) - - embeddings = np.concatenate( - (np.copy(orig_embeddings), np.zeros((new_tokens_added, orig_embeddings.shape[1]))), - axis=0, - ) - - for ti_name, _ in ti_list: - ti_tokens = [] - for i in range(ti_embedding.shape[0]): - embedding = ti_embedding[i].detach().numpy() - 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 embeddings[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" {embeddings[token_id].shape[0]}." - ) - - embeddings[token_id] = embedding - ti_tokens.append(token_id) - - if len(ti_tokens) > 1: - ti_manager.pad_tokens[ti_tokens[0]] = ti_tokens[1:] - - text_encoder.tensors["text_model.embeddings.token_embedding.weight"] = embeddings.astype( - orig_embeddings.dtype - ) - - yield ti_tokenizer, ti_manager - - finally: - # restore - if orig_embeddings is not None: - text_encoder.tensors["text_model.embeddings.token_embedding.weight"] = orig_embeddings diff --git a/invokeai/backend/onnx/onnx_runtime.py b/invokeai/backend/onnx/onnx_runtime.py index 9fcd4d093ff..a8132d4b233 100644 --- a/invokeai/backend/onnx/onnx_runtime.py +++ b/invokeai/backend/onnx/onnx_runtime.py @@ -10,7 +10,7 @@ from onnx import numpy_helper from onnxruntime import InferenceSession, SessionOptions, get_available_providers -from ..raw_model import RawModel +from invokeai.backend.raw_model import RawModel ONNX_WEIGHTS_NAME = "model.onnx" @@ -190,12 +190,7 @@ def __call__(self, **kwargs): return self.session.run(None, inputs) # compatability with RawModel ABC - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - non_blocking: bool = False, - ) -> None: + def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None: pass # compatability with diffusers load code 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..e14e35baa08 --- /dev/null +++ b/invokeai/backend/patches/layer_patcher.py @@ -0,0 +1,335 @@ +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), + ) + # Point at the module's live (expanded) parameter so the out-of-place weight + # update below lands on the module. `expanded_weight` is a detached raw tensor; + # reassigning its `.data` would not propagate to the newly-set Parameter. + module_param = module_to_patch.get_parameter(param_name) + 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) + # Apply out-of-place (assign a new tensor) rather than an in-place `copy_`. The weight we + # are patching may be the model's canonical CPU copy, which is shared across the + # per-device model caches in multi-GPU mode (see SharedCpuWeightsStore) and is also the + # cache's keep_ram_copy used to restore the model after unpatching. An in-place mutation + # here would corrupt that shared/cached tensor — and every other device's view of it. + # Reassigning `.data` leaves the original tensor untouched while giving this module the + # patched weights, and is memory-equivalent (the in-place form already allocated the + # `module_param.data + param_weight_converted` temporary). + module_param.data = 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..52203800c81 --- /dev/null +++ b/invokeai/backend/quantization/bnb_llm_int8.py @@ -0,0 +1,166 @@ +import warnings + +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 + +# bitsandbytes' LLM.int8 matmul kernel only supports fp16 activations. Our compute dtype for the +# (T5) encoder is bf16, so bitsandbytes casts bf16->fp16 internally and emits this UserWarning on +# *every* matmul of *every* layer. The cast is correct and intended for LLM.int8, so silence the +# warning here (once, at import) to avoid flooding the logs on each text-encode. +warnings.filterwarnings( + "ignore", + message=r"MatMul8bitLt: inputs will be cast from .* to float16 during quantization", + category=UserWarning, +) + + +# 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 index 7bca6945d98..23502b20cb6 100644 --- a/invokeai/backend/raw_model.py +++ b/invokeai/backend/raw_model.py @@ -1,15 +1,3 @@ -"""Base class for 'Raw' models. - -The RawModel class is the base class of LoRAModelRaw and TextualInversionModelRaw, -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. -""" - from abc import ABC, abstractmethod from typing import Optional @@ -17,13 +5,18 @@ class RawModel(ABC): - """Abstract base class for 'Raw' model wrappers.""" + """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, - non_blocking: bool = False, - ) -> None: + 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 index ed6782eefa4..6a6f2ebc49c 100644 --- a/invokeai/backend/stable_diffusion/__init__.py +++ b/invokeai/backend/stable_diffusion/__init__.py @@ -2,13 +2,14 @@ Initialization file for the invokeai.backend.stable_diffusion package """ -from .diffusers_pipeline import PipelineIntermediateState, StableDiffusionGeneratorPipeline # noqa: F401 -from .diffusion import InvokeAIDiffuserComponent # noqa: F401 -from .seamless import set_seamless # noqa: F401 +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", - "set_seamless", ] 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 index 8b90c815ae7..054e04dcb28 100644 --- a/invokeai/backend/stable_diffusion/diffusers_pipeline.py +++ b/invokeai/backend/stable_diffusion/diffusers_pipeline.py @@ -10,84 +10,37 @@ import psutil import torch import torchvision.transforms as T -from diffusers.models import AutoencoderKL, UNet2DConditionModel -from diffusers.models.controlnet import ControlNetModel +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 import KarrasDiffusionSchedulers -from diffusers.schedulers.scheduling_utils import SchedulerMixin +from diffusers.schedulers.scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin from diffusers.utils.import_utils import is_xformers_available from pydantic import Field -from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer +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 - - -@dataclass -class PipelineIntermediateState: - step: int - order: int - total_steps: int - timestep: int - latents: torch.Tensor - predicted_original: Optional[torch.Tensor] = None - - -@dataclass -class AddsMaskLatents: - """Add the channels required for inpainting model input. - - The inpainting model takes the normal latent channels as input, _plus_ a one-channel mask - and the latent encoding of the base image. - - This class assumes the same mask and base image should apply to all items in the batch. - """ - - forward: Callable[[torch.Tensor, torch.Tensor, torch.Tensor], torch.Tensor] - mask: torch.Tensor - initial_image_latents: torch.Tensor - - def __call__( - self, - latents: torch.Tensor, - t: torch.Tensor, - text_embeddings: torch.Tensor, - **kwargs, - ) -> torch.Tensor: - model_input = self.add_mask_channels(latents) - return self.forward(model_input, t, text_embeddings, **kwargs) - - def add_mask_channels(self, latents): - batch_size = latents.size(0) - # duplicate mask and latents for each batch - mask = einops.repeat(self.mask, "b c h w -> (repeat b) c h w", repeat=batch_size) - image_latents = einops.repeat(self.initial_image_latents, "b c h w -> (repeat b) c h w", repeat=batch_size) - # add mask and image as additional channels - model_input, _ = einops.pack([latents, mask, image_latents], "b * h w") - return model_input - - -def are_like_tensors(a: torch.Tensor, b: object) -> bool: - return isinstance(b, torch.Tensor) and (a.size() == b.size()) +from invokeai.backend.util.hotfixes import ControlNetModel @dataclass class AddsMaskGuidance: - mask: torch.FloatTensor - mask_latents: torch.FloatTensor + mask: torch.Tensor + mask_latents: torch.Tensor scheduler: SchedulerMixin noise: torch.Tensor - gradient_mask: bool + 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: + 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: @@ -100,7 +53,7 @@ def apply_mask(self, latents: torch.Tensor, t) -> torch.Tensor: # 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.gradient_mask: + 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) @@ -186,7 +139,7 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline): 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 ([`CLIPFeatureExtractor`]): + feature_extractor ([`CLIPImageProcessor`]): Model that extracts features from generated images to be used as inputs for the `safety_checker`. """ @@ -198,9 +151,8 @@ def __init__( unet: UNet2DConditionModel, scheduler: KarrasDiffusionSchedulers, safety_checker: Optional[StableDiffusionSafetyChecker], - feature_extractor: Optional[CLIPFeatureExtractor], + feature_extractor: Optional[CLIPImageProcessor], requires_safety_checker: bool = False, - control_model: ControlNetModel = None, ): super().__init__( vae=vae, @@ -214,15 +166,24 @@ def __init__( ) self.invokeai_diffuser = InvokeAIDiffuserComponent(self.unet, self._unet_forward) - self.control_model = control_model - self.use_ip_adapter = False 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": + if config.attention_type == "xformers" and is_xformers_available() and prefer_xformers: self.enable_xformers_memory_efficient_attention() return elif config.attention_type == "sliced": @@ -237,20 +198,24 @@ def _adjust_memory_efficient_attention(self, latents: torch.Tensor): self.disable_attention_slicing() return elif config.attention_type == "torch-sdp": - if hasattr(torch.nn.functional, "scaled_dot_product_attention"): - # diffusers enables sdp automatically - return - else: - raise Exception("torch-sdp attention slicing not available") + # 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' + # The remainder if this code is called when attention_type=='auto'. if self.unet.device.type == "cuda": - if is_xformers_available(): + if is_xformers_available() and prefer_xformers: self.enable_xformers_memory_efficient_attention() return - elif hasattr(torch.nn.functional, "scaled_dot_product_attention"): - # diffusers enables sdp automatically - 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 @@ -280,121 +245,136 @@ def _adjust_memory_efficient_attention(self, latents: torch.Tensor): 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, - num_inference_steps: int, scheduler_step_kwargs: dict[str, Any], conditioning_data: TextConditioningData, - *, noise: Optional[torch.Tensor], + seed: int, timesteps: torch.Tensor, init_timestep: torch.Tensor, - additional_guidance: List[Callable] = None, - callback: Callable[[PipelineIntermediateState], None] = None, - control_data: List[ControlNetData] = None, + 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, - gradient_mask: Optional[bool] = False, - seed: int, + 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 - if additional_guidance is None: - additional_guidance = [] - orig_latents = latents.clone() batch_size = latents.shape[0] - batched_t = init_timestep.expand(batch_size) + 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_t) - - if mask is not None: - if is_inpainting_model(self.unet): - if masked_latents is None: - raise Exception("Source image required for inpaint mask when inpaint model used!") - - self.invokeai_diffuser.model_forward_callback = AddsMaskLatents( - self._unet_forward, mask, masked_latents - ) - else: - # if no noise provided, noisify unmasked area based on seed - 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) - - additional_guidance.append(AddsMaskGuidance(mask, orig_latents, self.scheduler, noise, gradient_mask)) - - try: - latents = self.generate_latents_from_embeddings( - latents, - timesteps, - conditioning_data, - scheduler_step_kwargs=scheduler_step_kwargs, - additional_guidance=additional_guidance, - control_data=control_data, - ip_adapter_data=ip_adapter_data, - t2i_adapter_data=t2i_adapter_data, - callback=callback, - ) - finally: - self.invokeai_diffuser.model_forward_callback = self._unet_forward + latents = self.scheduler.add_noise(latents, noise, batched_init_timestep) - # restore unmasked part after the last step is completed - # in-process masking happens before each step - if mask is not None: - if 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 - - def generate_latents_from_embeddings( - self, - latents: torch.Tensor, - timesteps, - conditioning_data: TextConditioningData, - scheduler_step_kwargs: dict[str, Any], - *, - additional_guidance: List[Callable] = None, - control_data: List[ControlNetData] = None, - ip_adapter_data: Optional[list[IPAdapterData]] = None, - t2i_adapter_data: Optional[list[T2IAdapterData]] = None, - callback: Callable[[PipelineIntermediateState], None] = None, - ) -> torch.Tensor: self._adjust_memory_efficient_attention(latents) - if additional_guidance is None: - additional_guidance = [] - batch_size = latents.shape[0] - - if timesteps.shape[0] == 0: - return 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 - self.use_ip_adapter = use_ip_adapter 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} for ipa in ip_adapter_data] + [ + {"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 ) @@ -402,28 +382,28 @@ def generate_latents_from_embeddings( attn_ctx = unet_attention_patcher.apply_ip_adapter_attention(self.invokeai_diffuser.model) with attn_ctx: - if callback is not None: - callback( - PipelineIntermediateState( - step=-1, - order=self.scheduler.order, - total_steps=len(timesteps), - timestep=self.scheduler.config.num_train_timesteps, - latents=latents, - ) + callback( + PipelineIntermediateState( + step=0, # initial latents + order=self.scheduler.order, + total_steps=len(timesteps), + timestep=self.scheduler.config.num_train_timesteps, + latents=latents, ) + ) - # print("timesteps:", timesteps) for i, t in enumerate(self.progress_bar(timesteps)): batched_t = t.expand(batch_size) step_output = self.step( - batched_t, - latents, - conditioning_data, + t=batched_t, + latents=latents, + conditioning_data=conditioning_data, step_index=i, total_step_count=len(timesteps), scheduler_step_kwargs=scheduler_step_kwargs, - additional_guidance=additional_guidance, + 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, @@ -431,19 +411,28 @@ def generate_latents_from_embeddings( latents = step_output.prev_sample predicted_original = getattr(step_output, "pred_original_sample", None) - if callback is not None: - callback( - PipelineIntermediateState( - step=i, - order=self.scheduler.order, - total_steps=len(timesteps), - timestep=int(t), - latents=latents, - predicted_original=predicted_original, - ) + callback( + PipelineIntermediateState( + step=i + 1, # final latents + order=self.scheduler.order, + total_steps=len(timesteps), + timestep=int(t), + latents=latents, + predicted_original=predicted_original, ) + ) - return latents + # 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( @@ -454,19 +443,20 @@ def step( step_index: int, total_step_count: int, scheduler_step_kwargs: dict[str, Any], - additional_guidance: List[Callable] = None, - control_data: List[ControlNetData] = None, + 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] - if additional_guidance is None: - additional_guidance = [] - # one day we will expand this extension point, but for now it just does denoise masking - for guidance in additional_guidance: - latents = guidance(latents, timestep) + # 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 @@ -512,8 +502,49 @@ def step( 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 @@ -542,17 +573,18 @@ def step( # 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. - for guidance in additional_guidance: - # apply the mask to any "denoised" or "pred_original_sample" fields + # 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 = guidance(step_output.denoised, self.scheduler.timesteps[-1]) + 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 = guidance( + step_output.pred_original_sample = mask_guidance( step_output.pred_original_sample, self.scheduler.timesteps[-1] ) else: - step_output.pred_original_sample = guidance(latents, self.scheduler.timesteps[-1]) + step_output.pred_original_sample = mask_guidance(latents, self.scheduler.timesteps[-1]) return step_output @@ -575,17 +607,6 @@ def _unet_forward( **kwargs, ): """predict the noise residual""" - if is_inpainting_model(self.unet) and latents.size(1) == 4: - # Pad out normal non-inpainting inputs for an inpainting model. - # FIXME: There are too many layers of functions and we have too many different ways of - # overriding things! This should get handled in a way more consistent with the other - # use of AddsMaskLatents. - latents = AddsMaskLatents( - self._unet_forward, - mask=torch.ones_like(latents[:1, :1], device=latents.device, dtype=latents.dtype), - initial_image_latents=torch.zeros_like(latents[:1], device=latents.device, dtype=latents.dtype), - ).add_mask_channels(latents) - # First three args should be positional, not keywords, so torch hooks can see them. return self.unet( latents, diff --git a/invokeai/backend/stable_diffusion/diffusion/__init__.py b/invokeai/backend/stable_diffusion/diffusion/__init__.py index 854d127a369..712542f79cf 100644 --- a/invokeai/backend/stable_diffusion/diffusion/__init__.py +++ b/invokeai/backend/stable_diffusion/diffusion/__init__.py @@ -2,4 +2,6 @@ Initialization file for invokeai.models.diffusion """ -from .shared_invokeai_diffusion import InvokeAIDiffuserComponent # noqa: F401 +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 index 85950a01df5..6a9959f1e87 100644 --- a/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py +++ b/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py @@ -1,10 +1,17 @@ +from __future__ import annotations + import math -from dataclasses import dataclass -from typing import List, Optional, Union +from dataclasses import dataclass, field +from enum import Enum +from typing import TYPE_CHECKING, List, Optional, Tuple, Union import torch -from invokeai.backend.ip_adapter.ip_adapter import IPAdapter +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 @@ -18,11 +25,6 @@ def to(self, device, dtype=None): return self -@dataclass -class ConditioningFieldData: - conditionings: List[BasicConditioningInfo] - - @dataclass class SDXLConditioningInfo(BasicConditioningInfo): """SDXL text conditioning information produced by Compel.""" @@ -36,6 +38,115 @@ def to(self, device, dtype=None): 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 @@ -50,15 +161,29 @@ class IPAdapterConditioningInfo: @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] - - # Either a single weight applied to all steps, or a list of weights for each step. + 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) @@ -95,6 +220,12 @@ def __init__( assert self.masks.shape[1] == len(self.ranges) +class ConditioningMode(Enum): + Both = "both" + Negative = "negative" + Positive = "positive" + + class TextConditioningData: def __init__( self, @@ -103,7 +234,7 @@ def __init__( uncond_regions: Optional[TextConditioningRegions], cond_regions: Optional[TextConditioningRegions], guidance_scale: Union[float, List[float]], - guidance_rescale_multiplier: float = 0, + guidance_rescale_multiplier: float = 0, # TODO: old backend, remove ): self.uncond_text = uncond_text self.cond_text = cond_text @@ -114,6 +245,7 @@ def __init__( # 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 @@ -121,3 +253,114 @@ def __init__( 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 index 1334313fe6e..d0073ddbff8 100644 --- a/invokeai/backend/stable_diffusion/diffusion/custom_atttention.py +++ b/invokeai/backend/stable_diffusion/diffusion/custom_atttention.py @@ -14,6 +14,7 @@ class IPAdapterAttentionWeights: ip_adapter_weights: IPAttentionProcessorWeights skip: bool + negative: bool class CustomAttnProcessor2_0(AttnProcessor2_0): @@ -162,6 +163,10 @@ def __call__( # 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) diff --git a/invokeai/backend/stable_diffusion/diffusion/regional_prompt_data.py b/invokeai/backend/stable_diffusion/diffusion/regional_prompt_data.py index f09cc0a0d21..eddd31f0c42 100644 --- a/invokeai/backend/stable_diffusion/diffusion/regional_prompt_data.py +++ b/invokeai/backend/stable_diffusion/diffusion/regional_prompt_data.py @@ -1,9 +1,14 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + import torch import torch.nn.functional as F -from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( - TextConditioningRegions, -) +if TYPE_CHECKING: + from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( + TextConditioningRegions, + ) class RegionalPromptData: diff --git a/invokeai/backend/stable_diffusion/diffusion/unet_attention_patcher.py b/invokeai/backend/stable_diffusion/diffusion/unet_attention_patcher.py index ac00a8e06ea..00accea6258 100644 --- a/invokeai/backend/stable_diffusion/diffusion/unet_attention_patcher.py +++ b/invokeai/backend/stable_diffusion/diffusion/unet_attention_patcher.py @@ -12,7 +12,8 @@ class UNetIPAdapterData(TypedDict): ip_adapter: IPAdapter - target_blocks: List[str] + target_blocks: List[str] # Blocks where IP-Adapter should be applied + method: str # Style or other method type class UNetAttentionPatcher: @@ -39,12 +40,18 @@ def _prepare_attention_processors(self, unet: UNet2DConditionModel): 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 + ip_adapter_weights=ip_adapter_weights, skip=skip, negative=negative ) ip_adapter_attention_weights_collection.append(ip_adapter_attention_weights) diff --git a/invokeai/backend/stable_diffusion/diffusion_backend.py b/invokeai/backend/stable_diffusion/diffusion_backend.py new file mode 100644 index 00000000000..be3800411ad --- /dev/null +++ b/invokeai/backend/stable_diffusion/diffusion_backend.py @@ -0,0 +1,145 @@ +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 +from invokeai.backend.util.devices import TorchDevice + + +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( # noqa: B020 + tqdm(ctx.inputs.timesteps, desc=f"Denoising{TorchDevice.get_session_device_label()}") + ): + # 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 index 0b780d3ee27..6c02acda512 100644 --- a/invokeai/backend/stable_diffusion/schedulers/__init__.py +++ b/invokeai/backend/stable_diffusion/schedulers/__init__.py @@ -1,3 +1,3 @@ -from .schedulers import SCHEDULER_MAP # noqa: F401 +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 index 3a55d52d4a0..c883767e82d 100644 --- a/invokeai/backend/stable_diffusion/schedulers/schedulers.py +++ b/invokeai/backend/stable_diffusion/schedulers/schedulers.py @@ -1,3 +1,5 @@ +from typing import Any, Literal, Type + from diffusers import ( DDIMScheduler, DDPMScheduler, @@ -16,11 +18,52 @@ 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 = { +SCHEDULER_MAP: dict[SCHEDULER_NAME_VALUES, tuple[Type[SchedulerMixin], dict[str, Any]]] = { "ddim": (DDIMScheduler, {}), "ddpm": (DDPMScheduler, {}), - "deis": (DEISMultistepScheduler, {}), + "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, {}), @@ -29,17 +72,32 @@ "euler": (EulerDiscreteScheduler, {"use_karras_sigmas": False}), "euler_k": (EulerDiscreteScheduler, {"use_karras_sigmas": True}), "euler_a": (EulerAncestralDiscreteScheduler, {}), - "kdpm_2": (KDPM2DiscreteScheduler, {}), - "kdpm_2_a": (KDPM2AncestralDiscreteScheduler, {}), - "dpmpp_2s": (DPMSolverSinglestepScheduler, {"use_karras_sigmas": False}), - "dpmpp_2s_k": (DPMSolverSinglestepScheduler, {"use_karras_sigmas": True}), - "dpmpp_2m": (DPMSolverMultistepScheduler, {"use_karras_sigmas": False}), - "dpmpp_2m_k": (DPMSolverMultistepScheduler, {"use_karras_sigmas": True}), - "dpmpp_2m_sde": (DPMSolverMultistepScheduler, {"use_karras_sigmas": False, "algorithm_type": "sde-dpmsolver++"}), - "dpmpp_2m_sde_k": (DPMSolverMultistepScheduler, {"use_karras_sigmas": True, "algorithm_type": "sde-dpmsolver++"}), + "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}), - "unipc": (UniPCMultistepScheduler, {"cpu_only": True}), + "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/seamless.py b/invokeai/backend/stable_diffusion/seamless.py deleted file mode 100644 index 23ed978c6d0..00000000000 --- a/invokeai/backend/stable_diffusion/seamless.py +++ /dev/null @@ -1,51 +0,0 @@ -from contextlib import contextmanager -from typing import Callable, List, Optional, Tuple, Union - -import torch -import torch.nn as nn -from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL -from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny -from diffusers.models.lora import LoRACompatibleConv -from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel - - -@contextmanager -def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL, AutoencoderTiny], seamless_axes: List[str]): - if not seamless_axes: - yield - return - - # 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: - x_mode = "circular" if "x" in seamless_axes else "constant" - y_mode = "circular" if "y" in seamless_axes else "constant" - - conv_layers: List[torch.nn.Conv2d] = [] - - for module in model.modules(): - if isinstance(module, torch.nn.Conv2d): - conv_layers.append(module) - - for layer in conv_layers: - 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/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 index 0408176edb6..b83d769a8d1 100644 --- a/invokeai/backend/textual_inversion.py +++ b/invokeai/backend/textual_inversion.py @@ -9,7 +9,8 @@ from transformers import CLIPTokenizer from typing_extensions import Self -from .raw_model import RawModel +from invokeai.backend.raw_model import RawModel +from invokeai.backend.util.calc_tensor_size import calc_tensors_size class TextualInversionModelRaw(RawModel): @@ -65,17 +66,16 @@ def from_checkpoint( return result - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - non_blocking: bool = False, - ) -> None: + 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, non_blocking=non_blocking) + 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): diff --git a/invokeai/backend/util/__init__.py b/invokeai/backend/util/__init__.py index 1e4d467cd0d..f24b6db3e12 100644 --- a/invokeai/backend/util/__init__.py +++ b/invokeai/backend/util/__init__.py @@ -2,11 +2,10 @@ Initialization file for invokeai.backend.util """ -from .logging import InvokeAILogger -from .util import GIG, Chdir, directory_size +from invokeai.backend.util.logging import InvokeAILogger +from invokeai.backend.util.util import Chdir, directory_size __all__ = [ - "GIG", "directory_size", "Chdir", "InvokeAILogger", 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/db_maintenance.py b/invokeai/backend/util/db_maintenance.py deleted file mode 100644 index e7d3432121f..00000000000 --- a/invokeai/backend/util/db_maintenance.py +++ /dev/null @@ -1,577 +0,0 @@ -# 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/device_pool.py b/invokeai/backend/util/device_pool.py new file mode 100644 index 00000000000..1e6675161a6 --- /dev/null +++ b/invokeai/backend/util/device_pool.py @@ -0,0 +1,119 @@ +"""Process-global arbiter that lends idle generation GPUs for text-encoder offload. + +In multi-GPU mode (see ``generation_devices``) the session processor runs one generation worker +per GPU. When fewer sessions are running than there are GPUs, some GPUs sit idle. This arbiter lets +a busy worker temporarily *borrow* an idle GPU to host a text encoder, instead of churning the busy +GPU's denoise model in and out of VRAM. + +Correctness hinges on one rule: **a borrowed GPU must never run an encoder at the same time as a +native generation session on that same GPU.** They share that device's single ``ModelCache``, and a +model's forward pass (including in-place LoRA patching) runs with no cache lock held — so two +threads touching the same cached encoder concurrently corrupts it (garbled output). + +To enforce the rule, each generation device has one lock used for *both* roles: + +- A native session holds its device's lock for the entire run (blocking acquire). +- A borrower *try*-acquires another device's lock for the duration of one encoder node; if the lock + is already held (that GPU is running, or just started, a session) the borrow simply fails and the + encoder runs on the worker's own GPU instead. + +Because borrows are non-blocking try-acquires and a session only ever blocking-acquires its *own* +device lock, there is no lock-ordering cycle — the design is deadlock-free. The only cost is that, +in the startup race where a borrow wins the lock a moment before the lent GPU's own session starts, +that session waits out the (short) encoder node before beginning. +""" + +import threading +from typing import Optional + +import torch + +from invokeai.backend.util.devices import TorchDevice + + +class _GenerationDevicePool: + """Arbitrates exclusive use of each generation device between native sessions and borrowers.""" + + def __init__(self) -> None: + self._registry_lock = threading.Lock() + # Registration order is preserved so borrow selection is deterministic (and therefore sticky + # across repeated single-session generations, letting a cached encoder be reused). Maps + # normalized device string -> that device's exclusive-use lock. + self._device_locks: dict[str, threading.Lock] = {} + self._order: list[str] = [] + + def set_generation_devices(self, devices: list[torch.device]) -> None: + """Register the full set of generation devices (called once at processor startup). + + Only CUDA devices participate in idle-offload; others are ignored. + """ + with self._registry_lock: + self._device_locks = {} + self._order = [] + for device in devices: + if device.type != "cuda": + continue + key = str(TorchDevice.normalize(device)) + if key not in self._device_locks: + self._device_locks[key] = threading.Lock() + self._order.append(key) + + def _get_lock(self, device: torch.device) -> Optional[threading.Lock]: + key = str(TorchDevice.normalize(device)) + with self._registry_lock: + return self._device_locks.get(key) + + def acquire_session(self, device: Optional[torch.device]) -> None: + """Take exclusive use of ``device`` for a native generation session (blocking). + + Waits out any in-flight borrow that won the lock first, guaranteeing the session never runs + concurrently with a borrowed encoder on the same GPU. No-op for non-CUDA / unregistered + devices (e.g. legacy single-device mode). + """ + if device is None or device.type != "cuda": + return + lock = self._get_lock(device) + if lock is not None: + lock.acquire() + + def release_session(self, device: Optional[torch.device]) -> None: + """Release the exclusive use taken by :meth:`acquire_session`.""" + if device is None or device.type != "cuda": + return + lock = self._get_lock(device) + if lock is not None: + lock.release() + + def try_borrow(self, exclude: torch.device) -> Optional[torch.device]: + """Try to take exclusive use of an idle CUDA device other than ``exclude`` (non-blocking). + + Returns the borrowed device (whose lock the caller now holds and must release via + :meth:`release_borrow`), or ``None`` if no other registered device is currently free. + Selection is deterministic (lowest registration order) so repeated borrows reuse the same + GPU and the encoder cached there. + """ + if exclude.type != "cuda": + return None + exclude_key = str(TorchDevice.normalize(exclude)) + with self._registry_lock: + candidates = [(key, self._device_locks[key]) for key in self._order if key != exclude_key] + for key, lock in candidates: + if lock.acquire(blocking=False): + return torch.device(key) + return None + + def release_borrow(self, device: torch.device) -> None: + """Release a device taken by :meth:`try_borrow`.""" + lock = self._get_lock(device) + if lock is not None: + lock.release() + + def reset(self) -> None: + """Clear all registered devices (used by tests).""" + with self._registry_lock: + self._device_locks = {} + self._order = [] + + +# Process-global singleton. +GENERATION_DEVICE_POOL = _GenerationDevicePool() diff --git a/invokeai/backend/util/devices.py b/invokeai/backend/util/devices.py index e8380dc8bcd..7a5e8f3e8b9 100644 --- a/invokeai/backend/util/devices.py +++ b/invokeai/backend/util/devices.py @@ -1,3 +1,5 @@ +import threading +from collections import Counter, defaultdict from typing import Dict, Literal, Optional, Union import torch @@ -42,9 +44,56 @@ def torch_dtype(device: torch.device) -> torch.dtype: class TorchDevice: """Abstraction layer for torch devices.""" + CPU_DEVICE = torch.device("cpu") + CUDA_DEVICE = torch.device("cuda") + MPS_DEVICE = torch.device("mps") + + # Per-thread execution device. When set (by a session-processor worker thread bound to a + # specific GPU), `choose_torch_device()` returns it instead of consulting the global config. + # This is the lynchpin that makes the ~79 `choose_torch_device()` call sites (nodes, model + # patcher, etc.) resolve to the calling worker's GPU without per-call-site changes. + _session_device = threading.local() + + @classmethod + def set_session_device(cls, device: Union[str, torch.device]) -> None: + """Pin the calling thread's execution device. Used by multi-GPU session workers.""" + cls._session_device.device = cls.normalize(device) + + @classmethod + def get_session_device(cls) -> Optional[torch.device]: + """Return the calling thread's pinned execution device, or None if unset.""" + return getattr(cls._session_device, "device", None) + + @classmethod + def clear_session_device(cls) -> None: + """Remove the calling thread's pinned execution device, reverting to global config.""" + if hasattr(cls._session_device, "device"): + del cls._session_device.device + + @classmethod + def get_session_device_index(cls) -> Optional[int]: + """Return the CUDA index of the calling thread's effective device, or None if not on CUDA. + + Resolves the thread-local session device when a worker has pinned one (multi-GPU), otherwise + falls back to the globally-configured device. Used to annotate logs/progress with the GPU + number so concurrent sessions can be told apart. + """ + device = cls.get_session_device() or cls.choose_torch_device() + return device.index if device.type == "cuda" else None + + @classmethod + def get_session_device_label(cls) -> str: + """Return a ``" (#N)"`` suffix for the calling thread's CUDA device, or ``""`` when not on CUDA.""" + index = cls.get_session_device_index() + return f" (#{index})" if index is not None else "" + @classmethod def choose_torch_device(cls) -> torch.device: """Return the torch.device to use for accelerated inference.""" + # A worker thread pinned to a specific GPU takes precedence over the global config. + session_device = cls.get_session_device() + if session_device is not None: + return session_device app_config = get_config() if app_config.device != "auto": device = torch.device(app_config.device) @@ -83,11 +132,83 @@ def choose_torch_dtype(cls, device: Optional[torch.device] = None) -> torch.dtyp # CPU / safe fallback return cls._to_dtype("float32") + @classmethod + def get_device_name(cls, device: torch.device) -> str: + """Return the human-readable name for a torch device (e.g. 'AMD Radeon PRO W7900', 'CPU').""" + return torch.cuda.get_device_name(device) if device.type == "cuda" else device.type.upper() + @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() + return cls.get_device_name(cls.choose_torch_device()) + + @classmethod + def get_generation_devices_summary(cls, generation_devices: Union[str, list[str], None]) -> str: + """Build a human-readable summary of the devices that will be used for generation. + + For a single device, returns just its name (e.g. ``'AMD Radeon PRO W7900'`` or ``'CPU'``). For + multiple devices, returns a bracketed list annotating each with its GPU number and device id, + e.g. ``'[AMD Radeon PRO W7900 #1 (cuda:0), AMD Radeon PRO W7900 #2 (cuda:1)]'``. Identically + named GPUs get a 1-based ``#N`` suffix so they can be told apart; a uniquely named device gets + no suffix. + """ + devices = cls.get_generation_devices(generation_devices) + if not devices: + # Empty resolution (e.g. `generation_devices` set to an empty list) falls back to the + # single globally-configured device. + devices = [cls.choose_torch_device()] + + names = [cls.get_device_name(device) for device in devices] + if len(devices) == 1: + return names[0] + + name_counts = Counter(names) + ordinals: dict[str, int] = defaultdict(int) + parts: list[str] = [] + for device, name in zip(devices, names, strict=True): + ordinals[name] += 1 + label = f"{name} #{ordinals[name]}" if name_counts[name] > 1 else name + parts.append(f"{label} ({device})") + return "[" + ", ".join(parts) + "]" + + @classmethod + def get_generation_devices(cls, generation_devices: Union[str, list[str], None]) -> list[torch.device]: + """Resolve the configured `generation_devices` into a concrete, deduplicated device list. + + - ``"auto"`` (the default) expands to every visible CUDA device, or the single best available + device (mps/cpu) when CUDA is unavailable. + - An explicit list is normalized and deduplicated, with order preserved. + - ``None`` or an empty list yields an empty list; the caller decides the single-device fallback. + """ + if generation_devices == "auto": + if torch.cuda.is_available(): + device_strs: list[str] = [f"cuda:{index}" for index in range(torch.cuda.device_count())] + else: + device_strs = [str(cls.choose_torch_device())] + elif not generation_devices: + return [] + else: + device_strs = list(generation_devices) + + devices: list[torch.device] = [] + seen: set[str] = set() + for device_str in device_strs: + device = cls.normalize(device_str) + # Fail fast on a CUDA device that doesn't exist, rather than starting a worker pinned to + # it that only errors cryptically at the first tensor allocation. ("auto" only generates + # valid indices, so this just validates explicitly-configured devices.) + if device.type == "cuda": + if not torch.cuda.is_available(): + raise ValueError(f"generation_devices requested '{device_str}', but no CUDA device is available.") + if device.index is not None and device.index >= torch.cuda.device_count(): + raise ValueError( + f"generation_devices requested '{device_str}', but only {torch.cuda.device_count()} " + f"CUDA device(s) are available (valid indices 0-{torch.cuda.device_count() - 1})." + ) + if str(device) not in seen: + seen.add(str(device)) + devices.append(device) + return devices @classmethod def normalize(cls, device: Union[str, torch.device]) -> torch.device: @@ -108,3 +229,42 @@ def empty_cache(cls) -> None: @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..973f633e95d --- /dev/null +++ b/invokeai/backend/util/gallery_maintenance.py @@ -0,0 +1,594 @@ +# 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_image_filenames_recursive(self): # noqa D102 + """Return the set of all image filenames found anywhere under outputs_path. + + When an image_subfolder_strategy is configured, images are written to + subdirectories of outputs/images (e.g. by date, type or hash). This walks + the entire tree so that images stored in subfolders are not mistaken for + missing files. Thumbnails are stored as .webp and so are excluded by the + .png glob; the images-archive directory is a sibling of outputs/images and + is therefore not traversed. + """ + filepaths = glob.glob(os.path.join(self.outputs_path, "**", "*.png"), recursive=True) + return {os.path.basename(filepath) for filepath in filepaths} + + 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() + # Build an index of every image present anywhere under outputs/images, including any + # subfolders created by an image_subfolder_strategy. A db entry is only orphaned if its + # file is absent from this entire tree, not just the top-level outputs/images directory. + disk_image_filenames = file_mapper.get_all_image_filenames_recursive() + for db_file in db_files: + try: + if db_file not in disk_image_filenames: + 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 index 7e362fe9589..57b07f9d267 100644 --- a/invokeai/backend/util/hotfixes.py +++ b/invokeai/backend/util/hotfixes.py @@ -3,9 +3,9 @@ import diffusers import torch from diffusers.configuration_utils import ConfigMixin, register_to_config -from diffusers.loaders import FromOriginalControlNetMixin +from diffusers.loaders.single_file_model import FromOriginalModelMixin from diffusers.models.attention_processor import AttentionProcessor, AttnProcessor -from diffusers.models.controlnet import ControlNetConditioningEmbedding, ControlNetOutput, zero_module +from diffusers.models.controlnets.controlnet import ControlNetConditioningEmbedding, ControlNetOutput, zero_module from diffusers.models.embeddings import ( TextImageProjection, TextImageTimeEmbedding, @@ -23,6 +23,7 @@ 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 @@ -32,7 +33,9 @@ logger = InvokeAILogger.get_logger(__name__) -class ControlNetModel(ModelMixin, ConfigMixin, FromOriginalControlNetMixin): +# 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. @@ -405,7 +408,8 @@ def from_unet( 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.upcast_attention, + 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, @@ -773,7 +777,7 @@ def forward( diffusers.ControlNetModel = ControlNetModel -diffusers.models.controlnet.ControlNetModel = ControlNetModel +diffusers.models.controlnets.controlnet.ControlNetModel = ControlNetModel # patch LoRACompatibleConv to use original Conv2D forward function diff --git a/invokeai/backend/util/mps_fixes.py b/invokeai/backend/util/mps_fixes.py deleted file mode 100644 index ce21d33b88f..00000000000 --- a/invokeai/backend/util/mps_fixes.py +++ /dev/null @@ -1,245 +0,0 @@ -import math - -import diffusers -import torch - -if torch.backends.mps.is_available(): - torch.empty = torch.zeros - - -_torch_layer_norm = torch.nn.functional.layer_norm - - -def new_layer_norm(input, normalized_shape, weight=None, bias=None, eps=1e-05): - if input.device.type == "mps" and input.dtype == torch.float16: - input = input.float() - if weight is not None: - weight = weight.float() - if bias is not None: - bias = bias.float() - return _torch_layer_norm(input, normalized_shape, weight, bias, eps).half() - else: - return _torch_layer_norm(input, normalized_shape, weight, bias, eps) - - -torch.nn.functional.layer_norm = new_layer_norm - - -_torch_tensor_permute = torch.Tensor.permute - - -def new_torch_tensor_permute(input, *dims): - result = _torch_tensor_permute(input, *dims) - if input.device == "mps" and input.dtype == torch.float16: - result = result.contiguous() - return result - - -torch.Tensor.permute = new_torch_tensor_permute - - -_torch_lerp = torch.lerp - - -def new_torch_lerp(input, end, weight, *, out=None): - if input.device.type == "mps" and input.dtype == torch.float16: - input = input.float() - end = end.float() - if isinstance(weight, torch.Tensor): - weight = weight.float() - if out is not None: - out_fp32 = torch.zeros_like(out, dtype=torch.float32) - else: - out_fp32 = None - result = _torch_lerp(input, end, weight, out=out_fp32) - if out is not None: - out.copy_(out_fp32.half()) - del out_fp32 - return result.half() - - else: - return _torch_lerp(input, end, weight, out=out) - - -torch.lerp = new_torch_lerp - - -_torch_interpolate = torch.nn.functional.interpolate - - -def new_torch_interpolate( - input, - size=None, - scale_factor=None, - mode="nearest", - align_corners=None, - recompute_scale_factor=None, - antialias=False, -): - if input.device.type == "mps" and input.dtype == torch.float16: - return _torch_interpolate( - input.float(), size, scale_factor, mode, align_corners, recompute_scale_factor, antialias - ).half() - else: - return _torch_interpolate(input, size, scale_factor, mode, align_corners, recompute_scale_factor, antialias) - - -torch.nn.functional.interpolate = new_torch_interpolate - -# TODO: refactor it -_SlicedAttnProcessor = diffusers.models.attention_processor.SlicedAttnProcessor - - -class ChunkedSlicedAttnProcessor: - r""" - Processor for implementing sliced attention. - - Args: - slice_size (`int`, *optional*): - The number of steps to compute attention. Uses as many slices as `attention_head_dim // slice_size`, and - `attention_head_dim` must be a multiple of the `slice_size`. - """ - - def __init__(self, slice_size): - assert isinstance(slice_size, int) - slice_size = 1 # TODO: maybe implement chunking in batches too when enough memory - self.slice_size = slice_size - self._sliced_attn_processor = _SlicedAttnProcessor(slice_size) - - def __call__(self, attn, hidden_states, encoder_hidden_states=None, attention_mask=None): - if self.slice_size != 1 or attn.upcast_attention: - return self._sliced_attn_processor(attn, hidden_states, encoder_hidden_states, attention_mask) - - residual = hidden_states - - 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 - ) - attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) - - 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) - dim = query.shape[-1] - query = attn.head_to_batch_dim(query) - - 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) - key = attn.head_to_batch_dim(key) - value = attn.head_to_batch_dim(value) - - batch_size_attention, query_tokens, _ = query.shape - hidden_states = torch.zeros( - (batch_size_attention, query_tokens, dim // attn.heads), device=query.device, dtype=query.dtype - ) - - chunk_tmp_tensor = torch.empty( - self.slice_size, query.shape[1], key.shape[1], dtype=query.dtype, device=query.device - ) - - for i in range(batch_size_attention // self.slice_size): - start_idx = i * self.slice_size - end_idx = (i + 1) * self.slice_size - - query_slice = query[start_idx:end_idx] - key_slice = key[start_idx:end_idx] - attn_mask_slice = attention_mask[start_idx:end_idx] if attention_mask is not None else None - - self.get_attention_scores_chunked( - attn, - query_slice, - key_slice, - attn_mask_slice, - hidden_states[start_idx:end_idx], - value[start_idx:end_idx], - chunk_tmp_tensor, - ) - - hidden_states = attn.batch_to_head_dim(hidden_states) - - # linear proj - hidden_states = attn.to_out[0](hidden_states) - # dropout - hidden_states = attn.to_out[1](hidden_states) - - if input_ndim == 4: - 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 - - return hidden_states - - def get_attention_scores_chunked(self, attn, query, key, attention_mask, hidden_states, value, chunk): - # batch size = 1 - assert query.shape[0] == 1 - assert key.shape[0] == 1 - assert value.shape[0] == 1 - assert hidden_states.shape[0] == 1 - - # dtype = query.dtype - if attn.upcast_attention: - query = query.float() - key = key.float() - - # out_item_size = query.dtype.itemsize - # if attn.upcast_attention: - # out_item_size = torch.float32.itemsize - out_item_size = query.element_size() - if attn.upcast_attention: - out_item_size = 4 - - chunk_size = 2**29 - - out_size = query.shape[1] * key.shape[1] * out_item_size - chunks_count = min(query.shape[1], math.ceil((out_size - 1) / chunk_size)) - chunk_step = max(1, int(query.shape[1] / chunks_count)) - - key = key.transpose(-1, -2) - - def _get_chunk_view(tensor, start, length): - if start + length > tensor.shape[1]: - length = tensor.shape[1] - start - # print(f"view: [{tensor.shape[0]},{tensor.shape[1]},{tensor.shape[2]}] - start: {start}, length: {length}") - return tensor[:, start : start + length] - - for chunk_pos in range(0, query.shape[1], chunk_step): - if attention_mask is not None: - torch.baddbmm( - _get_chunk_view(attention_mask, chunk_pos, chunk_step), - _get_chunk_view(query, chunk_pos, chunk_step), - key, - beta=1, - alpha=attn.scale, - out=chunk, - ) - else: - torch.baddbmm( - torch.zeros((1, 1, 1), device=query.device, dtype=query.dtype), - _get_chunk_view(query, chunk_pos, chunk_step), - key, - beta=0, - alpha=attn.scale, - out=chunk, - ) - chunk = chunk.softmax(dim=-1) - torch.bmm(chunk, value, out=_get_chunk_view(hidden_states, chunk_pos, chunk_step)) - - # del chunk - - -diffusers.models.attention_processor.SlicedAttnProcessor = ChunkedSlicedAttnProcessor 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/test_utils.py b/invokeai/backend/util/test_utils.py index add394e71be..e4208dc848f 100644 --- a/invokeai/backend/util/test_utils.py +++ b/invokeai/backend/util/test_utils.py @@ -7,7 +7,8 @@ from invokeai.app.services.model_manager import ModelManagerServiceBase from invokeai.app.services.model_records import UnknownModelException -from invokeai.backend.model_manager import BaseModelType, LoadedModel, ModelType, SubModelType +from invokeai.backend.model_manager.load.load_base import LoadedModel +from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType @pytest.fixture(scope="session") diff --git a/invokeai/backend/util/util.py b/invokeai/backend/util/util.py index b3466ddba92..fb8671cec29 100644 --- a/invokeai/backend/util/util.py +++ b/invokeai/backend/util/util.py @@ -7,9 +7,6 @@ from PIL import Image -# actual size of a gig -GIG = 1073741824 - def slugify(value: str, allow_unicode: bool = False) -> str: """ @@ -43,11 +40,9 @@ def directory_size(directory: Path) -> int: Return the aggregate size of all files in a directory (bytes). """ sum = 0 - for root, dirs, files in os.walk(directory): + for root, _, files in os.walk(directory): for f in files: sum += Path(root, f).stat().st_size - for d in dirs: - sum += Path(root, d).stat().st_size return sum diff --git a/invokeai/backend/util/vae_working_memory.py b/invokeai/backend/util/vae_working_memory.py new file mode 100644 index 00000000000..8b91dc54161 --- /dev/null +++ b/invokeai/backend/util/vae_working_memory.py @@ -0,0 +1,149 @@ +from typing import Literal + +import torch +from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL +from diffusers.models.autoencoders.autoencoder_kl_qwenimage import AutoencoderKLQwenImage +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_qwen_image( + operation: Literal["encode", "decode"], image_tensor: torch.Tensor, vae: AutoencoderKLQwenImage +) -> int: + """Estimate the working memory required by the invocation in bytes. + + Without this, the Qwen Image VAE encode/decode passes no working-memory estimate to the model + cache, so the cache reserves only its small default and never offloads a large resident + transformer (the VAE weights themselves are tiny). The decode then OOMs on its activations. This + mirrors the other VAE estimators: peak working memory scales ~linearly with the number of output + pixels and the element size. The Qwen Image latents are 5D (B, C, frames, H, W); the trailing two + dims are spatial, same as the 2D VAEs. See #8414. + """ + 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() + + # Calibrated for the Qwen Image VAE, a 3D-conv (video) VAE whose decode allocates large conv3d + # feature maps — a ~1MP decode was measured to peak at ~17 GiB of VRAM, far above the 2D SD/FLUX + # VAEs the generic 2200/1100 constants were tuned for. The reservation must cover that peak AND be + # large enough to make the cache offload an otherwise-resident transformer + text encoder (which + # the decode doesn't need): the offload only frees ~(working_mem - free) bytes, so under-reserving + # leaves the big models resident and the decode OOMs. Over-reserving is safe here (it just offloads + # models the decode doesn't use). Encoding uses ~half the working memory of decoding. + # NOTE: this is linear in output pixels; a sufficiently large output (>~1.5MP) can still exceed + # the card even after offloading everything — that case needs tiled decode, handled separately. + scaling_constant = 13000 if operation == "decode" else 6500 + working_memory = h * w * element_size * scaling_constant + + 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..0be440e33e8 --- /dev/null +++ b/invokeai/backend/z_image/z_image_controlnet_extension.py @@ -0,0 +1,528 @@ +# 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. +""" + +import logging +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 + +logger = logging.getLogger(__name__) + + +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)] + + logger.debug("Actual num_control_blocks: %s", self._num_control_blocks) + logger.debug("control_places: %s", self._control_places) + + first_layer = control_adapter.control_layers[0] + if hasattr(first_layer, "after_proj"): + after_proj_norm = first_layer.after_proj.weight.norm().item() + logger.debug("First control layer after_proj weight norm: %s", after_proj_norm) + if after_proj_norm < 1e-6: + logger.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) + + if not hasattr(self, "_prepare_printed"): + self._prepare_printed = True + logger.debug("Control state prepared: shape %s", 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 + + if not hasattr(self, "_debug_printed"): + self._debug_printed = True + logger.debug("control_unified shape: %s", control_unified.shape) + logger.debug("unified_hidden_states shape: %s", unified_hidden_states.shape) + logger.debug("ctrl_item_seqlens: %s, x_item_seqlens: %s", ctrl_item_seqlens, x_item_seqlens) + + layer0 = self._adapter.control_layers[0] + if hasattr(layer0, "before_proj"): + logger.debug("before_proj weight norm: %.6f", layer0.before_proj.weight.norm().item()) + if hasattr(layer0, "after_proj"): + logger.debug("after_proj weight norm: %.6f", layer0.after_proj.weight.norm().item()) + + if len(self._adapter.control_noise_refiner) > 0: + refiner0 = self._adapter.control_noise_refiner[0] + if hasattr(refiner0, "attn"): + logger.debug("noise_refiner[0] attn.wq norm: %.6f", refiner0.attn.wq.weight.norm().item()) + + 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]) + + if not hasattr(self, "_hints_printed"): + self._hints_printed = True + logger.debug("Number of hints: %s", len(hints)) + if hints: + logger.debug("First hint shape: %s", hints[0].shape) + for i, h in enumerate(hints[:3]): # First 3 hints + logger.debug( + "Hint[%s] mean: %.6f, std: %.6f, min: %.6f, max: %.6f", + i, + h.mean().item(), + h.std().item(), + h.min().item(), + h.max().item(), + ) + + 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 + + if not hasattr(z_image_forward_with_control, "_layers_printed"): + z_image_forward_with_control._layers_printed = True + logger.debug("Base transformer has %s layers", len(transformer.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] + + if not hasattr(z_image_forward_with_control, "_injection_printed"): + z_image_forward_with_control._injection_printed = True + logger.debug("Injection at layer %s (control_layer %s)", layer_idx, control_layer_idx) + logger.debug("Hint mean: %.6f, std: %.6f", hint.mean().item(), hint.std().item()) + logger.debug("Unified mean: %.6f, std: %.6f", unified.mean().item(), unified.std().item()) + logger.debug("control_weight: %s, skip_layers: %s", control_weight, 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..a6707fb7f50 --- /dev/null +++ b/invokeai/backend/z_image/z_image_transformer_patch.py @@ -0,0 +1,240 @@ +"""Utilities for patching the ZImageTransformer2DModel to support regional attention masks.""" + +from contextlib import contextmanager +from typing import Callable, List, Optional, Tuple + +import torch + + +def create_regional_forward( + original_forward: Callable, + regional_attn_mask: torch.Tensor, + img_seq_len: int, + positive_cap_feats: torch.Tensor, +) -> Callable: + """Create a modified forward function that uses a regional attention mask. + + The regional attention mask replaces the internally computed padding mask on the + main transformer layers (alternating with the plain padding mask), allowing for + regional prompting where different image regions attend to different text prompts. + + This delegates to the model's own helper methods (``patchify_and_embed``, + ``_prepare_sequence``, ``_build_unified_sequence``) so it stays in sync with the + upstream diffusers ``ZImageTransformer2DModel.forward`` implementation. Only the + main-layer attention mask is overridden. + + Args: + original_forward: The original forward method of ZImageTransformer2DModel + (kept for signature compatibility; not used directly). + regional_attn_mask: Boolean attention mask of shape (seq_len, seq_len) where + seq_len = img_seq_len + txt_seq_len, ordered [img, txt]. + img_seq_len: Number of (unpadded) image tokens in the sequence. + positive_cap_feats: The exact caption-embedding tensor the regional mask was + built for (the conditioned/positive pass). The regional mask is applied only + to forward calls whose ``cap_feats`` is this same object; the negative/CFG + pass supplies a different tensor and is left to run with the plain padding + mask. Identity is used instead of a token-length heuristic so the positive + and negative passes can never be confused even when their padded lengths + coincide. + + 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. + + Mirrors the basic (non-omni) path of ZImageTransformer2DModel.forward but + injects a regional attention mask into the main transformer layers. + """ + assert patch_size in self.all_patch_size + assert f_patch_size in self.all_f_patch_size + + device = x[0].device + + # Identify which caption inputs belong to the conditioned (positive) pass the regional + # mask was built for. Capture this before patchify_and_embed reassigns ``cap_feats``. + # The negative/CFG pass supplies a different tensor, so object identity distinguishes the + # passes regardless of token length (avoids the positive mask leaking into the uncond + # prediction when prompt lengths happen to pad to the same multiple). + is_positive_pass = [ci is positive_cap_feats for ci in cap_feats] + + # Single adaLN embedding for all tokens (basic mode). + adaln_input = self.t_embedder(t * self.t_scale).type_as(x[0]) + + # Patchify & embed (basic mode: single image per batch item). + ( + x, + cap_feats, + x_size, + x_pos_ids, + cap_pos_ids, + x_pad_mask, + cap_pad_mask, + ) = self.patchify_and_embed(x, cap_feats, patch_size, f_patch_size) + + # X embed & refine. + x_seqlens = [len(xi) for xi in x] + x = self.all_x_embedder[f"{patch_size}-{f_patch_size}"](torch.cat(x, dim=0)) + x, x_freqs, x_mask, _, _ = self._prepare_sequence( + list(x.split(x_seqlens, dim=0)), x_pos_ids, x_pad_mask, self.x_pad_token, None, device + ) + for layer in self.noise_refiner: + x = layer(x, x_mask, x_freqs, adaln_input, None, None, None) + + # Cap embed & refine. + cap_seqlens = [len(ci) for ci in cap_feats] + cap_feats = self.cap_embedder(torch.cat(cap_feats, dim=0)) + cap_feats, cap_freqs, cap_mask, _, _ = self._prepare_sequence( + list(cap_feats.split(cap_seqlens, dim=0)), cap_pos_ids, cap_pad_mask, self.cap_pad_token, None, device + ) + for layer in self.context_refiner: + cap_feats = layer(cap_feats, cap_mask, cap_freqs) + + # Unified sequence: basic mode order [x, cap]. + unified, unified_freqs, unified_mask, _ = self._build_unified_sequence( + x, + x_freqs, + x_seqlens, + None, + cap_feats, + cap_freqs, + cap_seqlens, + None, + None, + None, + None, + None, + False, # omni_mode + device, + ) + + bsz = unified.shape[0] + unified_seqlen = unified.shape[1] + + # --- REGIONAL ATTENTION MASK INJECTION --- + # The regional mask is (S, S) with S = img_seq_len + txt_seq_len, ordered [img, txt], + # using the *unpadded* image and text token counts. In the unified sequence, however, + # both the image block and the caption block are individually padded to a multiple of + # SEQ_MULTI_OF, so the real layout per item is: + # [ img_real | img_pad | txt_real | txt_pad ] + # We therefore scatter the four regional sub-blocks (img-img, img-txt, txt-img, txt-txt) + # into their padding-aware positions instead of assuming a contiguous top-left block. + # + # The patched forward also runs for the negative/CFG pass (a different prompt). The + # regional mask was built for the positive prompt only, so we apply it only to the + # conditioned items and fall back to the plain padding mask otherwise. + regional = regional_attn_mask.to(device=device, dtype=torch.bool) + txt_seq_len = regional.shape[0] - img_seq_len + + # Decide per item whether the regional mask applies, using only cheap scalar checks, so + # that on passes that never match (e.g. every negative/CFG pass) we avoid materializing + # the (bsz, 1, S, S) float mask at all. + applied_regional = [ + is_positive_pass[i] + and txt_seq_len > 0 + and img_seq_len <= x_seqlens[i] + and x_seqlens[i] + cap_seqlens[i] <= unified_seqlen + for i in range(bsz) + ] + + # Main transformer layers: alternate regional mask (even) with plain padding mask (odd). + # If no item matched the positive pass, skip regional injection entirely. + use_regional = any(applied_regional) + + float_mask = None + if use_regional: + # Build a per-item additive float mask. Start from the plain padding mask (0 where a + # token is valid, -inf where it is padding) so non-matching items behave normally. + neg_inf = torch.finfo(unified.dtype).min + zero = torch.zeros((), dtype=unified.dtype, device=device) + float_mask = ( + torch.where( + unified_mask.bool().unsqueeze(1).unsqueeze(1), # (bsz, 1, 1, S) + zero, + torch.full((), neg_inf, dtype=unified.dtype, device=device), + ) + .expand(bsz, 1, unified_seqlen, unified_seqlen) + .clone() + ) + + for i in range(bsz): + if not applied_regional[i]: + continue + x_len = x_seqlens[i] + + ii, it = slice(0, img_seq_len), slice(img_seq_len, img_seq_len + txt_seq_len) + ui = slice(0, img_seq_len) # real image positions in unified item + ut = slice(x_len, x_len + txt_seq_len) # real text positions in unified item + + # Reset the masked region so only regional rules apply to real img/txt tokens; + # their rows start fully blocked and we open the allowed sub-blocks below. + float_mask[i, 0, ui, :] = neg_inf + float_mask[i, 0, ut, :] = neg_inf + + float_mask[i, 0, ui, ui] = torch.where(regional[ii, ii], zero, neg_inf) # img -> img + float_mask[i, 0, ui, ut] = torch.where(regional[ii, it], zero, neg_inf) # img -> txt + float_mask[i, 0, ut, ui] = torch.where(regional[it, ii], zero, neg_inf) # txt -> img + float_mask[i, 0, ut, ut] = torch.where(regional[it, it], zero, neg_inf) # txt -> txt + + for layer_idx, layer in enumerate(self.layers): + attn_mask = float_mask if (use_regional and layer_idx % 2 == 0) else unified_mask + unified = layer(unified, attn_mask, unified_freqs, adaln_input, None, None, None) + + # Final layer + unpatchify. + unified = self.all_final_layer[f"{patch_size}-{f_patch_size}"](unified, c=adaln_input) + x_out = self.unpatchify(list(unified.unbind(dim=0)), 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, + positive_cap_feats: Optional[torch.Tensor] = None, +): + """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. + positive_cap_feats: The caption-embedding tensor the regional mask was built for. + Required when ``regional_attn_mask`` is provided; the mask is applied only to + forward calls whose ``cap_feats`` is this exact object (the conditioned pass). + + Yields: + The (possibly patched) transformer. + """ + if regional_attn_mask is None: + # No regional prompting, use original forward + yield transformer + return + + if positive_cap_feats is None: + raise ValueError("positive_cap_feats is required when regional_attn_mask is provided") + + # 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, positive_cap_feats) + 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/frontend/web/.eslintignore b/invokeai/frontend/web/.eslintignore deleted file mode 100644 index 1cb448ea803..00000000000 --- a/invokeai/frontend/web/.eslintignore +++ /dev/null @@ -1,10 +0,0 @@ -dist/ -static/ -.husky/ -node_modules/ -patches/ -stats.html -index.html -.yarn/ -*.scss -src/services/api/schema.ts diff --git a/invokeai/frontend/web/.eslintrc.js b/invokeai/frontend/web/.eslintrc.js deleted file mode 100644 index 519e725fb4c..00000000000 --- a/invokeai/frontend/web/.eslintrc.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - extends: ['@invoke-ai/eslint-config-react'], - plugins: ['path', 'i18next'], - rules: { - // TODO(psyche): Enable this rule. Requires no default exports in components - many changes. - 'react-refresh/only-export-components': 'off', - // TODO(psyche): Enable this rule. Requires a lot of eslint-disable-next-line comments. - '@typescript-eslint/consistent-type-assertions': 'off', - // https://github.com/qdanik/eslint-plugin-path - 'path/no-relative-imports': ['error', { maxDepth: 0 }], - // https://github.com/edvardchen/eslint-plugin-i18next/blob/HEAD/docs/rules/no-literal-string.md - 'i18next/no-literal-string': 'error', - // https://eslint.org/docs/latest/rules/no-console - 'no-console': 'error', - }, - overrides: [ - /** - * Overrides for stories - */ - { - files: ['*.stories.tsx'], - rules: { - // We may not have i18n available in stories. - 'i18next/no-literal-string': 'off', - }, - }, - ], -}; diff --git a/invokeai/frontend/web/.gitignore b/invokeai/frontend/web/.gitignore index 757d6ebcc84..d71afec17e5 100644 --- a/invokeai/frontend/web/.gitignore +++ b/invokeai/frontend/web/.gitignore @@ -44,4 +44,5 @@ yalc.lock # vitest tsconfig.vitest-temp.json -coverage/ \ No newline at end of file +coverage/ +*.tgz diff --git a/invokeai/frontend/web/.prettierignore b/invokeai/frontend/web/.prettierignore index 0f53a0b0a8c..658baa261ed 100644 --- a/invokeai/frontend/web/.prettierignore +++ b/invokeai/frontend/web/.prettierignore @@ -14,3 +14,4 @@ static/ src/theme/css/overlayscrollbars.css src/theme_/css/overlayscrollbars.css pnpm-lock.yaml +.claude diff --git a/invokeai/frontend/web/.prettierrc.js b/invokeai/frontend/web/.prettierrc.js deleted file mode 100644 index c7f57d14753..00000000000 --- a/invokeai/frontend/web/.prettierrc.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - ...require('@invoke-ai/prettier-config-react'), - overrides: [ - { - files: ['public/locales/*.json'], - options: { - tabWidth: 4, - }, - }, - ], -}; 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 index d50d52754c2..b4989c75564 100644 --- a/invokeai/frontend/web/.storybook/ReduxInit.tsx +++ b/invokeai/frontend/web/.storybook/ReduxInit.tsx @@ -1,19 +1,23 @@ -import { PropsWithChildren, memo, useEffect } from 'react'; -import { modelChanged } from '../src/features/parameters/store/generationSlice'; -import { useAppDispatch } from '../src/app/store/storeHooks'; import { useGlobalModifiersInit } from '@invoke-ai/ui-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((props: PropsWithChildren) => { +export const ReduxInit = memo(({ children }: PropsWithChildren) => { const dispatch = useAppDispatch(); useGlobalModifiersInit(); useEffect(() => { - dispatch(modelChanged({ key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' })); - }, []); + dispatch( + modelChanged({ model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' } }) + ); + }, [dispatch]); - return props.children; + return children; }); ReduxInit.displayName = 'ReduxInit'; diff --git a/invokeai/frontend/web/.storybook/main.ts b/invokeai/frontend/web/.storybook/main.ts index 16638399039..e239c7030b9 100644 --- a/invokeai/frontend/web/.storybook/main.ts +++ b/invokeai/frontend/web/.storybook/main.ts @@ -2,19 +2,13 @@ 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-essentials', - '@storybook/addon-interactions', - '@storybook/addon-storysource', - ], + addons: ['@storybook/addon-links', '@storybook/addon-docs'], + framework: { name: '@storybook/react-vite', options: {}, }, - docs: { - autodocs: 'tag', - }, + core: { disableTelemetry: true, }, diff --git a/invokeai/frontend/web/.storybook/manager.ts b/invokeai/frontend/web/.storybook/manager.ts index 9d5347529a7..b3c26112d8a 100644 --- a/invokeai/frontend/web/.storybook/manager.ts +++ b/invokeai/frontend/web/.storybook/manager.ts @@ -1,5 +1,5 @@ -import { addons } from '@storybook/manager-api'; -import { themes } from '@storybook/theming'; +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 index 8b21b482304..eb3d0391db4 100644 --- a/invokeai/frontend/web/.storybook/preview.tsx +++ b/invokeai/frontend/web/.storybook/preview.tsx @@ -1,17 +1,17 @@ -import { Preview } from '@storybook/react'; -import { themes } from '@storybook/theming'; +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'; -import ThemeLocaleProvider from '../src/app/components/ThemeLocaleProvider'; -import { $baseUrl } from '../src/app/store/nanostores/baseUrl'; -import { createStore } from '../src/app/store/store'; + // 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'; -import { $store } from 'app/store/nanostores/store'; i18n.use(initReactI18next).init({ lng: 'en', @@ -25,9 +25,8 @@ i18n.use(initReactI18next).init({ returnNull: false, }); -const store = createStore(undefined, false); +const store = createStore(); $store.set(store); -$baseUrl.set('http://localhost:9090'); const preview: Preview = { decorators: [ @@ -46,6 +45,7 @@ const preview: Preview = { parameters: { docs: { theme: themes.dark, + codePanel: true, }, }, }; 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 index 995a2812b95..6374ace93ff 100644 --- a/invokeai/frontend/web/README.md +++ b/invokeai/frontend/web/README.md @@ -1,3 +1,3 @@ # Invoke UI - + diff --git a/invokeai/frontend/web/eslint.config.mjs b/invokeai/frontend/web/eslint.config.mjs new file mode 100644 index 00000000000..c1f1eeb0ff7 --- /dev/null +++ b/invokeai/frontend/web/eslint.config.mjs @@ -0,0 +1,247 @@ +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-latest'].rules, + ...pluginStorybook.configs.recommended.rules, + + 'react/jsx-no-bind': [ + 'error', + { + allowBind: true, + allowArrowFunctions: 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 index d74db800da5..5ff8a29d1ca 100644 --- a/invokeai/frontend/web/index.html +++ b/invokeai/frontend/web/index.html @@ -11,9 +11,11 @@ @@ -23,4 +25,4 @@ - + \ No newline at end of file diff --git a/invokeai/frontend/web/knip.ts b/invokeai/frontend/web/knip.ts index db89741fefc..7a32b8dd6ed 100644 --- a/invokeai/frontend/web/knip.ts +++ b/invokeai/frontend/web/knip.ts @@ -9,8 +9,18 @@ const config: KnipConfig = { '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', '@babel/preset-typescript', 'babel-plugin-react-compiler'], paths: { 'public/*': ['public/*'], }, diff --git a/invokeai/frontend/web/openapi.json b/invokeai/frontend/web/openapi.json new file mode 100644 index 00000000000..4ca744496d3 --- /dev/null +++ b/invokeai/frontend/web/openapi.json @@ -0,0 +1,74322 @@ +{ + "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_QwenImage_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_QwenImage_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_QwenImage_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_QwenImage_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_QwenImage_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_QwenImage_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/generation_device_options": { + "get": { + "tags": ["app"], + "summary": "Get Generation Device Options", + "description": "List the devices available for generation, for use with the `generation_devices` setting.", + "operationId": "get_generation_device_options", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/GenerationDeviceOption" + }, + "type": "array", + "title": "Response Get Generation Device Options" + } + } + } + } + } + } + }, + "/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.\n\nIDs for every user's items are returned (item ids carry no sensitive data on their own).\nWhen the corresponding items are hydrated via get_queue_items_by_item_ids, those belonging\nto other users are redacted by sanitize_queue_item_for_user. This lets a non-admin see\npartially-redacted entries for other users' jobs in the queue list, while still revealing\nonly timestamps and status for items they do not own.\n\ncurrent_user is required so the endpoint stays behind authentication in multiuser mode.", + "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 append: When true, recalled reference images (``ip_adapters`` and\n ``reference_images``) are appended to whatever reference images the\n frontend already has, instead of replacing the whole list. Mutually\n exclusive with ``strict`` (which clears omitted parameters).\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)." + }, + { + "name": "append", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "When true, recalled reference images (ip_adapters and reference_images) are appended to the frontend's existing reference-image list instead of replacing it. Mutually exclusive with strict.", + "default": false, + "title": "Append" + }, + "description": "When true, recalled reference images (ip_adapters and reference_images) are appended to the frontend's existing reference-image list instead of replacing it. Mutually exclusive with strict." + } + ], + "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_QwenImage_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", + "description": "Collect statistics on cache performance." + }, + "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." + }, + "GenerationDeviceOption": { + "properties": { + "device": { + "type": "string", + "title": "Device", + "description": "The device identifier, e.g. 'cuda:0', 'mps', or 'cpu'" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Human-readable device name" + } + }, + "type": "object", + "required": ["device", "name"], + "title": "GenerationDeviceOption", + "description": "A device that may be selected for generation." + }, + "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" + }, + "device": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The device processing this session, e.g. 'cuda:1' (set only when running on a CUDA GPU)", + "title": "Device" + } + }, + "required": [ + "timestamp", + "queue_id", + "item_id", + "batch_id", + "origin", + "destination", + "user_id", + "session_id", + "invocation", + "invocation_source_id", + "message", + "percentage", + "image", + "device" + ], + "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" + }, + "generation_devices": { + "anyOf": [ + { + "type": "string", + "const": "auto" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "title": "Generation Devices", + "description": "Devices to use for parallel generation. `auto` (the default) uses every available GPU, running one generation session per GPU concurrently and distributing jobs fairly across users. Provide an explicit list (e.g. `[cuda:0, cuda:1]`) to use specific devices, or a single-device list (e.g. `[cuda:0]`) to run serially. On systems without a GPU, `auto` resolves to the single `cpu`/`mps` device.
Valid values: `auto`, or a list whose entries are each `cpu`, `cuda`, `mps`, or `cuda:N` (where N is a device number)", + "default": "auto" + }, + "offload_text_encoders_to_idle_gpus": { + "type": "boolean", + "title": "Offload Text Encoders To Idle Gpus", + "description": "When running on multiple GPUs, load text encoders onto a currently-idle GPU instead of the one running the denoise pipeline. This avoids churning the denoise model in and out of VRAM to make room for the encoder, and lets a cached encoder be reused across generations. Has no effect unless at least two `generation_devices` are configured and a GPU is idle; under full load encoders run on the session's own GPU as before.", + "default": true + }, + "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_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": "checkpoint", + "title": "Format", + "default": "checkpoint" + }, + "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_Checkpoint_QwenImage_Config", + "description": "Model config for Qwen Image single-file checkpoint models (safetensors, etc).\n\nCovers both raw bf16/fp16 checkpoints and ComfyUI-style fp8_scaled checkpoints.\nThe loader dequantizes fp8 weights back to bf16 at load time; the\n`default_settings.fp8_storage` toggle can then optionally re-cast to fp8 for\nVRAM savings." + }, + "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_QwenImage_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_QwenImage_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_QwenImage_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_QwenImage_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_QwenImage_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" + }, + "device": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Device", + "description": "The device that processed this queue item, e.g. 'cuda:1' (set only when running on a CUDA GPU)" + }, + "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" + }, + "user_pending": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "User Pending", + "description": "Number of the requesting user's queue items with status 'pending' (None for admins/global callers)" + }, + "user_in_progress": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "User In Progress", + "description": "Number of the requesting user's queue items with status 'in_progress' (None for admins/global callers)" + } + }, + "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." + }, + "generation_devices": { + "title": "Generation Devices", + "description": "Devices to use for parallel generation. `auto` uses every available GPU; provide an explicit list (e.g. `[cuda:0, cuda:1]`) to use specific devices. Takes effect after restarting InvokeAI." + } + }, + "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 index f2210e4c680..6c7ea3f65ca 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -21,10 +21,10 @@ "scripts": { "dev": "vite dev", "dev:host": "vite dev --host", - "build": "pnpm run lint && vite build", + "build": "pnpm run lint && vitest run && vite build", "typegen": "node scripts/typegen.js", "preview": "vite preview", - "lint:knip": "knip", + "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 .", @@ -35,125 +35,139 @@ "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" }, - "madge": { - "excludeRegExp": [ - "^index.ts$" - ], - "detectiveOptions": { - "ts": { - "skipTypeImports": true - }, - "tsx": { - "skipTypeImports": true - } - } - }, "dependencies": { - "@chakra-ui/react-use-size": "^2.1.0", - "@dagrejs/dagre": "^1.1.2", - "@dagrejs/graphlib": "^2.2.2", - "@dnd-kit/core": "^6.1.0", - "@dnd-kit/sortable": "^8.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@fontsource-variable/inter": "^5.0.18", - "@invoke-ai/ui-library": "^0.0.25", - "@nanostores/react": "^0.7.2", - "@reduxjs/toolkit": "2.2.3", + "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", + "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2", + "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0", + "@dagrejs/dagre": "^1.1.8", + "@dagrejs/graphlib": "^2.2.4", + "@fontsource-variable/inter": "^5.2.8", + "@invoke-ai/ui-library": "github:invoke-ai/ui-library#v0.0.48", + "@nanostores/react": "^1.1.0", + "@observ33r/object-equals": "^1.1.6", + "@reduxjs/toolkit": "2.8.2", "@roarr/browser-log-writer": "^1.3.0", - "chakra-react-select": "^4.7.6", - "compare-versions": "^6.1.0", - "dateformat": "^5.0.3", - "fracturedjsonjs": "^4.0.1", - "framer-motion": "^11.1.8", - "i18next": "^23.11.3", - "i18next-http-backend": "^2.5.1", - "idb-keyval": "^6.2.1", - "jsondiffpatch": "^0.6.0", - "konva": "^9.3.6", - "lodash-es": "^4.17.21", - "nanostores": "^0.10.3", - "new-github-issue-url": "^1.0.0", - "overlayscrollbars": "^2.7.3", + "@xyflow/react": "^12.10.0", + "ag-psd": "^28.5.1", + "async-mutex": "^0.5.0", + "chakra-react-select": "^4.10.1", + "cmdk": "^1.1.1", + "compare-versions": "^6.1.1", + "dockview": "^4.12.0", + "es-toolkit": "^1.46.1", + "filesize": "^10.1.6", + "fracturedjsonjs": "^4.1.1", + "framer-motion": "^11.18.2", + "i18next": "^25.7.3", + "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.2", + "linkifyjs": "^4.3.2", + "lru-cache": "^11.2.4", + "mtwist": "^1.0.2", + "nanoid": "^5.1.6", + "nanostores": "^1.3.0", + "new-github-issue-url": "^1.1.0", + "overlayscrollbars": "^2.13.0", "overlayscrollbars-react": "^0.5.6", - "query-string": "^9.0.0", - "react": "^18.3.1", - "react-colorful": "^5.6.1", - "react-dom": "^18.3.1", - "react-dropzone": "^14.2.3", - "react-error-boundary": "^4.0.13", - "react-hook-form": "^7.51.4", + "perfect-freehand": "^1.2.2", + "query-string": "^9.3.1", + "raf-throttle": "^2.0.6", + "react": "^19.2.6", + "react-colorful": "^5.7.0", + "react-dom": "^19.2.6", + "react-dropzone": "^14.3.8", + "react-error-boundary": "^6.0.0", + "react-hook-form": "^7.69.0", "react-hotkeys-hook": "4.5.0", - "react-i18next": "^14.1.1", - "react-icons": "^5.2.0", - "react-konva": "^18.2.10", - "react-redux": "9.1.2", - "react-resizable-panels": "^2.0.19", - "react-select": "5.8.0", - "react-use": "^17.5.0", - "react-virtuoso": "^4.7.10", - "reactflow": "^11.11.3", - "redux-dynamic-middlewares": "^2.2.0", - "redux-remember": "^5.1.0", + "react-i18next": "^16.5.0", + "react-icons": "^5.5.0", + "react-redux": "9.2.0", + "react-resizable-panels": "^3.0.6", + "react-router-dom": "^7.12.0", + "react-textarea-autosize": "^8.5.9", + "react-use": "^17.6.0", + "react-virtuoso": "^4.18.6", + "redux-remember": "^5.3.0", "redux-undo": "^1.1.0", - "rfdc": "^1.3.1", - "roarr": "^7.21.1", - "serialize-error": "^11.0.3", - "socket.io-client": "^4.7.5", - "use-debounce": "^10.0.0", + "rfdc": "^1.4.1", + "roarr": "^7.21.2", + "serialize-error": "^12.0.0", + "socket.io-client": "^4.8.3", + "stable-hash": "^0.0.6", + "use-debounce": "^10.0.6", "use-device-pixel-ratio": "^1.1.2", - "use-image": "^1.1.1", - "uuid": "^9.0.1", - "zod": "^3.23.6", - "zod-validation-error": "^3.2.0" + "uuid": "^11.1.0", + "zod": "^4.2.1", + "zod-validation-error": "^4.0.2" }, "peerDependencies": { - "@chakra-ui/react": "^2.8.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "ts-toolbelt": "^9.6.0" + "react": "^18.2.0 || ^19.1.4", + "react-dom": "^18.2.0 || ^19.1.4" }, "devDependencies": { - "@invoke-ai/eslint-config-react": "^0.0.14", - "@invoke-ai/prettier-config-react": "^0.0.7", - "@storybook/addon-essentials": "^8.0.10", - "@storybook/addon-interactions": "^8.0.10", - "@storybook/addon-links": "^8.0.10", - "@storybook/addon-storysource": "^8.0.10", - "@storybook/manager-api": "^8.0.10", - "@storybook/react": "^8.0.10", - "@storybook/react-vite": "^8.0.10", - "@storybook/theming": "^8.0.10", - "@types/dateformat": "^5.0.2", - "@types/lodash-es": "^4.17.12", - "@types/node": "^20.12.10", - "@types/react": "^18.3.1", - "@types/react-dom": "^18.3.0", - "@types/uuid": "^9.0.8", - "@vitejs/plugin-react-swc": "^3.6.0", - "@vitest/coverage-v8": "^1.5.0", - "@vitest/ui": "^1.5.0", - "concurrently": "^8.2.2", + "@babel/preset-typescript": "^7.29.7", + "@eslint/js": "^9.39.2", + "@storybook/addon-docs": "^10.3.6", + "@storybook/addon-links": "^10.3.6", + "@storybook/react-vite": "^10.3.6", + "@types/node": "^22.19.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^8.59.2", + "@typescript-eslint/parser": "^8.59.2", + "@vitejs/plugin-react-swc": "^4.3.0", + "@vitest/coverage-v8": "^4.1.5", + "@vitest/ui": "^4.1.5", + "babel-plugin-react-compiler": "^1.0.0", + "concurrently": "^9.2.1", + "csstype": "^3.2.3", "dpdm": "^3.14.0", - "eslint": "^8.57.0", - "eslint-plugin-i18next": "^6.0.3", - "eslint-plugin-path": "^1.3.0", - "knip": "^5.12.3", + "eslint": "^9.39.2", + "eslint-plugin-i18next": "^6.1.3", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-path": "^2.1.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "eslint-plugin-simple-import-sort": "^13.0.0", + "eslint-plugin-storybook": "^10.3.6", + "eslint-plugin-unused-imports": "^4.4.1", + "globals": "^16.5.0", + "knip": "^5.77.4", + "magic-string": "^0.30.21", "openapi-types": "^12.1.3", - "openapi-typescript": "^6.7.5", - "prettier": "^3.2.5", - "rollup-plugin-visualizer": "^5.12.0", - "storybook": "^8.0.10", - "ts-toolbelt": "^9.6.0", - "tsafe": "^1.6.6", - "typescript": "^5.4.5", - "vite": "^5.2.11", - "vite-plugin-css-injected-by-js": "^3.5.1", - "vite-plugin-dts": "^3.9.1", + "openapi-typescript": "^7.10.1", + "prettier": "^3.8.3", + "rollup-plugin-visualizer": "^6.0.5", + "storybook": "^10.3.6", + "tsafe": "^1.8.12", + "type-fest": "^4.41.0", + "typescript": "^5.9.3", + "vite": "^8.0.11", + "vite-plugin-babel": "^1.6.0", "vite-plugin-eslint": "^1.8.1", - "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.6.0" - } + "vitest": "^4.1.5" + }, + "pnpm": { + "peerDependencyRules": { + "allowedVersions": { + "react": "19", + "react-dom": "19", + "zod": "4" + } + } + }, + "engines": { + "pnpm": "10" + }, + "packageManager": "pnpm@10.12.4" } diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 64189f0d82c..4901ad00405 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -1,7537 +1,7299 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - '@chakra-ui/react': - specifier: ^2.8.2 - version: 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/react-use-size': - specifier: ^2.1.0 - version: 2.1.0(react@18.3.1) - '@dagrejs/dagre': - specifier: ^1.1.2 - version: 1.1.2 - '@dagrejs/graphlib': - specifier: ^2.2.2 - version: 2.2.2 - '@dnd-kit/core': - specifier: ^6.1.0 - version: 6.1.0(react-dom@18.3.1)(react@18.3.1) - '@dnd-kit/sortable': - specifier: ^8.0.0 - version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.3.1) - '@dnd-kit/utilities': - specifier: ^3.2.2 - version: 3.2.2(react@18.3.1) - '@fontsource-variable/inter': - specifier: ^5.0.18 - version: 5.0.18 - '@invoke-ai/ui-library': - specifier: ^0.0.25 - version: 0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.18)(@internationalized/date@3.5.3)(@types/react@18.3.1)(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1) - '@nanostores/react': - specifier: ^0.7.2 - version: 0.7.2(nanostores@0.10.3)(react@18.3.1) - '@reduxjs/toolkit': - specifier: 2.2.3 - version: 2.2.3(react-redux@9.1.2)(react@18.3.1) - '@roarr/browser-log-writer': - specifier: ^1.3.0 - version: 1.3.0 - chakra-react-select: - specifier: ^4.7.6 - version: 4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.4)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - compare-versions: - specifier: ^6.1.0 - version: 6.1.0 - dateformat: - specifier: ^5.0.3 - version: 5.0.3 - fracturedjsonjs: - specifier: ^4.0.1 - version: 4.0.1 - framer-motion: - specifier: ^11.1.8 - version: 11.1.8(react-dom@18.3.1)(react@18.3.1) - i18next: - specifier: ^23.11.3 - version: 23.11.3 - i18next-http-backend: - specifier: ^2.5.1 - version: 2.5.1 - idb-keyval: - specifier: ^6.2.1 - version: 6.2.1 - jsondiffpatch: - specifier: ^0.6.0 - version: 0.6.0 - konva: - specifier: ^9.3.6 - version: 9.3.6 - lodash-es: - specifier: ^4.17.21 - version: 4.17.21 - nanostores: - specifier: ^0.10.3 - version: 0.10.3 - new-github-issue-url: - specifier: ^1.0.0 - version: 1.0.0 - overlayscrollbars: - specifier: ^2.7.3 - version: 2.7.3 - overlayscrollbars-react: - specifier: ^0.5.6 - version: 0.5.6(overlayscrollbars@2.7.3)(react@18.3.1) - query-string: - specifier: ^9.0.0 - version: 9.0.0 - 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-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) - react-dropzone: - specifier: ^14.2.3 - version: 14.2.3(react@18.3.1) - react-error-boundary: - specifier: ^4.0.13 - version: 4.0.13(react@18.3.1) - react-hook-form: - specifier: ^7.51.4 - version: 7.51.4(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-i18next: - specifier: ^14.1.1 - version: 14.1.1(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1) - react-icons: - specifier: ^5.2.0 - version: 5.2.0(react@18.3.1) - react-konva: - specifier: ^18.2.10 - version: 18.2.10(konva@9.3.6)(react-dom@18.3.1)(react@18.3.1) - react-redux: - specifier: 9.1.2 - version: 9.1.2(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1) - react-resizable-panels: - specifier: ^2.0.19 - version: 2.0.19(react-dom@18.3.1)(react@18.3.1) - react-select: - specifier: 5.8.0 - version: 5.8.0(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - react-use: - specifier: ^17.5.0 - version: 17.5.0(react-dom@18.3.1)(react@18.3.1) - react-virtuoso: - specifier: ^4.7.10 - version: 4.7.10(react-dom@18.3.1)(react@18.3.1) - reactflow: - specifier: ^11.11.3 - version: 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - redux-dynamic-middlewares: - specifier: ^2.2.0 - version: 2.2.0 - redux-remember: - specifier: ^5.1.0 - version: 5.1.0(redux@5.0.1) - redux-undo: - specifier: ^1.1.0 - version: 1.1.0 - rfdc: - specifier: ^1.3.1 - version: 1.3.1 - roarr: - specifier: ^7.21.1 - version: 7.21.1 - serialize-error: - specifier: ^11.0.3 - version: 11.0.3 - socket.io-client: - specifier: ^4.7.5 - version: 4.7.5 - use-debounce: - specifier: ^10.0.0 - version: 10.0.0(react@18.3.1) - use-device-pixel-ratio: - specifier: ^1.1.2 - version: 1.1.2(react@18.3.1) - use-image: - specifier: ^1.1.1 - version: 1.1.1(react-dom@18.3.1)(react@18.3.1) - uuid: - specifier: ^9.0.1 - version: 9.0.1 - zod: - specifier: ^3.23.6 - version: 3.23.6 - zod-validation-error: - specifier: ^3.2.0 - version: 3.2.0(zod@3.23.6) - -devDependencies: - '@invoke-ai/eslint-config-react': - specifier: ^0.0.14 - version: 0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5) - '@invoke-ai/prettier-config-react': - specifier: ^0.0.7 - version: 0.0.7(prettier@3.2.5) - '@storybook/addon-essentials': - specifier: ^8.0.10 - version: 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - '@storybook/addon-interactions': - specifier: ^8.0.10 - version: 8.0.10(vitest@1.6.0) - '@storybook/addon-links': - specifier: ^8.0.10 - version: 8.0.10(react@18.3.1) - '@storybook/addon-storysource': - specifier: ^8.0.10 - version: 8.0.10 - '@storybook/manager-api': - specifier: ^8.0.10 - version: 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@storybook/react': - specifier: ^8.0.10 - version: 8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5) - '@storybook/react-vite': - specifier: ^8.0.10 - version: 8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5)(vite@5.2.11) - '@storybook/theming': - specifier: ^8.0.10 - version: 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@types/dateformat': - specifier: ^5.0.2 - version: 5.0.2 - '@types/lodash-es': - specifier: ^4.17.12 - version: 4.17.12 - '@types/node': - specifier: ^20.12.10 - version: 20.12.10 - '@types/react': - specifier: ^18.3.1 - version: 18.3.1 - '@types/react-dom': - specifier: ^18.3.0 - version: 18.3.0 - '@types/uuid': - specifier: ^9.0.8 - version: 9.0.8 - '@vitejs/plugin-react-swc': - specifier: ^3.6.0 - version: 3.6.0(vite@5.2.11) - '@vitest/coverage-v8': - specifier: ^1.5.0 - version: 1.6.0(vitest@1.6.0) - '@vitest/ui': - specifier: ^1.5.0 - version: 1.6.0(vitest@1.6.0) - concurrently: - specifier: ^8.2.2 - version: 8.2.2 - dpdm: - specifier: ^3.14.0 - version: 3.14.0 - eslint: - specifier: ^8.57.0 - version: 8.57.0 - eslint-plugin-i18next: - specifier: ^6.0.3 - version: 6.0.3 - eslint-plugin-path: - specifier: ^1.3.0 - version: 1.3.0(eslint@8.57.0) - knip: - specifier: ^5.12.3 - version: 5.12.3(@types/node@20.12.10)(typescript@5.4.5) - openapi-types: - specifier: ^12.1.3 - version: 12.1.3 - openapi-typescript: - specifier: ^6.7.5 - version: 6.7.5 - prettier: - specifier: ^3.2.5 - version: 3.2.5 - rollup-plugin-visualizer: - specifier: ^5.12.0 - version: 5.12.0 - storybook: - specifier: ^8.0.10 - version: 8.0.10(react-dom@18.3.1)(react@18.3.1) - ts-toolbelt: - specifier: ^9.6.0 - version: 9.6.0 - tsafe: - specifier: ^1.6.6 - version: 1.6.6 - typescript: - specifier: ^5.4.5 - version: 5.4.5 - vite: - specifier: ^5.2.11 - version: 5.2.11(@types/node@20.12.10) - vite-plugin-css-injected-by-js: - specifier: ^3.5.1 - version: 3.5.1(vite@5.2.11) - vite-plugin-dts: - specifier: ^3.9.1 - version: 3.9.1(@types/node@20.12.10)(typescript@5.4.5)(vite@5.2.11) - vite-plugin-eslint: - specifier: ^1.8.1 - version: 1.8.1(eslint@8.57.0)(vite@5.2.11) - vite-tsconfig-paths: - specifier: ^4.3.2 - version: 4.3.2(typescript@5.4.5)(vite@5.2.11) - vitest: - specifier: ^1.6.0 - version: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) +importers: + + .: + dependencies: + '@atlaskit/pragmatic-drag-and-drop': + specifier: ^1.7.7 + version: 1.7.7 + '@atlaskit/pragmatic-drag-and-drop-auto-scroll': + specifier: ^2.1.2 + version: 2.1.2 + '@atlaskit/pragmatic-drag-and-drop-hitbox': + specifier: ^1.1.0 + version: 1.1.0 + '@dagrejs/dagre': + specifier: ^1.1.8 + version: 1.1.8 + '@dagrejs/graphlib': + specifier: ^2.2.4 + version: 2.2.4 + '@fontsource-variable/inter': + specifier: ^5.2.8 + version: 5.2.8 + '@invoke-ai/ui-library': + specifier: github:invoke-ai/ui-library#v0.0.48 + version: https://codeload.github.com/invoke-ai/ui-library/tar.gz/78ddd0a1670af523cfcac186849deb23bd07d419(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(@fontsource-variable/inter@5.2.8)(@types/react@19.2.14)(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3) + '@nanostores/react': + specifier: ^1.1.0 + version: 1.1.0(nanostores@1.3.0)(react@19.2.6) + '@observ33r/object-equals': + specifier: ^1.1.6 + version: 1.1.6 + '@reduxjs/toolkit': + specifier: 2.8.2 + version: 2.8.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6) + '@roarr/browser-log-writer': + specifier: ^1.3.0 + version: 1.3.0 + '@xyflow/react': + specifier: ^12.10.0 + version: 12.10.0(@types/react@19.2.14)(immer@10.2.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + ag-psd: + specifier: ^28.5.1 + version: 28.5.1 + async-mutex: + specifier: ^0.5.0 + version: 0.5.0 + chakra-react-select: + specifier: ^4.10.1 + version: 4.10.1(@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(framer-motion@10.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + compare-versions: + specifier: ^6.1.1 + version: 6.1.1 + dockview: + specifier: ^4.12.0 + version: 4.12.0(react@19.2.6) + es-toolkit: + specifier: ^1.46.1 + version: 1.46.1 + filesize: + specifier: ^10.1.6 + version: 10.1.6 + fracturedjsonjs: + specifier: ^4.1.1 + version: 4.1.1 + framer-motion: + specifier: ^11.18.2 + version: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + i18next: + specifier: ^25.7.3 + version: 25.7.3(typescript@5.9.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.2 + version: 4.3.2(linkifyjs@4.3.2)(react@19.2.6) + linkifyjs: + specifier: ^4.3.2 + version: 4.3.2 + lru-cache: + specifier: ^11.2.4 + version: 11.2.4 + mtwist: + specifier: ^1.0.2 + version: 1.0.2 + nanoid: + specifier: ^5.1.6 + version: 5.1.6 + nanostores: + specifier: ^1.3.0 + version: 1.3.0 + new-github-issue-url: + specifier: ^1.1.0 + version: 1.1.0 + overlayscrollbars: + specifier: ^2.13.0 + version: 2.13.0 + overlayscrollbars-react: + specifier: ^0.5.6 + version: 0.5.6(overlayscrollbars@2.13.0)(react@19.2.6) + perfect-freehand: + specifier: ^1.2.2 + version: 1.2.2 + query-string: + specifier: ^9.3.1 + version: 9.3.1 + raf-throttle: + specifier: ^2.0.6 + version: 2.0.6 + react: + specifier: ^19.2.6 + version: 19.2.6 + react-colorful: + specifier: ^5.7.0 + version: 5.7.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-dom: + specifier: ^19.2.6 + version: 19.2.6(react@19.2.6) + react-dropzone: + specifier: ^14.3.8 + version: 14.3.8(react@19.2.6) + react-error-boundary: + specifier: ^6.0.0 + version: 6.0.0(react@19.2.6) + react-hook-form: + specifier: ^7.69.0 + version: 7.69.0(react@19.2.6) + react-hotkeys-hook: + specifier: 4.5.0 + version: 4.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-i18next: + specifier: ^16.5.0 + version: 16.5.0(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.2.6) + react-redux: + specifier: 9.2.0 + version: 9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1) + react-resizable-panels: + specifier: ^3.0.6 + version: 3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-router-dom: + specifier: ^7.12.0 + version: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-textarea-autosize: + specifier: ^8.5.9 + version: 8.5.9(@types/react@19.2.14)(react@19.2.6) + react-use: + specifier: ^17.6.0 + version: 17.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-virtuoso: + specifier: ^4.18.6 + version: 4.18.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + redux-remember: + specifier: ^5.3.0 + version: 5.3.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.2 + version: 7.21.2 + serialize-error: + specifier: ^12.0.0 + version: 12.0.0 + socket.io-client: + specifier: ^4.8.3 + version: 4.8.3 + stable-hash: + specifier: ^0.0.6 + version: 0.0.6 + use-debounce: + specifier: ^10.0.6 + version: 10.0.6(react@19.2.6) + use-device-pixel-ratio: + specifier: ^1.1.2 + version: 1.1.2(react@19.2.6) + uuid: + specifier: ^11.1.0 + version: 11.1.0 + zod: + specifier: ^4.2.1 + version: 4.2.1 + zod-validation-error: + specifier: ^4.0.2 + version: 4.0.2(zod@4.2.1) + devDependencies: + '@babel/preset-typescript': + specifier: ^7.29.7 + version: 7.29.7(@babel/core@7.28.5) + '@eslint/js': + specifier: ^9.39.2 + version: 9.39.2 + '@storybook/addon-docs': + specifier: ^10.3.6 + version: 10.3.6(@types/react@19.2.14)(esbuild@0.27.7)(rollup@4.54.0)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) + '@storybook/addon-links': + specifier: ^10.3.6 + version: 10.3.6(react@19.2.6)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)) + '@storybook/react-vite': + specifier: ^10.3.6 + version: 10.3.6(esbuild@0.27.7)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(rollup@4.54.0)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) + '@types/node': + specifier: ^22.19.3 + version: 22.19.3 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.59.2 + version: 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.59.2 + version: 8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@vitejs/plugin-react-swc': + specifier: ^4.3.0 + version: 4.3.0(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) + '@vitest/coverage-v8': + specifier: ^4.1.5 + version: 4.1.5(vitest@4.1.5) + '@vitest/ui': + specifier: ^4.1.5 + version: 4.1.5(vitest@4.1.5) + babel-plugin-react-compiler: + specifier: ^1.0.0 + version: 1.0.0 + concurrently: + specifier: ^9.2.1 + version: 9.2.1 + csstype: + specifier: ^3.2.3 + version: 3.2.3 + dpdm: + specifier: ^3.14.0 + version: 3.14.0 + eslint: + specifier: ^9.39.2 + version: 9.39.2(jiti@2.6.1) + eslint-plugin-i18next: + specifier: ^6.1.3 + version: 6.1.3 + eslint-plugin-import: + specifier: ^2.32.0 + version: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-path: + specifier: ^2.1.0 + version: 2.1.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react: + specifier: ^7.37.5 + version: 7.37.5(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-simple-import-sort: + specifier: ^13.0.0 + version: 13.0.0(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-storybook: + specifier: ^10.3.6 + version: 10.3.6(eslint@9.39.2(jiti@2.6.1))(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3) + eslint-plugin-unused-imports: + specifier: ^4.4.1 + version: 4.4.1(@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) + globals: + specifier: ^16.5.0 + version: 16.5.0 + knip: + specifier: ^5.77.4 + version: 5.77.4(@types/node@22.19.3)(typescript@5.9.3) + magic-string: + specifier: ^0.30.21 + version: 0.30.21 + openapi-types: + specifier: ^12.1.3 + version: 12.1.3 + openapi-typescript: + specifier: ^7.10.1 + version: 7.10.1(typescript@5.9.3) + prettier: + specifier: ^3.8.3 + version: 3.8.3 + rollup-plugin-visualizer: + specifier: ^6.0.5 + version: 6.0.5(rolldown@1.0.0-rc.18)(rollup@4.54.0) + storybook: + specifier: ^10.3.6 + version: 10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + tsafe: + specifier: ^1.8.12 + version: 1.8.12 + type-fest: + specifier: ^4.41.0 + version: 4.41.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^8.0.11 + version: 8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1) + vite-plugin-babel: + specifier: ^1.6.0 + version: 1.6.0(@babel/core@7.28.5)(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) + vite-plugin-eslint: + specifier: ^1.8.1 + version: 1.8.1(eslint@9.39.2(jiti@2.6.1))(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) + vitest: + specifier: ^4.1.5 + version: 4.1.5(@types/node@22.19.3)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) packages: - /@adobe/css-tools@4.3.3: - resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} - dev: true + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} - /@ampproject/remapping@2.3.0: - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - dev: true - - /@ark-ui/anatomy@1.3.0(@internationalized/date@3.5.3): - resolution: {integrity: sha512-1yG2MrzUlix6KthjQMCNiHnkXrWwEdFAX6D+HqGJaNu0XvaGul2J+wDNtjsdX+gxiWu1nXXEEOAWlFVYMUf65w==} - dependencies: - '@zag-js/accordion': 0.32.1 - '@zag-js/anatomy': 0.32.1 - '@zag-js/avatar': 0.32.1 - '@zag-js/carousel': 0.32.1 - '@zag-js/checkbox': 0.32.1 - '@zag-js/color-picker': 0.32.1 - '@zag-js/color-utils': 0.32.1 - '@zag-js/combobox': 0.32.1 - '@zag-js/date-picker': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3) - '@zag-js/dialog': 0.32.1 - '@zag-js/editable': 0.32.1 - '@zag-js/file-upload': 0.32.1 - '@zag-js/hover-card': 0.32.1 - '@zag-js/menu': 0.32.1 - '@zag-js/number-input': 0.32.1 - '@zag-js/pagination': 0.32.1 - '@zag-js/pin-input': 0.32.1 - '@zag-js/popover': 0.32.1 - '@zag-js/presence': 0.32.1 - '@zag-js/progress': 0.32.1 - '@zag-js/radio-group': 0.32.1 - '@zag-js/rating-group': 0.32.1 - '@zag-js/select': 0.32.1 - '@zag-js/slider': 0.32.1 - '@zag-js/splitter': 0.32.1 - '@zag-js/switch': 0.32.1 - '@zag-js/tabs': 0.32.1 - '@zag-js/tags-input': 0.32.1 - '@zag-js/toast': 0.32.1 - '@zag-js/toggle-group': 0.32.1 - '@zag-js/tooltip': 0.32.1 - transitivePeerDependencies: - - '@internationalized/date' - dev: false + '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.2': + resolution: {integrity: sha512-6BgAUxSNbQFiG3uqNxf53cDQADn5mSeh/JsQzCHo46GPQnVWIJk77zWC8yZ++0Mfg1ECy02zNrbniF7SgHAhXQ==} - /@ark-ui/react@1.3.0(@internationalized/date@3.5.3)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-JHjNoIX50+mUCTaEGMjfGQWGGi31pKsV646jZJlR/1xohpYJigzg8BvO97cTsVk8fwtur+cm11gz3Nf7f5QUnA==} - peerDependencies: - react: '>=18.0.0' - react-dom: '>=18.0.0' - dependencies: - '@ark-ui/anatomy': 1.3.0(@internationalized/date@3.5.3) - '@zag-js/accordion': 0.32.1 - '@zag-js/avatar': 0.32.1 - '@zag-js/carousel': 0.32.1 - '@zag-js/checkbox': 0.32.1 - '@zag-js/color-picker': 0.32.1 - '@zag-js/color-utils': 0.32.1 - '@zag-js/combobox': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/date-picker': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3) - '@zag-js/dialog': 0.32.1 - '@zag-js/editable': 0.32.1 - '@zag-js/file-upload': 0.32.1 - '@zag-js/hover-card': 0.32.1 - '@zag-js/menu': 0.32.1 - '@zag-js/number-input': 0.32.1 - '@zag-js/pagination': 0.32.1 - '@zag-js/pin-input': 0.32.1 - '@zag-js/popover': 0.32.1 - '@zag-js/presence': 0.32.1 - '@zag-js/progress': 0.32.1 - '@zag-js/radio-group': 0.32.1 - '@zag-js/rating-group': 0.32.1 - '@zag-js/react': 0.32.1(react-dom@18.3.1)(react@18.3.1) - '@zag-js/select': 0.32.1 - '@zag-js/slider': 0.32.1 - '@zag-js/splitter': 0.32.1 - '@zag-js/switch': 0.32.1 - '@zag-js/tabs': 0.32.1 - '@zag-js/tags-input': 0.32.1 - '@zag-js/toast': 0.32.1 - '@zag-js/toggle-group': 0.32.1 - '@zag-js/tooltip': 0.32.1 - '@zag-js/types': 0.32.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - transitivePeerDependencies: - - '@internationalized/date' - dev: false + '@atlaskit/pragmatic-drag-and-drop-hitbox@1.1.0': + resolution: {integrity: sha512-JWt6eVp6Br2FPHRM8s0dUIHQk/jFInGP1f3ti5CdtM1Ji5/pt8Akm44wDC063Gv2i5RGseixtbW0z/t6RYtbdg==} - /@aw-web-design/x-default-browser@1.4.126: - resolution: {integrity: sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==} - hasBin: true - dependencies: - default-browser-id: 3.0.0 - dev: true + '@atlaskit/pragmatic-drag-and-drop@1.7.7': + resolution: {integrity: sha512-jX+68AoSTqO/fhCyJDTZ38Ey6/wyL2Iq+J/moanma0YyktpnoHxevjY1UNJHYp0NCburdQDZSL1ZFac1mO1osQ==} - /@babel/code-frame@7.24.2: - resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.24.5 - picocolors: 1.0.0 - /@babel/compat-data@7.24.4: - resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} - dev: true - /@babel/core@7.24.5: - resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) - '@babel/helpers': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 - convert-source-map: 2.0.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/generator@7.24.5: - resolution: {integrity: sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==} + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 - dev: true - /@babel/helper-annotate-as-pure@7.22.5: - resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: - resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-compilation-targets@7.23.6: - resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/compat-data': 7.24.4 - '@babel/helper-validator-option': 7.23.5 - browserslist: 4.23.0 - lru-cache: 5.1.1 - semver: 6.3.1 - dev: true - /@babel/helper-create-class-features-plugin@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-member-expression-to-functions': 7.24.5 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - semver: 6.3.1 - dev: true - /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.5): - resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - regexpu-core: 5.3.2 - semver: 6.3.1 - dev: true - /@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.5): - resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.5 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.8 - transitivePeerDependencies: - - supports-color - dev: true + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} - /@babel/helper-environment-visitor@7.22.20: - resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-function-name@7.23.0: - resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.24.0 - '@babel/types': 7.24.5 - dev: true - /@babel/helper-hoist-variables@7.22.5: - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-member-expression-to-functions@7.24.5: - resolution: {integrity: sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-module-imports@7.24.3: - resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - /@babel/helper-module-transforms@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==} + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-simple-access': 7.24.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/helper-validator-identifier': 7.24.5 - dev: true - /@babel/helper-optimise-call-expression@7.22.5: - resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - - /@babel/helper-plugin-utils@7.24.5: - resolution: {integrity: sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==} - engines: {node: '>=6.9.0'} - dev: true + peerDependencies: + '@babel/core': ^7.0.0 - /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.5): - resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-wrap-function': 7.24.5 - dev: true - /@babel/helper-replace-supers@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==} + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-member-expression-to-functions': 7.24.5 - '@babel/helper-optimise-call-expression': 7.22.5 - dev: true - /@babel/helper-simple-access@7.24.5: - resolution: {integrity: sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==} + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-skip-transparent-expression-wrappers@7.22.5: - resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-split-export-declaration@7.24.5: - resolution: {integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/helper-string-parser@7.24.1: - resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-identifier@7.24.5: - resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-option@7.23.5: - resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-wrap-function@7.24.5: - resolution: {integrity: sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-function-name': 7.23.0 - '@babel/template': 7.24.0 - '@babel/types': 7.24.5 - dev: true - /@babel/helpers@7.24.5: - resolution: {integrity: sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==} + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/highlight@7.24.5: - resolution: {integrity: sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==} + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.24.5 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.0.0 - /@babel/parser@7.24.5: - resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true - dependencies: - '@babel/types': 7.24.5 - dev: true - /@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-LdXRi1wEMTrHVR4Zc9F8OewC3vdm5h4QB6L71zy6StmYeqGi1b3ttIO8UC+BfZKcH9jdr4aI249rBkm+3+YvHw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==} + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.13.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.5) - dev: true + '@babel/core': ^7.0.0-0 - /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==} + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@babel/core': ^7.0.0-0 - /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.5): - resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + '@babel/plugin-transform-modules-commonjs@7.29.7': + resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - dev: true - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.5): - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + '@babel/plugin-transform-typescript@7.29.7': + resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.5): - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + '@babel/preset-typescript@7.29.7': + resolution: {integrity: sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.5): - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.5): - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.5): - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} - /@babel/plugin-syntax-flow@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA==} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-import-assertions@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-import-attributes@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==} + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.5): - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.5): - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} - /@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.5): - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.5): - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.5): - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.5): - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@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==} - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.5): - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + '@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: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + react: '>=18' - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.5): - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + '@chakra-ui/hooks@2.4.5': + resolution: {integrity: sha512-601fWfHE2i7UjaxK/9lDLlOni6vk/I+04YDbM0BrelJy+eqxdlOmoN8Z6MZ3PzFh7ofERUASor+vL+/HaCaZ7w==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + react: '>=18' - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.5): - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} + '@chakra-ui/icon@3.2.0': + resolution: {integrity: sha512-xxjGLvlX2Ys4H0iHrI16t74rG9EBcpFvJ3Y3B7KMQTrnW34Kf7Da/UC8J67Gtx85mTHW020ml85SVPKORWNNKQ==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@chakra-ui/system': '>=2.0.0' + react: '>=18' - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.5): - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} + '@chakra-ui/icons@2.2.4': + resolution: {integrity: sha512-l5QdBgwrAg3Sc2BRqtNkJpfuLw/pWRDwwT58J6c4PqQT6wzXxyNa8Q0PForu1ltB5qEiFb1kxr/F/HO1EwNa6g==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@chakra-ui/react': '>=2.0.0' + react: '>=18' - /@babel/plugin-syntax-typescript@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==} - engines: {node: '>=6.9.0'} + '@chakra-ui/layout@2.3.1': + resolution: {integrity: sha512-nXuZ6WRbq0WdgnRgLw+QuxWAHuhDtVX8ElWqcTK+cSMFg/52eVP47czYBE5F35YhnoW2XBwfNoNgZ7+e8Z01Rg==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@chakra-ui/system': '>=2.0.0' + react: '>=18' - /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.5): - resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} - engines: {node: '>=6.9.0'} + '@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: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - dev: true + react: '>=18' + react-dom: '>=18' - /@babel/plugin-transform-arrow-functions@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==} - engines: {node: '>=6.9.0'} + '@chakra-ui/react-children-utils@2.0.6': + resolution: {integrity: sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + react: '>=18' - /@babel/plugin-transform-async-generator-functions@7.24.3(@babel/core@7.24.5): - resolution: {integrity: sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==} - engines: {node: '>=6.9.0'} + '@chakra-ui/react-context@2.1.0': + resolution: {integrity: sha512-iahyStvzQ4AOwKwdPReLGfDesGG+vWJfEsn0X/NoGph/SkN+HXtv2sCfYFFR9k7bb+Kvc6YfpLlSuLvKMHi2+w==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.5) - dev: true + react: '>=18' - /@babel/plugin-transform-async-to-generator@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==} - engines: {node: '>=6.9.0'} + '@chakra-ui/react-use-safe-layout-effect@2.1.0': + resolution: {integrity: sha512-Knbrrx/bcPwVS1TorFdzrK/zWA8yuU/eaXDkNj24IrKoRlQrSBFarcgAEzlCHtzuhufP3OULPkELTzz91b0tCw==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.5) - dev: true + react: '>=18' - /@babel/plugin-transform-block-scoped-functions@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==} - engines: {node: '>=6.9.0'} + '@chakra-ui/react-utils@2.0.12': + resolution: {integrity: sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + react: '>=18' - /@babel/plugin-transform-block-scoping@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==} - engines: {node: '>=6.9.0'} + '@chakra-ui/react@2.10.9': + resolution: {integrity: sha512-lhdcgoocOiURwBNR3L8OioCNIaGCZqRfuKioLyaQLjOanl4jr0PQclsGb+w0cmito252vEWpsz2xRqF7y+Flrw==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@emotion/react': '>=11' + '@emotion/styled': '>=11' + framer-motion: '>=4.0.0' + react: '>=18' + react-dom: '>=18' - /@babel/plugin-transform-class-properties@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==} - engines: {node: '>=6.9.0'} + '@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: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@emotion/react': ^11.0.0 + '@emotion/styled': ^11.0.0 + react: '>=18' - /@babel/plugin-transform-class-static-block@7.24.4(@babel/core@7.24.5): - resolution: {integrity: sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==} - engines: {node: '>=6.9.0'} + '@chakra-ui/theme-tools@2.1.2': + resolution: {integrity: sha512-Qdj8ajF9kxY4gLrq7gA+Azp8CtFHGO9tWMN2wfF9aQNgG9AuMhPrUzMq9AMQ0MXiYcgNq/FD3eegB43nHVmXVA==} peerDependencies: - '@babel/core': ^7.12.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.5) - dev: true + '@chakra-ui/styled-system': '>=2.0.0' - /@babel/plugin-transform-classes@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==} - engines: {node: '>=6.9.0'} + '@chakra-ui/theme-tools@2.2.6': + resolution: {integrity: sha512-3UhKPyzKbV3l/bg1iQN9PBvffYp+EBOoYMUaeTUdieQRPFzo2jbYR0lNCxqv8h5aGM/k54nCHU2M/GStyi9F2A==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) - '@babel/helper-split-export-declaration': 7.24.5 - globals: 11.12.0 - dev: true - - /@babel/plugin-transform-computed-properties@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==} - engines: {node: '>=6.9.0'} + '@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: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/template': 7.24.0 - dev: true + '@chakra-ui/styled-system': '>=2.0.0' - /@babel/plugin-transform-destructuring@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==} - engines: {node: '>=6.9.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: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@chakra-ui/styled-system': '>=2.8.0' - /@babel/plugin-transform-dotall-regex@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==} - engines: {node: '>=6.9.0'} + '@chakra-ui/theme@3.4.9': + resolution: {integrity: sha512-GAom2SjSdRWTcX76/2yJOFJsOWHQeBgaynCUNBsHq62OafzvELrsSHDUw0bBqBb1c2ww0CclIvGilPup8kXBFA==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@chakra-ui/styled-system': '>=2.8.0' - /@babel/plugin-transform-duplicate-keys@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==} - engines: {node: '>=6.9.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: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + react: '>=16.8.0' - /@babel/plugin-transform-dynamic-import@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==} - engines: {node: '>=6.9.0'} + '@chakra-ui/utils@2.2.5': + resolution: {integrity: sha512-KTBCK+M5KtXH6p54XS39ImQUMVtAx65BoZDoEms3LuObyTo1+civ1sMm4h3nRT320U6H5H7D35WnABVQjqU/4g==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.5) - dev: true + react: '>=16.8.0' - /@babel/plugin-transform-exponentiation-operator@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@dagrejs/dagre@1.1.8': + resolution: {integrity: sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw==} - /@babel/plugin-transform-export-namespace-from@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.5) - dev: true + '@dagrejs/graphlib@2.2.4': + resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==} + engines: {node: '>17.0.0'} - /@babel/plugin-transform-flow-strip-types@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-iIYPIWt3dUmUKKE10s3W+jsQ3icFkw0JyRVyY1B7G4yK/nngAOHLVx8xlhA6b/Jzl/Y0nis8gjqhqKtRDQqHWQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-flow': 7.24.1(@babel/core@7.24.5) - dev: true + '@dmsnell/diff-match-patch@1.1.0': + resolution: {integrity: sha512-yejLPmM5pjsGvxS9gXablUSbInW7H976c/FJ4iQxWIm7/38xBySRemTPDe34lhg1gVLbJntX0+sH0jYfU+PN9A==} - /@babel/plugin-transform-for-of@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - dev: true + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - /@babel/plugin-transform-function-name@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@emnapi/core@1.7.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} - /@babel/plugin-transform-json-strings@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.5) - dev: true + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - /@babel/plugin-transform-literals@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} - /@babel/plugin-transform-logical-assignment-operators@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.5) - dev: true + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - /@babel/plugin-transform-member-expression-literals@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - /@babel/plugin-transform-modules-amd@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} - /@babel/plugin-transform-modules-commonjs@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-simple-access': 7.24.5 - dev: true + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} - /@babel/plugin-transform-modules-systemjs@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-validator-identifier': 7.24.5 - dev: true + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} - /@babel/plugin-transform-modules-umd@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@emotion/is-prop-valid@0.8.8': + resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} - /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.5): - resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} - /@babel/plugin-transform-new-target@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@emotion/memoize@0.7.4': + resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} - /@babel/plugin-transform-nullish-coalescing-operator@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5) - dev: true + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} - /@babel/plugin-transform-numeric-separator@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==} - engines: {node: '>=6.9.0'} + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.5) - dev: true + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true - /@babel/plugin-transform-object-rest-spread@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-transform-parameters': 7.24.5(@babel/core@7.24.5) - dev: true + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} - /@babel/plugin-transform-object-super@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) - dev: true + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} - /@babel/plugin-transform-optional-catch-binding@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==} - engines: {node: '>=6.9.0'} + '@emotion/styled@11.14.1': + resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.5) - dev: true + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true - /@babel/plugin-transform-optional-chaining@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5) - dev: true + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} - /@babel/plugin-transform-parameters@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==} - engines: {node: '>=6.9.0'} + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + react: '>=16.8.0' - /@babel/plugin-transform-private-methods@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} - /@babel/plugin-transform-private-property-in-object@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.5) - dev: true + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} - /@babel/plugin-transform-property-literals@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] - /@babel/plugin-transform-regenerator@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - regenerator-transform: 0.15.2 - dev: true + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] - /@babel/plugin-transform-reserved-words@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] - /@babel/plugin-transform-shorthand-properties@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] - /@babel/plugin-transform-spread@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - dev: true + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] - /@babel/plugin-transform-sticky-regex@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] - /@babel/plugin-transform-template-literals@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] - /@babel/plugin-transform-typeof-symbol@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] - /@babel/plugin-transform-typescript@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.5) - dev: true + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] - /@babel/plugin-transform-unicode-escapes@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] - /@babel/plugin-transform-unicode-property-regex@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] - /@babel/plugin-transform-unicode-regex@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] - /@babel/plugin-transform-unicode-sets-regex@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) - '@babel/helper-plugin-utils': 7.24.5 - dev: true + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] - /@babel/preset-env@7.24.5(@babel/core@7.24.5): - resolution: {integrity: sha512-UGK2ifKtcC8i5AI4cH+sbLLuLc2ktYSFJgBAXorKAsHUZmrQ1q6aQ6i3BvU24wWs2AAKqQB6kq3N9V9Gw1HiMQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.24.4 - '@babel/core': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.5) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.5) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-import-assertions': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-syntax-import-attributes': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.5) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.5) - '@babel/plugin-transform-arrow-functions': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-async-generator-functions': 7.24.3(@babel/core@7.24.5) - '@babel/plugin-transform-async-to-generator': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-block-scoped-functions': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-block-scoping': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-class-static-block': 7.24.4(@babel/core@7.24.5) - '@babel/plugin-transform-classes': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-computed-properties': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-destructuring': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-dotall-regex': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-duplicate-keys': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-dynamic-import': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-exponentiation-operator': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-export-namespace-from': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-for-of': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-function-name': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-json-strings': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-literals': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-logical-assignment-operators': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-member-expression-literals': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-modules-amd': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-modules-systemjs': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-modules-umd': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.5) - '@babel/plugin-transform-new-target': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-numeric-separator': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-object-rest-spread': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-object-super': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-optional-catch-binding': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-parameters': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-private-property-in-object': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-property-literals': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-regenerator': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-reserved-words': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-shorthand-properties': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-spread': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-sticky-regex': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-template-literals': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-typeof-symbol': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-unicode-escapes': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-unicode-property-regex': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-unicode-regex': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-unicode-sets-regex': 7.24.1(@babel/core@7.24.5) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.5) - babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.5) - babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.5) - babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.5) - core-js-compat: 3.37.0 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - dev: true + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] - /@babel/preset-flow@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-sWCV2G9pcqZf+JHyv/RyqEIpFypxdCSxWIxQjpdaQxenNog7cN1pr76hg8u0Fz8Qgg0H4ETkGcJnXL8d4j0PPA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-transform-flow-strip-types': 7.24.1(@babel/core@7.24.5) - dev: true + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] - /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.5): - resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} - peerDependencies: - '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/types': 7.24.5 - esutils: 2.0.3 - dev: true + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] - /@babel/preset-typescript@7.24.1(@babel/core@7.24.5): - resolution: {integrity: sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-plugin-utils': 7.24.5 - '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-typescript': 7.24.5(@babel/core@7.24.5) - dev: true + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] - /@babel/register@7.23.7(@babel/core@7.24.5): - resolution: {integrity: sha512-EjJeB6+kvpk+Y5DAkEAmbOBEFkh9OASx0huoEkqYTFxAZHzOAX2Oh5uwAUuL2rUddqfM0SA+KPXV2TbzoZ2kvQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.24.5 - clone-deep: 4.0.1 - find-cache-dir: 2.1.0 - make-dir: 2.1.0 - pirates: 4.0.6 - source-map-support: 0.5.21 - dev: true + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] - /@babel/regjsgen@0.8.0: - resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} - dev: true + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] - /@babel/runtime@7.23.9: - resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.14.1 - dev: false + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] - /@babel/runtime@7.24.1: - resolution: {integrity: sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.14.1 - dev: false + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] - /@babel/runtime@7.24.5: - resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.14.1 + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] - /@babel/template@7.24.0: - resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.24.2 - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 - dev: true - - /@babel/traverse@7.24.5: - resolution: {integrity: sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] - /@babel/types@7.24.5: - resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.24.1 - '@babel/helper-validator-identifier': 7.24.5 - to-fast-properties: 2.0.0 + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] - /@base2/pretty-print-object@1.0.1: - resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} - dev: true + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] - /@bcoe/v8-coverage@0.2.3: - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - dev: true + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] - /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): - resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==} + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/descendant': 3.1.0(react@18.3.1) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) - framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - dev: false + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): - resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/descendant': 3.1.0(react@18.3.1) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) - framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - dev: false + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - /@chakra-ui/alert@2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-jHg4LYMRNOJH830ViLuicjb3F+v6iriE/2G5T+Sd0Hna04nukNJ1MxUmBPE+vI22me2dIflfelu2v9wdB6Pojw==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - /@chakra-ui/anatomy@2.2.2: - resolution: {integrity: sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg==} - dev: false + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - /@chakra-ui/avatar@2.3.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-8gKSyLfygnaotbJbDMHDiJoF38OHXUYVme4gGxZ1fLnQEdPVEaIWfH+NndIjOM0z8S+YEFnT9KyGMUtvPrBk3g==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@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.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - /@chakra-ui/breadcrumb@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-4cWCG24flYBxjruRi4RJREWTGF74L/KzI2CognAW/d/zWR0CjiScuJhf37Am3LFbCySP6WSoyBOtTIoTA4yLEA==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@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.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - /@chakra-ui/breakpoint-utils@2.0.8: - resolution: {integrity: sha512-Pq32MlEX9fwb5j5xx8s18zJMARNHlQZH2VH1RZgfgRDpp7DcEgtRW5AInfN5CfqdHLO1dGxA7I3MqEuL5JnIsA==} - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - dev: false + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - /@chakra-ui/button@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-95CplwlRKmmUXkdEp/21VkEWgnwcx2TOBG6NfYlsuLBDHSLlo5FKIiE2oSi4zXc4TLcopGcWPNcm/NDaSC5pvA==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - /@chakra-ui/card@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-xUB/k5MURj4CtPAhdSoXZidUbm8j3hci9vnc+eZJVDqhDOShNlD6QeniQNRPRys4lWAQLCbFcrwL29C8naDi6g==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - /@chakra-ui/checkbox@2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-85g38JIXMEv6M+AcyIGLh7igNtfpAN6KGQFYxY9tBj0eWvWk4NKQxvqqyVta0bSAyIl1rixNIIezNpNWk2iO4g==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@zag-js/focus-visible': 0.16.0 - react: 18.3.1 - dev: false + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - /@chakra-ui/clickable@2.1.0(react@18.3.1): - resolution: {integrity: sha512-flRA/ClPUGPYabu+/GLREZVZr9j2uyyazCAUHAdrTUEdDYCr31SVGhgh7dgKdtq23bOvAQJpIJjw/0Bs0WvbXw==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - react: 18.3.1 - dev: false + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} - /@chakra-ui/close-button@2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-gnpENKOanKexswSVpVz7ojZEALl2x5qjLYNqSQGbxz+aP9sOXPfUS56ebyBrre7T7exuWGiFeRwnM0oVeGPaiw==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} - /@chakra-ui/color-mode@2.2.0(react@18.3.1): - resolution: {integrity: sha512-niTEA8PALtMWRI9wJ4LL0CSBDo8NBfLNp4GD6/0hstcm3IlbBHTVKxN6HwSaoNYfphDQLxCjT4yG+0BJA5tFpg==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - /@chakra-ui/control-box@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-gVrRDyXFdMd8E7rulL0SKeoljkLQiPITFnsyMO8EFHNZ+AHt5wK4LIguYVEq88APqAGZGfHFWXr79RYrNiE3Mg==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@fontsource-variable/inter@5.2.8': + resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} - /@chakra-ui/counter@2.1.0(react@18.3.1): - resolution: {integrity: sha512-s6hZAEcWT5zzjNz2JIWUBzRubo9la/oof1W7EKZVVfPYHERnl5e16FmBC79Yfq8p09LQ+aqFKm/etYoJMMgghw==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/number-utils': 2.0.7 - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - react: 18.3.1 - dev: false + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} - /@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.4)(react@18.3.1): - resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==} - peerDependencies: - '@emotion/react': '>=10.0.35' - react: '>=18' - dependencies: - '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) - react: 18.3.1 - dev: false + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} - /@chakra-ui/descendant@3.1.0(react@18.3.1): - resolution: {integrity: sha512-VxCIAir08g5w27klLyi7PVo8BxhW4tgU/lxQyujkmi4zx7hT9ZdrcQLAted/dAa+aSIZ14S1oV0Q9lGjsAdxUQ==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} - /@chakra-ui/dom-utils@2.1.0: - resolution: {integrity: sha512-ZmF2qRa1QZ0CMLU8M1zCfmw29DmPNtfjR9iTo74U5FPr3i1aoAh7fbJ4qAlZ197Xw9eAW28tvzQuoVWeL5C7fQ==} - dev: false + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} - /@chakra-ui/editable@3.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-j2JLrUL9wgg4YA6jLlbU88370eCRyor7DZQD9lzpY95tSOXpTljeg3uF9eOmDnCs6fxp3zDWIfkgMm/ExhcGTg==} + '@invoke-ai/ui-library@https://codeload.github.com/invoke-ai/ui-library/tar.gz/78ddd0a1670af523cfcac186849deb23bd07d419': + resolution: {tarball: https://codeload.github.com/invoke-ai/ui-library/tar.gz/78ddd0a1670af523cfcac186849deb23bd07d419} + version: 0.0.48 peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@fontsource-variable/inter': ^5.0.16 + react: ^18.2.0 + react-dom: ^18.2.0 - /@chakra-ui/event-utils@2.0.8: - resolution: {integrity: sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==} - dev: false + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} - /@chakra-ui/focus-lock@2.1.0(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==} + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': + resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/dom-utils': 2.1.0 - react: 18.3.1 - react-focus-lock: 2.11.1(@types/react@18.3.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - dev: false + typescript: '>= 4.3.x' + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true - /@chakra-ui/form-control@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-wehLC1t4fafCVJ2RvJQT2jyqsAwX7KymmiGqBu7nQoQz8ApTkGABWpo/QwDh3F/dBLrouHDoOvGmYTqft3Mirw==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - /@chakra-ui/hooks@2.2.1(react@18.3.1): - resolution: {integrity: sha512-RQbTnzl6b1tBjbDPf9zGRo9rf/pQMholsOudTxjy4i9GfTfz6kgp5ValGjQm2z7ng6Z31N1cnjZ1AlSzQ//ZfQ==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-utils': 2.0.12(react@18.3.1) - '@chakra-ui/utils': 2.0.15 - compute-scroll-into-view: 3.0.3 - copy-to-clipboard: 3.3.3 - react: 18.3.1 - dev: false + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - /@chakra-ui/icon@3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-xxjGLvlX2Ys4H0iHrI16t74rG9EBcpFvJ3Y3B7KMQTrnW34Kf7Da/UC8J67Gtx85mTHW020ml85SVPKORWNNKQ==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} - /@chakra-ui/icons@2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-3p30hdo4LlRZTT5CwoAJq3G9fHI0wDc0pBaMHj4SUn0yomO+RcDRlzhdXqdr5cVnzax44sqXJVnf3oQG0eI+4g==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - /@chakra-ui/image@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-bskumBYKLiLMySIWDGcz0+D9Th0jPvmX6xnRMs4o92tT3Od/bW26lahmV2a2Op2ItXeCmRMY+XxJH5Gy1i46VA==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - /@chakra-ui/input@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-GiBbb3EqAA8Ph43yGa6Mc+kUPjh4Spmxp1Pkelr8qtudpc3p2PJOOebLpd90mcqw8UePPa+l6YhhPtp6o0irhw==} + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(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.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@types/react': '>=16' + react: '>=16' - /@chakra-ui/layout@2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-nXuZ6WRbq0WdgnRgLw+QuxWAHuhDtVX8ElWqcTK+cSMFg/52eVP47czYBE5F35YhnoW2XBwfNoNgZ7+e8Z01Rg==} + '@nanostores/react@1.1.0': + resolution: {integrity: sha512-MbH35fjhcf7LAubYX5vhOChYUfTLzNLqH/mBGLVsHkcvjy0F8crO1WQwdmQ2xKbAmtpalDa2zBt3Hlg5kqr8iw==} + engines: {node: ^20.0.0 || >=22.0.0} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/breakpoint-utils': 2.0.8 - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(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.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + nanostores: ^1.2.0 + react: '>=18.0.0' - /@chakra-ui/lazy-utils@2.0.5: - resolution: {integrity: sha512-UULqw7FBvcckQk2n3iPO56TMJvDsNv0FKZI6PlUNJVaGsPbsYxK/8IQ60vZgaTVPtVcjY6BE+y6zg8u9HOqpyg==} - dev: false + '@napi-rs/wasm-runtime@1.1.0': + resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==} - /@chakra-ui/live-region@2.1.0(react@18.3.1): - resolution: {integrity: sha512-ZOxFXwtaLIsXjqnszYYrVuswBhnIHHP+XIgK1vC6DePKtyK590Wg+0J0slDwThUAd4MSSIUa/nNX84x1GMphWw==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: - react: '>=18' - dependencies: - react: 18.3.1 - dev: false + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 - /@chakra-ui/media-query@3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-IsTGgFLoICVoPRp9ykOgqmdMotJG0CnPsKvGQeSFOB/dZfIujdVb14TYxDU4+MURXry1MhJ7LzZhv+Ml7cr8/g==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/breakpoint-utils': 2.0.8 - '@chakra-ui/react-env': 3.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} - /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): - resolution: {integrity: sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/clickable': 2.1.0(react@18.3.1) - '@chakra-ui/descendant': 3.1.0(react@18.3.1) - '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.3.1) - '@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/react-use-animation-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-outside-click': 2.2.0(react@18.3.1) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) - framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - dev: false + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} - /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): - resolution: {integrity: sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/clickable': 2.1.0(react@18.3.1) - '@chakra-ui/descendant': 3.1.0(react@18.3.1) - '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.3.1) - '@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/react-use-animation-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-outside-click': 2.2.0(react@18.3.1) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) - framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - dev: false + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} - /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) - '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) - aria-hidden: 1.2.3 - 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-remove-scroll: 2.5.7(@types/react@18.3.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - dev: false + '@observ33r/object-equals@1.1.6': + resolution: {integrity: sha512-jJTaf7lP5ploIZ1PmSiUFI82LM9C+b53coE7X7RoiO+YEF5vjQcOeSN4gxlm4BYF/NC4KDku3/rm98R3PwkZow==} - /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) - '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) - aria-hidden: 1.2.3 - framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.5.7(@types/react@18.3.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - dev: false + '@oxc-project/types@0.128.0': + resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} - /@chakra-ui/number-input@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-pfOdX02sqUN0qC2ysuvgVDiws7xZ20XDIlcNhva55Jgm095xjm8eVdIBfNm3SFbSUNxyXvLTW/YQanX74tKmuA==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/counter': 2.1.0(react@18.3.1) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-interval': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@oxc-resolver/binding-android-arm-eabi@11.16.2': + resolution: {integrity: sha512-lVJbvydLQIDZHKUb6Zs9Rq80QVTQ9xdCQE30eC9/cjg4wsMoEOg65QZPymUAIVJotpUAWJD0XYcwE7ugfxx5kQ==} + cpu: [arm] + os: [android] - /@chakra-ui/number-utils@2.0.7: - resolution: {integrity: sha512-yOGxBjXNvLTBvQyhMDqGU0Oj26s91mbAlqKHiuw737AXHt0aPllOthVUqQMeaYLwLCjGMg0jtI7JReRzyi94Dg==} - dev: false + '@oxc-resolver/binding-android-arm64@11.16.2': + resolution: {integrity: sha512-fEk+g/g2rJ6LnBVPqeLcx+/alWZ/Db1UlXG+ZVivip0NdrnOzRL48PAmnxTMGOrLwsH1UDJkwY3wOjrrQltCqg==} + cpu: [arm64] + os: [android] - /@chakra-ui/object-utils@2.1.0: - resolution: {integrity: sha512-tgIZOgLHaoti5PYGPTwK3t/cqtcycW0owaiOXoZOcpwwX/vlVb+H1jFsQyWiiwQVPt9RkoSLtxzXamx+aHH+bQ==} - dev: false + '@oxc-resolver/binding-darwin-arm64@11.16.2': + resolution: {integrity: sha512-Pkbp1qi7kdUX6k3Fk1PvAg6p7ruwaWKg1AhOlDgrg2vLXjtv9ZHo7IAQN6kLj0W771dPJZWqNxoqTPacp2oYWA==} + cpu: [arm64] + os: [darwin] - /@chakra-ui/pin-input@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-x4vBqLStDxJFMt+jdAHHS8jbh294O53CPQJoL4g228P513rHylV/uPscYUHrVJXRxsHfRztQO9k45jjTYaPRMw==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/descendant': 3.1.0(react@18.3.1) - '@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/react-use-controllable-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@oxc-resolver/binding-darwin-x64@11.16.2': + resolution: {integrity: sha512-FYCGcU1iSoPkADGLfQbuj0HWzS+0ItjDCt9PKtu2Hzy6T0dxO4Y1enKeCOxCweOlmLEkSxUlW5UPT4wvT3LnAg==} + cpu: [x64] + os: [darwin] - /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): - resolution: {integrity: sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - dev: false + '@oxc-resolver/binding-freebsd-x64@11.16.2': + resolution: {integrity: sha512-1zHCoK6fMcBjE54P2EG/z70rTjcRxvyKfvk4E/QVrWLxNahuGDFZIxoEoo4kGnnEcmPj41F0c2PkrQbqlpja5g==} + cpu: [x64] + os: [freebsd] - /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): - resolution: {integrity: sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - dev: false + '@oxc-resolver/binding-linux-arm-gnueabihf@11.16.2': + resolution: {integrity: sha512-+ucLYz8EO5FDp6kZ4o1uDmhoP+M98ysqiUW4hI3NmfiOJQWLrAzQjqaTdPfIOzlCXBU9IHp5Cgxu6wPjVb8dbA==} + cpu: [arm] + os: [linux] - /@chakra-ui/popper@3.1.0(react@18.3.1): - resolution: {integrity: sha512-ciDdpdYbeFG7og6/6J8lkTFxsSvwTdMLFkpVylAF6VNC22jssiWfquj2eyD4rJnzkRFPvIWJq8hvbfhsm+AjSg==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@popperjs/core': 2.11.8 - react: 18.3.1 - dev: false + '@oxc-resolver/binding-linux-arm-musleabihf@11.16.2': + resolution: {integrity: sha512-qq+TpNXyw1odDgoONRpMLzH4hzhwnEw55398dL8rhKGvvYbio71WrJ00jE+hGlEi7H1Gkl11KoPJRaPlRAVGPw==} + cpu: [arm] + os: [linux] - /@chakra-ui/portal@2.1.0(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-9q9KWf6SArEcIq1gGofNcFPSWEyl+MfJjEUg/un1SMlQjaROOh3zYr+6JAwvcORiX7tyHosnmWC3d3wI2aPSQg==} - peerDependencies: - react: '>=18' - react-dom: '>=18' - 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) - dev: false + '@oxc-resolver/binding-linux-arm64-gnu@11.16.2': + resolution: {integrity: sha512-xlMh4gNtplNQEwuF5icm69udC7un0WyzT5ywOeHrPMEsghKnLjXok2wZgAA7ocTm9+JsI+nVXIQa5XO1x+HPQg==} + cpu: [arm64] + os: [linux] - /@chakra-ui/progress@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-qUXuKbuhN60EzDD9mHR7B67D7p/ZqNS2Aze4Pbl1qGGZfulPW0PY8Rof32qDtttDQBkzQIzFGE8d9QpAemToIQ==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@oxc-resolver/binding-linux-arm64-musl@11.16.2': + resolution: {integrity: sha512-OZs33QTMi0xmHv/4P0+RAKXJTBk7UcMH5tpTaCytWRXls/DGaJ48jOHmriQGK2YwUqXl+oneuNyPOUO0obJ+Hg==} + cpu: [arm64] + os: [linux] - /@chakra-ui/provider@2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==} - peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.3.1) - '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/react-env': 3.1.0(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false + '@oxc-resolver/binding-linux-ppc64-gnu@11.16.2': + resolution: {integrity: sha512-UVyuhaV32dJGtF6fDofOcBstg9JwB2Jfnjfb8jGlu3xcG+TsubHRhuTwQ6JZ1sColNT1nMxBiu7zdKUEZi1kwg==} + cpu: [ppc64] + os: [linux] - /@chakra-ui/radio@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-n10M46wJrMGbonaghvSRnZ9ToTv/q76Szz284gv4QUWvyljQACcGrXIONUnQ3BIwbOfkRqSk7Xl/JgZtVfll+w==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@zag-js/focus-visible': 0.16.0 - react: 18.3.1 - dev: false + '@oxc-resolver/binding-linux-riscv64-gnu@11.16.2': + resolution: {integrity: sha512-YZZS0yv2q5nE1uL/Fk4Y7m9018DSEmDNSG8oJzy1TJjA1jx5HL52hEPxi98XhU6OYhSO/vC1jdkJeE8TIHugug==} + cpu: [riscv64] + os: [linux] - /@chakra-ui/react-children-utils@2.0.6(react@18.3.1): - resolution: {integrity: sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA==} - peerDependencies: - react: '>=18' - dependencies: - react: 18.3.1 - dev: false + '@oxc-resolver/binding-linux-riscv64-musl@11.16.2': + resolution: {integrity: sha512-9VYuypwtx4kt1lUcwJAH4dPmgJySh4/KxtAPdRoX2BTaZxVm/yEXHq0mnl/8SEarjzMvXKbf7Cm6UBgptm3DZw==} + cpu: [riscv64] + os: [linux] - /@chakra-ui/react-context@2.1.0(react@18.3.1): - resolution: {integrity: sha512-iahyStvzQ4AOwKwdPReLGfDesGG+vWJfEsn0X/NoGph/SkN+HXtv2sCfYFFR9k7bb+Kvc6YfpLlSuLvKMHi2+w==} - peerDependencies: - react: '>=18' - dependencies: - react: 18.3.1 - dev: false + '@oxc-resolver/binding-linux-s390x-gnu@11.16.2': + resolution: {integrity: sha512-3gbwQ+xlL5gpyzgSDdC8B4qIM4mZaPDLaFOi3c/GV7CqIdVJc5EZXW4V3T6xwtPBOpXPXfqQLbhTnUD4SqwJtA==} + cpu: [s390x] + os: [linux] - /@chakra-ui/react-env@3.1.0(react@18.3.1): - resolution: {integrity: sha512-Vr96GV2LNBth3+IKzr/rq1IcnkXv+MLmwjQH6C8BRtn3sNskgDFD5vLkVXcEhagzZMCh8FR3V/bzZPojBOyNhw==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@oxc-resolver/binding-linux-x64-gnu@11.16.2': + resolution: {integrity: sha512-m0WcK0j54tSwWa+hQaJMScZdWneqE7xixp/vpFqlkbhuKW9dRHykPAFvSYg1YJ3MJgu9ZzVNpYHhPKJiEQq57Q==} + cpu: [x64] + os: [linux] - /@chakra-ui/react-types@2.0.7(react@18.3.1): - resolution: {integrity: sha512-12zv2qIZ8EHwiytggtGvo4iLT0APris7T0qaAWqzpUGS0cdUtR8W+V1BJ5Ocq+7tA6dzQ/7+w5hmXih61TuhWQ==} - peerDependencies: - react: '>=18' - dependencies: - react: 18.3.1 - dev: false + '@oxc-resolver/binding-linux-x64-musl@11.16.2': + resolution: {integrity: sha512-ZjUm3w96P2t47nWywGwj1A2mAVBI/8IoS7XHhcogWCfXnEI3M6NPIRQPYAZW4s5/u3u6w1uPtgOwffj2XIOb/g==} + cpu: [x64] + os: [linux] - /@chakra-ui/react-use-animation-state@2.1.0(react@18.3.1): - resolution: {integrity: sha512-CFZkQU3gmDBwhqy0vC1ryf90BVHxVN8cTLpSyCpdmExUEtSEInSCGMydj2fvn7QXsz/za8JNdO2xxgJwxpLMtg==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@oxc-resolver/binding-openharmony-arm64@11.16.2': + resolution: {integrity: sha512-OFVQ2x3VenTp13nIl6HcQ/7dmhFmM9dg2EjKfHcOtYfrVLQdNR6THFU7GkMdmc8DdY1zLUeilHwBIsyxv5hkwQ==} + cpu: [arm64] + os: [openharmony] - /@chakra-ui/react-use-callback-ref@2.1.0(react@18.3.1): - resolution: {integrity: sha512-efnJrBtGDa4YaxDzDE90EnKD3Vkh5a1t3w7PhnRQmsphLy3g2UieasoKTlT2Hn118TwDjIv5ZjHJW6HbzXA9wQ==} - peerDependencies: - react: '>=18' - dependencies: - react: 18.3.1 - dev: false + '@oxc-resolver/binding-wasm32-wasi@11.16.2': + resolution: {integrity: sha512-+O1sY3RrGyA2AqDnd3yaDCsqZqCblSTEpY7TbbaOaw0X7iIbGjjRLdrQk9StG3QSiZuBy9FdFwotIiSXtwvbAQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] - /@chakra-ui/react-use-controllable-state@2.1.0(react@18.3.1): - resolution: {integrity: sha512-QR/8fKNokxZUs4PfxjXuwl0fj/d71WPrmLJvEpCTkHjnzu7LnYvzoe2wB867IdooQJL0G1zBxl0Dq+6W1P3jpg==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@oxc-resolver/binding-win32-arm64-msvc@11.16.2': + resolution: {integrity: sha512-jMrMJL+fkx6xoSMFPOeyQ1ctTFjavWPOSZEKUY5PebDwQmC9cqEr4LhdTnGsOtFrWYLXlEU4xWeMdBoc/XKkOA==} + cpu: [arm64] + os: [win32] - /@chakra-ui/react-use-disclosure@2.1.0(react@18.3.1): - resolution: {integrity: sha512-Ax4pmxA9LBGMyEZJhhUZobg9C0t3qFE4jVF1tGBsrLDcdBeLR9fwOogIPY9Hf0/wqSlAryAimICbr5hkpa5GSw==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@oxc-resolver/binding-win32-ia32-msvc@11.16.2': + resolution: {integrity: sha512-tl0xDA5dcQplG2yg2ZhgVT578dhRFafaCfyqMEAXq8KNpor85nJ53C3PLpfxD2NKzPioFgWEexNsjqRi+kW2Mg==} + cpu: [ia32] + os: [win32] - /@chakra-ui/react-use-event-listener@2.1.0(react@18.3.1): - resolution: {integrity: sha512-U5greryDLS8ISP69DKDsYcsXRtAdnTQT+jjIlRYZ49K/XhUR/AqVZCK5BkR1spTDmO9H8SPhgeNKI70ODuDU/Q==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@oxc-resolver/binding-win32-x64-msvc@11.16.2': + resolution: {integrity: sha512-M7z0xjYQq1HdJk2DxTSLMvRMyBSI4wn4FXGcVQBsbAihgXevAReqwMdb593nmCK/OiFwSNcOaGIzUvzyzQ+95w==} + cpu: [x64] + os: [win32] - /@chakra-ui/react-use-focus-effect@2.1.0(react@18.3.1): - resolution: {integrity: sha512-xzVboNy7J64xveLcxTIJ3jv+lUJKDwRM7Szwn9tNzUIPD94O3qwjV7DDCUzN2490nSYDF4OBMt/wuDBtaR3kUQ==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} - /@chakra-ui/react-use-focus-on-pointer-down@2.1.0(react@18.3.1): - resolution: {integrity: sha512-2jzrUZ+aiCG/cfanrolsnSMDykCAbv9EK/4iUyZno6BYb3vziucmvgKuoXbMPAzWNtwUwtuMhkby8rc61Ue+Lg==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - /@chakra-ui/react-use-interval@2.1.0(react@18.3.1): - resolution: {integrity: sha512-8iWj+I/+A0J08pgEXP1J1flcvhLBHkk0ln7ZvGIyXiEyM6XagOTJpwNhiu+Bmk59t3HoV/VyvyJTa+44sEApuw==} - peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - /@chakra-ui/react-use-latest-ref@2.1.0(react@18.3.1): - resolution: {integrity: sha512-m0kxuIYqoYB0va9Z2aW4xP/5b7BzlDeWwyXCH6QpT2PpW3/281L3hLCm1G0eOUcdVlayqrQqOeD6Mglq+5/xoQ==} - peerDependencies: - react: '>=18' - dependencies: - react: 18.3.1 - dev: false + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} - /@chakra-ui/react-use-merge-refs@2.1.0(react@18.3.1): - resolution: {integrity: sha512-lERa6AWF1cjEtWSGjxWTaSMvneccnAVH4V4ozh8SYiN9fSPZLlSG3kNxfNzdFvMEhM7dnP60vynF7WjGdTgQbQ==} + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: - react: '>=18' - dependencies: - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/react-use-outside-click@2.2.0(react@18.3.1): - resolution: {integrity: sha512-PNX+s/JEaMneijbgAM4iFL+f3m1ga9+6QK0E5Yh4s8KZJQ/bLwZzdhMz8J/+mL+XEXQ5J0N8ivZN28B82N1kNw==} + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/react-use-pan-event@2.1.0(react@18.3.1): - resolution: {integrity: sha512-xmL2qOHiXqfcj0q7ZK5s9UjTh4Gz0/gL9jcWPA6GVf+A0Od5imEDa/Vz+533yQKWiNSm1QGrIj0eJAokc7O4fg==} + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/event-utils': 2.0.8 - '@chakra-ui/react-use-latest-ref': 2.1.0(react@18.3.1) - framesync: 6.1.2 - react: 18.3.1 - dev: false + '@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 - /@chakra-ui/react-use-previous@2.1.0(react@18.3.1): - resolution: {integrity: sha512-pjxGwue1hX8AFcmjZ2XfrQtIJgqbTF3Qs1Dy3d1krC77dEsiCUbQ9GzOBfDc8pfd60DrB5N2tg5JyHbypqh0Sg==} + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} peerDependencies: - react: '>=18' - dependencies: - react: 18.3.1 - dev: false + '@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 - /@chakra-ui/react-use-safe-layout-effect@2.1.0(react@18.3.1): - resolution: {integrity: sha512-Knbrrx/bcPwVS1TorFdzrK/zWA8yuU/eaXDkNj24IrKoRlQrSBFarcgAEzlCHtzuhufP3OULPkELTzz91b0tCw==} + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} peerDependencies: - react: '>=18' - dependencies: - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/react-use-size@2.1.0(react@18.3.1): - resolution: {integrity: sha512-tbLqrQhbnqOjzTaMlYytp7wY8BW1JpL78iG7Ru1DlV4EWGiAmXFGvtnEt9HftU0NJ0aJyjgymkxfVGI55/1Z4A==} + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} peerDependencies: - react: '>=18' - dependencies: - '@zag-js/element-size': 0.10.5 - react: 18.3.1 - dev: false + '@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 - /@chakra-ui/react-use-timeout@2.1.0(react@18.3.1): - resolution: {integrity: sha512-cFN0sobKMM9hXUhyCofx3/Mjlzah6ADaEl/AXl5Y+GawB5rgedgAcu2ErAgarEkwvsKdP6c68CKjQ9dmTQlJxQ==} + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/react-use-update-effect@2.1.0(react@18.3.1): - resolution: {integrity: sha512-ND4Q23tETaR2Qd3zwCKYOOS1dfssojPLJMLvUtUbW5M9uW1ejYWgGUobeAiOVfSplownG8QYMmHTP86p/v0lbA==} + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} peerDependencies: - react: '>=18' - dependencies: - react: 18.3.1 - dev: false + '@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 - /@chakra-ui/react-utils@2.0.12(react@18.3.1): - resolution: {integrity: sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==} + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} peerDependencies: - react: '>=18' - dependencies: - '@chakra-ui/utils': 2.0.15 - react: 18.3.1 - dev: false + '@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 - /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/counter': 2.1.0(react@18.3.1) - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.3.1) - '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/hooks': 2.2.1(react@18.3.1) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/live-region': 2.1.0(react@18.3.1) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) - '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) - '@chakra-ui/popper': 3.1.0(react@18.3.1) - '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/react-env': 3.1.0(react@18.3.1) - '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) - '@chakra-ui/utils': 2.0.15 - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) - 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) - transitivePeerDependencies: - - '@types/react' - dev: false + '@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 - /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/counter': 2.1.0(react@18.3.1) - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.3.1) - '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/hooks': 2.2.1(react@18.3.1) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/live-region': 2.1.0(react@18.3.1) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) - '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) - '@chakra-ui/popper': 3.1.0(react@18.3.1) - '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/react-env': 3.1.0(react@18.3.1) - '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) - '@chakra-ui/utils': 2.0.15 - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) - framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - dev: false + '@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 - /@chakra-ui/select@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==} + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false - - /@chakra-ui/shared-utils@2.0.5: - resolution: {integrity: sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q==} - dev: false + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/skeleton@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-JNRuMPpdZGd6zFVKjVQ0iusu3tXAdI29n4ZENYwAJEMf/fN0l12sVeirOxkJ7oEL0yOx2AgEYFSKdbcAgfUsAQ==} + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/react-use-previous': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/skip-nav@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-Hk+FG+vadBSH0/7hwp9LJnLjkO0RPGnx7gBJWI4/SpoJf3e4tZlWYtwGj0toYY4aGKl93jVghuwGbDBEMoHDug==} + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/slider@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-lUOBcLMCnFZiA/s2NONXhELJh6sY5WtbRykPtclGfynqqOo47lwWJx+VP7xaeuhDOPcWSSecWc9Y1BfPOCz9cQ==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/number-utils': 2.0.7 - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-latest-ref': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-pan-event': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-size': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false - - /@chakra-ui/spinner@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-hczbnoXt+MMv/d3gE+hjQhmkzLiKuoTo42YhUG7Bs9OSv2lg1fZHW1fGNRFP3wTi6OIbD044U1P9HK+AOgFH3g==} + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/stat@2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-LDn0d/LXQNbAn2KaR3F1zivsZCewY4Jsy1qShmfBMKwn6rI8yVlbvu6SiA3OpHS0FhxbsZxQI6HefEoIgtqY6Q==} + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(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.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/stepper@2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-ky77lZbW60zYkSXhYz7kbItUpAQfEdycT0Q4bkHLxfqbuiGMf8OmgZOQkOB9uM4v0zPwy2HXhe0vq4Dd0xa55Q==} + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(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.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false - - /@chakra-ui/styled-system@2.9.2: - resolution: {integrity: sha512-To/Z92oHpIE+4nk11uVMWqo2GGRS86coeMmjxtpnErmWRdLcp1WVCVRAvn+ZwpLiNR+reWFr2FFqJRsREuZdAg==} - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - csstype: 3.1.3 - lodash.mergewith: 4.6.2 - dev: false + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): - resolution: {integrity: sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==} + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): - resolution: {integrity: sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==} - peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - dev: false + '@redocly/ajv@8.17.1': + resolution: {integrity: sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==} - /@chakra-ui/system@2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1): - resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==} - peerDependencies: - '@emotion/react': ^11.0.0 - '@emotion/styled': ^11.0.0 - react: '>=18' - 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.11.4(@types/react@18.3.1)(react@18.3.1) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) - react: 18.3.1 - react-fast-compare: 3.2.2 - dev: false + '@redocly/config@0.22.2': + resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} + + '@redocly/openapi-core@1.34.6': + resolution: {integrity: sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} - /@chakra-ui/table@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-o5OrjoHCh5uCLdiUb0Oc0vq9rIAeHSIRScc2ExTC9Qg/uVZl2ygLrjToCaKfaaKl1oQexIeAcZDKvPG8tVkHyQ==} + '@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/binding-android-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.18': + resolution: {integrity: sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.18': + resolution: {integrity: sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': + resolution: {integrity: sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': + resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': + resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': + resolution: {integrity: sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': + resolution: {integrity: sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': + resolution: {integrity: sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.18': + resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@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.54.0': + resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.54.0': + resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.54.0': + resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.54.0': + resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.54.0': + resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.54.0': + resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.54.0': + resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.54.0': + resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.54.0': + resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.54.0': + resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.54.0': + resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.54.0': + resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==} + 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.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@storybook/addon-docs@10.3.6': + resolution: {integrity: sha512-TvIdADVPtauxW0LzXIpIv7X6GxwetorhyNh+6+7MHC27XSBCWVxxRUwL63YeLlHTuXsIk0quG3b1xgwVRzWOJA==} + peerDependencies: + storybook: ^10.3.6 + + '@storybook/addon-links@10.3.6': + resolution: {integrity: sha512-tv9Xd68qRGBAvEubaxNo3FuFq4GwuMiBriD+gLGuFK0+/u3cnkuA264aoR1v6YCH3sT3er3+MBimuyKM3jLDxg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.6 + peerDependenciesMeta: + react: + optional: true + + '@storybook/builder-vite@10.3.6': + resolution: {integrity: sha512-gpvR/sE4BcrFtmQZ+Ker7zD23oQzoVeqD9nF6cK6yzY+Q0svJXyX2EPmFG4y+EwygD5/vNzDpP84gGMut8VRwg==} + peerDependencies: + storybook: ^10.3.6 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/csf-plugin@10.3.6': + resolution: {integrity: sha512-9kBf7VRdRqTSIYo+rPtVn5yjYYyK8kP2QhEYx3oiXvfwy4RexmbJnhk/tXa/lNiTqukA1TqaWQ2+5MqF4fu6YQ==} + peerDependencies: + esbuild: '*' + rollup: '*' + storybook: ^10.3.6 + vite: '*' + webpack: '*' + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@2.0.2': + resolution: {integrity: sha512-KZBCpXsshAIjczYNXR/rlxEtCUX/eAbpFNwKi8bcOomrLA4t/SyPz5RF+lVPO2oZBUE4sAkt43mfJUevQDSEEw==} + 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 + + '@storybook/react-dom-shim@10.3.6': + resolution: {integrity: sha512-/Tu1gPu+Fw+zOnAGmxRmOD30FX3a04LxcTAKflEtdpmtIMVR5bA3qpjy+f5YhoyDCecbXyKmL1OeIU2FIIZHqQ==} + 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 + storybook: ^10.3.6 + + '@storybook/react-vite@10.3.6': + resolution: {integrity: sha512-tySQRc+8q7V2NkylQMNJjDV8zXy6tkxb8oDqw/DIhHhI9Xn77MTKVZ8Cihbo5NMm7HYTB6xDKr6wqdSMgdufYQ==} + 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 + storybook: ^10.3.6 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/react@10.3.6': + resolution: {integrity: sha512-oZQZ6xayWe5IdHmFUTL0TL8rX/gpNNh9gWhT2vzW5eeUvlkVG/RBKdsja6Ndrk2s1D9vcnwiI6r6CNXy3IEEmg==} + 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 + storybook: ^10.3.6 + typescript: '>= 4.9.x' + peerDependenciesMeta: + typescript: + optional: true + + '@swc/core-darwin-arm64@1.15.33': + resolution: {integrity: sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.33': + resolution: {integrity: sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.33': + resolution: {integrity: sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.33': + resolution: {integrity: sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.33': + resolution: {integrity: sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-ppc64-gnu@1.15.33': + resolution: {integrity: sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + + '@swc/core-linux-s390x-gnu@1.15.33': + resolution: {integrity: sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.33': + resolution: {integrity: sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.33': + resolution: {integrity: sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.33': + resolution: {integrity: sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.33': + resolution: {integrity: sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.33': + resolution: {integrity: sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.33': + resolution: {integrity: sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==} + 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.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + 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.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@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.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@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/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@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.21': + resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/node@22.19.3': + resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@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.59.2': + resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.2': + resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.50.1': + resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.59.2': + resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.50.1': + resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.59.2': + resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.50.1': + resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/tsconfig-utils@8.59.2': + resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.2': + resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.50.1': + resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.59.2': + resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.50.1': + resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/typescript-estree@8.59.2': + resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.50.1': + resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.59.2': + resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.50.1': + resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.59.2': + resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react-swc@4.3.0': + resolution: {integrity: sha512-mOkXCII839dHyAt/gpoSlm28JIVDwhZ6tnG6wJxUy2bmOx7UaPjvOyIDf3SFv5s7Eo7HVaq6kRcu6YMEzt5Z7w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7 || ^8 + + '@vitest/coverage-v8@4.1.5': + resolution: {integrity: sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==} + peerDependencies: + '@vitest/browser': 4.1.5 + vitest: 4.1.5 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + + '@vitest/ui@4.1.5': + resolution: {integrity: sha512-3Z9HNFiV0IF1fk0JPiK+7kE1GcaIPefQQIBYur6PM5yFIq6agys3uqP/0t966e1wXfmjbRCHDe7qW236Xjwnag==} + peerDependencies: + vitest: 4.1.5 + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + + '@webcontainer/env@1.1.1': + resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} + + '@xobotyi/scrollbar-width@1.9.5': + resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} + + '@xyflow/react@12.10.0': + resolution: {integrity: sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.74': + resolution: {integrity: sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==} + + '@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 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ag-psd@28.5.1: + resolution: {integrity: sha512-oDzQ4VctavkqGiI1mFN9PIMjVrv0T6MpoIS6SAbzAQXCQs2b3UdyI9VQTrUQdfq9AaHQcR0cIX/hAZ7fyh7Cgw==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + 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.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + 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.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + 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@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + + 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'} + + babel-plugin-react-compiler@1.0.0: + resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.29: + resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + hasBin: true + + 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==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + 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.30001761: + resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} + + caniuse-lite@1.0.30001792: + resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + 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@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.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + 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.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + 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.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + 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.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + 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==} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + 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-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + 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-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + 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.12.0: + resolution: {integrity: sha512-J9jFIFy9nNZrq7WF3l6X9osm/CmojDpzY0unTfnsWFNu2TkTc4GJ2qnpvzmyC0qgwp4NcUdXJTfoswc9Uvfuhw==} + + dockview@4.12.0: + resolution: {integrity: sha512-WPzUTqMd72GE2hwKnCcj5A0M0Ofgc9VMofEzBTNOD1Z8letRNazOAmRNnCgp12yfziKY5LyY32837KW2EqtmLA==} + 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.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + electron-to-chromium@1.5.353: + resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + + engine.io-client@6.6.4: + resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + 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.2: + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + 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.46.1: + resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + 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.3: + resolution: {integrity: sha512-z/h4oBRd9wI1ET60HqcLSU6XPeAh/EPOrBBTyCdkWeMoYrWAaUVA+DOQkWTiNIyCltG4NTmy62SQisVXxoXurw==} + 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.1.0: + resolution: {integrity: sha512-J5l6qTi+eZyHJWPlpfK8iL/+mYgz62dHVmOtwnSzuBfH+DrcLadolCh6Bpe33/kwyx1M8agwENz4/94192czBg==} + engines: {node: '>= 12.22.0'} + peerDependencies: + eslint: '>=9.0.0' + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + 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@13.0.0: + resolution: {integrity: sha512-McAc+/Nlvcg4byY/CABGH8kqnefWBj8s3JA2okEtz8ixbECQgU46p0HkTUKa4YS7wvgGceimlc34p1nXqbWqtA==} + peerDependencies: + eslint: '>=5.0.0' + + eslint-plugin-storybook@10.3.6: + resolution: {integrity: sha512-8udrL+Rmp5LFaZvgRe4J226X1MYls25bWCyHuzR5X8s2qbFTryX+wKC+o/0Ato4A1AvwnDg8OOMPc6yWJ9JpcA==} + peerDependencies: + eslint: '>=8' + storybook: ^10.3.6 + + eslint-plugin-unused-imports@4.4.1: + resolution: {integrity: sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^10.0.0 || ^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-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + 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.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + 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==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastest-stable-stringify@2.0.2: + resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + 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'} + + 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==} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + 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.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + + fracturedjsonjs@4.1.1: + resolution: {integrity: sha512-zpxgrF586btwidqdDRggOviFnihJh2uqompWfV8EkSd0zPROgQKToY2QCPkG+2uzHvt1fmRkqZIWklAnsKuRsA==} + + 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.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + 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==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + 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.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + 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==} + + 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'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + 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.7.3: + resolution: {integrity: sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==} + 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.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + 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.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + 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-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + 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-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-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.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + 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'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + 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-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + 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.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + 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@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + 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.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + 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.77.4: + resolution: {integrity: sha512-CmRd3UabOBqA4lDUAMA8CJeepIoQPD2qRqq0wCnLz9Z3FTlG1iucZ7puwe+i3zV0gUaIWVYgC8cXoDMZEC+DyA==} + engines: {node: '>=18.18.0'} + hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4 <7' + + 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==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + linkify-react@4.3.2: + resolution: {integrity: sha512-mi744h1hf+WDsr+paJgSBBgYNLMWNSHyM9V9LVUo03RidNGdw1VpI7Twnt+K3pEh3nIzB4xiiAgZxpd61ItKpQ==} + peerDependencies: + linkifyjs: ^4.0.0 + react: '>= 15.0.0' + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + + liqe@3.8.4: + resolution: {integrity: sha512-ruUnzKq8FyTIJKhO2wr2qBr/Ga0qgMzXVbJ8il9Xd4VYAMwmwWStonJa6irUkoOG5rmghL5WHBSQ6EKkoMrzUQ==} + 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'} + + 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.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + 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.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + 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@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + 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'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + 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.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + + nanostores@1.3.0: + resolution: {integrity: sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA==} + 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.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + 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'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + 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.10.1: + resolution: {integrity: sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==} + 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.12.0: + resolution: {integrity: sha512-mWJ5MOkcZ/ljHwfLw8+bN0V9ziGCoNoqULcp994j5DTGNQvnkWKWkA7rnO29Kyew5AoHxUnJ4Ndqfcl0HSQjXg==} + + overlayscrollbars@2.13.0: + resolution: {integrity: sha512-uQGpLESrbFDLTWucWAKX9ceIANj7detMwH/2yJ315Llt72ZcWN3P6ckMotoqVv2Mk29R/pnhDtgYjy4K+kwAyQ==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + oxc-resolver@11.16.2: + resolution: {integrity: sha512-Uy76u47vwhhF7VAmVY61Srn+ouiOobf45MU9vGct9GD2ARy6hKoqEElyHDB0L+4JOM6VLuZ431KiLwyjI/A21g==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + 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-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-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + 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.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + 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.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + 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.3.1: + resolution: {integrity: sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==} + 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.7.0: + resolution: {integrity: sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg==} + 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.3: + resolution: {integrity: sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==} + engines: {node: ^20.9.0 || >=22} + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + 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@6.0.0: + resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==} + peerDependencies: + react: '>=16.13.1' + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-focus-lock@2.13.7: + resolution: {integrity: sha512-20lpZHEQrXPb+pp1tzd4ULL6DyO5D2KnR0G69tTDdydrmNhU7pdFmbQUYVyHUgp+xN29IuFR0PVuhOmvaZL9Og==} + 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.69.0: + resolution: {integrity: sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw==} + 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.7.4: + resolution: {integrity: sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==} + peerDependencies: + i18next: '>= 23.4.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-i18next@16.5.0: + resolution: {integrity: sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==} + peerDependencies: + i18next: '>= 25.6.2' + 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.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + 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.6: + resolution: {integrity: sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==} + 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.15.0: + resolution: {integrity: sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ==} + engines: {node: '>=20.0.0'} peerDependencies: - '@chakra-ui/system': '>=2.0.0' react: '>=18' - dependencies: - '@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.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + react-dom: '>=18' + + react-router@7.15.0: + resolution: {integrity: sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==} + 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.18.6: + resolution: {integrity: sha512-CrT3P6HyjJMHZVWSste2bG2q5aWGlHfW2QuySZjiFwB2Qok/xsvgy+k8Z2jeDP8PP5KsBip7zNrl/F0QoxeyKw==} + peerDependencies: + react: '>=16 || >=17 || >= 18 || >= 19' + react-dom: '>=16 || >=17 || >= 18 || >=19' + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + 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.3.0: + resolution: {integrity: sha512-Nll/rYVRly3KxR13VIKteLJvJLetGA1ok8Q1sW+L1SC80flhjkeXUPDs9t6Ana3a3KCmC97UaVqsBF/boR2rmQ==} + 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.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + 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.2: + resolution: {integrity: sha512-RyXI+aNxwVyfF71a9cqz/jhXWbycnVh7GXnnJUniIBXKTOJQF3rmpNexStXt8TUcKyiXCwyfYzboZLMYUllPDA==} + engines: {node: '>=18.0'} + + rolldown@1.0.0-rc.18: + resolution: {integrity: sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup-plugin-visualizer@6.0.5: + resolution: {integrity: sha512-9+HlNgKCVbJDs8tVtjQ43US12eqaiHyyiLMdBwQ7vSZPiHMysGNo2E88TAp1si5wx8NAoYriI2A5kuKfIakmJg==} + 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.54.0: + resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rtl-css-js@1.16.1: + resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + 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.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + 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.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + 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.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + smol-toml@1.6.0: + resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} + engines: {node: '>= 18'} + + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.5: + resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} + 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.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + 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@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + storybook@10.3.6: + resolution: {integrity: sha512-vbSz7g/1rGMC1uAULqMZjALkIuLu2QABqfhRYhyr/11kzyesi+vAmwyJLukZP1FfecxGOgMwOh6GS0YsGpHAvQ==} + hasBin: true + peerDependencies: + prettier: ^2 || ^3 + vite-plus: ^0.1.15 + peerDependenciesMeta: + prettier: + optional: true + vite-plus: + 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.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + 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.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + 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-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'} + + 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@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + 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-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + 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.12: + resolution: {integrity: sha512-nFRqW0ttu/2o6XTXsHiVZWJBCOaxhVqZLg7dgs3coZNsCMPXPfwz+zPHAQA+70fNnVJLAPg1EgGIqK9Q84tvAw==} + + 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.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + 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==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + 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.6: + resolution: {integrity: sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg==} + 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' - /@chakra-ui/tabs@3.0.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-6Mlclp8L9lqXmsGWF5q5gmemZXOiOYuh0SGT/7PgJVNPz3LXREXlXg2an4MBUD8W5oTkduCX+3KTMCwRrVrDYw==} + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/clickable': 2.1.0(react@18.3.1) - '@chakra-ui/descendant': 3.1.0(react@18.3.1) - '@chakra-ui/lazy-utils': 2.0.5 - '@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/react-use-controllable-state': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/tag@3.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-Bdel79Dv86Hnge2PKOU+t8H28nm/7Y3cKd4Kfk9k3lOpUh4+nkSGe58dhRzht59lEqa4N9waCgQiBdkydjvBXQ==} + use-latest@1.3.0: + resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/textarea@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-ip7tvklVCZUb2fOHDb23qPy/Fr2mzDOGdkrpbNi50hDCiV4hFX02jdQJdi3ydHZUyVgZVBKPOJ+lT9i7sKA2wA==} + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' - dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - /@chakra-ui/theme-tools@2.1.2(@chakra-ui/styled-system@2.9.2): - resolution: {integrity: sha512-Qdj8ajF9kxY4gLrq7gA+Azp8CtFHGO9tWMN2wfF9aQNgG9AuMhPrUzMq9AMQ0MXiYcgNq/FD3eegB43nHVmXVA==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: - '@chakra-ui/styled-system': '>=2.0.0' - 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 - dev: false + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - /@chakra-ui/theme-utils@2.0.21: - resolution: {integrity: sha512-FjH5LJbT794r0+VSCXB3lT4aubI24bLLRWB+CuRKHijRvsOg717bRdUN/N1fEmEpFnRVrbewttWh/OQs0EWpWw==} - 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 - dev: false + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - /@chakra-ui/theme@3.3.1(@chakra-ui/styled-system@2.9.2): - resolution: {integrity: sha512-Hft/VaT8GYnItGCBbgWd75ICrIrIFrR7lVOhV/dQnqtfGqsVDlrztbSErvMkoPKt0UgAkd9/o44jmZ6X4U2nZQ==} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + vite-plugin-babel@1.6.0: + resolution: {integrity: sha512-VtYA4FSmQREA2oaZ7+jfLS/fBk1/xZMUR94YZzB5s6U9WyptbvThUD1HSSv7oNDU28jGuHmdBZ1wTVGNIoChoQ==} peerDependencies: - '@chakra-ui/styled-system': '>=2.8.0' - 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) - dev: false + '@babel/core': ^7.0.0 + vite: ^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==} + vite-plugin-eslint@1.8.1: + resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} peerDependencies: - '@chakra-ui/system': 2.6.2 - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-timeout': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - 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) - dev: false + eslint: '>=7' + vite: '>=2' - /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==} + vite@8.0.11: + resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true peerDependencies: - '@chakra-ui/system': 2.6.2 - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/react-context': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-timeout': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.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 + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true - /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==} + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/popper': 3.1.0(react@18.3.1) - '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - 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) - dev: false + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + 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'} - /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==} + 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.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - framer-motion: '>=4.0.0' - react: '>=18' - react-dom: '>=18' - dependencies: - '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/popper': 3.1.0(react@18.3.1) - '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/react-types': 2.0.7(react@18.3.1) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) - '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true - /@chakra-ui/transition@2.1.0(framer-motion@10.18.0)(react@18.3.1): - resolution: {integrity: sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} peerDependencies: - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - dev: false + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + 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'} - /@chakra-ui/transition@2.1.0(framer-motion@11.1.8)(react@18.3.1): - resolution: {integrity: sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==} + 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'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} peerDependencies: - framer-motion: '>=4.0.0' - react: '>=18' - dependencies: - '@chakra-ui/shared-utils': 2.0.5 - framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - dev: false + zod: ^3.25.0 || ^4.0.0 - /@chakra-ui/utils@2.0.15: - resolution: {integrity: sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==} - dependencies: - '@types/lodash.mergewith': 4.6.7 - css-box-model: 1.2.1 - framesync: 6.1.2 - lodash.mergewith: 4.6.2 - dev: false + zod@4.2.1: + resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} - /@chakra-ui/visually-hidden@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): - resolution: {integrity: sha512-KmKDg01SrQ7VbTD3+cPWf/UfpF5MSwm3v7MWi0n5t8HnnadT13MF0MJCDSXbBWnzLv1ZKJ6zlyAOeARWX+DpjQ==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} peerDependencies: - '@chakra-ui/system': '>=2.0.0' - react: '>=18' + '@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.4': {} + + '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.2': dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - react: 18.3.1 - dev: false + '@atlaskit/pragmatic-drag-and-drop': 1.7.7 + '@babel/runtime': 7.28.4 - /@colors/colors@1.5.0: - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - requiresBuild: true - dev: true - optional: true + '@atlaskit/pragmatic-drag-and-drop-hitbox@1.1.0': + dependencies: + '@atlaskit/pragmatic-drag-and-drop': 1.7.7 + '@babel/runtime': 7.28.4 - /@dagrejs/dagre@1.1.2: - resolution: {integrity: sha512-F09dphqvHsbe/6C2t2unbmpr5q41BNPEfJCdn8Z7aEBpVSy/zFQ/b4SWsweQjWNsYMDvE2ffNUN8X0CeFsEGNw==} + '@atlaskit/pragmatic-drag-and-drop@1.7.7': dependencies: - '@dagrejs/graphlib': 2.2.2 - dev: false + '@babel/runtime': 7.28.4 + bind-event-listener: 3.0.0 + raf-schd: 4.0.3 - /@dagrejs/graphlib@2.2.2: - resolution: {integrity: sha512-CbyGpCDKsiTg/wuk79S7Muoj8mghDGAESWGxcSyhHX5jD35vYMBZochYVFzlHxynpE9unpu6O+4ZuhrLxASsOg==} - engines: {node: '>17.0.0'} - dev: false + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 - /@discoveryjs/json-ext@0.5.7: - resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} - engines: {node: '>=10.0.0'} - dev: true + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 - /@dnd-kit/accessibility@3.1.0(react@18.3.1): - resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} - peerDependencies: - react: '>=16.8.0' + '@babel/code-frame@7.29.7': dependencies: - react: 18.3.1 - tslib: 2.6.2 - dev: false + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 - /@dnd-kit/core@6.1.0(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' + '@babel/compat-data@7.28.5': {} + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.28.5': dependencies: - '@dnd-kit/accessibility': 3.1.0(react@18.3.1) - '@dnd-kit/utilities': 3.2.2(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tslib: 2.6.2 - dev: false + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@10.2.2) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.3.1): - resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} - peerDependencies: - '@dnd-kit/core': ^6.1.0 - react: '>=16.8.0' + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@10.2.2) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': dependencies: - '@dnd-kit/core': 6.1.0(react-dom@18.3.1)(react@18.3.1) - '@dnd-kit/utilities': 3.2.2(react@18.3.1) - react: 18.3.1 - tslib: 2.6.2 - dev: false + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 - /@dnd-kit/utilities@3.2.2(react@18.3.1): - resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} - peerDependencies: - react: '>=16.8.0' + '@babel/generator@7.29.1': dependencies: - react: 18.3.1 - tslib: 2.6.2 - dev: false + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 - /@emotion/babel-plugin@11.11.0: - resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} + '@babel/generator@7.29.7': dependencies: - '@babel/helper-module-imports': 7.24.3 - '@babel/runtime': 7.24.5 - '@emotion/hash': 0.9.1 - '@emotion/memoize': 0.8.1 - '@emotion/serialize': 1.1.4 - 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 - dev: false + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 - /@emotion/cache@11.11.0: - resolution: {integrity: sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==} + '@babel/helper-annotate-as-pure@7.29.7': dependencies: - '@emotion/memoize': 0.8.1 - '@emotion/sheet': 1.2.2 - '@emotion/utils': 1.2.1 - '@emotion/weak-memoize': 0.3.1 - stylis: 4.2.0 - dev: false + '@babel/types': 7.29.7 - /@emotion/hash@0.9.1: - resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} - dev: false + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 - /@emotion/is-prop-valid@0.8.8: - resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} - requiresBuild: true + '@babel/helper-compilation-targets@7.28.6': dependencies: - '@emotion/memoize': 0.7.4 - dev: false - optional: true + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 - /@emotion/is-prop-valid@1.2.2: - resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.28.5)': dependencies: - '@emotion/memoize': 0.8.1 - dev: false + '@babel/core': 7.28.5 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.28.5) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.7 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - /@emotion/memoize@0.7.4: - resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} - requiresBuild: true - dev: false - optional: true + '@babel/helper-globals@7.28.0': {} - /@emotion/memoize@0.8.1: - resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} - dev: false + '@babel/helper-globals@7.29.7': {} - /@emotion/react@11.11.4(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==} - peerDependencies: - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true + '@babel/helper-member-expression-to-functions@7.29.7': dependencies: - '@babel/runtime': 7.24.5 - '@emotion/babel-plugin': 11.11.0 - '@emotion/cache': 11.11.0 - '@emotion/serialize': 1.1.4 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) - '@emotion/utils': 1.2.1 - '@emotion/weak-memoize': 0.3.1 - '@types/react': 18.3.1 - hoist-non-react-statics: 3.3.2 - react: 18.3.1 - dev: false + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color - /@emotion/serialize@1.1.4: - resolution: {integrity: sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==} + '@babel/helper-module-imports@7.27.1': dependencies: - '@emotion/hash': 0.9.1 - '@emotion/memoize': 0.8.1 - '@emotion/unitless': 0.8.1 - '@emotion/utils': 1.2.1 - csstype: 3.1.3 - dev: false - - /@emotion/sheet@1.2.2: - resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} - dev: false + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color - /@emotion/styled@11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==} - peerDependencies: - '@emotion/react': ^11.0.0-rc.0 - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true + '@babel/helper-module-imports@7.28.6': dependencies: - '@babel/runtime': 7.24.5 - '@emotion/babel-plugin': 11.11.0 - '@emotion/is-prop-valid': 1.2.2 - '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) - '@emotion/serialize': 1.1.4 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) - '@emotion/utils': 1.2.1 - '@types/react': 18.3.1 - react: 18.3.1 - dev: false + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color - /@emotion/unitless@0.8.1: - resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} - dev: false + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color - /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.3.1): - resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} - peerDependencies: - react: '>=16.8.0' + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: - react: 18.3.1 + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color - /@emotion/utils@1.2.1: - resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} - dev: false + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color - /@emotion/weak-memoize@0.3.1: - resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} - dev: false + '@babel/helper-module-transforms@7.29.7(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color - /@ericcornelissen/bash-parser@0.5.2: - resolution: {integrity: sha512-4pIMTa1nEFfMXitv7oaNEWOdM+zpOZavesa5GaiWTgda6Zk32CFGxjUp/iIaN0PwgUW1yTq/fztSjbpE8SLGZQ==} - engines: {node: '>=4'} + '@babel/helper-optimise-call-expression@7.29.7': dependencies: - array-last: 1.3.0 - babylon: 6.18.0 - compose-function: 3.0.3 - deep-freeze: 0.0.1 - filter-iterator: 0.0.1 - filter-obj: 1.1.0 - has-own-property: 0.1.0 - identity-function: 1.0.0 - is-iterable: 1.1.1 - iterable-lookahead: 1.0.0 - lodash.curry: 4.1.1 - magic-string: 0.16.0 - map-obj: 2.0.0 - object-pairs: 0.1.0 - object-values: 1.0.0 - reverse-arguments: 1.0.0 - shell-quote-word: 1.0.1 - to-pascal-case: 1.0.0 - unescape-js: 1.1.4 - dev: true - - /@esbuild/aix-ppc64@0.20.2: - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true - optional: true + '@babel/types': 7.29.7 - /@esbuild/android-arm64@0.20.2: - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true + '@babel/helper-plugin-utils@7.29.7': {} - /@esbuild/android-arm@0.20.2: - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true + '@babel/helper-replace-supers@7.29.7(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color - /@esbuild/android-x64@0.20.2: - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color - /@esbuild/darwin-arm64@0.20.2: - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true + '@babel/helper-string-parser@7.27.1': {} - /@esbuild/darwin-x64@0.20.2: - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true + '@babel/helper-string-parser@7.29.7': {} - /@esbuild/freebsd-arm64@0.20.2: - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true + '@babel/helper-validator-identifier@7.28.5': {} - /@esbuild/freebsd-x64@0.20.2: - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true + '@babel/helper-validator-identifier@7.29.7': {} - /@esbuild/linux-arm64@0.20.2: - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true + '@babel/helper-validator-option@7.27.1': {} - /@esbuild/linux-arm@0.20.2: - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true + '@babel/helper-validator-option@7.29.7': {} - /@esbuild/linux-ia32@0.20.2: - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 - /@esbuild/linux-loong64@0.20.2: - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 - /@esbuild/linux-mips64el@0.20.2: - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 - /@esbuild/linux-ppc64@0.20.2: - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 - /@esbuild/linux-riscv64@0.20.2: - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 - /@esbuild/linux-s390x@0.20.2: - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.29.7 - /@esbuild/linux-x64@0.20.2: - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.29.7 - /@esbuild/netbsd-x64@0.20.2: - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color - /@esbuild/openbsd-x64@0.20.2: - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color - /@esbuild/sunos-x64@0.20.2: - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true + '@babel/preset-typescript@7.29.7(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.28.5) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.28.5) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color - /@esbuild/win32-arm64@0.20.2: - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true + '@babel/runtime@7.28.4': {} - /@esbuild/win32-ia32@0.20.2: - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true + '@babel/runtime@7.29.2': {} - /@esbuild/win32-x64@0.20.2: - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 - /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@babel/template@7.28.6': dependencies: - eslint: 8.57.0 - eslint-visitor-keys: 3.4.3 - dev: true + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 - /@eslint-community/regexpp@4.10.0: - resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 - /@eslint/eslintrc@2.1.4: - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@babel/traverse@7.28.5': dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.1 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color - dev: true - /@eslint/js@8.57.0: - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color - /@fal-works/esbuild-plugin-global-externals@2.1.2: - resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} - dev: true + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color - /@fastify/busboy@2.1.1: - resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} - engines: {node: '>=14'} - dev: true + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 - /@floating-ui/core@1.6.1: - resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==} + '@babel/types@7.29.0': dependencies: - '@floating-ui/utils': 0.2.2 - dev: false + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 - /@floating-ui/dom@1.5.4: - resolution: {integrity: sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==} + '@babel/types@7.29.7': dependencies: - '@floating-ui/core': 1.6.1 - '@floating-ui/utils': 0.2.2 - dev: false + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} - /@floating-ui/dom@1.6.5: - resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==} + '@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: - '@floating-ui/core': 1.6.1 - '@floating-ui/utils': 0.2.2 - dev: false + '@chakra-ui/shared-utils': 2.0.5 - /@floating-ui/utils@0.2.2: - resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==} - dev: false + '@chakra-ui/color-mode@2.2.0(react@19.2.6)': + dependencies: + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@19.2.6) + react: 19.2.6 - /@fontsource-variable/inter@5.0.18: - resolution: {integrity: sha512-rJzSrtJ3b7djiGFvRuTe6stDfbYJGhdQSfn2SI2WfXviee7Er0yKAHE5u7FU7OWVQQQ1x3+cxdmx9NdiAkcrcA==} - dev: false + '@chakra-ui/hooks@2.4.5(react@19.2.6)': + dependencies: + '@chakra-ui/utils': 2.2.5(react@19.2.6) + '@zag-js/element-size': 0.31.1 + copy-to-clipboard: 3.3.3 + framesync: 6.1.2 + react: 19.2.6 - /@humanwhocodes/config-array@0.11.14: - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} + '@chakra-ui/icon@3.2.0(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react@19.2.6)': dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react: 19.2.6 - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true - - /@humanwhocodes/object-schema@2.0.3: - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - dev: true - - /@internationalized/date@3.5.3: - resolution: {integrity: sha512-X9bi8NAEHAjD8yzmPYT2pdJsbe+tYSEBAfowtlxJVJdZR3aK8Vg7ZUT1Fm5M47KLzp/M1p1VwAaeSma3RT7biw==} - dependencies: - '@swc/helpers': 0.5.11 - dev: false - - /@internationalized/number@3.5.2: - resolution: {integrity: sha512-4FGHTi0rOEX1giSkt5MH4/te0eHBq3cvAYsfLlpguV6pzJAReXymiYpE5wPCqKqjkUO3PIsyvk+tBiIV1pZtbA==} - dependencies: - '@swc/helpers': 0.5.11 - dev: false - - /@invoke-ai/eslint-config-react@0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5): - resolution: {integrity: sha512-6ZUY9zgdDhv2WUoLdDKOQdU9ImnH0CBOFtRlOaNOh34IOsNRfn+JA7wqA0PKnkiNrlfPkIQWhn4GRJp68NT5bw==} - peerDependencies: - eslint: ^8.56.0 - prettier: ^3.2.5 - typescript: ^5.3.3 - dependencies: - '@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 - eslint-config-prettier: 9.1.0(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.8.0)(eslint@8.57.0) - eslint-plugin-react: 7.34.1(eslint@8.57.0) - eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) - eslint-plugin-react-refresh: 0.4.6(eslint@8.57.0) - eslint-plugin-simple-import-sort: 12.1.0(eslint@8.57.0) - eslint-plugin-storybook: 0.8.0(eslint@8.57.0)(typescript@5.4.5) - eslint-plugin-unused-imports: 3.2.0(@typescript-eslint/eslint-plugin@7.8.0)(eslint@8.57.0) - prettier: 3.2.5 - typescript: 5.4.5 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: true + '@chakra-ui/icons@2.2.4(@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(framer-motion@10.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)': + dependencies: + '@chakra-ui/react': 2.10.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 - /@invoke-ai/prettier-config-react@0.0.7(prettier@3.2.5): - resolution: {integrity: sha512-vQeWzqwih116TBlIJII93L8ictj6uv7PxcSlAGNZrzG2UcaCFMsQqKCsB/qio26uihgv/EtvN6XAF96SnE0TKw==} - peerDependencies: - prettier: ^3.2.5 + '@chakra-ui/layout@2.3.1(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react@19.2.6)': dependencies: - prettier: 3.2.5 - dev: true + '@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@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react@19.2.6) + '@chakra-ui/object-utils': 2.1.0 + '@chakra-ui/react-children-utils': 2.0.6(react@19.2.6) + '@chakra-ui/react-context': 2.1.0(react@19.2.6) + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react: 19.2.6 - /@invoke-ai/ui-library@0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.18)(@internationalized/date@3.5.3)(@types/react@18.3.1)(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Fmjdlu62NXHgairYXGjcuCrxPEAl1G6Q6ban8g3excF6pDDdBeS7CmSNCyEDMxnSIOZrQlI04OhaMB17Imi9Uw==} - peerDependencies: - '@fontsource-variable/inter': ^5.0.16 - react: ^18.2.0 - react-dom: ^18.2.0 + '@chakra-ui/object-utils@2.1.0': {} + + '@chakra-ui/portal@2.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@ark-ui/react': 1.3.0(@internationalized/date@3.5.3)(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/anatomy': 2.2.2 - '@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/react': 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) - '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2) - '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) - '@fontsource-variable/inter': 5.0.18 - '@nanostores/react': 0.7.2(nanostores@0.9.5)(react@18.3.1) - chakra-react-select: 4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.4)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) - lodash-es: 4.17.21 - nanostores: 0.9.5 - overlayscrollbars: 2.7.3 - overlayscrollbars-react: 0.5.6(overlayscrollbars@2.7.3)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-i18next: 14.1.1(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1) - react-icons: 5.2.0(react@18.3.1) - react-select: 5.8.0(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - transitivePeerDependencies: - - '@chakra-ui/form-control' - - '@chakra-ui/icon' - - '@chakra-ui/media-query' - - '@chakra-ui/menu' - - '@chakra-ui/spinner' - - '@chakra-ui/system' - - '@internationalized/date' - - '@types/react' - - i18next - - react-native - dev: false + '@chakra-ui/react-context': 2.1.0(react@19.2.6) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - /@isaacs/cliui@8.0.2: - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} + '@chakra-ui/react-children-utils@2.0.6(react@19.2.6)': 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 - dev: true + react: 19.2.6 - /@istanbuljs/schema@0.1.3: - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - dev: true + '@chakra-ui/react-context@2.1.0(react@19.2.6)': + dependencies: + react: 19.2.6 - /@jest/schemas@29.6.3: - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@chakra-ui/react-use-safe-layout-effect@2.1.0(react@19.2.6)': dependencies: - '@sinclair/typebox': 0.27.8 - dev: true + react: 19.2.6 - /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.4.5)(vite@5.2.11): - resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==} - peerDependencies: - typescript: '>= 4.3.x' - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + '@chakra-ui/react-utils@2.0.12(react@19.2.6)': dependencies: - glob: 7.2.3 - glob-promise: 4.2.2(glob@7.2.3) - magic-string: 0.27.0 - react-docgen-typescript: 2.2.2(typescript@5.4.5) - typescript: 5.4.5 - vite: 5.2.11(@types/node@20.12.10) - dev: true + '@chakra-ui/utils': 2.0.15 + react: 19.2.6 - /@jridgewell/gen-mapping@0.3.5: - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} + '@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.25 - dev: true + '@chakra-ui/hooks': 2.4.5(react@19.2.6) + '@chakra-ui/styled-system': 2.12.4(react@19.2.6) + '@chakra-ui/theme': 3.4.9(@chakra-ui/styled-system@2.12.4(react@19.2.6))(react@19.2.6) + '@chakra-ui/utils': 2.2.5(react@19.2.6) + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.6) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6) + '@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.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-fast-compare: 3.2.2 + react-focus-lock: 2.13.7(@types/react@19.2.14)(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.6) + transitivePeerDependencies: + - '@types/react' - /@jridgewell/resolve-uri@3.1.2: - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - dev: true + '@chakra-ui/shared-utils@2.0.5': {} - /@jridgewell/set-array@1.2.1: - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - dev: true + '@chakra-ui/styled-system@2.12.0(react@19.2.6)': + dependencies: + '@chakra-ui/utils': 2.2.2(react@19.2.6) + csstype: 3.2.3 + transitivePeerDependencies: + - react + + '@chakra-ui/styled-system@2.12.4(react@19.2.6)': + dependencies: + '@chakra-ui/utils': 2.2.5(react@19.2.6) + csstype: 3.2.3 + transitivePeerDependencies: + - react - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + '@chakra-ui/styled-system@2.9.2': + dependencies: + '@chakra-ui/shared-utils': 2.0.5 + csstype: 3.2.3 + lodash.mergewith: 4.6.2 - /@jridgewell/trace-mapping@0.3.25: - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true + '@chakra-ui/color-mode': 2.2.0(react@19.2.6) + '@chakra-ui/object-utils': 2.1.0 + '@chakra-ui/react-utils': 2.0.12(react@19.2.6) + '@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@19.2.14)(react@19.2.6) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-fast-compare: 3.2.2 - /@mdx-js/react@3.0.1(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==} - peerDependencies: - '@types/react': '>=16' - react: '>=16' + '@chakra-ui/theme-tools@2.1.2(@chakra-ui/styled-system@2.9.2)': dependencies: - '@types/mdx': 2.0.13 - '@types/react': 18.3.1 - react: 18.3.1 - dev: true + '@chakra-ui/anatomy': 2.2.2 + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/styled-system': 2.9.2 + color2k: 2.0.3 - /@microsoft/api-extractor-model@7.28.13(@types/node@20.12.10): - resolution: {integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==} + '@chakra-ui/theme-tools@2.2.6(@chakra-ui/styled-system@2.12.0(react@19.2.6))(react@19.2.6)': dependencies: - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.0.2(@types/node@20.12.10) + '@chakra-ui/anatomy': 2.3.4 + '@chakra-ui/styled-system': 2.12.0(react@19.2.6) + '@chakra-ui/utils': 2.2.2(react@19.2.6) + color2k: 2.0.3 transitivePeerDependencies: - - '@types/node' - dev: true + - react - /@microsoft/api-extractor@7.43.0(@types/node@20.12.10): - resolution: {integrity: sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==} - hasBin: true + '@chakra-ui/theme-tools@2.2.9(@chakra-ui/styled-system@2.12.4(react@19.2.6))(react@19.2.6)': dependencies: - '@microsoft/api-extractor-model': 7.28.13(@types/node@20.12.10) - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.0.2(@types/node@20.12.10) - '@rushstack/rig-package': 0.5.2 - '@rushstack/terminal': 0.10.0(@types/node@20.12.10) - '@rushstack/ts-command-line': 4.19.1(@types/node@20.12.10) - lodash: 4.17.21 - minimatch: 3.0.8 - resolve: 1.22.8 - semver: 7.5.4 - source-map: 0.6.1 - typescript: 5.4.2 + '@chakra-ui/anatomy': 2.3.6 + '@chakra-ui/styled-system': 2.12.4(react@19.2.6) + '@chakra-ui/utils': 2.2.5(react@19.2.6) + color2k: 2.0.3 transitivePeerDependencies: - - '@types/node' - dev: true + - react - /@microsoft/tsdoc-config@0.16.2: - resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} + '@chakra-ui/theme-utils@2.0.21': dependencies: - '@microsoft/tsdoc': 0.14.2 - ajv: 6.12.6 - jju: 1.4.0 - resolve: 1.19.0 - dev: true + '@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 - /@microsoft/tsdoc@0.14.2: - resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} - dev: true + '@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) - /@nanostores/react@0.7.2(nanostores@0.10.3)(react@18.3.1): - resolution: {integrity: sha512-e3OhHJFv3NMSFYDgREdlAQqkyBTHJM91s31kOZ4OvZwJKdFk5BLk0MLbh51EOGUz9QGX2aCHfy1RvweSi7fgwA==} - engines: {node: ^18.0.0 || >=20.0.0} - peerDependencies: - nanostores: ^0.9.0 || ^0.10.0 - react: '>=18.0.0' + '@chakra-ui/theme@3.4.9(@chakra-ui/styled-system@2.12.4(react@19.2.6))(react@19.2.6)': dependencies: - nanostores: 0.10.3 - react: 18.3.1 - dev: false + '@chakra-ui/anatomy': 2.3.6 + '@chakra-ui/styled-system': 2.12.4(react@19.2.6) + '@chakra-ui/theme-tools': 2.2.9(@chakra-ui/styled-system@2.12.4(react@19.2.6))(react@19.2.6) + '@chakra-ui/utils': 2.2.5(react@19.2.6) + transitivePeerDependencies: + - react - /@nanostores/react@0.7.2(nanostores@0.9.5)(react@18.3.1): - resolution: {integrity: sha512-e3OhHJFv3NMSFYDgREdlAQqkyBTHJM91s31kOZ4OvZwJKdFk5BLk0MLbh51EOGUz9QGX2aCHfy1RvweSi7fgwA==} - engines: {node: ^18.0.0 || >=20.0.0} - peerDependencies: - nanostores: ^0.9.0 || ^0.10.0 - react: '>=18.0.0' + '@chakra-ui/utils@2.0.15': dependencies: - nanostores: 0.9.5 - react: 18.3.1 - dev: false + '@types/lodash.mergewith': 4.6.7 + css-box-model: 1.2.1 + framesync: 6.1.2 + lodash.mergewith: 4.6.2 - /@ndelangen/get-tarball@3.0.9: - resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==} + '@chakra-ui/utils@2.2.2(react@19.2.6)': dependencies: - gunzip-maybe: 1.4.2 - pump: 3.0.0 - tar-fs: 2.1.1 - dev: true + '@types/lodash.mergewith': 4.6.9 + lodash.mergewith: 4.6.2 + react: 19.2.6 - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + '@chakra-ui/utils@2.2.5(react@19.2.6)': dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true + '@types/lodash.mergewith': 4.6.9 + lodash.mergewith: 4.6.2 + react: 19.2.6 - /@nodelib/fs.scandir@3.0.0: - resolution: {integrity: sha512-ktI9+PxfHYtKjF3cLTUAh2N+b8MijCRPNwKJNqTVdL0gB0QxLU2rIRaZ1t71oEa3YBDE6bukH1sR0+CDnpp/Mg==} - engines: {node: '>=16.14.0'} + '@dagrejs/dagre@1.1.8': dependencies: - '@nodelib/fs.stat': 3.0.0 - run-parallel: 1.2.0 - dev: true + '@dagrejs/graphlib': 2.2.4 - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true + '@dagrejs/graphlib@2.2.4': {} - /@nodelib/fs.stat@3.0.0: - resolution: {integrity: sha512-2tQOI38s19P9i7X/Drt0v8iMA+KMsgdhB/dyPER+e+2Y8L1Z7QvnuRdW/uLuf5YRFUYmnj4bMA6qCuZHFI1GDQ==} - engines: {node: '>=16.14.0'} - dev: true + '@dmsnell/diff-match-patch@1.1.0': {} - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + '@emnapi/core@1.10.0': dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - dev: true + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true - /@nodelib/fs.walk@2.0.0: - resolution: {integrity: sha512-54voNDBobGdMl3BUXSu7UaDh1P85PGHWlJ5e0XhPugo1JulOyCtp2I+5ri4wplGDJ8QGwPEQW7/x3yTLU7yF1A==} - engines: {node: '>=16.14.0'} + '@emnapi/core@1.7.1': dependencies: - '@nodelib/fs.scandir': 3.0.0 - fastq: 1.17.1 - dev: true - - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - requiresBuild: true - dev: true + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 optional: true - /@polka/url@1.0.0-next.25: - resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} - dev: true - - /@popperjs/core@2.11.8: - resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - dev: false + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true - /@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - '@types/react': - optional: true + '@emnapi/runtime@1.7.1': dependencies: - '@babel/runtime': 7.24.5 - '@types/react': 18.3.1 - react: 18.3.1 - dev: true + tslib: 2.8.1 + optional: true - /@radix-ui/react-slot@1.0.2(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - '@types/react': - optional: true + '@emnapi/wasi-threads@1.1.0': dependencies: - '@babel/runtime': 7.24.5 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@types/react': 18.3.1 - react: 18.3.1 - dev: true + tslib: 2.8.1 + optional: true - /@reactflow/background@11.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-hkvpVEhgvfTDyCvdlitw4ioKCYLaaiRXnuEG+1QM3Np+7N1DiWF1XOv5I8AFyNoJL07yXEkbECUTsHvkBvcG5A==} - peerDependencies: - react: '>=17' - react-dom: '>=17' + '@emnapi/wasi-threads@1.2.1': dependencies: - '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - dev: false + tslib: 2.8.1 + optional: true - /@reactflow/controls@11.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-3xgEg6ALIVkAQCS4NiBjb7ad8Cb3D8CtA7Vvl4Hf5Ar2PIVs6FOaeft9s2iDZGtsWP35ECDYId1rIFVhQL8r+A==} - peerDependencies: - react: '>=17' - react-dom: '>=17' + '@emotion/babel-plugin@11.13.5': dependencies: - '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) + '@babel/helper-module-imports': 7.27.1 + '@babel/runtime': 7.28.4 + '@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: - - '@types/react' - - immer - dev: false + - supports-color - /@reactflow/core@11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-+adHdUa7fJSEM93fWfjQwyWXeI92a1eLKwWbIstoCakHpL8UjzwhEh6sn+mN2h/59MlVI7Ehr1iGTt3MsfcIFA==} - peerDependencies: - react: '>=17' - react-dom: '>=17' + '@emotion/cache@11.14.0': dependencies: - '@types/d3': 7.4.3 - '@types/d3-drag': 3.0.7 - '@types/d3-selection': 3.0.10 - '@types/d3-zoom': 3.0.8 - classcat: 5.0.5 - d3-drag: 3.0.0 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - dev: false + '@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 - /@reactflow/minimap@11.7.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-m2MvdiGSyOu44LEcERDEl1Aj6x//UQRWo3HEAejNU4HQTlJnYrSN8tgrYF8TxC1+c/9UdyzQY5VYgrTwW4QWdg==} - peerDependencies: - react: '>=17' - react-dom: '>=17' + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@0.8.8': dependencies: - '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - '@types/d3-selection': 3.0.10 - '@types/d3-zoom': 3.0.8 - classcat: 5.0.5 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - dev: false + '@emotion/memoize': 0.7.4 + optional: true - /@reactflow/node-resizer@2.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-X7ceQ2s3jFLgbkg03n2RYr4hm3jTVrzkW2W/8ANv/SZfuVmF8XJxlERuD8Eka5voKqLda0ywIZGAbw9GoHLfUQ==} - peerDependencies: - react: '>=17' - react-dom: '>=17' + '@emotion/is-prop-valid@1.4.0': dependencies: - '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - classcat: 5.0.5 - d3-drag: 3.0.0 - d3-selection: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - dev: false + '@emotion/memoize': 0.9.0 - /@reactflow/node-toolbar@1.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-aknvNICO10uWdthFSpgD6ctY/CTBeJUMV9co8T9Ilugr08Nb89IQ4uD0dPmr031ewMQxixtYIkw+sSDDzd2aaQ==} - peerDependencies: - react: '>=17' - react-dom: '>=17' + '@emotion/memoize@0.7.4': + optional: true + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6)': dependencies: - '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) + '@babel/runtime': 7.28.4 + '@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@19.2.6) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 transitivePeerDependencies: - - '@types/react' - - immer - dev: false + - supports-color - /@reduxjs/toolkit@2.2.3(react-redux@9.1.2)(react@18.3.1): - resolution: {integrity: sha512-76dll9EnJXg4EVcI5YNxZA/9hSAmZsFqzMmNRHvIlzw2WS/twfcVX3ysYrWGJMClwEmChQFC4yRq74tn6fdzRA==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true + '@emotion/serialize@1.3.3': dependencies: - immer: 10.1.1 - react: 18.3.1 - react-redux: 9.1.2(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1) - redux: 5.0.1 - redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.1.0 - dev: false + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 - /@roarr/browser-log-writer@1.3.0: - resolution: {integrity: sha512-RTzjxrm0CpTSoESmsO6104VymAksDS/yJEkaZrL/OLfbM6q+J+jLRBLtJxhJHSY03pBWOEE3wRh+pVwfKtBPqg==} - engines: {node: '>=12.0'} - dependencies: - boolean: 3.2.0 - globalthis: 1.0.4 - liqe: 3.8.0 - dev: false + '@emotion/sheet@1.4.0': {} - /@rollup/pluginutils@4.2.1: - resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} - engines: {node: '>= 8.0.0'} + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6)': dependencies: - estree-walker: 2.0.2 - picomatch: 2.3.1 - dev: true + '@babel/runtime': 7.28.4 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.4.0 + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.6) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.6) + '@emotion/utils': 1.4.2 + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + transitivePeerDependencies: + - supports-color - /@rollup/pluginutils@5.1.0: - resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.6)': dependencies: - '@types/estree': 1.0.5 - estree-walker: 2.0.2 - picomatch: 2.3.1 - dev: true + react: 19.2.6 - /@rollup/rollup-android-arm-eabi@4.17.2: - resolution: {integrity: sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@esbuild/aix-ppc64@0.27.7': optional: true - /@rollup/rollup-android-arm64@4.17.2: - resolution: {integrity: sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true + '@esbuild/android-arm64@0.27.7': optional: true - /@rollup/rollup-darwin-arm64@4.17.2: - resolution: {integrity: sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true + '@esbuild/android-arm@0.27.7': optional: true - /@rollup/rollup-darwin-x64@4.17.2: - resolution: {integrity: sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true + '@esbuild/android-x64@0.27.7': optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.17.2: - resolution: {integrity: sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/darwin-arm64@0.27.7': optional: true - /@rollup/rollup-linux-arm-musleabihf@4.17.2: - resolution: {integrity: sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/darwin-x64@0.27.7': optional: true - /@rollup/rollup-linux-arm64-gnu@4.17.2: - resolution: {integrity: sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/freebsd-arm64@0.27.7': optional: true - /@rollup/rollup-linux-arm64-musl@4.17.2: - resolution: {integrity: sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/freebsd-x64@0.27.7': optional: true - /@rollup/rollup-linux-powerpc64le-gnu@4.17.2: - resolution: {integrity: sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-arm64@0.27.7': optional: true - /@rollup/rollup-linux-riscv64-gnu@4.17.2: - resolution: {integrity: sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-arm@0.27.7': optional: true - /@rollup/rollup-linux-s390x-gnu@4.17.2: - resolution: {integrity: sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-ia32@0.27.7': optional: true - /@rollup/rollup-linux-x64-gnu@4.17.2: - resolution: {integrity: sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-loong64@0.27.7': optional: true - /@rollup/rollup-linux-x64-musl@4.17.2: - resolution: {integrity: sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-mips64el@0.27.7': optional: true - /@rollup/rollup-win32-arm64-msvc@4.17.2: - resolution: {integrity: sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true + '@esbuild/linux-ppc64@0.27.7': optional: true - /@rollup/rollup-win32-ia32-msvc@4.17.2: - resolution: {integrity: sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true + '@esbuild/linux-riscv64@0.27.7': optional: true - /@rollup/rollup-win32-x64-msvc@4.17.2: - resolution: {integrity: sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true + '@esbuild/linux-s390x@0.27.7': optional: true - /@rushstack/node-core-library@4.0.2(@types/node@20.12.10): - resolution: {integrity: sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - 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 + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@2.6.1))': + dependencies: + eslint: 9.39.2(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + dependencies: + eslint: 9.39.2(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': dependencies: - '@types/node': 20.12.10 - fs-extra: 7.0.1 - import-lazy: 4.0.0 - jju: 1.4.0 - resolve: 1.22.8 - semver: 7.5.4 - z-schema: 5.0.5 - dev: true + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3(supports-color@10.2.2) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} - /@rushstack/rig-package@0.5.2: - resolution: {integrity: sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==} + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': dependencies: - resolve: 1.22.8 - strip-json-comments: 3.1.1 - dev: true + '@eslint/core': 0.17.0 + levn: 0.4.1 - /@rushstack/terminal@0.10.0(@types/node@20.12.10): - resolution: {integrity: sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true + '@floating-ui/core@1.7.3': dependencies: - '@rushstack/node-core-library': 4.0.2(@types/node@20.12.10) - '@types/node': 20.12.10 - supports-color: 8.1.1 - dev: true + '@floating-ui/utils': 0.2.10 - /@rushstack/ts-command-line@4.19.1(@types/node@20.12.10): - resolution: {integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==} + '@floating-ui/dom@1.7.4': dependencies: - '@rushstack/terminal': 0.10.0(@types/node@20.12.10) - '@types/argparse': 1.0.38 - argparse: 1.0.10 - string-argv: 0.3.2 - transitivePeerDependencies: - - '@types/node' - dev: true + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 - /@sinclair/typebox@0.27.8: - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - dev: true + '@floating-ui/utils@0.2.10': {} - /@snyk/github-codeowners@1.1.0: - resolution: {integrity: sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw==} - engines: {node: '>=8.10'} - hasBin: true - dependencies: - commander: 4.1.1 - ignore: 5.3.1 - p-map: 4.0.0 - dev: true + '@fontsource-variable/inter@5.2.8': {} - /@socket.io/component-emitter@3.1.2: - resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - dev: false + '@humanfs/core@0.19.1': {} - /@storybook/addon-actions@8.0.10: - resolution: {integrity: sha512-IEuc30UAFl7Ws0GwaY/whjBnGaViVEVjmPc+MXUym2wwwJbnCbI+BKJxPoYi/I7QJb5aUNToAE6pl2pDda2g3Q==} + '@humanfs/node@0.16.7': dependencies: - '@storybook/core-events': 8.0.10 - '@storybook/global': 5.0.0 - '@types/uuid': 9.0.8 - dequal: 2.0.3 - polished: 4.3.1 - uuid: 9.0.1 - dev: true + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 - /@storybook/addon-backgrounds@8.0.10: - resolution: {integrity: sha512-445SUQqOH5xFJWlNeMu74FEgk26O9Zm/5aqnvmeteB0Q2JLaw7k2q9i/W6XFu97QkRxqA1EGbDxLR3+e1xCjaA==} - dependencies: - '@storybook/global': 5.0.0 - memoizerific: 1.11.3 - ts-dedent: 2.2.0 - dev: true + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} - /@storybook/addon-controls@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-MAUtIJGayNSsfn3VZ6SjQwpRkb4ky+10oVfos+xX9GQ5+7RCs+oYMuE4+aiQvvfXNdV8v0pUGPUPeUzqfJmhOA==} + '@invoke-ai/ui-library@https://codeload.github.com/invoke-ai/ui-library/tar.gz/78ddd0a1670af523cfcac186849deb23bd07d419(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(@fontsource-variable/inter@5.2.8)(@types/react@19.2.14)(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3)': dependencies: - '@storybook/blocks': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - lodash: 4.17.21 - ts-dedent: 2.2.0 - transitivePeerDependencies: - - '@types/react' - - encoding - - react - - react-dom - - supports-color - dev: true - - /@storybook/addon-docs@8.0.10: - resolution: {integrity: sha512-y+Agoez/hXZHKUMIZHU96T5V1v0cs4ArSNfjqDg9DPYcyQ88ihJNb6ZabIgzmEaJF/NncCW+LofWeUtkTwalkw==} - dependencies: - '@babel/core': 7.24.5 - '@mdx-js/react': 3.0.1(@types/react@18.3.1)(react@18.3.1) - '@storybook/blocks': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - '@storybook/client-logger': 8.0.10 - '@storybook/components': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - '@storybook/csf-plugin': 8.0.10 - '@storybook/csf-tools': 8.0.10 - '@storybook/global': 5.0.0 - '@storybook/node-logger': 8.0.10 - '@storybook/preview-api': 8.0.10 - '@storybook/react-dom-shim': 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@storybook/types': 8.0.10 - '@types/react': 18.3.1 - fs-extra: 11.2.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - rehype-external-links: 3.0.0 - rehype-slug: 6.0.0 - ts-dedent: 2.2.0 - transitivePeerDependencies: - - encoding - - supports-color - dev: true - - /@storybook/addon-essentials@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Uy3+vm7QX+b/9rhW/iFa3EYAAbV1T2LljY9Bj4aTPZHas9Bpvl5ZPnOm/PhybcE8UFHEoVTJ0v3uWb0dsUEigw==} - dependencies: - '@storybook/addon-actions': 8.0.10 - '@storybook/addon-backgrounds': 8.0.10 - '@storybook/addon-controls': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - '@storybook/addon-docs': 8.0.10 - '@storybook/addon-highlight': 8.0.10 - '@storybook/addon-measure': 8.0.10 - '@storybook/addon-outline': 8.0.10 - '@storybook/addon-toolbars': 8.0.10 - '@storybook/addon-viewport': 8.0.10 - '@storybook/core-common': 8.0.10 - '@storybook/manager-api': 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@storybook/node-logger': 8.0.10 - '@storybook/preview-api': 8.0.10 - ts-dedent: 2.2.0 + '@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@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(framer-motion@10.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react@19.2.6) + '@chakra-ui/portal': 2.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@chakra-ui/react': 2.10.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@chakra-ui/styled-system': 2.12.0(react@19.2.6) + '@chakra-ui/theme-tools': 2.2.6(@chakra-ui/styled-system@2.12.0(react@19.2.6))(react@19.2.6) + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.6) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6) + '@fontsource-variable/inter': 5.2.8 + '@nanostores/react': 1.1.0(nanostores@1.3.0)(react@19.2.6) + chakra-react-select: 4.10.1(@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(framer-motion@10.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + es-toolkit: 1.46.1 + framer-motion: 10.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + math-expression-evaluator: 2.0.7 + nanostores: 1.3.0 + overlayscrollbars: 2.12.0 + overlayscrollbars-react: 0.5.6(overlayscrollbars@2.12.0)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-i18next: 15.7.4(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3) + react-icons: 5.5.0(react@19.2.6) + react-select: 5.10.2(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) transitivePeerDependencies: + - '@chakra-ui/system' - '@types/react' - - encoding - - react - - react-dom + - i18next + - react-native - supports-color - dev: true - - /@storybook/addon-highlight@8.0.10: - resolution: {integrity: sha512-40GB82t1e2LCCjqXcC6Z5lq1yIpA1+Yl5E2tKeggOVwg5HHAX02ESNDdBaIOlCqMkU3WKzjGPurDNOLUAbsV2g==} - dependencies: - '@storybook/global': 5.0.0 - dev: true + - typescript - /@storybook/addon-interactions@8.0.10(vitest@1.6.0): - resolution: {integrity: sha512-6yFNmk6+7082/8TRVyjUsKlwumalEdO0XQ5amPbVGuECzc3HFn0ELwzPrQ4TBlN5MRtX4+buoh5dc/1RUDrh9w==} + '@isaacs/cliui@8.0.2': dependencies: - '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.0.10 - '@storybook/test': 8.0.10(vitest@1.6.0) - '@storybook/types': 8.0.10 - polished: 4.3.1 - ts-dedent: 2.2.0 - transitivePeerDependencies: - - '@jest/globals' - - '@types/bun' - - '@types/jest' - - jest - - vitest - dev: true + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 - /@storybook/addon-links@8.0.10(react@18.3.1): - resolution: {integrity: sha512-+mIyH2UcrgQfAyRM4+ARkB/D0OOY8UMwkZsD8dD23APZ8oru7W/NHX3lXl0WjPfQcOIx/QwWNWI3+DgVZJY3jw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@5.9.3)(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1))': dependencies: - '@storybook/csf': 0.1.7 - '@storybook/global': 5.0.0 - react: 18.3.1 - ts-dedent: 2.2.0 - dev: true + glob: 13.0.6 + react-docgen-typescript: 2.4.0(typescript@5.9.3) + vite: 8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1) + optionalDependencies: + typescript: 5.9.3 - /@storybook/addon-measure@8.0.10: - resolution: {integrity: sha512-quXQwmZJUhOxDIlbXTH6aKYQkwkDpL0UQRkUZn1xuZ2sVKJeaee73QSWqw8HDD4Rz9huS+OrAdVoq/Cz5FoC6A==} + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@storybook/global': 5.0.0 - tiny-invariant: 1.3.3 - dev: true + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - /@storybook/addon-outline@8.0.10: - resolution: {integrity: sha512-1eDO2s/vHhhSJo7W5SetqjleUBTZLI08VNP89c4j7vdRKiMZ1DYhr0dqUGIC3w7cDsawI/nQ24wancHHayAnqw==} + '@jridgewell/remapping@2.3.5': dependencies: - '@storybook/global': 5.0.0 - ts-dedent: 2.2.0 - dev: true + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - /@storybook/addon-storysource@8.0.10: - resolution: {integrity: sha512-LCNgp5pWyI9ZlJMFeN0nvt9gvgHMWneDjfUoAHTOP7Smi0xz4lUDYKB4P53kgE1peHn2+nxAauSBdA1IEFBIRA==} - dependencies: - '@storybook/source-loader': 8.0.10 - estraverse: 5.3.0 - tiny-invariant: 1.3.3 - dev: true + '@jridgewell/resolve-uri@3.1.2': {} - /@storybook/addon-toolbars@8.0.10: - resolution: {integrity: sha512-67HP6mTJU/gjRju01Z5HjeqoRiJMDlrMvMvjGBg7w5+tPNtjYqdelfe2+kcfU+Hf6dfcuqaBDwaUUGSv+RYtRQ==} - dev: true + '@jridgewell/sourcemap-codec@1.5.5': {} - /@storybook/addon-viewport@8.0.10: - resolution: {integrity: sha512-NJ88Nd/tXreHLyLeF3VP+b8Fu2KtUuJ0L4JYpEMmcdaejGARTrJJOU+pcZBiUqEHFeXQ8rDY8DKXhUJZQFQ1Wg==} + '@jridgewell/trace-mapping@0.3.31': dependencies: - memoizerific: 1.11.3 - dev: true + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 - /@storybook/blocks@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-LOaxvcO2d4dT4YoWlQ0bq/c8qA3aHoqtyuvBjwbVn+359bjMtgj/91YuP9Y2+ggZZ4p+ttgvk39PcmJlNXlJsw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true + '@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.6)': dependencies: - '@storybook/channels': 8.0.10 - '@storybook/client-logger': 8.0.10 - '@storybook/components': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - '@storybook/core-events': 8.0.10 - '@storybook/csf': 0.1.7 - '@storybook/docs-tools': 8.0.10 - '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.3.1)(react@18.3.1) - '@storybook/manager-api': 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@storybook/preview-api': 8.0.10 - '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@storybook/types': 8.0.10 - '@types/lodash': 4.17.1 - color-convert: 2.0.1 - dequal: 2.0.3 - lodash: 4.17.21 - markdown-to-jsx: 7.3.2(react@18.3.1) - memoizerific: 1.11.3 - polished: 4.3.1 - react: 18.3.1 - react-colorful: 5.6.1(react-dom@18.3.1)(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) - telejson: 7.2.0 - tocbot: 4.27.19 - ts-dedent: 2.2.0 - util-deprecate: 1.0.2 - transitivePeerDependencies: - - '@types/react' - - encoding - - supports-color - dev: true - - /@storybook/builder-manager@8.0.10: - resolution: {integrity: sha512-lo57jeeYuYCKYrmGOdLg25rMyiGYSTwJ+zYsQ3RvClVICjP6X0I1RCKAJDzkI0BixH6s1+w5ynD6X3PtDnhUuw==} - dependencies: - '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@storybook/core-common': 8.0.10 - '@storybook/manager': 8.0.10 - '@storybook/node-logger': 8.0.10 - '@types/ejs': 3.1.5 - '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.20.2) - browser-assert: 1.2.1 - ejs: 3.1.10 - esbuild: 0.20.2 - esbuild-plugin-alias: 0.2.1 - express: 4.19.2 - fs-extra: 11.2.0 - process: 0.11.10 - util: 0.12.5 - transitivePeerDependencies: - - encoding - - supports-color - dev: true + '@types/mdx': 2.0.13 + '@types/react': 19.2.14 + react: 19.2.6 - /@storybook/builder-vite@8.0.10(typescript@5.4.5)(vite@5.2.11): - resolution: {integrity: sha512-Rod/2jYvF4Ng1MjIMZEXe/3z0lPuxkRtetCTr3ECPgi83lHXpHJ+N0NVfJEMs+pXsVqkLP3iGt2hLn6D6yFMwA==} - peerDependencies: - '@preact/preset-vite': '*' - typescript: '>= 4.3.x' - vite: ^4.0.0 || ^5.0.0 - vite-plugin-glimmerx: '*' - peerDependenciesMeta: - '@preact/preset-vite': - optional: true - typescript: - optional: true - vite-plugin-glimmerx: - optional: true + '@nanostores/react@1.1.0(nanostores@1.3.0)(react@19.2.6)': dependencies: - '@storybook/channels': 8.0.10 - '@storybook/client-logger': 8.0.10 - '@storybook/core-common': 8.0.10 - '@storybook/core-events': 8.0.10 - '@storybook/csf-plugin': 8.0.10 - '@storybook/node-logger': 8.0.10 - '@storybook/preview': 8.0.10 - '@storybook/preview-api': 8.0.10 - '@storybook/types': 8.0.10 - '@types/find-cache-dir': 3.2.1 - browser-assert: 1.2.1 - es-module-lexer: 0.9.3 - express: 4.19.2 - find-cache-dir: 3.3.2 - fs-extra: 11.2.0 - magic-string: 0.30.10 - ts-dedent: 2.2.0 - typescript: 5.4.5 - vite: 5.2.11(@types/node@20.12.10) - transitivePeerDependencies: - - encoding - - supports-color - dev: true + nanostores: 1.3.0 + react: 19.2.6 - /@storybook/channels@8.0.10: - resolution: {integrity: sha512-3JLxfD7czlx31dAGvAYJ4J4BNE/Y2+hhj/dsV3xlQTHKVpnWknaoeYEC1a6YScyfsH6W+XmP2rzZKzH4EkLSGQ==} + '@napi-rs/wasm-runtime@1.1.0': dependencies: - '@storybook/client-logger': 8.0.10 - '@storybook/core-events': 8.0.10 - '@storybook/global': 5.0.0 - telejson: 7.2.0 - tiny-invariant: 1.3.3 - dev: true + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true - /@storybook/cli@8.0.10(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-KUZEO2lyvOS2sRJEFXovt6+5b65iWsh7F8e8S1cM20fCM1rZAlWtwmoxmDVXDmyEp0wTrq4FrRxKnbo9UO518w==} - hasBin: true + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@babel/core': 7.24.5 - '@babel/types': 7.24.5 - '@ndelangen/get-tarball': 3.0.9 - '@storybook/codemod': 8.0.10 - '@storybook/core-common': 8.0.10 - '@storybook/core-events': 8.0.10 - '@storybook/core-server': 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@storybook/csf-tools': 8.0.10 - '@storybook/node-logger': 8.0.10 - '@storybook/telemetry': 8.0.10 - '@storybook/types': 8.0.10 - '@types/semver': 7.5.8 - '@yarnpkg/fslib': 2.10.3 - '@yarnpkg/libzip': 2.3.0 - chalk: 4.1.2 - commander: 6.2.1 - cross-spawn: 7.0.3 - detect-indent: 6.1.0 - envinfo: 7.13.0 - execa: 5.1.1 - find-up: 5.0.0 - fs-extra: 11.2.0 - get-npm-tarball-url: 2.1.0 - giget: 1.2.3 - globby: 11.1.0 - jscodeshift: 0.15.2(@babel/preset-env@7.24.5) - leven: 3.1.0 - ora: 5.4.1 - prettier: 3.2.5 - prompts: 2.4.2 - read-pkg-up: 7.0.1 - semver: 7.6.0 - strip-json-comments: 3.1.1 - tempy: 1.0.1 - tiny-invariant: 1.3.3 - ts-dedent: 2.2.0 - transitivePeerDependencies: - - '@babel/preset-env' - - bufferutil - - encoding - - react - - react-dom - - supports-color - - utf-8-validate - dev: true + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true - /@storybook/client-logger@8.0.10: - resolution: {integrity: sha512-u38SbZNAunZzxZNHMJb9jkUwFkLyWxmvp4xtiRM3u9sMUShXoTnzbw1yKrxs+kYJjg+58UQPZ1JhEBRcHt5Oww==} + '@nodelib/fs.scandir@2.1.5': dependencies: - '@storybook/global': 5.0.0 - dev: true - - /@storybook/codemod@8.0.10: - resolution: {integrity: sha512-t45jKGs/eyR/nKVX6QgRtMZSAjJo5aXWWk3B24xVbW6ywr0jt1LC100FkHG4Af8cApIfh8uUmS9X05hMG5zGGA==} - dependencies: - '@babel/core': 7.24.5 - '@babel/preset-env': 7.24.5(@babel/core@7.24.5) - '@babel/types': 7.24.5 - '@storybook/csf': 0.1.7 - '@storybook/csf-tools': 8.0.10 - '@storybook/node-logger': 8.0.10 - '@storybook/types': 8.0.10 - '@types/cross-spawn': 6.0.6 - cross-spawn: 7.0.3 - globby: 11.1.0 - jscodeshift: 0.15.2(@babel/preset-env@7.24.5) - lodash: 4.17.21 - prettier: 3.2.5 - recast: 0.23.6 - tiny-invariant: 1.3.3 - transitivePeerDependencies: - - supports-color - dev: true + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 - /@storybook/components@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-eo+oDDcm35YBB3dtDYDfcjJypNVPmRty85VWpAOBsJXpwp/fgU8csx0DM3KmhrQ4cWLf2WzcFowJwI1w+J88Sw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.3.1) - '@storybook/client-logger': 8.0.10 - '@storybook/csf': 0.1.7 - '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.3.1)(react@18.3.1) - '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@storybook/types': 8.0.10 - memoizerific: 1.11.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - util-deprecate: 1.0.2 - transitivePeerDependencies: - - '@types/react' - dev: true + '@nodelib/fs.stat@2.0.5': {} - /@storybook/core-common@8.0.10: - resolution: {integrity: sha512-hsFlPieputaDQoxstnPa3pykTc4bUwEDgCHf8U43+/Z7qmLOQ9fpG+2CFW930rsCRghYpPreOvsmhY7lsGKWLQ==} + '@nodelib/fs.walk@1.2.8': dependencies: - '@storybook/core-events': 8.0.10 - '@storybook/csf-tools': 8.0.10 - '@storybook/node-logger': 8.0.10 - '@storybook/types': 8.0.10 - '@yarnpkg/fslib': 2.10.3 - '@yarnpkg/libzip': 2.3.0 - chalk: 4.1.2 - cross-spawn: 7.0.3 - esbuild: 0.20.2 - esbuild-register: 3.5.0(esbuild@0.20.2) - execa: 5.1.1 - file-system-cache: 2.3.0 - find-cache-dir: 3.3.2 - find-up: 5.0.0 - fs-extra: 11.2.0 - glob: 10.3.12 - handlebars: 4.7.8 - lazy-universal-dotenv: 4.0.0 - node-fetch: 2.7.0 - picomatch: 2.3.1 - pkg-dir: 5.0.0 - pretty-hrtime: 1.0.3 - resolve-from: 5.0.0 - semver: 7.6.0 - tempy: 1.0.1 - tiny-invariant: 1.3.3 - ts-dedent: 2.2.0 - util: 0.12.5 - transitivePeerDependencies: - - encoding - - supports-color - dev: true + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 - /@storybook/core-events@8.0.10: - resolution: {integrity: sha512-TuHPS6p5ZNr4vp4butLb4R98aFx0NRYCI/7VPhJEUH5rPiqNzE3PZd8DC8rnVxavsJ+jO1/y+egNKXRYkEcoPQ==} - dependencies: - ts-dedent: 2.2.0 - dev: true - - /@storybook/core-server@8.0.10(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-HYDw2QFBxg1X/d6g0rUhirOB5Jq6g90HBnyrZzxKoqKWJCNsCADSgM+h9HgtUw0jA97qBpIqmNO9n3mXFPWU/Q==} - dependencies: - '@aw-web-design/x-default-browser': 1.4.126 - '@babel/core': 7.24.5 - '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 8.0.10 - '@storybook/channels': 8.0.10 - '@storybook/core-common': 8.0.10 - '@storybook/core-events': 8.0.10 - '@storybook/csf': 0.1.7 - '@storybook/csf-tools': 8.0.10 - '@storybook/docs-mdx': 3.0.0 - '@storybook/global': 5.0.0 - '@storybook/manager': 8.0.10 - '@storybook/manager-api': 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@storybook/node-logger': 8.0.10 - '@storybook/preview-api': 8.0.10 - '@storybook/telemetry': 8.0.10 - '@storybook/types': 8.0.10 - '@types/detect-port': 1.3.5 - '@types/node': 18.19.32 - '@types/pretty-hrtime': 1.0.3 - '@types/semver': 7.5.8 - better-opn: 3.0.2 - chalk: 4.1.2 - cli-table3: 0.6.4 - compression: 1.7.4 - detect-port: 1.5.1 - express: 4.19.2 - fs-extra: 11.2.0 - globby: 11.1.0 - ip: 2.0.1 - lodash: 4.17.21 - open: 8.4.2 - pretty-hrtime: 1.0.3 - prompts: 2.4.2 - read-pkg-up: 7.0.1 - semver: 7.6.0 - telejson: 7.2.0 - tiny-invariant: 1.3.3 - ts-dedent: 2.2.0 - util: 0.12.5 - util-deprecate: 1.0.2 - watchpack: 2.4.1 - ws: 8.17.0 - transitivePeerDependencies: - - bufferutil - - encoding - - react - - react-dom - - supports-color - - utf-8-validate - dev: true + '@observ33r/object-equals@1.1.6': {} + + '@oxc-project/types@0.128.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.16.2': + optional: true + + '@oxc-resolver/binding-android-arm64@11.16.2': + optional: true - /@storybook/csf-plugin@8.0.10: - resolution: {integrity: sha512-0EsyEx/06sCjI8sn40r7cABtBU1vUKPMPD+S5mJiZymm73BgdARj0qZOlLoK2LP+t2pcaB/Cn7KX/uyhhv7M2g==} + '@oxc-resolver/binding-darwin-arm64@11.16.2': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.16.2': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.16.2': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.16.2': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.16.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.16.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.16.2': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.16.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.16.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.16.2': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.16.2': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.16.2': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.16.2': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.16.2': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.16.2': dependencies: - '@storybook/csf-tools': 8.0.10 - unplugin: 1.10.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@storybook/csf-tools@8.0.10: - resolution: {integrity: sha512-xUc6fVIKoCujf/7JZhkYjrVXeNsTSoDrZFNmqLEmtfktJVqYdXY4LuSAtlBmAIyETi09ULTuuVexrcKFwjzuBA==} - dependencies: - '@babel/generator': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 - '@storybook/csf': 0.1.7 - '@storybook/types': 8.0.10 - fs-extra: 11.2.0 - recast: 0.23.6 - ts-dedent: 2.2.0 - transitivePeerDependencies: - - supports-color - dev: true + '@napi-rs/wasm-runtime': 1.1.0 + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.16.2': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.16.2': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.16.2': + optional: true - /@storybook/csf@0.0.1: - resolution: {integrity: sha512-USTLkZze5gkel8MYCujSRBVIrUQ3YPBrLOx7GNk/0wttvVtlzWXAq9eLbQ4p/NicGxP+3T7KPEMVV//g+yubpw==} + '@pkgjs/parseargs@0.11.0': + optional: true + + '@polka/url@1.0.0-next.29': {} + + '@popperjs/core@2.11.8': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.6)': dependencies: - lodash: 4.17.21 - dev: true + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /@storybook/csf@0.1.7: - resolution: {integrity: sha512-53JeLZBibjQxi0Ep+/AJTfxlofJlxy1jXcSKENlnKxHjWEYyHQCumMP5yTFjf7vhNnMjEpV3zx6t23ssFiGRyw==} + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.6)': dependencies: - type-fest: 2.19.0 - dev: true + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + aria-hidden: 1.2.6 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - /@storybook/docs-mdx@3.0.0: - resolution: {integrity: sha512-NmiGXl2HU33zpwTv1XORe9XG9H+dRUC1Jl11u92L4xr062pZtrShLmD4VKIsOQujxhhOrbxpwhNOt+6TdhyIdQ==} - dev: true + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.6)': + dependencies: + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /@storybook/docs-tools@8.0.10: - resolution: {integrity: sha512-rg9KS81vEh13VMr4mAgs+7L4kYqoRtG7kVfV1WHxzJxjR3wYcVR0kP9gPTWV4Xha/TA3onHu9sxKxMTWha0urQ==} + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@storybook/core-common': 8.0.10 - '@storybook/core-events': 8.0.10 - '@storybook/preview-api': 8.0.10 - '@storybook/types': 8.0.10 - '@types/doctrine': 0.0.3 - assert: 2.1.0 - doctrine: 3.0.0 - lodash: 4.17.21 - transitivePeerDependencies: - - encoding - - supports-color - dev: true + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - /@storybook/global@5.0.0: - resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} - dev: true + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.6)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /@storybook/icons@1.2.9(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-cOmylsz25SYXaJL/gvTk/dl3pyk7yBFRfeXTsHvTA3dfhoU/LWSq0NKL9nM7WBasJyn6XPSGnLS4RtKXLw5EUg==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: true + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - /@storybook/instrumenter@8.0.10: - resolution: {integrity: sha512-6IYjWeQFA5x68xRoW5dU4yAc1Hwq1ZBkZbXVgJbr5LJw5x+y8eKdZzIaOmSsSKOI96R7J5YWWd2WA1Q0nRurtg==} + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@storybook/channels': 8.0.10 - '@storybook/client-logger': 8.0.10 - '@storybook/core-events': 8.0.10 - '@storybook/global': 5.0.0 - '@storybook/preview-api': 8.0.10 - '@vitest/utils': 1.6.0 - util: 0.12.5 - dev: true - - /@storybook/manager-api@8.0.10(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-LLu6YKQLWf5QB3h3RO8IevjLrSOew7aidIQPr9DIr9xC8wA7N2fQabr+qrJdE306p3cHZ0nzhYNYZxSjm4Dvdw==} - dependencies: - '@storybook/channels': 8.0.10 - '@storybook/client-logger': 8.0.10 - '@storybook/core-events': 8.0.10 - '@storybook/csf': 0.1.7 - '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.3.1)(react@18.3.1) - '@storybook/router': 8.0.10 - '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@storybook/types': 8.0.10 - dequal: 2.0.3 - lodash: 4.17.21 - memoizerific: 1.11.3 - store2: 2.14.3 - telejson: 7.2.0 - ts-dedent: 2.2.0 - transitivePeerDependencies: - - react - - react-dom - dev: true + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - /@storybook/manager@8.0.10: - resolution: {integrity: sha512-bojGglUQNry48L4siURc2zQKswavLzMh69rqsfL3ZXx+i+USfRfB7593azTlaZh0q6HO4bUAjB24RfQCyifLLQ==} - dev: true + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - /@storybook/node-logger@8.0.10: - resolution: {integrity: sha512-UMmaUaA3VOX/mKLsSvOnbZre2/1tZ6hazA6H0eAnClKb51jRD1AJrsBYK+uHr/CAp7t710bB5U8apPov7hayDw==} - dev: true + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) - /@storybook/preview-api@8.0.10: - resolution: {integrity: sha512-uZ6btF7Iloz9TnDcKLQ5ydi2YK0cnulv/8FLQhBCwSrzLLLb+T2DGz0cAeuWZEvMUNWNmkWJ9PAFQFs09/8p/Q==} + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.6)': dependencies: - '@storybook/channels': 8.0.10 - '@storybook/client-logger': 8.0.10 - '@storybook/core-events': 8.0.10 - '@storybook/csf': 0.1.7 - '@storybook/global': 5.0.0 - '@storybook/types': 8.0.10 - '@types/qs': 6.9.15 - dequal: 2.0.3 - lodash: 4.17.21 - memoizerific: 1.11.3 - qs: 6.12.1 - tiny-invariant: 1.3.3 - ts-dedent: 2.2.0 - util-deprecate: 1.0.2 - dev: true + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /@storybook/preview@8.0.10: - resolution: {integrity: sha512-op7gZqop8PSFyPA4tc1Zds8jG6VnskwpYUUsa44pZoEez9PKEFCf4jE+7AQwbBS3hnuCb0CKBfASN8GRyoznbw==} - dev: true + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.6)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /@storybook/react-dom-shim@8.0.10(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-3x8EWEkZebpWpp1pwXEzdabGINwOQt8odM5+hsOlDRtFZBmUqmmzK0rtn7orlcGlOXO4rd6QuZj4Tc5WV28dVQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.6)': dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: true + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /@storybook/react-vite@8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5)(vite@5.2.11): - resolution: {integrity: sha512-J0Tw1jWSQYzc37AWaJCbrFQLlWsCHby0ie0yPx8DVehlnTT6xZWkohiKBq5iwMyYfF9SGrOfZ/dVRiB5q2sOIA==} - engines: {node: '>=18.0.0'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - vite: ^4.0.0 || ^5.0.0 + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.6)': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.4.5)(vite@5.2.11) - '@rollup/pluginutils': 5.1.0 - '@storybook/builder-vite': 8.0.10(typescript@5.4.5)(vite@5.2.11) - '@storybook/node-logger': 8.0.10 - '@storybook/react': 8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5) - find-up: 5.0.0 - magic-string: 0.30.10 - react: 18.3.1 - react-docgen: 7.0.3 - react-dom: 18.3.1(react@18.3.1) - resolve: 1.22.8 - tsconfig-paths: 4.2.0 - vite: 5.2.11(@types/node@20.12.10) - transitivePeerDependencies: - - '@preact/preset-vite' - - encoding - - rollup - - supports-color - - typescript - - vite-plugin-glimmerx - dev: true + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /@storybook/react@8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5): - resolution: {integrity: sha512-/MIMc02TNmiNXDzk55dm9+ujfNE5LVNeqqK+vxXWLlCZ0aXRAd1/ZLYeRFuYLgEETB7mh7IP8AXjvM68NX5HYg==} - engines: {node: '>=18.0.0'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - typescript: '>= 4.2.x' - peerDependenciesMeta: - typescript: - optional: true + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.6)': dependencies: - '@storybook/client-logger': 8.0.10 - '@storybook/docs-tools': 8.0.10 - '@storybook/global': 5.0.0 - '@storybook/preview-api': 8.0.10 - '@storybook/react-dom-shim': 8.0.10(react-dom@18.3.1)(react@18.3.1) - '@storybook/types': 8.0.10 - '@types/escodegen': 0.0.6 - '@types/estree': 0.0.51 - '@types/node': 18.19.32 - acorn: 7.4.1 - acorn-jsx: 5.3.2(acorn@7.4.1) - acorn-walk: 7.2.0 - escodegen: 2.1.0 - html-tags: 3.3.1 - lodash: 4.17.21 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-element-to-jsx-string: 15.0.0(react-dom@18.3.1)(react@18.3.1) - semver: 7.6.0 - ts-dedent: 2.2.0 - type-fest: 2.19.0 - typescript: 5.4.5 - util-deprecate: 1.0.2 - transitivePeerDependencies: - - encoding - - supports-color - dev: true + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /@storybook/router@8.0.10: - resolution: {integrity: sha512-AZhgiet+EK0ZsPbaDgbbVTAHW2LAMCP1z/Un2uMBbdDeD0Ys29Af47AbEj/Ome5r1cqasLvzq2WXJlVXPNB0Zw==} + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.6)': dependencies: - '@storybook/client-logger': 8.0.10 - memoizerific: 1.11.3 - qs: 6.12.1 - dev: true + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /@storybook/source-loader@8.0.10: - resolution: {integrity: sha512-bv9FRPzELjcoMJLWLDqkUNh1zY0DiCgcvM+9qsZva8pxAD4fzrX+mgCS2vZVJHRg8wMAhw/ymdXixDUodHAvsw==} + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.6)': dependencies: - '@storybook/csf': 0.1.7 - '@storybook/types': 8.0.10 - estraverse: 5.3.0 - lodash: 4.17.21 - prettier: 3.2.5 - dev: true + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /@storybook/telemetry@8.0.10: - resolution: {integrity: sha512-s4Uc+KZQkdmD2d+64Qf8wYknhQZwmjf2CxjIjv9b4KLsU/nyfDheK7Fzd1jhBKb2UQUlLW5HhZkBgs1RsZcDHA==} + '@redocly/ajv@8.17.1': dependencies: - '@storybook/client-logger': 8.0.10 - '@storybook/core-common': 8.0.10 - '@storybook/csf-tools': 8.0.10 - chalk: 4.1.2 - detect-package-manager: 2.0.1 - fetch-retry: 5.0.6 - fs-extra: 11.2.0 - read-pkg-up: 7.0.1 + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + '@redocly/config@0.22.2': {} + + '@redocly/openapi-core@1.34.6(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.17.1 + '@redocly/config': 0.22.2 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.1.1 + minimatch: 5.1.6 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 transitivePeerDependencies: - - encoding - supports-color - dev: true - - /@storybook/test@8.0.10(vitest@1.6.0): - resolution: {integrity: sha512-VqjzKJiOCjaZ0CjLeKygYk8uetiaiKbpIox+BrND9GtpEBHcRZA5AeFY2P1aSCOhsaDwuh4KRBxJWFug7DhWGQ==} - dependencies: - '@storybook/client-logger': 8.0.10 - '@storybook/core-events': 8.0.10 - '@storybook/instrumenter': 8.0.10 - '@storybook/preview-api': 8.0.10 - '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.4.5(vitest@1.6.0) - '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) - '@vitest/expect': 1.3.1 - '@vitest/spy': 1.6.0 - util: 0.12.5 - transitivePeerDependencies: - - '@jest/globals' - - '@types/bun' - - '@types/jest' - - jest - - vitest - dev: true - /@storybook/theming@8.0.10(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-7NHt7bMC7lPkwz9KdDpa6DkLoQZz5OV6jsx/qY91kcdLo1rpnRPAiVlJvmWesFxi1oXOpVDpHHllWzf8KDBv8A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true + '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6)': dependencies: - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) - '@storybook/client-logger': 8.0.10 - '@storybook/global': 5.0.0 - memoizerific: 1.11.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: true + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 10.2.0 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.6 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1) - /@storybook/types@8.0.10: - resolution: {integrity: sha512-S/hKS7+SqNnYIehwxdQ4M2nnlfGDdYWAXdtPCVJCmS+YF2amgAxeuisiHbUg7eypds6VL0Oxk/j2nPEHOHk9pg==} + '@roarr/browser-log-writer@1.3.0': dependencies: - '@storybook/channels': 8.0.10 - '@types/express': 4.17.21 - file-system-cache: 2.3.0 - dev: true + boolean: 3.2.0 + globalthis: 1.0.4 + liqe: 3.8.4 - /@swc/core-darwin-arm64@1.5.3: - resolution: {integrity: sha512-kRmmV2XqWegzGXvJfVVOj10OXhLgaVOOBjaX3p3Aqg7Do5ksg+bY5wi1gAN/Eul7B08Oqf7GG7WJevjDQGWPOg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true + '@rolldown/binding-android-arm64@1.0.0-rc.18': optional: true - /@swc/core-darwin-x64@1.5.3: - resolution: {integrity: sha512-EYs0+ovaRw6ZN9GBr2nIeC7gUXWA0q4RYR+Og3Vo0Qgv2Mt/XudF44A2lPK9X7M3JIfu6JjnxnTuvsK1Lqojfw==} - engines: {node: '>=10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true + '@rolldown/binding-darwin-arm64@1.0.0-rc.18': optional: true - /@swc/core-linux-arm-gnueabihf@1.5.3: - resolution: {integrity: sha512-RBVUTidSf4wgPdv98VrgJ4rMzMDN/3LBWdT7l+R7mNFH+mtID7ZAhTON0o/m1HkECgAgi1xcbTOVAw1xgd5KLA==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true + '@rolldown/binding-darwin-x64@1.0.0-rc.18': optional: true - /@swc/core-linux-arm64-gnu@1.5.3: - resolution: {integrity: sha512-DCC6El3MiTYfv98CShxz/g2s4Pxn6tV0mldCQ0UdRqaN2ApUn7E+zTrqaj5bk7yII3A43WhE9Mr6wNPbXUeVyg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + '@rolldown/binding-freebsd-x64@1.0.0-rc.18': optional: true - /@swc/core-linux-arm64-musl@1.5.3: - resolution: {integrity: sha512-p04ysjYXEyaCGpJvwHm0T0nkPawXtdKBTThWnlh8M5jYULVNVA1YmC9azG2Avs1GDaLgBPVUgodmFYpdSupOYA==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': optional: true - /@swc/core-linux-x64-gnu@1.5.3: - resolution: {integrity: sha512-/l4KJu0xwYm6tcVSOvF8RbXrIeIHJAhWnKvuX4ZnYKFkON968kB8Ghx+1yqBQcZf36tMzSuZUC5xBUA9u66lGA==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': optional: true - /@swc/core-linux-x64-musl@1.5.3: - resolution: {integrity: sha512-54DmSnrTXq4fYEKNR0nFAImG3+FxsHlQ6Tol/v3l+rxmg2K0FeeDOpH7wTXeWhMGhFlGrLIyLSnA+SzabfoDIA==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': optional: true - /@swc/core-win32-arm64-msvc@1.5.3: - resolution: {integrity: sha512-piUMqoHNwDXChBfaaFIMzYgoxepfd8Ci1uXXNVEnuiRKz3FiIcNLmvXaBD7lKUwKcnGgVziH/CrndX6SldKQNQ==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': optional: true - /@swc/core-win32-ia32-msvc@1.5.3: - resolution: {integrity: sha512-zV5utPYBUzYhBOomCByAjKAvfVBcOCJtnszx7Zlfz7SAv/cGm8D1QzPDCvv6jDhIlUtLj6KyL8JXeFr+f95Fjw==} - engines: {node: '>=10'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': optional: true - /@swc/core-win32-x64-msvc@1.5.3: - resolution: {integrity: sha512-QmUiXiPIV5gBADfDh8e2jKynEhyRC+dcKP/zF9y5KqDUErYzlhocLd68uYS4uIegP6AylYlmigHgcaktGEE9VQ==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': optional: true - /@swc/core@1.5.3: - resolution: {integrity: sha512-pSEglypnBGLHBoBcv3aYS7IM2t2LRinubYMyP88UoFIcD2pear2CeB15CbjJ2IzuvERD0ZL/bthM7cDSR9g+aQ==} - engines: {node: '>=10'} - requiresBuild: true - peerDependencies: - '@swc/helpers': ^0.5.0 - peerDependenciesMeta: - '@swc/helpers': - optional: true - dependencies: - '@swc/counter': 0.1.3 - '@swc/types': 0.1.6 - optionalDependencies: - '@swc/core-darwin-arm64': 1.5.3 - '@swc/core-darwin-x64': 1.5.3 - '@swc/core-linux-arm-gnueabihf': 1.5.3 - '@swc/core-linux-arm64-gnu': 1.5.3 - '@swc/core-linux-arm64-musl': 1.5.3 - '@swc/core-linux-x64-gnu': 1.5.3 - '@swc/core-linux-x64-musl': 1.5.3 - '@swc/core-win32-arm64-msvc': 1.5.3 - '@swc/core-win32-ia32-msvc': 1.5.3 - '@swc/core-win32-x64-msvc': 1.5.3 - dev: true - - /@swc/counter@0.1.3: - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - dev: true - - /@swc/helpers@0.5.11: - resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==} - dependencies: - tslib: 2.6.2 - dev: false - - /@swc/types@0.1.6: - resolution: {integrity: sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==} - dependencies: - '@swc/counter': 0.1.3 - dev: true + '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': + optional: true - /@testing-library/dom@9.3.4: - resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} - engines: {node: '>=14'} - dependencies: - '@babel/code-frame': 7.24.2 - '@babel/runtime': 7.24.5 - '@types/aria-query': 5.0.4 - aria-query: 5.1.3 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - dev: true + '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': + optional: true - /@testing-library/jest-dom@6.4.5(vitest@1.6.0): - resolution: {integrity: sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - peerDependencies: - '@jest/globals': '>= 28' - '@types/bun': latest - '@types/jest': '>= 28' - jest: '>= 28' - vitest: '>= 0.32' - peerDependenciesMeta: - '@jest/globals': - optional: true - '@types/bun': - optional: true - '@types/jest': - optional: true - jest: - optional: true - vitest: - optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': dependencies: - '@adobe/css-tools': 4.3.3 - '@babel/runtime': 7.24.5 - aria-query: 5.3.0 - chalk: 3.0.0 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - lodash: 4.17.21 - redent: 3.0.0 - vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) - dev: true + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true - /@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4): - resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - dependencies: - '@testing-library/dom': 9.3.4 - dev: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': + optional: true - /@types/argparse@1.0.38: - resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} - dev: true + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': + optional: true - /@types/aria-query@5.0.4: - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - dev: true + '@rolldown/pluginutils@1.0.0-rc.18': {} - /@types/babel__core@7.20.5: - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - dependencies: - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 - '@types/babel__generator': 7.6.8 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.5 - dev: true + '@rolldown/pluginutils@1.0.0-rc.7': {} - /@types/babel__generator@7.6.8: - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + '@rollup/pluginutils@4.2.1': dependencies: - '@babel/types': 7.24.5 - dev: true + estree-walker: 2.0.2 + picomatch: 2.3.1 - /@types/babel__template@7.4.4: - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + '@rollup/pluginutils@5.3.0(rollup@4.54.0)': dependencies: - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 - dev: true + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.54.0 - /@types/babel__traverse@7.20.5: - resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} - dependencies: - '@babel/types': 7.24.5 - dev: true + '@rollup/rollup-android-arm-eabi@4.54.0': + optional: true - /@types/body-parser@1.19.5: - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - dependencies: - '@types/connect': 3.4.38 - '@types/node': 20.12.10 - dev: true + '@rollup/rollup-android-arm64@4.54.0': + optional: true - /@types/connect@3.4.38: - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - dependencies: - '@types/node': 20.12.10 - dev: true + '@rollup/rollup-darwin-arm64@4.54.0': + optional: true - /@types/cross-spawn@6.0.6: - resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} - dependencies: - '@types/node': 20.12.10 - dev: true + '@rollup/rollup-darwin-x64@4.54.0': + optional: true - /@types/d3-array@3.2.1: - resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} - dev: false + '@rollup/rollup-freebsd-arm64@4.54.0': + optional: true - /@types/d3-axis@3.0.6: - resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} - dependencies: - '@types/d3-selection': 3.0.10 - dev: false + '@rollup/rollup-freebsd-x64@4.54.0': + optional: true - /@types/d3-brush@3.0.6: - resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} - dependencies: - '@types/d3-selection': 3.0.10 - dev: false + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + optional: true - /@types/d3-chord@3.0.6: - resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} - dev: false + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + optional: true - /@types/d3-color@3.1.3: - resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - dev: false + '@rollup/rollup-linux-arm64-gnu@4.54.0': + optional: true - /@types/d3-contour@3.0.6: - resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} - dependencies: - '@types/d3-array': 3.2.1 - '@types/geojson': 7946.0.14 - dev: false + '@rollup/rollup-linux-arm64-musl@4.54.0': + optional: true - /@types/d3-delaunay@6.0.4: - resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} - dev: false + '@rollup/rollup-linux-loong64-gnu@4.54.0': + optional: true - /@types/d3-dispatch@3.0.6: - resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==} - dev: false + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + optional: true - /@types/d3-drag@3.0.7: - resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} - dependencies: - '@types/d3-selection': 3.0.10 - dev: false + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + optional: true - /@types/d3-dsv@3.0.7: - resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} - dev: false + '@rollup/rollup-linux-riscv64-musl@4.54.0': + optional: true - /@types/d3-ease@3.0.2: - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - dev: false + '@rollup/rollup-linux-s390x-gnu@4.54.0': + optional: true - /@types/d3-fetch@3.0.7: - resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} - dependencies: - '@types/d3-dsv': 3.0.7 - dev: false + '@rollup/rollup-linux-x64-gnu@4.54.0': + optional: true - /@types/d3-force@3.0.9: - resolution: {integrity: sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==} - dev: false + '@rollup/rollup-linux-x64-musl@4.54.0': + optional: true - /@types/d3-format@3.0.4: - resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} - dev: false + '@rollup/rollup-openharmony-arm64@4.54.0': + optional: true - /@types/d3-geo@3.1.0: - resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} - dependencies: - '@types/geojson': 7946.0.14 - dev: false + '@rollup/rollup-win32-arm64-msvc@4.54.0': + optional: true - /@types/d3-hierarchy@3.1.7: - resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} - dev: false + '@rollup/rollup-win32-ia32-msvc@4.54.0': + optional: true - /@types/d3-interpolate@3.0.4: - resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - dependencies: - '@types/d3-color': 3.1.3 - dev: false + '@rollup/rollup-win32-x64-gnu@4.54.0': + optional: true - /@types/d3-path@3.1.0: - resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} - dev: false + '@rollup/rollup-win32-x64-msvc@4.54.0': + optional: true - /@types/d3-polygon@3.0.2: - resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} - dev: false + '@rtsao/scc@1.1.0': {} - /@types/d3-quadtree@3.0.6: - resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} - dev: false + '@socket.io/component-emitter@3.1.2': {} - /@types/d3-random@3.0.3: - resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} - dev: false + '@standard-schema/spec@1.1.0': {} - /@types/d3-scale-chromatic@3.0.3: - resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} - dev: false + '@standard-schema/utils@0.3.0': {} - /@types/d3-scale@4.0.8: - resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + '@storybook/addon-docs@10.3.6(@types/react@19.2.14)(esbuild@0.27.7)(rollup@4.54.0)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1))': dependencies: - '@types/d3-time': 3.0.3 - dev: false + '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.6) + '@storybook/csf-plugin': 10.3.6(esbuild@0.27.7)(rollup@4.54.0)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) + '@storybook/icons': 2.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@storybook/react-dom-shim': 10.3.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + storybook: 10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + - esbuild + - rollup + - vite + - webpack - /@types/d3-selection@3.0.10: - resolution: {integrity: sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==} - dev: false + '@storybook/addon-links@10.3.6(react@19.2.6)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + optionalDependencies: + react: 19.2.6 - /@types/d3-shape@3.1.6: - resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + '@storybook/builder-vite@10.3.6(esbuild@0.27.7)(rollup@4.54.0)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1))': dependencies: - '@types/d3-path': 3.1.0 - dev: false + '@storybook/csf-plugin': 10.3.6(esbuild@0.27.7)(rollup@4.54.0)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) + storybook: 10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + ts-dedent: 2.2.0 + vite: 8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1) + transitivePeerDependencies: + - esbuild + - rollup + - webpack - /@types/d3-time-format@4.0.3: - resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} - dev: false + '@storybook/csf-plugin@10.3.6(esbuild@0.27.7)(rollup@4.54.0)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1))': + dependencies: + storybook: 10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.27.7 + rollup: 4.54.0 + vite: 8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1) - /@types/d3-time@3.0.3: - resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} - dev: false + '@storybook/global@5.0.0': {} - /@types/d3-timer@3.0.2: - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - dev: false + '@storybook/icons@2.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - /@types/d3-transition@3.0.8: - resolution: {integrity: sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==} + '@storybook/react-dom-shim@10.3.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))': dependencies: - '@types/d3-selection': 3.0.10 - dev: false + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + storybook: 10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - /@types/d3-zoom@3.0.8: - resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + '@storybook/react-vite@10.3.6(esbuild@0.27.7)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(rollup@4.54.0)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1))': dependencies: - '@types/d3-interpolate': 3.0.4 - '@types/d3-selection': 3.0.10 - dev: false + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) + '@rollup/pluginutils': 5.3.0(rollup@4.54.0) + '@storybook/builder-vite': 10.3.6(esbuild@0.27.7)(rollup@4.54.0)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) + '@storybook/react': 10.3.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3) + empathic: 2.0.1 + magic-string: 0.30.21 + react: 19.2.6 + react-docgen: 8.0.3 + react-dom: 19.2.6(react@19.2.6) + resolve: 1.22.12 + storybook: 10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + tsconfig-paths: 4.2.0 + vite: 8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1) + transitivePeerDependencies: + - esbuild + - rollup + - supports-color + - typescript + - webpack - /@types/d3@7.4.3: - resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@storybook/react@10.3.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)': dependencies: - '@types/d3-array': 3.2.1 - '@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.6 - '@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.9 - '@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.0 - '@types/d3-polygon': 3.0.2 - '@types/d3-quadtree': 3.0.6 - '@types/d3-random': 3.0.3 - '@types/d3-scale': 4.0.8 - '@types/d3-scale-chromatic': 3.0.3 - '@types/d3-selection': 3.0.10 - '@types/d3-shape': 3.1.6 - '@types/d3-time': 3.0.3 - '@types/d3-time-format': 4.0.3 - '@types/d3-timer': 3.0.2 - '@types/d3-transition': 3.0.8 - '@types/d3-zoom': 3.0.8 - dev: false + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 10.3.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)) + react: 19.2.6 + react-docgen: 8.0.3 + react-docgen-typescript: 2.4.0(typescript@5.9.3) + react-dom: 19.2.6(react@19.2.6) + storybook: 10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@swc/core-darwin-arm64@1.15.33': + optional: true - /@types/dateformat@5.0.2: - resolution: {integrity: sha512-M95hNBMa/hnwErH+a+VOD/sYgTmo15OTYTM2Hr52/e0OdOuY+Crag+kd3/ioZrhg0WGbl9Sm3hR7UU+MH6rfOw==} - dev: true + '@swc/core-darwin-x64@1.15.33': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.33': + optional: true - /@types/detect-port@1.3.5: - resolution: {integrity: sha512-Rf3/lB9WkDfIL9eEKaSYKc+1L/rNVYBjThk22JTqQw0YozXarX8YljFAz+HCoC6h4B4KwCMsBPZHaFezwT4BNA==} - dev: true + '@swc/core-linux-arm64-gnu@1.15.33': + optional: true - /@types/diff-match-patch@1.0.36: - resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} - dev: false + '@swc/core-linux-arm64-musl@1.15.33': + optional: true - /@types/doctrine@0.0.3: - resolution: {integrity: sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==} - dev: true + '@swc/core-linux-ppc64-gnu@1.15.33': + optional: true - /@types/doctrine@0.0.9: - resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} - dev: true + '@swc/core-linux-s390x-gnu@1.15.33': + optional: true - /@types/ejs@3.1.5: - resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} - dev: true + '@swc/core-linux-x64-gnu@1.15.33': + optional: true + + '@swc/core-linux-x64-musl@1.15.33': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.33': + optional: true - /@types/emscripten@1.39.11: - resolution: {integrity: sha512-dOeX2BeNA7j6BTEqJQL3ut0bRCfsyQMd5i4FT8JfHfYhAOuJPCGh0dQFbxVJxUyQ+75x6enhDdndGb624/QszA==} - dev: true + '@swc/core-win32-ia32-msvc@1.15.33': + optional: true - /@types/escodegen@0.0.6: - resolution: {integrity: sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==} - dev: true + '@swc/core-win32-x64-msvc@1.15.33': + optional: true - /@types/eslint@8.56.10: - resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} + '@swc/core@1.15.33': dependencies: - '@types/estree': 1.0.5 - '@types/json-schema': 7.0.15 - dev: true + '@swc/counter': 0.1.3 + '@swc/types': 0.1.26 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.33 + '@swc/core-darwin-x64': 1.15.33 + '@swc/core-linux-arm-gnueabihf': 1.15.33 + '@swc/core-linux-arm64-gnu': 1.15.33 + '@swc/core-linux-arm64-musl': 1.15.33 + '@swc/core-linux-ppc64-gnu': 1.15.33 + '@swc/core-linux-s390x-gnu': 1.15.33 + '@swc/core-linux-x64-gnu': 1.15.33 + '@swc/core-linux-x64-musl': 1.15.33 + '@swc/core-win32-arm64-msvc': 1.15.33 + '@swc/core-win32-ia32-msvc': 1.15.33 + '@swc/core-win32-x64-msvc': 1.15.33 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.26': + dependencies: + '@swc/counter': 0.1.3 + + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@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 - /@types/estree@0.0.51: - resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} - dev: true + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 - /@types/estree@1.0.5: - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - dev: true + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 - /@types/express-serve-static-core@4.19.0: - resolution: {integrity: sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==} + '@tybys/wasm-util@0.10.1': dependencies: - '@types/node': 20.12.10 - '@types/qs': 6.9.15 - '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 - dev: true + tslib: 2.8.1 + optional: true - /@types/express@4.17.21: - resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + '@tybys/wasm-util@0.10.2': dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.19.0 - '@types/qs': 6.9.15 - '@types/serve-static': 1.15.7 - dev: true + tslib: 2.8.1 + optional: true - /@types/find-cache-dir@3.2.1: - resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==} - dev: true + '@types/aria-query@5.0.4': {} - /@types/geojson@7946.0.14: - resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} - dev: false + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 - /@types/glob@7.2.0: - resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + '@types/babel__generator@7.27.0': dependencies: - '@types/minimatch': 5.1.2 - '@types/node': 20.12.10 - dev: true + '@babel/types': 7.29.0 - /@types/hast@3.0.4: - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/babel__template@7.4.4': dependencies: - '@types/unist': 3.0.2 - dev: true + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 - /@types/http-errors@2.0.4: - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - dev: true + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 - /@types/js-cookie@2.2.7: - resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} - dev: false + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 - /@types/json-schema@7.0.15: - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - dev: true + '@types/d3-color@3.1.3': {} - /@types/json5@0.0.29: - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - dev: true + '@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/lodash-es@4.17.12: - resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + '@types/d3-selection@3.0.11': {} + + '@types/d3-transition@3.0.9': dependencies: - '@types/lodash': 4.17.1 - dev: true + '@types/d3-selection': 3.0.11 - /@types/lodash.mergewith@4.6.7: - resolution: {integrity: sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==} + '@types/d3-zoom@3.0.8': dependencies: - '@types/lodash': 4.17.1 - dev: false + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 - /@types/lodash@4.17.1: - resolution: {integrity: sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==} + '@types/deep-eql@4.0.2': {} - /@types/mdx@2.0.13: - resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - dev: true + '@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/estree@1.0.9': {} + + '@types/js-cookie@2.2.7': {} - /@types/mime@1.3.5: - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - dev: true + '@types/json-schema@7.0.15': {} - /@types/minimatch@5.1.2: - resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - dev: true + '@types/json5@0.0.29': {} - /@types/node@18.19.32: - resolution: {integrity: sha512-2bkg93YBSDKk8DLmmHnmj/Rwr18TLx7/n+I23BigFwgexUJoMHZOd8X1OFxuF/W3NN0S2W2E5sVabI5CPinNvA==} + '@types/lodash.mergewith@4.6.7': dependencies: - undici-types: 5.26.5 - dev: true + '@types/lodash': 4.17.24 - /@types/node@20.12.10: - resolution: {integrity: sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==} + '@types/lodash.mergewith@4.6.9': dependencies: - undici-types: 5.26.5 - dev: true + '@types/lodash': 4.17.21 - /@types/normalize-package-data@2.4.4: - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - dev: true + '@types/lodash@4.17.21': {} - /@types/parse-json@4.0.2: - resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - dev: false - - /@types/pretty-hrtime@1.0.3: - resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==} - dev: true + '@types/lodash@4.17.24': {} - /@types/prop-types@15.7.12: - resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + '@types/mdx@2.0.13': {} - /@types/qs@6.9.15: - resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} - dev: true + '@types/node@22.19.3': + dependencies: + undici-types: 6.21.0 - /@types/range-parser@1.2.7: - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - dev: true + '@types/parse-json@4.0.2': {} - /@types/react-dom@18.3.0: - resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: - '@types/react': 18.3.1 - dev: true + '@types/react': 19.2.14 - /@types/react-reconciler@0.28.8: - resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==} + '@types/react-transition-group@4.4.12(@types/react@19.2.14)': dependencies: - '@types/react': 18.3.1 - dev: false + '@types/react': 19.2.14 - /@types/react-transition-group@4.4.10: - resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} + '@types/react@19.2.14': dependencies: - '@types/react': 18.3.1 - dev: false + csstype: 3.2.3 - /@types/react@18.3.1: - resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} - dependencies: - '@types/prop-types': 15.7.12 - csstype: 3.1.3 + '@types/resolve@1.20.6': {} - /@types/resolve@1.20.6: - resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} - dev: true + '@types/use-sync-external-store@0.0.6': {} - /@types/semver@7.5.8: - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - dev: true + '@types/uuid@10.0.0': {} - /@types/send@0.17.4: - resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.12.10 - dev: true + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/type-utils': 8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.2 + eslint: 9.39.2(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color - /@types/serve-static@1.15.7: - resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@typescript-eslint/parser@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@types/http-errors': 2.0.4 - '@types/node': 20.12.10 - '@types/send': 0.17.4 - dev: true - - /@types/unist@3.0.2: - resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} - dev: true - - /@types/use-sync-external-store@0.0.3: - resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} - dev: false - - /@types/uuid@9.0.8: - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - dev: true + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3(supports-color@10.2.2) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color - /@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/project-service@8.50.1(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/scope-manager': 7.8.0 - '@typescript-eslint/type-utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.8.0 - debug: 4.3.4 - eslint: 8.57.0 - graphemer: 1.4.0 - ignore: 5.3.1 - natural-compare: 1.4.0 - semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + debug: 4.4.3(supports-color@10.2.2) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/project-service@8.59.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 7.8.0 - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.8.0 - debug: 4.3.4 - eslint: 8.57.0 - typescript: 5.4.5 + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + debug: 4.4.3(supports-color@10.2.2) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/scope-manager@5.62.0: - resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/scope-manager@8.50.1': dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 - dev: true + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 - /@typescript-eslint/scope-manager@7.8.0: - resolution: {integrity: sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@8.59.2': dependencies: - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/visitor-keys': 7.8.0 - dev: true + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 - /@typescript-eslint/type-utils@7.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) - '@typescript-eslint/utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - debug: 4.3.4 - eslint: 8.57.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3(supports-color@10.2.2) + eslint: 9.39.2(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/types@5.62.0: - resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + '@typescript-eslint/types@8.50.1': {} - /@typescript-eslint/types@7.8.0: - resolution: {integrity: sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==} - engines: {node: ^18.18.0 || >=20.0.0} - dev: true + '@typescript-eslint/types@8.59.2': {} - /@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.5): - resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.6.0 - tsutils: 3.21.0(typescript@5.4.5) - typescript: 5.4.5 + '@typescript-eslint/project-service': 8.50.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/typescript-estree@7.8.0(typescript@5.4.5): - resolution: {integrity: sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/visitor-keys': 7.8.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.4 - semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 + '@typescript-eslint/typescript-estree@8.59.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 10.2.5 + semver: 7.8.0 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) - eslint: 8.57.0 - eslint-scope: 5.1.1 - semver: 7.6.0 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - - typescript - dev: true - /@typescript-eslint/utils@7.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 + '@typescript-eslint/utils@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 7.8.0 - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) - eslint: 8.57.0 - semver: 7.6.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - - typescript - dev: true - /@typescript-eslint/visitor-keys@5.62.0: - resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/visitor-keys@8.50.1': dependencies: - '@typescript-eslint/types': 5.62.0 - eslint-visitor-keys: 3.4.3 - dev: true + '@typescript-eslint/types': 8.50.1 + eslint-visitor-keys: 4.2.1 - /@typescript-eslint/visitor-keys@7.8.0: - resolution: {integrity: sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/visitor-keys@8.59.2': dependencies: - '@typescript-eslint/types': 7.8.0 - eslint-visitor-keys: 3.4.3 - dev: true + '@typescript-eslint/types': 8.59.2 + eslint-visitor-keys: 5.0.1 - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: true - - /@vitejs/plugin-react-swc@3.6.0(vite@5.2.11): - resolution: {integrity: sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==} - peerDependencies: - vite: ^4 || ^5 + '@vitejs/plugin-react-swc@4.3.0(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1))': dependencies: - '@swc/core': 1.5.3 - vite: 5.2.11(@types/node@20.12.10) + '@rolldown/pluginutils': 1.0.0-rc.7 + '@swc/core': 1.15.33 + vite: 8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1) transitivePeerDependencies: - '@swc/helpers' - dev: true - /@vitest/coverage-v8@1.6.0(vitest@1.6.0): - resolution: {integrity: sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==} - peerDependencies: - vitest: 1.6.0 + '@vitest/coverage-v8@4.1.5(vitest@4.1.5)': dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.4 + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.5 + ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.4 - istanbul-reports: 3.1.7 - magic-string: 0.30.10 - magicast: 0.3.4 - picocolors: 1.0.0 - std-env: 3.7.0 - strip-literal: 2.1.0 - test-exclude: 6.0.0 - vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) - transitivePeerDependencies: - - supports-color - dev: true + istanbul-reports: 3.2.0 + magicast: 0.5.2 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.5(@types/node@22.19.3)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) - /@vitest/expect@1.3.1: - resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==} + '@vitest/expect@3.2.4': dependencies: - '@vitest/spy': 1.3.1 - '@vitest/utils': 1.3.1 - chai: 4.4.1 - dev: true + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 - /@vitest/expect@1.6.0: - resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} + '@vitest/expect@4.1.5': dependencies: - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - chai: 4.4.1 - dev: true + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + chai: 6.2.2 + tinyrainbow: 3.1.0 - /@vitest/runner@1.6.0: - resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} + '@vitest/mocker@4.1.5(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1))': dependencies: - '@vitest/utils': 1.6.0 - p-limit: 5.0.0 - pathe: 1.1.2 - dev: true + '@vitest/spy': 4.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1) - /@vitest/snapshot@1.6.0: - resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} + '@vitest/pretty-format@3.2.4': dependencies: - magic-string: 0.30.10 - pathe: 1.1.2 - pretty-format: 29.7.0 - dev: true + tinyrainbow: 2.0.0 - /@vitest/spy@1.3.1: - resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==} + '@vitest/pretty-format@4.1.5': dependencies: - tinyspy: 2.2.1 - dev: true + tinyrainbow: 3.1.0 - /@vitest/spy@1.6.0: - resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} + '@vitest/runner@4.1.5': dependencies: - tinyspy: 2.2.1 - dev: true + '@vitest/utils': 4.1.5 + pathe: 2.0.3 - /@vitest/ui@1.6.0(vitest@1.6.0): - resolution: {integrity: sha512-k3Lyo+ONLOgylctiGovRKy7V4+dIN2yxstX3eY5cWFXH6WP+ooVX79YSyi0GagdTQzLmT43BF27T0s6dOIPBXA==} - peerDependencies: - vitest: 1.6.0 + '@vitest/snapshot@4.1.5': dependencies: - '@vitest/utils': 1.6.0 - fast-glob: 3.3.2 - fflate: 0.8.2 - flatted: 3.3.1 - pathe: 1.1.2 - picocolors: 1.0.0 - sirv: 2.0.4 - vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) - dev: true + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 + magic-string: 0.30.21 + pathe: 2.0.3 - /@vitest/utils@1.3.1: - resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==} + '@vitest/spy@3.2.4': dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 - dev: true + tinyspy: 4.0.4 - /@vitest/utils@1.6.0: - resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} - dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 - dev: true + '@vitest/spy@4.1.5': {} - /@volar/language-core@1.11.1: - resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} + '@vitest/ui@4.1.5(vitest@4.1.5)': dependencies: - '@volar/source-map': 1.11.1 - dev: true + '@vitest/utils': 4.1.5 + fflate: 0.8.2 + flatted: 3.4.2 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vitest: 4.1.5(@types/node@22.19.3)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) - /@volar/source-map@1.11.1: - resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==} + '@vitest/utils@3.2.4': dependencies: - muggle-string: 0.3.1 - dev: true + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 - /@volar/typescript@1.11.1: - resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==} + '@vitest/utils@4.1.5': dependencies: - '@volar/language-core': 1.11.1 - path-browserify: 1.0.1 - dev: true + '@vitest/pretty-format': 4.1.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 - /@vue/compiler-core@3.4.26: - resolution: {integrity: sha512-N9Vil6Hvw7NaiyFUFBPXrAyETIGlQ8KcFMkyk6hW1Cl6NvoqvP+Y8p1Eqvx+UdqsnrnI9+HMUEJegzia3mhXmQ==} - dependencies: - '@babel/parser': 7.24.5 - '@vue/shared': 3.4.26 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.0 - dev: true + '@webcontainer/env@1.1.1': {} - /@vue/compiler-dom@3.4.26: - resolution: {integrity: sha512-4CWbR5vR9fMg23YqFOhr6t6WB1Fjt62d6xdFPyj8pxrYub7d+OgZaObMsoxaF9yBUHPMiPFK303v61PwAuGvZA==} - dependencies: - '@vue/compiler-core': 3.4.26 - '@vue/shared': 3.4.26 - dev: true + '@xobotyi/scrollbar-width@1.9.5': {} - /@vue/language-core@1.8.27(typescript@5.4.5): - resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@volar/language-core': 1.11.1 - '@volar/source-map': 1.11.1 - '@vue/compiler-dom': 3.4.26 - '@vue/shared': 3.4.26 - computeds: 0.0.1 - minimatch: 9.0.4 - muggle-string: 0.3.1 - path-browserify: 1.0.1 - typescript: 5.4.5 - vue-template-compiler: 2.7.16 - dev: true - - /@vue/shared@3.4.26: - resolution: {integrity: sha512-Fg4zwR0GNnjzodMt3KRy2AWGMKQXByl56+4HjN87soxLNU9P5xcJkstAlIeEF3cU6UYOzmJl1tV0dVPGIljCnQ==} - dev: true - - /@xobotyi/scrollbar-width@1.9.5: - resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} - dev: false - - /@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.20.2): - resolution: {integrity: sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA==} - engines: {node: '>=14.15.0'} - peerDependencies: - esbuild: '>=0.10.0' - dependencies: - esbuild: 0.20.2 - tslib: 2.6.2 - dev: true - - /@yarnpkg/fslib@2.10.3: - resolution: {integrity: sha512-41H+Ga78xT9sHvWLlFOZLIhtU6mTGZ20pZ29EiZa97vnxdohJD2AF42rCoAoWfqUz486xY6fhjMH+DYEM9r14A==} - engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'} - dependencies: - '@yarnpkg/libzip': 2.3.0 - tslib: 1.14.1 - dev: true - - /@yarnpkg/libzip@2.3.0: - resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==} - engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'} - dependencies: - '@types/emscripten': 1.39.11 - tslib: 1.14.1 - dev: true - - /@zag-js/accordion@0.32.1: - resolution: {integrity: sha512-16beDVpEhXFQsQRMZLmHFruhGphSprJ5XrRu6+OM2U7aTulo1w3ENUd9uI+mIs4oTVO66lYI4Lp+dFcT2UUIYA==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/anatomy@0.32.1: - resolution: {integrity: sha512-bR+tfFfkbxwhBzGGjEQG+RUnbeCjMx7tWJxykGnGdVLwAh0wKTQBEfHEOCOQh5qU8RhKUieqemAdvc7oP3Tp4w==} - dev: false - - /@zag-js/aria-hidden@0.32.1: - resolution: {integrity: sha512-kznwxvUUHDax8Kd7YNVVCzQcwGARTRaZNOcIkw7MTLE8g/pU+C4pYkwR9iqA7/8imGfjYrZfSsQqZRTb4bkS0g==} - dependencies: - '@zag-js/dom-query': 0.32.1 - dev: false - - /@zag-js/auto-resize@0.32.1: - resolution: {integrity: sha512-MO6N5gPs2xDKbFgrakn6LDWv1GgN8uhfwpsqchLJX+EaZVvLIz8cXFD+jDv3RjK+5GRWV4mIF+26SXuHRSt9Ug==} - dependencies: - '@zag-js/dom-query': 0.32.1 - dev: false - - /@zag-js/avatar@0.32.1: - resolution: {integrity: sha512-5P+95pkMX2Na4yljN1etdgYyA+3HPORjWKn0Y3JamkYIAqJwRFO+taEdSm/xcRkuT6aGA3luheUowjt8wZssyA==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/mutation-observer': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/carousel@0.32.1: - resolution: {integrity: sha512-S7dUrPtiLr42Fa+S3O18kqKVqSu2yuk67bqGDtppIZSaFOugYHK4feBkZqjKw+eF12NVRRVO2j+A40d3MvxbSA==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/checkbox@0.32.1: - resolution: {integrity: sha512-5reRreGyDZ5IlBNd5m1QrYXCehVIl/pmfKMEcAfad5DcgCaHGv5j76eahxbKln/8TEdwz4eWzBrqNtwSkKL5+w==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - '@zag-js/visually-hidden': 0.32.1 - dev: false - - /@zag-js/collection@0.32.1: - resolution: {integrity: sha512-dAzcVQ/n+xAYoxWB/65/CQinv66RNVuq5ig0fEYszBqP+HjFnOpeGkIrEvP+bFI38hFEViiGtfr6oGAsVByOVQ==} - dev: false - - /@zag-js/color-picker@0.32.1: - resolution: {integrity: sha512-ov3FC+c2WBYmEGRXWFVb2jih2Ecejj5JqBjDL9iMLBs2KNY9jnpvtH7WnZbijNY+RMDBj+C/DNI7K2NVaamSIA==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/color-utils': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dismissable': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/popper': 0.32.1 - '@zag-js/tabbable': 0.32.1 - '@zag-js/text-selection': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - '@zag-js/visually-hidden': 0.32.1 - dev: false - - /@zag-js/color-utils@0.32.1: - resolution: {integrity: sha512-AzupfOD7oD0mE+H9roTzwnLqtw1wYiJGOQKLPAwdwPQdznJUQD6sMOpxR/6RBuITVTm8Bl12Mr4+7s29LVJruw==} - dependencies: - '@zag-js/numeric-range': 0.32.1 - dev: false - - /@zag-js/combobox@0.32.1: - resolution: {integrity: sha512-skz2C5UxLD5JoYNP4hcPaQJu2cW7vycKqjDNI9ZtygSkZHOHx+JxpYiACBnr1vqzXatIOuDQm/HUuWW9yOT4eA==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/aria-hidden': 0.32.1 - '@zag-js/collection': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dismissable': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/mutation-observer': 0.32.1 - '@zag-js/popper': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/core@0.32.1: - resolution: {integrity: sha512-F9F7920/CisoLWALQACIhqbMvemgbv86qBULJ+UEe+a/9XgGwPh9UGn/H/q5EWkNpgEapz2b3pl3ONgKmXsK1A==} - dependencies: - '@zag-js/store': 0.32.1 - klona: 2.0.6 - dev: false - - /@zag-js/date-picker@0.32.1: - resolution: {integrity: sha512-n/hYmF+/R4+NuyfPRzCgeuLT6LJihKSuKzK29STPWy3sC/tBBHiqhNv1/4UKbatHUJXdBW2XF+N8Rw08RffcFQ==} - dependencies: - '@internationalized/date': 3.5.3 - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.3) - '@zag-js/dismissable': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/live-region': 0.32.1 - '@zag-js/popper': 0.32.1 - '@zag-js/text-selection': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/date-utils@0.32.1(@internationalized/date@3.5.3): - resolution: {integrity: sha512-dbBDRSVr5pRUw3rXndyGuSshZiWqQI5JQO4D2KIFGkXzorj6WzoOpcO910Z7AdM/9cCAMpCjUrka8d8o9BpJBg==} - peerDependencies: - '@internationalized/date': '>=3.0.0' - dependencies: - '@internationalized/date': 3.5.3 - dev: false - - /@zag-js/dialog@0.32.1: - resolution: {integrity: sha512-czp+qXcdAOM70SrvDo4gBpYZx6gS6HXyrpiptW3+EHa2eiCfc/Z2w+Nu+ZadOTEQGgNWlKlCLW7Ery0i9mMDsw==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/aria-hidden': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dismissable': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/remove-scroll': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - focus-trap: 7.5.4 - dev: false - - /@zag-js/dismissable@0.32.1: - resolution: {integrity: sha512-UIkG+9Eb5wrus2F2Dy4zqk0pwCV53sdnMYBxk9dpvDzBJHzW+InhVeg3UeKmPL8ELcYlhH/Bap99XCRJvxsXow==} - dependencies: - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/interact-outside': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/dom-event@0.32.1: - resolution: {integrity: sha512-wN6f5Kkf7C/YFN3wbEG3gUockSebyy1fPNL2BuL4C8PIP8vOD14hnHTzZWg5yYfO+veybIAL38r8I46C+bOVBQ==} - dependencies: - '@zag-js/text-selection': 0.32.1 - '@zag-js/types': 0.32.1 - dev: false - - /@zag-js/dom-query@0.16.0: - resolution: {integrity: sha512-Oqhd6+biWyKnhKwFFuZrrf6lxBz2tX2pRQe6grUnYwO6HJ8BcbqZomy2lpOdr+3itlaUqx+Ywj5E5ZZDr/LBfQ==} - dev: false - - /@zag-js/dom-query@0.32.1: - resolution: {integrity: sha512-u6hrQHQ0/dcUi6xJn8d2Mu1ClN4KZpPqOKrJFSaxadWjSy+x0qp48WY2CBQ6gZ3j8IwR/XjzU9bu9wY5jJfHgA==} - dev: false - - /@zag-js/editable@0.32.1: - resolution: {integrity: sha512-QEGnfp2P9nWVp9vGNWtszspvQcF3KtBRToZrv5/DT30Mpo/uPDKtqijLs0SnB/W60ELzcIRhK4J9taGoK8O8uw==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/interact-outside': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/element-rect@0.32.1: - resolution: {integrity: sha512-tAmxgxU2LsByK8PIs/Cj6cBJ8xZCVXE9RoStxthhuPL7xKYUfZvFGuhHVOHTHd6sDKEqbj6K1ds/TGPuglIh4w==} - dev: false - - /@zag-js/element-size@0.10.5: - resolution: {integrity: sha512-uQre5IidULANvVkNOBQ1tfgwTQcGl4hliPSe69Fct1VfYb2Fd0jdAcGzqQgPhfrXFpR62MxLPB7erxJ/ngtL8w==} - dev: false - - /@zag-js/element-size@0.32.1: - resolution: {integrity: sha512-ACklufmJQpah2UqwZUlYFaKi6uWfZBeTghtbfYHcDfzRbg2Hni612v8L1JeS4vAgjeDpcdHQpXXR4AZSpGZgNw==} - dev: false - - /@zag-js/file-upload@0.32.1: - resolution: {integrity: sha512-cD0NRIDof9Vv2DemmnYe9ZPZxOZ6b8XZl8eq4G0e8+WLYtnRXyEURl8Dw0QJpfdDPQaHnnD4CNxPTQcLgP+9Sg==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/file-utils': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - '@zag-js/visually-hidden': 0.32.1 - dev: false - - /@zag-js/file-utils@0.32.1: - resolution: {integrity: sha512-0PxTrljW51Lf9OCuYNlZuaLgF0v1NoVRzXa/osZ9HGceQjfo77R5G9u+/TP3u53W2PTxajEZ4eNzTibgpzNXFg==} - dev: false - - /@zag-js/focus-visible@0.16.0: - resolution: {integrity: sha512-a7U/HSopvQbrDU4GLerpqiMcHKEkQkNPeDZJWz38cw/6Upunh41GjHetq5TB84hxyCaDzJ6q2nEdNoBQfC0FKA==} - dependencies: - '@zag-js/dom-query': 0.16.0 - dev: false - - /@zag-js/form-utils@0.32.1: - resolution: {integrity: sha512-OemLBlHCHHm7t8wNcf78FRudRA7FegSgsNEzAjrRTyx+lJztDyHRLaoyI1gCEIg+0Kzl2nMxjOl4MStGsDj8iw==} - dependencies: - '@zag-js/mutation-observer': 0.32.1 - dev: false - - /@zag-js/hover-card@0.32.1: - resolution: {integrity: sha512-k66YK0z0P4LuK78+jnRoUPxJiM9GA0sbEEz3oPlvcFVXMMwnRTPNIw1OjksfAPI+Nvgg7H6D3A+7HCdRI/oBjw==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dismissable': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/popper': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/interact-outside@0.32.1: - resolution: {integrity: sha512-8zHuswfTAgfMCaQnp3N4WStvnL32VyxURafb21+mE4neAF/DaKfJHWnJpeUMG1Qh/eXsrMRBxVoX+nBMhHj9bg==} - dependencies: - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/tabbable': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/live-region@0.32.1: - resolution: {integrity: sha512-6/9QMLVZbTRh/G6MoJc/auN8r5vjdY9vUgNT680C2LOa2vnRR5/y0DkIpVgttNh1rSenQ/eLEYxp8hQF1rIYNw==} - dependencies: - '@zag-js/visually-hidden': 0.32.1 - dev: false - - /@zag-js/menu@0.32.1: - resolution: {integrity: sha512-IPsTljVF0N9xTwub1cpGl3GAG5ttAq3h38PdZERREzT3qRgw4v3K/I1TG2vIiDXgJz8UZzUKox6ZYdU7UIAkRA==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dismissable': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/mutation-observer': 0.32.1 - '@zag-js/popper': 0.32.1 - '@zag-js/rect-utils': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/mutation-observer@0.32.1: - resolution: {integrity: sha512-/hlObxGnhAaYYVnwRJC227md0M3kSE6mO24vkqVGwq2GglS+u4zbVcBBUuWgHdMML+ZjIQrZuVycCBMfVlHq0g==} - dev: false - - /@zag-js/number-input@0.32.1: - resolution: {integrity: sha512-atyIOvoMITb4hZtQym7yD6I7grvPW83UeMFO8hCQg3HWwd2zR4+63mouWuyMoWb4QrzVFRVQBaU8OG5xGlknEw==} - dependencies: - '@internationalized/number': 3.5.2 - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/mutation-observer': 0.32.1 - '@zag-js/number-utils': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/number-utils@0.32.1: - resolution: {integrity: sha512-x/nttU31TtFVTqFBM8e3ZH/0MCc+u15WAfk0rT6ESkoZcdb80rTzZVMokCKCUdpi/JdB1vjEeCLSnj+ig8oAIQ==} - dev: false - - /@zag-js/numeric-range@0.32.1: - resolution: {integrity: sha512-1Qe2URTenlrdsWuArlnQ+v5bBH2mHZD3XsK6jYV+C2lgatVzdcoN4GCSNTiF7w+So6J+NTeLMkVHMGCW1Kzx1g==} - dev: false - - /@zag-js/pagination@0.32.1: - resolution: {integrity: sha512-lhogzKxJnx5D2Xoni/xm5rkOuy15KWSxqBHVwe8+j5aSNqMy7+aRtEN2F2VQCDVL/v1fdciQvOCA9udm37kZ4w==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/pin-input@0.32.1: - resolution: {integrity: sha512-d18cCXKUr7INL0Xm5KyIoiTRSNsPXfIlIEMl2HrAvM3r70wtEag0PmiDNA5NS2tB4LnnX0XowchGB4HsdFS/ng==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - '@zag-js/visually-hidden': 0.32.1 - dev: false - - /@zag-js/popover@0.32.1: - resolution: {integrity: sha512-B01if49v3crCjkvtSvIX4CBdT/475nj3DttOObc36s0YOxCEt3UihMITBD5JvIKwEqjZ6oU5t0sLcUYOqQ4f2A==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/aria-hidden': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dismissable': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/popper': 0.32.1 - '@zag-js/remove-scroll': 0.32.1 - '@zag-js/tabbable': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - focus-trap: 7.5.4 - dev: false - - /@zag-js/popper@0.32.1: - resolution: {integrity: sha512-aQgogW1N4VreNACSQhXQoZeXtQQtB//FXUvt1CBnW2DtmZ6YkNnaAfn186Q2lkw2/T0chITRy3eYeviwMmMrqg==} - dependencies: - '@floating-ui/dom': 1.5.4 - '@zag-js/dom-query': 0.32.1 - '@zag-js/element-rect': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/presence@0.32.1: - resolution: {integrity: sha512-8189QMUf/L1dztAZdurx18ZwPyWlq58Mrd+GdATSaf8JstgrI1ovzVs606inQghWptKHMsH7dUIaV9UkhbSx3Q==} - dependencies: - '@zag-js/core': 0.32.1 - '@zag-js/types': 0.32.1 - dev: false - - /@zag-js/progress@0.32.1: - resolution: {integrity: sha512-ClkQvNYnuIpKfAPUceZXY5E2m/3NnIm21cvHe4gAoJ88YdqEHd5rIRoHP63g8ET8Ct/2KkBRkgR+LrQnGQOomA==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/radio-group@0.32.1: - resolution: {integrity: sha512-NvdSjwRF38qIh0oM68jERf71uiwV2JFTrGeQEs3EIqONzULwL6jR2p4P1wm3JJNBAkSYBKZMER5cVUUcqM3kjQ==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/element-rect': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - '@zag-js/visually-hidden': 0.32.1 - dev: false - - /@zag-js/rating-group@0.32.1: - resolution: {integrity: sha512-RBaFRCw7P00bgTrEjUHT3h/OGRO8XmXKkQYqqhm1tsVbeTsT47iwHoc6XnMEiGBonaJDwN/J0oFasw7GNg5sow==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/react@0.32.1(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-b1SB7hXXv1K6CmXkcy5Y7mb0YRWkyvulyhK8VW5O5hIAPuGxOTx70psmVeZbmVzhjdORCiro9jKx8Ec0LfolFg==} - peerDependencies: - react: '>=18.0.0' - react-dom: '>=18.0.0' - dependencies: - '@zag-js/core': 0.32.1 - '@zag-js/store': 0.32.1 - '@zag-js/types': 0.32.1 - proxy-compare: 2.5.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@zag-js/rect-utils@0.32.1: - resolution: {integrity: sha512-cI07kgldjUZP+SLhXeG9VSl47nrENlC96Fs7jWcTfHj62rhdY8WsBJ0tiTztvwar9m1chwxXZwJowHN+nPIgDQ==} - dev: false - - /@zag-js/remove-scroll@0.32.1: - resolution: {integrity: sha512-LyXt2rNMSKb9MKeJRyKTgpk4R7jdA+9kEQTSG5qyA94jo1og7FVgA1W/E+pNkdxDEk1VplL768VU6y7E/L3DHg==} - dependencies: - '@zag-js/dom-query': 0.32.1 - dev: false - - /@zag-js/select@0.32.1: - resolution: {integrity: sha512-jSzmTKCN1Fk/ZDDWM8TVGOtwgpYUDgyceegjYT+hW1mmEetu4tQcEvAr0557NOzh8akqLvcVFbg/kMj0IriKAA==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/collection': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dismissable': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/mutation-observer': 0.32.1 - '@zag-js/popper': 0.32.1 - '@zag-js/tabbable': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - '@zag-js/visually-hidden': 0.32.1 - dev: false - - /@zag-js/slider@0.32.1: - resolution: {integrity: sha512-iZSB3Y8/Maakxem0Ha3rBRa8AyAplhN5K50Bgz+wsv0VEzNNUmK4QgaTWReWd6SfeTRpnC5ftKCcfM2aQrLm6g==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/element-size': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/numeric-range': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/splitter@0.32.1: - resolution: {integrity: sha512-NdHLUXtQAlnz6QpdPwcqZCqYul7LaVqsp0hgtXR2PN4HbH+VAaDfY76pUk6LBerUcykChGZvtM9U0A5FCo1x4A==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/number-utils': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/store@0.32.1: - resolution: {integrity: sha512-hKwzpqAPljw06oOI+eO+Is2udpmY9GsGfmdoqvZVYoK4f5sawpZY9EC/84tbK9QKWUDTbFS+0Ujc254GUThmDA==} - dependencies: - proxy-compare: 2.5.1 - dev: false - - /@zag-js/switch@0.32.1: - resolution: {integrity: sha512-+5w/AtINA+jpORX1cuUrnyIFXrfjhqV7667EKK/zbPi0Pf1E10+TEihpfFjY6bgms9CSNWZVEb6w2f2C0PNBDA==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - '@zag-js/visually-hidden': 0.32.1 - dev: false - - /@zag-js/tabbable@0.32.1: - resolution: {integrity: sha512-fMXtVgBiX7z3Qmdv+McrfihiSkqsDbNX2nn3e63L7jdy9ZpgnR3jG9BwUZvv7hvzkuOAFhhdKgBYYT+fkBavGg==} - dependencies: - '@zag-js/dom-query': 0.32.1 - dev: false - - /@zag-js/tabs@0.32.1: - resolution: {integrity: sha512-5l8/k2Pw9Kbfsvvx6HWcVqK7Ns7ca+nyPGLSZtZLMp/Zn2q3xSG32C1U3oDaYtQVIQSiEHdnMjw0C2v+CxGDMA==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/element-rect': 0.32.1 - '@zag-js/tabbable': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/tags-input@0.32.1: - resolution: {integrity: sha512-oliLhiMpRNbWFixHF+Oe7hySQBp7NKtL/s8rN5dLT1G1GFRMzuuht/QnmL1h8EoGGpTwaaokMo4zl4uVzHbwyw==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/auto-resize': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/form-utils': 0.32.1 - '@zag-js/interact-outside': 0.32.1 - '@zag-js/live-region': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/text-selection@0.32.1: - resolution: {integrity: sha512-aK1uswWYF76PFoxGL+3HW/kth9uldFWSW4lOh89NfEcc6Ym7qS5B+P0HKJVM9DuQbihvQX9dyc9PvM7/LJTSRA==} - dependencies: - '@zag-js/dom-query': 0.32.1 - dev: false - - /@zag-js/toast@0.32.1: - resolution: {integrity: sha512-HrfVzFX7ANS9qOewCr8qOCbgko635bZxYKMv+ojjo4U/TtwkGb43+lVU7/qwZj0z18/OtXBH5YQjFwQZXg5x8g==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/toggle-group@0.32.1: - resolution: {integrity: sha512-MM1XI4J45rRCZiDHcMtZWud0+bWMu6IcMnrbd9oig330YAF3RzcjTlxX93YRY35F04OUMBq5el9qe3qc2vyMuw==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/tooltip@0.32.1: - resolution: {integrity: sha512-+rsmDYTELFBHoYKg5iKShGfRD3H9FJDaZRq915Uc9YnyePMXCnWRgnVp+lk3zI+FDgysQm67SDLRJsR24Iioqg==} - dependencies: - '@zag-js/anatomy': 0.32.1 - '@zag-js/core': 0.32.1 - '@zag-js/dom-event': 0.32.1 - '@zag-js/dom-query': 0.32.1 - '@zag-js/popper': 0.32.1 - '@zag-js/types': 0.32.1 - '@zag-js/utils': 0.32.1 - dev: false - - /@zag-js/types@0.32.1: - resolution: {integrity: sha512-BLfqb+im4vtXXJqhd2ZUg/4LquEd1qPt9XN56XVjudGDTftN8n3EDpuail7VKxdL59W4jR7wW8lvl4sSgrQKWw==} - dependencies: - csstype: 3.1.3 - dev: false - - /@zag-js/utils@0.32.1: - resolution: {integrity: sha512-jrcmWYcA3L6TO4fZbPFvpSGEy2Z/mbWt6bPQbmcVgq/BltSS0YxxfPl+eD+S/rZI9aneszwsr04Z5TpladFiVA==} - dev: false - - /@zag-js/visually-hidden@0.32.1: - resolution: {integrity: sha512-Vzieo4vNulzY/0zqmVfeYW/LcFJp5xtEoyUgR1FBctH8uBPBRhTIEXxKtoMablW6/vccOVo7zcu0UrR5Vx+eYQ==} - dev: false - - /accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - dev: true - - /acorn-jsx@5.3.2(acorn@7.4.1): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@xyflow/react@12.10.0(@types/react@19.2.14)(immer@10.2.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - acorn: 7.4.1 - dev: true + '@xyflow/system': 0.0.74 + classcat: 5.0.5 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + zustand: 4.5.7(@types/react@19.2.14)(immer@10.2.0)(react@19.2.6) + transitivePeerDependencies: + - '@types/react' + - immer - /acorn-jsx@5.3.2(acorn@8.11.3): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@xyflow/system@0.0.74': dependencies: - acorn: 8.11.3 - dev: true + '@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 - /acorn-walk@7.2.0: - resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} - engines: {node: '>=0.4.0'} - dev: true + '@zag-js/dom-query@0.31.1': {} - /acorn-walk@8.3.2: - resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} - engines: {node: '>=0.4.0'} - dev: true + '@zag-js/element-size@0.31.1': {} - /acorn@7.4.1: - resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true + '@zag-js/focus-visible@0.31.1': + dependencies: + '@zag-js/dom-query': 0.31.1 - /acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 - /address@1.2.2: - resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} - engines: {node: '>= 10.0.0'} - dev: true + acorn@8.15.0: {} - /aggregate-error@3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} + acorn@8.16.0: {} + + ag-psd@28.5.1: dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - dev: true + base64-js: 1.5.1 + pako: 2.1.0 - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + agent-base@7.1.4: {} + + 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 - dev: true - - /ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - dev: true - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true + ansi-colors@4.1.3: {} - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - dev: true + ansi-regex@5.0.1: {} - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 + ansi-regex@6.2.2: {} - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - dev: true - - /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - dev: true - - /ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - dev: true - - /anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: true - /app-root-dir@1.0.2: - resolution: {integrity: sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==} - dev: true - - /argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - dependencies: - sprintf-js: 1.0.3 - dev: true + ansi-styles@5.2.0: {} - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true + ansi-styles@6.2.3: {} - /aria-hidden@1.2.3: - resolution: {integrity: sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==} - engines: {node: '>=10'} - dependencies: - tslib: 2.6.2 - dev: false + argparse@2.0.1: {} - /aria-query@5.1.3: - resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + aria-hidden@1.2.6: dependencies: - deep-equal: 2.2.3 - dev: true + tslib: 2.8.1 - /aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.0: dependencies: dequal: 2.0.3 - dev: true - /arity-n@1.0.4: - resolution: {integrity: sha512-fExL2kFDC1Q2DUOx3whE/9KoN66IzkY4b4zUHUBFM1ojEYjZZYDcUW3bek/ufGionX9giIKDC5redH2IlGqcQQ==} - dev: true + aria-query@5.3.2: {} - /array-buffer-byte-length@1.0.1: - resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} - engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: dependencies: - call-bind: 1.0.7 - is-array-buffer: 3.0.4 - dev: true - - /array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - dev: true + call-bound: 1.0.4 + is-array-buffer: 3.0.5 - /array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} - engines: {node: '>= 0.4'} + array-includes@3.1.9: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.3 - es-object-atoms: 1.0.0 - get-intrinsic: 1.2.4 - is-string: 1.0.7 - dev: true - - /array-last@1.3.0: - resolution: {integrity: sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==} - engines: {node: '>=0.10.0'} - dependencies: - is-number: 4.0.0 - dev: true - - /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - dev: true + es-abstract: 1.24.1 + 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: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - engines: {node: '>= 0.4'} + array.prototype.findlast@1.2.5: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.3 + es-abstract: 1.24.1 es-errors: 1.3.0 - es-object-atoms: 1.0.0 - es-shim-unscopables: 1.0.2 - dev: true + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 - /array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} - engines: {node: '>= 0.4'} + array.prototype.findlastindex@1.2.6: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.3 + es-abstract: 1.24.1 es-errors: 1.3.0 - es-object-atoms: 1.0.0 - es-shim-unscopables: 1.0.2 - dev: true - - /array.prototype.flat@1.3.2: - resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-shim-unscopables: 1.0.2 - dev: true + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 - /array.prototype.flatmap@1.3.2: - resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} - engines: {node: '>= 0.4'} + array.prototype.flat@1.3.3: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.3 - es-shim-unscopables: 1.0.2 - dev: true + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 - /array.prototype.toreversed@1.1.2: - resolution: {integrity: sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==} + array.prototype.flatmap@1.3.3: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.3 - es-shim-unscopables: 1.0.2 - dev: true + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 - /array.prototype.tosorted@1.1.3: - resolution: {integrity: sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==} + array.prototype.tosorted@1.1.4: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.3 + es-abstract: 1.24.1 es-errors: 1.3.0 - es-shim-unscopables: 1.0.2 - dev: true + es-shim-unscopables: 1.1.0 - /arraybuffer.prototype.slice@1.0.3: - resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} - engines: {node: '>= 0.4'} + arraybuffer.prototype.slice@1.0.4: dependencies: - array-buffer-byte-length: 1.0.1 - call-bind: 1.0.7 + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.3 + es-abstract: 1.24.1 es-errors: 1.3.0 - get-intrinsic: 1.2.4 - is-array-buffer: 3.0.4 - is-shared-array-buffer: 1.0.3 - dev: true - - /assert@2.1.0: - resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} - dependencies: - call-bind: 1.0.7 - is-nan: 1.3.2 - object-is: 1.1.6 - object.assign: 4.1.5 - util: 0.12.5 - dev: true - - /assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - dev: true - - /ast-types@0.16.1: - resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} - engines: {node: '>=4'} - dependencies: - tslib: 2.6.2 - dev: true - - /async@3.2.5: - resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} - dev: true + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 - /attr-accept@2.2.2: - resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==} - engines: {node: '>=4'} - dev: false + assertion-error@2.0.1: {} - /available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} + ast-types@0.16.1: dependencies: - possible-typed-array-names: 1.0.0 - dev: true + tslib: 2.8.1 - /babel-core@7.0.0-bridge.0(@babel/core@7.24.5): - resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} - peerDependencies: - '@babel/core': ^7.0.0-0 + ast-v8-to-istanbul@1.0.0: dependencies: - '@babel/core': 7.24.5 - dev: true + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 - /babel-plugin-macros@3.1.0: - resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} - engines: {node: '>=10', npm: '>=6'} + async-function@1.0.0: {} + + async-mutex@0.5.0: dependencies: - '@babel/runtime': 7.24.5 - cosmiconfig: 7.1.0 - resolve: 1.22.8 - dev: false + tslib: 2.8.1 - /babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.5): - resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + attr-accept@2.2.5: {} + + available-typed-arrays@1.0.7: dependencies: - '@babel/compat-data': 7.24.4 - '@babel/core': 7.24.5 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - dev: true + possible-typed-array-names: 1.1.0 - /babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.5): - resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-macros@3.1.0: dependencies: - '@babel/core': 7.24.5 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) - core-js-compat: 3.37.0 - transitivePeerDependencies: - - supports-color - dev: true + '@babel/runtime': 7.28.4 + cosmiconfig: 7.1.0 + resolve: 1.22.11 - /babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.5): - resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-react-compiler@1.0.0: dependencies: - '@babel/core': 7.24.5 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) - transitivePeerDependencies: - - supports-color - dev: true + '@babel/types': 7.28.5 - /babylon@6.18.0: - resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} - hasBin: true - dev: true + balanced-match@1.0.2: {} - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true + balanced-match@4.0.4: {} - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true + base64-js@1.5.1: {} - /better-opn@3.0.2: - resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} - engines: {node: '>=12.0.0'} - dependencies: - open: 8.4.2 - dev: true + baseline-browser-mapping@2.10.29: {} - /big-integer@1.6.52: - resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} - engines: {node: '>=0.6'} - dev: true + baseline-browser-mapping@2.9.11: {} - /binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - dev: true + bind-event-listener@3.0.0: {} - /bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - dev: true - - /body-parser@1.20.2: - resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.11.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - dev: false - - /bplist-parser@0.2.0: - resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} - engines: {node: '>= 5.10.0'} - dependencies: - big-integer: 1.6.52 - dev: true + boolean@3.2.0: {} - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 - dev: true - - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - - /browser-assert@1.2.1: - resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} - dev: true - - /browserify-zlib@0.1.4: - resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} - dependencies: - pako: 0.2.9 - dev: true - - /browserslist@4.23.0: - resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001616 - electron-to-chromium: 1.4.757 - node-releases: 2.0.14 - update-browserslist-db: 1.0.15(browserslist@4.23.0) - dev: true - - /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: true - - /buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: true - - /bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} - dev: true - - /bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - dev: true - - /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - dev: true - - /call-bind@1.0.7: - resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} - engines: {node: '>= 0.4'} - dependencies: - es-define-property: 1.0.0 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.2.4 - set-function-length: 1.2.2 - dev: true - - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - /caniuse-lite@1.0.30001616: - resolution: {integrity: sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==} - dev: true - - /chai@4.4.1: - resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} - engines: {node: '>=4'} - dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.3 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.0.8 - dev: true - - /chakra-react-select@4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.4)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-ZL43hyXPnWf1g/HjsZDecbeJ4F2Q6tTPYJozlKWkrQ7lIX7ORP0aZYwmc5/Wly4UNzMimj2Vuosl6MmIXH+G2g==} - peerDependencies: - '@chakra-ui/form-control': ^2.0.0 - '@chakra-ui/icon': ^3.0.0 - '@chakra-ui/layout': ^2.0.0 - '@chakra-ui/media-query': ^3.0.0 - '@chakra-ui/menu': ^2.0.0 - '@chakra-ui/spinner': ^2.0.0 - '@chakra-ui/system': ^2.0.0 - '@emotion/react': ^11.8.1 - react: ^18.0.0 - react-dom: ^18.0.0 - dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) - '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-select: 5.7.7(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - dev: false - - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - /chalk@3.0.0: - resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} - engines: {node: '>=8'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: false - - /check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} - dependencies: - get-func-name: 2.0.2 - dev: true - - /chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - dev: true - - /chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - dev: true - /citty@0.1.6: - resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + brace-expansion@5.0.6: dependencies: - consola: 3.2.3 - dev: true + balanced-match: 4.0.4 - /classcat@5.0.5: - resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} - dev: false - - /clean-stack@2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - dev: true - - /cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} + braces@3.0.3: dependencies: - restore-cursor: 3.1.0 - dev: true + fill-range: 7.1.1 - /cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - dev: true + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001761 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) - /cli-table3@0.6.4: - resolution: {integrity: sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==} - engines: {node: 10.* || >= 12.*} + browserslist@4.28.2: dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - dev: true + baseline-browser-mapping: 2.10.29 + caniuse-lite: 1.0.30001792 + electron-to-chromium: 1.5.353 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) - /cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + buffer@5.7.1: dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - dev: true + base64-js: 1.5.1 + ieee754: 1.2.1 - /clone-deep@4.0.1: - resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} - engines: {node: '>=6'} + bundle-name@4.1.0: dependencies: - is-plain-object: 2.0.4 - kind-of: 6.0.3 - shallow-clone: 3.0.1 - dev: true + run-applescript: 7.1.0 - /clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - requiresBuild: true - dev: true + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + call-bind@1.0.8: dependencies: - color-name: 1.1.3 + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + call-bound@1.0.4: dependencies: - color-name: 1.1.4 - dev: true + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + callsites@3.1.0: {} - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true + caniuse-lite@1.0.30001761: {} - /color2k@2.0.3: - resolution: {integrity: sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==} - dev: false + caniuse-lite@1.0.30001792: {} - /commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - dev: false + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 - /commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - dev: true + chai@6.2.2: {} - /commander@6.2.1: - resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} - engines: {node: '>= 6'} - dev: true + chakra-react-select@4.10.1(@chakra-ui/react@2.10.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(framer-motion@10.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + '@chakra-ui/react': 2.10.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(react@19.2.6))(@types/react@19.2.14)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-select: 5.8.3(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + transitivePeerDependencies: + - '@types/react' + - supports-color - /commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - requiresBuild: true - dev: true - optional: true + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + change-case@5.4.4: {} - /commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - dev: true + check-error@2.1.3: {} - /compare-versions@6.1.0: - resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==} - dev: false + classcat@5.0.5: {} - /compose-function@3.0.3: - resolution: {integrity: sha512-xzhzTJ5eC+gmIzvZq+C3kCJHsp9os6tJkrigDRZclyGtOKINbZtE8n1Tzmeh32jW+BUDPbvZpibwvJHBLGMVwg==} + cli-cursor@3.1.0: dependencies: - arity-n: 1.0.4 - dev: true + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} - /compressible@2.0.18: - resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} - engines: {node: '>= 0.6'} + cliui@8.0.1: dependencies: - mime-db: 1.52.0 - dev: true + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 - /compression@1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} - engines: {node: '>= 0.8.0'} + clone@1.0.4: {} + + cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - accepts: 1.3.8 - bytes: 3.0.0 - compressible: 2.0.18 - debug: 2.6.9 - on-headers: 1.0.2 - safe-buffer: 5.1.2 - vary: 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) transitivePeerDependencies: - - supports-color - dev: true + - '@types/react' + - '@types/react-dom' + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 - /compute-scroll-into-view@3.0.3: - resolution: {integrity: sha512-nadqwNxghAGTamwIqQSG433W6OADZx2vCo3UXHNrzTRHK/htu+7+L0zhjEoaeaQVNAi3YgqWDv8+tzf0hRfR+A==} - dev: false + color-name@1.1.4: {} - /computeds@0.0.1: - resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} - dev: true + color2k@2.0.3: {} - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true + colorette@1.4.0: {} - /concurrently@8.2.2: - resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} - engines: {node: ^14.13.0 || >=16.0.0} - hasBin: true + commander@2.20.3: {} + + compare-versions@6.1.1: {} + + concat-map@0.0.1: {} + + concurrently@9.2.1: dependencies: chalk: 4.1.2 - date-fns: 2.30.0 - lodash: 4.17.21 - rxjs: 7.8.1 - shell-quote: 1.8.1 - spawn-command: 0.0.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 supports-color: 8.1.1 tree-kill: 1.2.2 yargs: 17.7.2 - dev: true - - /confbox@0.1.7: - resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} - dev: true - /consola@3.2.3: - resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} - engines: {node: ^14.18.0 || >=16.10.0} - dev: true + convert-source-map@1.9.0: {} - /content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - dependencies: - safe-buffer: 5.2.1 - dev: true - - /content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - dev: true - - /convert-source-map@1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - dev: false - - /convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - dev: true - - /cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - dev: true + convert-source-map@2.0.0: {} - /cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} - dev: true + cookie@1.1.1: {} - /copy-to-clipboard@3.3.3: - resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 - dev: false - - /core-js-compat@3.37.0: - resolution: {integrity: sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==} - dependencies: - browserslist: 4.23.0 - dev: true - /core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: true + core-util-is@1.0.3: {} - /cosmiconfig@7.1.0: - resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} - engines: {node: '>=10'} + cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 - import-fresh: 3.3.0 + import-fresh: 3.3.1 parse-json: 5.2.0 path-type: 4.0.0 yaml: 1.10.2 - dev: false - /cross-fetch@4.0.0: - resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + cross-fetch@4.0.0: dependencies: node-fetch: 2.7.0 transitivePeerDependencies: - encoding - dev: false - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - dev: true - /crypto-random-string@2.0.0: - resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} - engines: {node: '>=8'} - dev: true - - /css-box-model@1.2.1: - resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + css-box-model@1.2.1: dependencies: tiny-invariant: 1.3.3 - dev: false - /css-in-js-utils@3.1.0: - resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + css-in-js-utils@3.1.0: dependencies: - hyphenate-style-name: 1.0.4 - dev: false + hyphenate-style-name: 1.1.0 - /css-tree@1.1.3: - resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} - engines: {node: '>=8.0.0'} + css-tree@1.1.3: dependencies: mdn-data: 2.0.14 source-map: 0.6.1 - dev: false - /css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - dev: true + css.escape@1.5.1: {} - /csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: {} - /d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - dev: false + d3-color@3.1.0: {} - /d3-dispatch@3.0.1: - resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} - engines: {node: '>=12'} - dev: false + d3-dispatch@3.0.1: {} - /d3-drag@3.0.0: - resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} - engines: {node: '>=12'} + d3-drag@3.0.0: dependencies: d3-dispatch: 3.0.1 d3-selection: 3.0.0 - dev: false - /d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - dev: false + d3-ease@3.0.1: {} - /d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} + d3-interpolate@3.0.1: dependencies: d3-color: 3.1.0 - dev: false - /d3-selection@3.0.0: - resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} - engines: {node: '>=12'} - dev: false + d3-selection@3.0.0: {} - /d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - dev: false + d3-timer@3.0.1: {} - /d3-transition@3.0.1(d3-selection@3.0.0): - resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} - engines: {node: '>=12'} - peerDependencies: - d3-selection: 2 - 3 + d3-transition@3.0.1(d3-selection@3.0.0): dependencies: d3-color: 3.1.0 d3-dispatch: 3.0.1 @@ -7539,2978 +7301,1330 @@ packages: d3-interpolate: 3.0.1 d3-selection: 3.0.0 d3-timer: 3.0.1 - dev: false - /d3-zoom@3.0.0: - resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} - engines: {node: '>=12'} + 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) - dev: false - /data-view-buffer@1.0.1: - resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} - engines: {node: '>= 0.4'} + data-view-buffer@1.0.2: dependencies: - call-bind: 1.0.7 + call-bound: 1.0.4 es-errors: 1.3.0 - is-data-view: 1.0.1 - dev: true + is-data-view: 1.0.2 - /data-view-byte-length@1.0.1: - resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} - engines: {node: '>= 0.4'} + data-view-byte-length@1.0.2: dependencies: - call-bind: 1.0.7 + call-bound: 1.0.4 es-errors: 1.3.0 - is-data-view: 1.0.1 - dev: true + is-data-view: 1.0.2 - /data-view-byte-offset@1.0.0: - resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} - engines: {node: '>= 0.4'} + data-view-byte-offset@1.0.1: dependencies: - call-bind: 1.0.7 + call-bound: 1.0.4 es-errors: 1.3.0 - is-data-view: 1.0.1 - dev: true - - /date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} - dependencies: - '@babel/runtime': 7.24.5 - dev: true + is-data-view: 1.0.2 - /dateformat@5.0.3: - resolution: {integrity: sha512-Kvr6HmPXUMerlLcLF+Pwq3K7apHpYmGDVqrxcDasBg86UcKeTSNWbEzU8bwdXnxnR44FtMhJAxI4Bov6Y/KUfA==} - engines: {node: '>=12.20'} - dev: false - - /de-indent@1.0.2: - resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} - dev: true - - /debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.0.0 - dev: true - - /debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + debug@3.2.7: dependencies: ms: 2.1.3 - dev: true - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + debug@4.4.3(supports-color@10.2.2): dependencies: - ms: 2.1.2 - - /decode-uri-component@0.4.1: - resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} - engines: {node: '>=14.16'} - dev: false + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 - /deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} - engines: {node: '>=6'} - dependencies: - type-detect: 4.0.8 - dev: true + decode-uri-component@0.4.1: {} - /deep-equal@2.2.3: - resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} - engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.1 - call-bind: 1.0.7 - es-get-iterator: 1.1.3 - get-intrinsic: 1.2.4 - is-arguments: 1.1.1 - is-array-buffer: 3.0.4 - is-date-object: 1.0.5 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.3 - isarray: 2.0.5 - object-is: 1.1.6 - object-keys: 1.1.1 - object.assign: 4.1.5 - regexp.prototype.flags: 1.5.2 - side-channel: 1.0.6 - which-boxed-primitive: 1.0.2 - which-collection: 1.0.2 - which-typed-array: 1.1.15 - dev: true + deep-eql@5.0.2: {} - /deep-freeze@0.0.1: - resolution: {integrity: sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg==} - dev: true + deep-is@0.1.4: {} - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true + default-browser-id@5.0.1: {} - /default-browser-id@3.0.0: - resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} - engines: {node: '>=12'} + default-browser@5.5.0: dependencies: - bplist-parser: 0.2.0 - untildify: 4.0.0 - dev: true + bundle-name: 4.1.0 + default-browser-id: 5.0.1 - /defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - requiresBuild: true + defaults@1.0.4: dependencies: clone: 1.0.4 - dev: true - /define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} + define-data-property@1.1.4: dependencies: - es-define-property: 1.0.0 + es-define-property: 1.0.1 es-errors: 1.3.0 - gopd: 1.0.1 + gopd: 1.2.0 - /define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - dev: true + define-lazy-prop@2.0.0: {} - /define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} + define-lazy-prop@3.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 - /defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - dev: true - - /del@6.1.1: - resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} - engines: {node: '>=10'} - dependencies: - globby: 11.1.0 - graceful-fs: 4.2.11 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 4.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - dev: true - - /depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dev: true - - /dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: true - - /destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dev: true - - /detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - dev: true - - /detect-node-es@1.1.0: - resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - dev: false + dequal@2.0.3: {} - /detect-package-manager@2.0.1: - resolution: {integrity: sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==} - engines: {node: '>=12'} - dependencies: - execa: 5.1.1 - dev: true + detect-libc@2.1.2: {} - /detect-port@1.5.1: - resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==} - hasBin: true - dependencies: - address: 1.2.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true + detect-node-es@1.1.0: {} - /diff-match-patch@1.0.5: - resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} - dev: false + discontinuous-range@1.0.0: {} - /diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true + dockview-core@4.12.0: {} - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} + dockview@4.12.0(react@19.2.6): dependencies: - path-type: 4.0.0 - dev: true - - /discontinuous-range@1.0.0: - resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==} - dev: false + dockview-core: 4.12.0 + react: 19.2.6 - /doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} + doctrine@2.1.0: dependencies: esutils: 2.0.3 - dev: true - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} + doctrine@3.0.0: dependencies: esutils: 2.0.3 - dev: true - /dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - dev: true + dom-accessibility-api@0.5.16: {} - /dom-accessibility-api@0.6.3: - resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dev: true + dom-accessibility-api@0.6.3: {} - /dom-helpers@5.2.1: - resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.24.5 - csstype: 3.1.3 - dev: false + '@babel/runtime': 7.28.4 + csstype: 3.2.3 - /dotenv-expand@10.0.0: - resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} - engines: {node: '>=12'} - dev: true - - /dotenv@16.4.5: - resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} - engines: {node: '>=12'} - dev: true - - /dpdm@3.14.0: - resolution: {integrity: sha512-YJzsFSyEtj88q5eTELg3UWU7TVZkG1dpbF4JDQ3t1b07xuzXmdoGeSz9TKOke1mUuOpWlk4q+pBh+aHzD6GBTg==} - hasBin: true + dpdm@3.14.0: dependencies: chalk: 4.1.2 - fs-extra: 11.2.0 - glob: 10.3.12 + fs-extra: 11.3.3 + glob: 10.5.0 ora: 5.4.1 - tslib: 2.6.2 - typescript: 5.4.5 + tslib: 2.8.1 + typescript: 5.9.3 yargs: 17.7.2 - dev: true - - /duplexify@3.7.1: - resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} - dependencies: - end-of-stream: 1.4.4 - inherits: 2.0.4 - readable-stream: 2.3.8 - stream-shift: 1.0.3 - dev: true - - /eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: true - /easy-table@1.2.0: - resolution: {integrity: sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==} + dunder-proto@1.0.1: dependencies: - ansi-regex: 5.0.1 - optionalDependencies: - wcwidth: 1.0.1 - dev: true - - /ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - dev: true + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 - /ejs@3.1.10: - resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} - engines: {node: '>=0.10.0'} - hasBin: true - dependencies: - jake: 10.9.1 - dev: true + eastasianwidth@0.2.0: {} - /electron-to-chromium@1.4.757: - resolution: {integrity: sha512-jftDaCknYSSt/+KKeXzH3LX5E2CvRLm75P3Hj+J/dv3CL0qUYcOt13d5FN1NiL5IJbbhzHrb3BomeG2tkSlZmw==} - dev: true + electron-to-chromium@1.5.267: {} - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true + electron-to-chromium@1.5.353: {} - /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - dev: true + emoji-regex@8.0.0: {} - /encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - dev: true + emoji-regex@9.2.2: {} - /end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - dependencies: - once: 1.4.0 - dev: true + empathic@2.0.1: {} - /engine.io-client@6.5.3: - resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} + engine.io-client@6.6.4: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.4 - engine.io-parser: 5.2.2 - ws: 8.11.0 - xmlhttprequest-ssl: 2.0.0 + debug: 4.4.3(supports-color@10.2.2) + engine.io-parser: 5.2.3 + ws: 8.18.3 + xmlhttprequest-ssl: 2.1.2 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - dev: false - - /engine.io-parser@5.2.2: - resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} - engines: {node: '>=10.0.0'} - dev: false - - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - dev: true - /envinfo@7.13.0: - resolution: {integrity: sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==} - engines: {node: '>=4'} - hasBin: true - dev: true + engine.io-parser@5.2.3: {} - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 - /error-stack-parser@2.1.4: - resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + error-stack-parser@2.1.4: dependencies: stackframe: 1.3.4 - dev: false - /es-abstract@1.23.3: - resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} - engines: {node: '>= 0.4'} + es-abstract@1.24.1: dependencies: - array-buffer-byte-length: 1.0.1 - arraybuffer.prototype.slice: 1.0.3 + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - data-view-buffer: 1.0.1 - data-view-byte-length: 1.0.1 - data-view-byte-offset: 1.0.0 - es-define-property: 1.0.0 + 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.0.0 - es-set-tostringtag: 2.0.3 - es-to-primitive: 1.2.1 - function.prototype.name: 1.1.6 - get-intrinsic: 1.2.4 - get-symbol-description: 1.0.2 + 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.0.1 + gopd: 1.2.0 has-property-descriptors: 1.0.2 - has-proto: 1.0.3 - has-symbols: 1.0.3 + has-proto: 1.2.0 + has-symbols: 1.1.0 hasown: 2.0.2 - internal-slot: 1.0.7 - is-array-buffer: 3.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 is-callable: 1.2.7 - is-data-view: 1.0.1 + is-data-view: 1.0.2 is-negative-zero: 2.0.3 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.3 - is-string: 1.0.7 - is-typed-array: 1.1.13 - is-weakref: 1.0.2 - object-inspect: 1.13.1 + 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.5 - regexp.prototype.flags: 1.5.2 - safe-array-concat: 1.1.2 - safe-regex-test: 1.0.3 - string.prototype.trim: 1.2.9 - string.prototype.trimend: 1.0.8 + 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.2 - typed-array-byte-length: 1.0.1 - typed-array-byte-offset: 1.0.2 - typed-array-length: 1.0.6 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.15 - dev: true - - /es-define-property@1.0.0: - resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.4 + 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-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + es-define-property@1.0.1: {} - /es-get-iterator@1.1.3: - resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 - is-arguments: 1.1.1 - is-map: 2.0.3 - is-set: 2.0.3 - is-string: 1.0.7 - isarray: 2.0.5 - stop-iteration-iterator: 1.0.0 - dev: true + es-errors@1.3.0: {} - /es-iterator-helpers@1.0.19: - resolution: {integrity: sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==} - engines: {node: '>= 0.4'} + es-iterator-helpers@1.2.2: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.3 + es-abstract: 1.24.1 es-errors: 1.3.0 - es-set-tostringtag: 2.0.3 + es-set-tostringtag: 2.1.0 function-bind: 1.1.2 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 globalthis: 1.0.4 + gopd: 1.2.0 has-property-descriptors: 1.0.2 - has-proto: 1.0.3 - has-symbols: 1.0.3 - internal-slot: 1.0.7 - iterator.prototype: 1.1.2 - safe-array-concat: 1.1.2 - dev: true - - /es-module-lexer@0.9.3: - resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==} - dev: true - - /es-object-atoms@1.0.0: - resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} - engines: {node: '>= 0.4'} + 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@2.1.0: {} + + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 - dev: true - /es-set-tostringtag@2.0.3: - resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} - engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: dependencies: - get-intrinsic: 1.2.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 hasown: 2.0.2 - dev: true - - /es-shim-unscopables@1.0.2: - resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} - dependencies: - hasown: 2.0.2 - dev: true - - /es-to-primitive@1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} - dependencies: - is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.0.4 - dev: true - - /esbuild-plugin-alias@0.2.1: - resolution: {integrity: sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ==} - dev: true - - /esbuild-register@3.5.0(esbuild@0.20.2): - resolution: {integrity: sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==} - peerDependencies: - esbuild: '>=0.12 <1' - dependencies: - debug: 4.3.4 - esbuild: 0.20.2 - transitivePeerDependencies: - - supports-color - dev: true - - /esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 - dev: true - - /escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} - engines: {node: '>=6'} - dev: true - - /escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - dev: true - - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - /escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} - hasBin: true + + es-shim-unscopables@1.1.0: dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionalDependencies: - source-map: 0.6.1 - dev: true + hasown: 2.0.2 - /eslint-config-prettier@9.1.0(eslint@8.57.0): - resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' + es-to-primitive@1.3.0: dependencies: - eslint: 8.57.0 - dev: true + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 - /eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + es-toolkit@1.46.1: {} + + 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 + + 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.13.1 - resolve: 1.22.8 + is-core-module: 2.16.1 + resolve: 1.22.11 transitivePeerDependencies: - supports-color - dev: true - /eslint-module-utils@2.8.1(@typescript-eslint/parser@7.8.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): - resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} - 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-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)): dependencies: - '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) debug: 3.2.7 - eslint: 8.57.0 + optionalDependencies: + '@typescript-eslint/parser': 8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - dev: true - /eslint-plugin-i18next@6.0.3: - resolution: {integrity: sha512-RtQXYfg6PZCjejIQ/YG+dUj/x15jPhufJ9hUDGH0kCpJ6CkVMAWOQ9exU1CrbPmzeykxLjrXkjAaOZF/V7+DOA==} - engines: {node: '>=0.10.0'} + eslint-plugin-i18next@6.1.3: dependencies: lodash: 4.17.21 requireindex: 1.1.0 - dev: true - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.8.0)(eslint@8.57.0): - resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)): dependencies: - '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 - array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 + '@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: 8.57.0 + eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.8.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 - is-core-module: 2.13.1 + 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.0 + 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.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - dev: true - /eslint-plugin-path@1.3.0(eslint@8.57.0): - resolution: {integrity: sha512-Q/8AusuMaATyh67mjhCURrf7IW1Uq3jcKlfPVHSb6mNRFEJuIIx4xYvewP8n8eRmzBic8457Vw/sBL5U0y9S/g==} - engines: {node: '>= 12.22.0'} - peerDependencies: - eslint: '>=6.0.0' + eslint-plugin-path@2.1.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - eslint: 8.57.0 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) load-tsconfig: 0.2.5 - dev: true + transitivePeerDependencies: + - supports-color + - typescript - /eslint-plugin-react-hooks@4.6.2(eslint@8.57.0): - resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + eslint-plugin-react-hooks@7.1.1(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 8.57.0 - dev: true + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + eslint: 9.39.2(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.2.1 + zod-validation-error: 4.0.2(zod@4.2.1) + transitivePeerDependencies: + - supports-color - /eslint-plugin-react-refresh@0.4.6(eslint@8.57.0): - resolution: {integrity: sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==} - peerDependencies: - eslint: '>=7' + eslint-plugin-react-refresh@0.5.2(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 8.57.0 - dev: true + eslint: 9.39.2(jiti@2.6.1) - /eslint-plugin-react@7.34.1(eslint@8.57.0): - resolution: {integrity: sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): dependencies: - array-includes: 3.1.8 + array-includes: 3.1.9 array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.2 - array.prototype.toreversed: 1.1.2 - array.prototype.tosorted: 1.1.3 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 - es-iterator-helpers: 1.0.19 - eslint: 8.57.0 + es-iterator-helpers: 1.2.2 + eslint: 9.39.2(jiti@2.6.1) estraverse: 5.3.0 + hasown: 2.0.2 jsx-ast-utils: 3.3.5 minimatch: 3.1.2 - object.entries: 1.1.8 + object.entries: 1.1.9 object.fromentries: 2.0.8 - object.hasown: 1.1.4 - object.values: 1.2.0 + 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.11 - dev: true + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 - /eslint-plugin-simple-import-sort@12.1.0(eslint@8.57.0): - resolution: {integrity: sha512-Y2fqAfC11TcG/WP3TrI1Gi3p3nc8XJyEOJYHyEPEGI/UAgNx6akxxlX74p7SbAQdLcgASKhj8M0GKvH3vq/+ig==} - peerDependencies: - eslint: '>=5.0.0' + eslint-plugin-simple-import-sort@13.0.0(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 8.57.0 - dev: true + eslint: 9.39.2(jiti@2.6.1) - /eslint-plugin-storybook@0.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-CZeVO5EzmPY7qghO2t64oaFM+8FTaD4uzOEjHKp516exyTKo+skKAL9GI3QALS2BXhyALJjNtwbmr1XinGE8bA==} - engines: {node: '>= 18'} - peerDependencies: - eslint: '>=6' + eslint-plugin-storybook@10.3.6(eslint@9.39.2(jiti@2.6.1))(storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3): dependencies: - '@storybook/csf': 0.0.1 - '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 - requireindex: 1.2.0 - ts-dedent: 2.2.0 + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + storybook: 10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) transitivePeerDependencies: - supports-color - typescript - dev: true - - /eslint-plugin-unused-imports@3.2.0(@typescript-eslint/eslint-plugin@7.8.0)(eslint@8.57.0): - resolution: {integrity: sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': 6 - 7 - eslint: '8' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - dependencies: - '@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 - eslint-rule-composer: 0.3.0 - dev: true - /eslint-rule-composer@0.3.0: - resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} - engines: {node: '>=4.0.0'} - dev: true - - /eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)): dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - dev: true + eslint: 9.39.2(jiti@2.6.1) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 - dev: true - /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} - dev: true + eslint-visitor-keys@3.4.3: {} - /eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.2(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.10.0 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.5.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: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.1 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color - dev: true - /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@10.4.0: dependencies: - acorn: 8.11.3 - acorn-jsx: 5.3.2(acorn@8.11.3) - eslint-visitor-keys: 3.4.3 - dev: true + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 - /esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: true + esprima@4.0.1: {} - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} + esquery@1.6.0: dependencies: estraverse: 5.3.0 - dev: true - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 - dev: true - /estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - dev: true - - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true + estraverse@5.3.0: {} - /estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true + estree-walker@2.0.2: {} - /estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.5 - dev: true - - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true + '@types/estree': 1.0.8 - /etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - dev: true + esutils@2.0.3: {} - /execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - dev: true - - /execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - dependencies: - cross-spawn: 7.0.3 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - dev: true - - /express@4.19.2: - resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} - engines: {node: '>= 0.10.0'} - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.2 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.6.0 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.2.0 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.1 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.7 - proxy-addr: 2.0.7 - qs: 6.11.0 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - dev: true + expect-type@1.3.0: {} - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-deep-equal@3.1.3: {} - /fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} + 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.5 - dev: true + micromatch: 4.0.8 - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true + fast-json-stable-stringify@2.1.0: {} - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true + fast-levenshtein@2.0.6: {} - /fast-loops@1.1.3: - resolution: {integrity: sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==} - dev: false + fast-printf@1.6.10: {} - /fast-printf@1.6.9: - resolution: {integrity: sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==} - engines: {node: '>=10.0'} - dependencies: - boolean: 3.2.0 - dev: false + fast-shallow-equal@1.0.0: {} - /fast-shallow-equal@1.0.0: - resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} - dev: false + fast-uri@3.1.0: {} - /fastest-stable-stringify@2.0.2: - resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} - dev: false + fastest-stable-stringify@2.0.2: {} - /fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fastq@1.20.1: dependencies: - reusify: 1.0.4 - dev: true - - /fetch-retry@5.0.6: - resolution: {integrity: sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==} - dev: true - - /fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - dev: true + reusify: 1.1.0 - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + fd-package-json@2.0.0: dependencies: - flat-cache: 3.2.0 - dev: true + walk-up-path: 4.0.0 - /file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - dependencies: - flat-cache: 4.0.1 - dev: true + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 - /file-selector@0.6.0: - resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} - engines: {node: '>= 12'} - dependencies: - tslib: 2.6.2 - dev: false + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 - /file-system-cache@2.3.0: - resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==} - dependencies: - fs-extra: 11.1.1 - ramda: 0.29.0 - dev: true + fflate@0.8.2: {} - /filelist@1.0.4: - resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + file-entry-cache@8.0.0: dependencies: - minimatch: 5.1.6 - dev: true + flat-cache: 4.0.1 - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} + file-selector@2.1.2: dependencies: - to-regex-range: 5.0.1 - dev: true - - /filter-iterator@0.0.1: - resolution: {integrity: sha512-v4lhL7Qa8XpbW3LN46CEnmhGk3eHZwxfNl5at20aEkreesht4YKb/Ba3BUIbnPhAC/r3dmu7ABaGk6MAvh2alA==} - dev: true - - /filter-obj@1.1.0: - resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} - engines: {node: '>=0.10.0'} - dev: true - - /filter-obj@5.1.0: - resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} - engines: {node: '>=14.16'} - dev: false - - /finalhandler@1.2.0: - resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} - engines: {node: '>= 0.8'} - dependencies: - debug: 2.6.9 - encodeurl: 1.0.2 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: true + tslib: 2.8.1 - /find-cache-dir@2.1.0: - resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} - engines: {node: '>=6'} - dependencies: - commondir: 1.0.1 - make-dir: 2.1.0 - pkg-dir: 3.0.0 - dev: true + filesize@10.1.6: {} - /find-cache-dir@3.3.2: - resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} - engines: {node: '>=8'} + fill-range@7.1.1: dependencies: - commondir: 1.0.1 - make-dir: 3.1.0 - pkg-dir: 4.2.0 - dev: true - - /find-root@1.1.0: - resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} - dev: false + to-regex-range: 5.0.1 - /find-up@3.0.0: - resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} - engines: {node: '>=6'} - dependencies: - locate-path: 3.0.0 - dev: true + filter-obj@5.1.0: {} - /find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - dev: true + find-root@1.1.0: {} - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + find-up@5.0.0: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - dev: true - - /flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flatted: 3.3.1 - keyv: 4.5.4 - rimraf: 3.0.2 - dev: true - /flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} + flat-cache@4.0.1: dependencies: - flatted: 3.3.1 + flatted: 3.3.3 keyv: 4.5.4 - dev: true - /flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - dev: true + flatted@3.3.3: {} - /flow-parser@0.235.1: - resolution: {integrity: sha512-s04193L4JE+ntEcQXbD6jxRRlyj9QXcgEl2W6xSjH4l9x4b0eHoCHfbYHjqf9LdZFUiM5LhgpiqsvLj/AyOyYQ==} - engines: {node: '>=0.4.0'} - dev: true - - /focus-lock@1.3.3: - resolution: {integrity: sha512-hfXkZha7Xt4RQtrL1HBfspAuIj89Y0fb6GX0dfJilb8S2G/lvL4akPAcHq6xoD2NuZnDMCnZL/zQesMyeu6Psg==} - engines: {node: '>=10'} - dependencies: - tslib: 2.6.2 - dev: false + flatted@3.4.2: {} - /focus-trap@7.5.4: - resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==} + focus-lock@1.3.6: dependencies: - tabbable: 6.2.0 - dev: false + tslib: 2.8.1 - /for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + for-each@0.3.5: dependencies: is-callable: 1.2.7 - dev: true - /foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} + foreground-child@3.3.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 - dev: true - /forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - dev: true + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 - /fracturedjsonjs@4.0.1: - resolution: {integrity: sha512-KMhSx7o45aPVj4w27dwdQyKJkNU8oBqw8UiK/s3VzsQB3+pKQ/3AqG/YOEQblV2BDuYE5dKp0OMf8RDsshrjTA==} - dev: false + fracturedjsonjs@4.1.1: {} - /framer-motion@10.18.0(react-dom@18.3.1)(react@18.3.1): - 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@10.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tslib: 2.6.2 + tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 0.8.8 - dev: false + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - /framer-motion@11.1.8(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-W2OGZmNfUarhh6A/rLXernq/JthjekbgeRWqzigPpbaShe/+HfQKUDSjiEdL302XOlINtO+SCFCiR1hlqN3uOA==} - peerDependencies: - '@emotion/is-prop-valid': '*' - react: ^18.0.0 - react-dom: ^18.0.0 - peerDependenciesMeta: - '@emotion/is-prop-valid': - optional: true - react: - optional: true - react-dom: - optional: true + framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tslib: 2.6.2 - dev: false + motion-dom: 11.18.1 + motion-utils: 11.18.1 + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 1.4.0 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - /framesync@6.1.2: - resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} + framesync@6.1.2: dependencies: tslib: 2.4.0 - dev: false - - /fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - dev: true - - /fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - dev: true - - /fs-extra@11.1.1: - resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} - engines: {node: '>=14.14'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - dev: true - /fs-extra@11.2.0: - resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} - engines: {node: '>=14.14'} + fs-extra@11.3.3: dependencies: graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.0 universalify: 2.0.1 - dev: true - - /fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - dev: true - - /fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - dev: true - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true - - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true + fsevents@2.3.3: optional: true - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function-bind@1.1.2: {} - /function.prototype.name@1.1.6: - resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} - engines: {node: '>= 0.4'} + function.prototype.name@1.1.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.3 functions-have-names: 1.2.3 - dev: true + hasown: 2.0.2 + is-callable: 1.2.7 - /functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - dev: true + functions-have-names@1.2.3: {} - /gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - dev: true + generator-function@2.0.1: {} - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - dev: true + gensync@1.0.0-beta.2: {} - /get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - dev: true + get-caller-file@2.0.5: {} - /get-intrinsic@1.2.4: - resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} - engines: {node: '>= 0.4'} + 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 - has-proto: 1.0.3 - has-symbols: 1.0.3 + 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: - resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} - engines: {node: '>=6'} - dev: false - - /get-npm-tarball-url@2.1.0: - resolution: {integrity: sha512-ro+DiMu5DXgRBabqXupW38h7WPZ9+Ad8UjwhvsmmN8w1sU7ab0nzAXvVZ4kqYg57OrqomRtJvepX5/xvFKNtjA==} - engines: {node: '>=12.17'} - dev: true - - /get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - dev: true - - /get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - dev: true + get-nonce@1.0.1: {} - /get-symbol-description@1.0.2: - resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} - engines: {node: '>= 0.4'} + get-proto@1.0.1: dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - get-intrinsic: 1.2.4 - dev: true + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 - /giget@1.2.3: - resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==} - hasBin: true - dependencies: - citty: 0.1.6 - consola: 3.2.3 - defu: 6.1.4 - node-fetch-native: 1.6.4 - nypm: 0.3.8 - ohash: 1.1.3 - pathe: 1.1.2 - tar: 6.2.1 - dev: true - - /github-slugger@2.0.0: - resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} - dev: true - - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + get-symbol-description@1.1.0: dependencies: - is-glob: 4.0.3 - dev: true + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 - dev: true - - /glob-promise@4.2.2(glob@7.2.3): - resolution: {integrity: sha512-xcUzJ8NWN5bktoTIX7eOclO1Npxd/dyVqUJxlLIDasT4C7KZyqlPIwkdJ0Ypiy3p2ZKahTjK4M9uC3sNSfNMzw==} - engines: {node: '>=12'} - peerDependencies: - glob: ^7.1.6 - dependencies: - '@types/glob': 7.2.0 - glob: 7.2.3 - dev: true - - /glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - dev: true - - /glob@10.3.12: - resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.6 - minimatch: 9.0.4 - minipass: 7.1.0 - path-scurry: 1.10.2 - dev: true - - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: true - - /globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - dev: true - - /globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.20.2 - dev: true - - /globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - dependencies: - define-properties: 1.2.1 - gopd: 1.0.1 - /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} + glob-parent@6.0.2: dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.1 - merge2: 1.4.1 - slash: 3.0.0 - dev: true + is-glob: 4.0.3 - /globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - dev: true + glob@10.5.0: + 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 - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + glob@13.0.6: dependencies: - get-intrinsic: 1.2.4 + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: true + globals@14.0.0: {} - /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true + globals@16.5.0: {} - /gunzip-maybe@1.4.2: - resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} - hasBin: true - dependencies: - browserify-zlib: 0.1.4 - is-deflate: 1.0.0 - is-gzip: 1.0.0 - peek-stream: 1.1.3 - pumpify: 1.5.1 - through2: 2.0.5 - dev: true - - /handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true + globalthis@1.0.4: dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.17.4 - dev: true + define-properties: 1.2.1 + gopd: 1.2.0 - /has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - dev: true + gopd@1.2.0: {} - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} + graceful-fs@4.2.11: {} - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true + has-bigints@1.1.0: {} - /has-own-property@0.1.0: - resolution: {integrity: sha512-14qdBKoonU99XDhWcFKZTShK+QV47qU97u8zzoVo9cL5TZ3BmBHXogItSt9qJjR0KUMFRhcCW8uGIGl8nkl7Aw==} - dev: true + has-flag@4.0.0: {} - /has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-property-descriptors@1.0.2: dependencies: - es-define-property: 1.0.0 + es-define-property: 1.0.1 - /has-proto@1.0.3: - resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} - engines: {node: '>= 0.4'} + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} + has-symbols@1.1.0: {} - /has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: dependencies: - has-symbols: 1.0.3 - dev: true + has-symbols: 1.1.0 - /hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} + hasown@2.0.2: dependencies: function-bind: 1.1.2 - /hast-util-heading-rank@3.0.0: - resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} + hasown@2.0.3: dependencies: - '@types/hast': 3.0.4 - dev: true + function-bind: 1.1.2 - /hast-util-is-element@3.0.0: - resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - dependencies: - '@types/hast': 3.0.4 - dev: true + hermes-estree@0.25.1: {} - /hast-util-to-string@3.0.0: - resolution: {integrity: sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==} + hermes-parser@0.25.1: dependencies: - '@types/hast': 3.0.4 - dev: true - - /he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - dev: true + hermes-estree: 0.25.1 - /hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 - dev: false - /hosted-git-info@2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - dev: true + html-escaper@2.0.2: {} - /html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - dev: true - - /html-parse-stringify@3.0.1: - resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 - dev: false - - /html-tags@3.3.1: - resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} - engines: {node: '>=8'} - dev: true - /http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} + https-proxy-agent@7.0.6(supports-color@10.2.2): dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - dev: true - - /human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - dev: true - - /human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - dev: true + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color - /hyphenate-style-name@1.0.4: - resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} - dev: false + hyphenate-style-name@1.1.0: {} - /i18next-http-backend@2.5.1: - resolution: {integrity: sha512-+rNX1tghdVxdfjfPt0bI1sNg5ahGW9kA7OboG7b4t03Fp69NdDlRIze6yXhIbN8rbHxJ8IP4dzRm/okZ15lkQg==} + i18next-http-backend@3.0.2: dependencies: cross-fetch: 4.0.0 transitivePeerDependencies: - encoding - dev: false - /i18next@23.11.3: - resolution: {integrity: sha512-Pq/aSKowir7JM0rj+Wa23Kb6KKDUGno/HjG+wRQu0PxoTbpQ4N89MAT0rFGvXmLkRLNMb1BbBOKGozl01dabzg==} + i18next@25.7.3(typescript@5.9.3): dependencies: - '@babel/runtime': 7.24.5 - dev: false + '@babel/runtime': 7.28.4 + optionalDependencies: + typescript: 5.9.3 - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: true + idb-keyval@6.2.1: {} - /idb-keyval@6.2.1: - resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} - dev: false + ieee754@1.2.1: {} - /identity-function@1.0.0: - resolution: {integrity: sha512-kNrgUK0qI+9qLTBidsH85HjDLpZfrrS0ElquKKe/fJFdB3D7VeKdXXEvOPDUHSHOzdZKCAAaQIWWyp0l2yq6pw==} - dev: true + ignore@5.3.2: {} - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true + ignore@7.0.5: {} - /ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} - engines: {node: '>= 4'} - dev: true + immediate@3.0.6: {} - /immer@10.1.1: - resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} - dev: false + immer@10.2.0: {} - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - /import-lazy@4.0.0: - resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} - engines: {node: '>=8'} - dev: true - - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true + imurmurhash@0.1.4: {} - /indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - dev: true + indent-string@4.0.0: {} - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - dev: true + index-to-position@1.2.0: {} - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true + inherits@2.0.4: {} - /inline-style-prefixer@7.0.0: - resolution: {integrity: sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==} + inline-style-prefixer@7.0.1: dependencies: css-in-js-utils: 3.1.0 - fast-loops: 1.1.3 - dev: false - /internal-slot@1.0.7: - resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} - engines: {node: '>= 0.4'} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 hasown: 2.0.2 - side-channel: 1.0.6 - dev: true + side-channel: 1.1.0 - /invariant@2.2.4: - resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + is-array-buffer@3.0.5: dependencies: - loose-envify: 1.4.0 - dev: false - - /ip@2.0.1: - resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} - dev: true + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 - /ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - dev: true + is-arrayish@0.2.1: {} - /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} - dev: true - - /is-arguments@1.1.1: - resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} - engines: {node: '>= 0.4'} + is-async-function@2.1.1: dependencies: - call-bind: 1.0.7 + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 has-tostringtag: 1.0.2 - dev: true + safe-regex-test: 1.1.0 - /is-array-buffer@3.0.4: - resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} - engines: {node: '>= 0.4'} + is-bigint@1.1.0: dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 - dev: true + has-bigints: 1.1.0 - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - /is-async-function@2.0.0: - resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} - engines: {node: '>= 0.4'} + is-boolean-object@1.2.2: dependencies: + call-bound: 1.0.4 has-tostringtag: 1.0.2 - dev: true - - /is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} - dependencies: - has-bigints: 1.0.2 - dev: true - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - dependencies: - binary-extensions: 2.3.0 - dev: true + is-callable@1.2.7: {} - /is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} + is-core-module@2.16.1: dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - dev: true - - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - dev: true + hasown: 2.0.2 - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + is-core-module@2.16.2: dependencies: - hasown: 2.0.2 + hasown: 2.0.3 - /is-data-view@1.0.1: - resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} - engines: {node: '>= 0.4'} + is-data-view@1.0.2: dependencies: - is-typed-array: 1.1.13 - dev: true + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 - /is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} + is-date-object@1.1.0: dependencies: + call-bound: 1.0.4 has-tostringtag: 1.0.2 - dev: true - /is-deflate@1.0.0: - resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} - dev: true + is-docker@2.2.1: {} - /is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - dev: true + is-docker@3.0.0: {} - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true + is-extglob@2.1.1: {} - /is-finalizationregistry@1.0.2: - resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} + is-finalizationregistry@1.1.1: dependencies: - call-bind: 1.0.7 - dev: true + call-bound: 1.0.4 - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: true + is-fullwidth-code-point@3.0.0: {} - /is-generator-function@1.0.10: - resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} - engines: {node: '>= 0.4'} + is-generator-function@1.1.2: dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 has-tostringtag: 1.0.2 - dev: true + safe-regex-test: 1.1.0 - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - dev: true - - /is-gzip@1.0.0: - resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==} - engines: {node: '>=0.10.0'} - dev: true - - /is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - dev: true - - /is-iterable@1.1.1: - resolution: {integrity: sha512-EdOZCr0NsGE00Pot+x1ZFx9MJK3C6wy91geZpXwvwexDLJvA4nzYyZf7r+EIwSeVsOLDdBz7ATg9NqKTzuNYuQ==} - engines: {node: '>= 4'} - dev: true - - /is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - dev: true - /is-nan@1.3.2: - resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - dev: true - - /is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - dev: true - - /is-number-object@1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} + is-inside-container@1.0.0: dependencies: - has-tostringtag: 1.0.2 - dev: true - - /is-number@4.0.0: - resolution: {integrity: sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==} - engines: {node: '>=0.10.0'} - dev: true + is-docker: 3.0.0 - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true + is-interactive@1.0.0: {} - /is-path-cwd@2.2.0: - resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} - engines: {node: '>=6'} - dev: true + is-map@2.0.3: {} - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true + is-negative-zero@2.0.3: {} - /is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} + is-number-object@1.1.1: dependencies: - isobject: 3.0.1 - dev: true + call-bound: 1.0.4 + has-tostringtag: 1.0.2 - /is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - dev: true + is-number@7.0.0: {} - /is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} + is-regex@1.2.1: dependencies: - call-bind: 1.0.7 + call-bound: 1.0.4 + gopd: 1.2.0 has-tostringtag: 1.0.2 - dev: true + hasown: 2.0.2 - /is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - dev: true + is-set@2.0.3: {} - /is-shared-array-buffer@1.0.3: - resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} - engines: {node: '>= 0.4'} + is-shared-array-buffer@1.0.4: dependencies: - call-bind: 1.0.7 - dev: true - - /is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - dev: true - - /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + call-bound: 1.0.4 - /is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} + is-string@1.1.1: dependencies: + call-bound: 1.0.4 has-tostringtag: 1.0.2 - dev: true - /is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} + is-symbol@1.1.1: dependencies: - has-symbols: 1.0.3 - dev: true + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 - /is-typed-array@1.1.13: - resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} - engines: {node: '>= 0.4'} + is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.15 - dev: true + which-typed-array: 1.1.19 - /is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - dev: true + is-unicode-supported@0.1.0: {} - /is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - dev: true + is-weakmap@2.0.2: {} - /is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + is-weakref@1.1.1: dependencies: - call-bind: 1.0.7 - dev: true + call-bound: 1.0.4 - /is-weakset@2.0.3: - resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} - engines: {node: '>= 0.4'} + is-weakset@2.0.4: dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 - dev: true + call-bound: 1.0.4 + get-intrinsic: 1.3.0 - /is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 - dev: true - /isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: true + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 - /isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - dev: true + isarray@1.0.0: {} - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true + isarray@2.0.5: {} - /isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} - dev: true + isexe@2.0.0: {} - /istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - dev: true + istanbul-lib-coverage@3.2.2: {} - /istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} + istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 - dev: true - - /istanbul-lib-source-maps@5.0.4: - resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==} - engines: {node: '>=10'} - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - debug: 4.3.4 - istanbul-lib-coverage: 3.2.2 - transitivePeerDependencies: - - supports-color - dev: true - /istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} - engines: {node: '>=8'} + istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - dev: true - - /iterable-lookahead@1.0.0: - resolution: {integrity: sha512-hJnEP2Xk4+44DDwJqUQGdXal5VbyeWLaPyDl2AQc242Zr7iqz4DgpQOrEzglWVMGHMDCkguLHEKxd1+rOsmgSQ==} - engines: {node: '>=4'} - dev: true - /iterator.prototype@1.1.2: - resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} + iterator.prototype@1.1.5: dependencies: - define-properties: 1.2.1 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 - reflect.getprototypeof: 1.0.6 + 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 - dev: true - - /its-fine@1.2.5(react@18.3.1): - resolution: {integrity: sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==} - peerDependencies: - react: '>=18.0' - dependencies: - '@types/react-reconciler': 0.28.8 - react: 18.3.1 - dev: false - /jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 - dev: true - - /jake@10.9.1: - resolution: {integrity: sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==} - engines: {node: '>=10'} - hasBin: true - dependencies: - async: 3.2.5 - chalk: 4.1.2 - filelist: 1.0.4 - minimatch: 3.1.2 - dev: true - /jiti@1.21.0: - resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} - hasBin: true - dev: true + jiti@2.6.1: {} - /jju@1.4.0: - resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - dev: true + js-cookie@2.2.1: {} - /js-cookie@2.2.1: - resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} - dev: false + js-levenshtein@1.1.6: {} - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@10.0.0: {} - /js-tokens@9.0.0: - resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} - dev: true + js-tokens@4.0.0: {} - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true + js-yaml@4.1.1: dependencies: argparse: 2.0.1 - dev: true - - /jscodeshift@0.15.2(@babel/preset-env@7.24.5): - resolution: {integrity: sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==} - hasBin: true - peerDependencies: - '@babel/preset-env': ^7.1.6 - peerDependenciesMeta: - '@babel/preset-env': - optional: true - dependencies: - '@babel/core': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.5) - '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.5) - '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.5) - '@babel/preset-env': 7.24.5(@babel/core@7.24.5) - '@babel/preset-flow': 7.24.1(@babel/core@7.24.5) - '@babel/preset-typescript': 7.24.1(@babel/core@7.24.5) - '@babel/register': 7.23.7(@babel/core@7.24.5) - babel-core: 7.0.0-bridge.0(@babel/core@7.24.5) - chalk: 4.1.2 - flow-parser: 0.235.1 - graceful-fs: 4.2.11 - micromatch: 4.0.5 - neo-async: 2.6.2 - node-dir: 0.1.17 - recast: 0.23.6 - temp: 0.8.4 - write-file-atomic: 2.4.3 - transitivePeerDependencies: - - supports-color - dev: true - /jsesc@0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - dev: true + jsesc@3.1.0: {} - /jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - dev: true + json-buffer@3.0.1: {} - /json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: true + json-parse-even-better-errors@2.3.1: {} - /json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: {} - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true + json-schema-traverse@1.0.0: {} - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true + json-stable-stringify-without-jsonify@1.0.1: {} - /json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true + json5@1.0.2: dependencies: minimist: 1.2.8 - dev: true - /json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - dev: true + json5@2.2.3: {} - /jsondiffpatch@0.6.0: - resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true + jsondiffpatch@0.7.3: dependencies: - '@types/diff-match-patch': 1.0.36 - chalk: 5.3.0 - diff-match-patch: 1.0.5 - dev: false - - /jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - optionalDependencies: - graceful-fs: 4.2.11 - dev: true + '@dmsnell/diff-match-patch': 1.1.0 - /jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.2.0: dependencies: universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 - dev: true - /jsx-ast-utils@3.3.5: - resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} - engines: {node: '>=4.0'} + jsx-ast-utils@3.3.5: dependencies: - array-includes: 3.1.8 - array.prototype.flat: 1.3.2 - object.assign: 4.1.5 - object.values: 1.2.0 - dev: true + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 - /keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + jszip@3.10.1: dependencies: - json-buffer: 3.0.1 - dev: true - - /kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: true - - /kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - dev: true + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 - /klona@2.0.6: - resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} - engines: {node: '>= 8'} - dev: false + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 - /knip@5.12.3(@types/node@20.12.10)(typescript@5.4.5): - resolution: {integrity: sha512-LL+NsE+3H0TkUnQW6icHQ+5qSrPENmjHJyMHgzjiZPmunstrIsaRG+QjahnzoH/FjMjVJwrdwVOSvksa8ixFbw==} - engines: {node: '>=18.6.0'} - hasBin: true - peerDependencies: - '@types/node': '>=18' - typescript: '>=5.0.4' - dependencies: - '@ericcornelissen/bash-parser': 0.5.2 - '@nodelib/fs.walk': 2.0.0 - '@snyk/github-codeowners': 1.1.0 - '@types/node': 20.12.10 - easy-table: 1.2.0 - fast-glob: 3.3.2 - file-entry-cache: 8.0.0 - jiti: 1.21.0 - js-yaml: 4.1.0 - minimist: 1.2.8 - picocolors: 1.0.0 - picomatch: 4.0.2 - pretty-ms: 9.0.0 - resolve: 1.22.8 - smol-toml: 1.1.4 - strip-json-comments: 5.0.1 - summary: 2.1.0 - typescript: 5.4.5 - zod: 3.23.6 - zod-validation-error: 3.2.0(zod@3.23.6) - dev: true - - /kolorist@1.8.0: - resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - dev: true - - /konva@9.3.6: - resolution: {integrity: sha512-dqR8EbcM0hjuilZCBP6xauQ5V3kH3m9kBcsDkqPypQuRgsXbcXUrxqYxhNbdvKZpYNW8Amq94jAD/C0NY3qfBQ==} - dev: false - - /lazy-universal-dotenv@4.0.0: - resolution: {integrity: sha512-aXpZJRnTkpK6gQ/z4nk+ZBLd/Qdp118cvPruLSIQzQNRhKwEcdXCOzXuF55VDqIiuAaY3UGZ10DJtvZzDcvsxg==} - engines: {node: '>=14.0.0'} + knip@5.77.4(@types/node@22.19.3)(typescript@5.9.3): dependencies: - app-root-dir: 1.0.2 - dotenv: 16.4.5 - dotenv-expand: 10.0.0 - dev: true + '@nodelib/fs.walk': 1.2.8 + '@types/node': 22.19.3 + fast-glob: 3.3.3 + formatly: 0.3.0 + jiti: 2.6.1 + js-yaml: 4.1.1 + minimist: 1.2.8 + oxc-resolver: 11.16.2 + picocolors: 1.1.1 + picomatch: 4.0.3 + smol-toml: 1.6.0 + strip-json-comments: 5.0.3 + typescript: 5.9.3 + zod: 4.2.1 - /leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - dev: true + konva@9.3.22: {} - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true - - /lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - /liqe@3.8.0: - resolution: {integrity: sha512-cZ1rDx4XzxONBTskSPBp7/KwJ9qbUdF8EPnY4VjKXwHF1Krz9lgnlMTh1G7kd+KtPYvUte1mhuZeQSnk7KiSBg==} - engines: {node: '>=12.0'} - dependencies: - nearley: 2.20.1 - ts-error: 1.0.6 - dev: false - /load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} - engines: {node: '>=14'} - dependencies: - mlly: 1.7.0 - pkg-types: 1.1.0 - dev: true - - /locate-path@3.0.0: - resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} - engines: {node: '>=6'} - dependencies: - p-locate: 3.0.0 - path-exists: 3.0.0 - dev: true - - /locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - dependencies: - p-locate: 4.1.0 - dev: true - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + lie@3.3.0: dependencies: - p-locate: 5.0.0 - dev: true - - /lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - dev: false + immediate: 3.0.6 - /lodash.curry@4.1.1: - resolution: {integrity: sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==} - dev: true - - /lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - dev: true - - /lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - dev: true - - /lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - dev: true - - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true - - /lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - dev: false - - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true + lightningcss-android-arm64@1.32.0: + optional: true - /log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - dev: true + lightningcss-darwin-arm64@1.32.0: + optional: true - /loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - dependencies: - js-tokens: 4.0.0 + lightningcss-darwin-x64@1.32.0: + optional: true - /loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - dependencies: - get-func-name: 2.0.2 - dev: true + lightningcss-freebsd-x64@1.32.0: + optional: true - /lru-cache@10.2.2: - resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} - engines: {node: 14 || >=16.14} - dev: true + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true - /lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - dependencies: - yallist: 3.1.1 - dev: true + lightningcss-linux-arm64-gnu@1.32.0: + optional: true - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true + lightningcss-linux-arm64-musl@1.32.0: + optional: true - /lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - dev: true + lightningcss-linux-x64-gnu@1.32.0: + optional: true - /magic-string@0.16.0: - resolution: {integrity: sha512-c4BEos3y6G2qO0B9X7K0FVLOPT9uGrjYwYRLFmDqyl5YMboUviyecnXWp94fJTSMwPw2/sf+CEYt5AGpmklkkQ==} - dependencies: - vlq: 0.2.3 - dev: true + lightningcss-linux-x64-musl@1.32.0: + optional: true - /magic-string@0.27.0: - resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true + lightningcss-win32-arm64-msvc@1.32.0: + optional: true - /magic-string@0.30.10: - resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true + lightningcss-win32-x64-msvc@1.32.0: + optional: true - /magicast@0.3.4: - resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==} + lightningcss@1.32.0: dependencies: - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 - source-map-js: 1.2.0 - dev: true + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 - /make-dir@2.1.0: - resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} - engines: {node: '>=6'} - dependencies: - pify: 4.0.1 - semver: 5.7.2 - dev: true + lines-and-columns@1.2.4: {} - /make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} + linkify-react@4.3.2(linkifyjs@4.3.2)(react@19.2.6): dependencies: - semver: 6.3.1 - dev: true + linkifyjs: 4.3.2 + react: 19.2.6 - /make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} - dependencies: - semver: 7.6.0 - dev: true + linkifyjs@4.3.2: {} - /map-obj@2.0.0: - resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==} - engines: {node: '>=4'} - dev: true + liqe@3.8.4: + dependencies: + nearley: 2.20.1 + ts-error: 1.0.6 - /map-or-similar@1.5.0: - resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} - dev: true + load-tsconfig@0.2.5: {} - /markdown-to-jsx@7.3.2(react@18.3.1): - resolution: {integrity: sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==} - engines: {node: '>= 10'} - peerDependencies: - react: '>= 0.14.0' + locate-path@6.0.0: dependencies: - react: 18.3.1 - dev: true + p-locate: 5.0.0 - /mdn-data@2.0.14: - resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} - dev: false + lodash.merge@4.6.2: {} - /media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - dev: true + lodash.mergewith@4.6.2: {} - /memoize-one@6.0.0: - resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} - dev: false + lodash@4.17.21: {} - /memoizerific@1.11.3: - resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} + log-symbols@4.1.0: dependencies: - map-or-similar: 1.5.0 - dev: true + chalk: 4.1.2 + is-unicode-supported: 0.1.0 - /merge-descriptors@1.0.1: - resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} - dev: true + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true + loupe@3.2.1: {} - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: true + lru-cache@10.4.3: {} - /methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - dev: true + lru-cache@11.2.4: {} - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} + lru-cache@5.1.1: dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - dev: true + yallist: 3.1.1 - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - dev: true + lz-string@1.5.0: {} - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + magic-string@0.30.21: dependencies: - mime-db: 1.52.0 - dev: true + '@jridgewell/sourcemap-codec': 1.5.5 - /mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - dev: true + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: true + make-dir@4.0.0: + dependencies: + semver: 7.8.0 - /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true + math-expression-evaluator@2.0.7: {} - /min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - dev: true + math-intrinsics@1.1.0: {} - /minimatch@3.0.8: - resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} - dependencies: - brace-expansion: 1.1.11 - dev: true + mdn-data@2.0.14: {} - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true + memoize-one@6.0.0: {} - /minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} - dependencies: - brace-expansion: 2.0.1 - dev: true + merge2@1.4.1: {} - /minimatch@9.0.4: - resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} - engines: {node: '>=16 || 14 >=14.17'} + micromatch@4.0.8: dependencies: - brace-expansion: 2.0.1 - dev: true + braces: 3.0.3 + picomatch: 2.3.1 - /minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true + mimic-fn@2.1.0: {} - /minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} + min-indent@1.0.1: {} + + minimatch@10.2.5: dependencies: - yallist: 4.0.0 - dev: true + brace-expansion: 5.0.6 - /minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - dev: true + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 - /minipass@7.1.0: - resolution: {integrity: sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==} - engines: {node: '>=16 || 14 >=14.17'} - dev: true + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 - /minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} + minimatch@9.0.5: dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - dev: true + brace-expansion: 2.0.2 - /mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - dev: true + minimist@1.2.8: {} - /mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - dev: true + minipass@7.1.2: {} - /mlly@1.7.0: - resolution: {integrity: sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==} - dependencies: - acorn: 8.11.3 - pathe: 1.1.2 - pkg-types: 1.1.0 - ufo: 1.5.3 - dev: true + minipass@7.1.3: {} - /moo@0.5.2: - resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} - dev: false + moo@0.5.2: {} - /mrmime@2.0.0: - resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} - engines: {node: '>=10'} - dev: true + motion-dom@11.18.1: + dependencies: + motion-utils: 11.18.1 - /ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - dev: true + motion-utils@11.18.1: {} - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + mrmime@2.0.1: {} - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true + ms@2.1.3: {} - /muggle-string@0.3.1: - resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} - dev: true + mtwist@1.0.2: {} - /nano-css@5.6.1(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==} - peerDependencies: - react: '*' - react-dom: '*' + nano-css@5.6.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.5 css-tree: 1.1.3 - csstype: 3.1.3 + csstype: 3.2.3 fastest-stable-stringify: 2.0.2 - inline-style-prefixer: 7.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + inline-style-prefixer: 7.0.1 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) rtl-css-js: 1.16.1 stacktrace-js: 2.0.2 - stylis: 4.3.2 - dev: false + stylis: 4.3.6 - /nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true + nanoid@3.3.12: {} - /nanostores@0.10.3: - resolution: {integrity: sha512-Nii8O1XqmawqSCf9o2aWqVxhKRN01+iue9/VEd1TiJCr9VT5XxgPFbF1Edl1XN6pwJcZRsl8Ki+z01yb/T/C2g==} - engines: {node: ^18.0.0 || >=20.0.0} - dev: false + nanoid@5.1.6: {} - /nanostores@0.9.5: - resolution: {integrity: sha512-Z+p+g8E7yzaWwOe5gEUB2Ox0rCEeXWYIZWmYvw/ajNYX8DlXdMvMDj8DWfM/subqPAcsf8l8Td4iAwO1DeIIRQ==} - engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} - dev: false + nanostores@1.3.0: {} - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true + natural-compare@1.4.0: {} - /nearley@2.20.1: - resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} - hasBin: true + nearley@2.20.1: dependencies: commander: 2.20.3 moo: 0.5.2 railroad-diagrams: 1.0.0 randexp: 0.4.6 - dev: false - - /negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - dev: true - - /neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - dev: true - - /new-github-issue-url@1.0.0: - resolution: {integrity: sha512-wa9jlUFg3v6S3ddijQiB18SY4u9eJYcUe5sHa+6SB8m1UUbtX+H/bBglxOLnhhF1zIHuhWXnKBAa8kBeKRIozQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: false - - /node-dir@0.1.17: - resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} - engines: {node: '>= 0.10.5'} - dependencies: - minimatch: 3.1.2 - dev: true - /node-fetch-native@1.6.4: - resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} - dev: true + new-github-issue-url@1.1.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-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 - /node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - dev: true - - /normalize-package-data@2.5.0: - resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} - dependencies: - hosted-git-info: 2.8.9 - resolve: 1.22.8 - semver: 5.7.2 - validate-npm-package-license: 3.0.4 - dev: true - - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - dev: true - - /npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - dependencies: - path-key: 3.1.1 - dev: true - - /npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - path-key: 4.0.0 - dev: true - - /nypm@0.3.8: - resolution: {integrity: sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og==} - engines: {node: ^14.16.0 || >=16.10.0} - hasBin: true - dependencies: - citty: 0.1.6 - consola: 3.2.3 - execa: 8.0.1 - pathe: 1.1.2 - ufo: 1.5.3 - dev: true - - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} + node-releases@2.0.27: {} - /object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} - dev: true - - /object-is@1.1.6: - resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - dev: true + node-releases@2.0.38: {} - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} + object-assign@4.1.1: {} - /object-pairs@0.1.0: - resolution: {integrity: sha512-3ECr6K831I4xX/Mduxr9UC+HPOz/d6WKKYj9p4cmC8Lg8p7g8gitzsxNX5IWlSIgFWN/a4JgrJaoAMKn20oKwA==} - dev: true + object-inspect@1.13.4: {} - /object-values@1.0.0: - resolution: {integrity: sha512-+8hwcz/JnQ9EpLIXzN0Rs7DLsBpJNT/xYehtB/jU93tHYr5BFEO8E+JGQNOSqE7opVzz5cGksKFHt7uUJVLSjQ==} - engines: {node: '>=0.10.0'} - dev: true + object-keys@1.1.1: {} - /object.assign@4.1.5: - resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} - engines: {node: '>= 0.4'} + object.assign@4.1.7: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 - has-symbols: 1.0.3 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 object-keys: 1.1.1 - dev: true - - /object.entries@1.1.8: - resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-object-atoms: 1.0.0 - dev: true - /object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} + object.entries@1.1.9: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.3 - es-object-atoms: 1.0.0 - dev: true + es-object-atoms: 1.1.1 - /object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} + object.fromentries@2.0.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.3 - dev: true + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 - /object.hasown@1.1.4: - resolution: {integrity: sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==} - engines: {node: '>= 0.4'} + object.groupby@1.0.3: dependencies: + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.3 - es-object-atoms: 1.0.0 - dev: true + es-abstract: 1.24.1 - /object.values@1.2.0: - resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} - engines: {node: '>= 0.4'} + object.values@1.2.1: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.0.0 - dev: true - - /ohash@1.1.3: - resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} - dev: true - - /on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - dependencies: - ee-first: 1.1.1 - dev: true - - /on-headers@1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} - engines: {node: '>= 0.8'} - dev: true + es-object-atoms: 1.1.1 - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - dev: true + obug@2.1.1: {} - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 - dev: true - /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} + open@10.2.0: dependencies: - mimic-fn: 4.0.0 - dev: true + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 - /open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} + open@8.4.2: dependencies: define-lazy-prop: 2.0.0 is-docker: 2.2.1 is-wsl: 2.2.0 - dev: true - /openapi-types@12.1.3: - resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} - dev: true + openapi-types@12.1.3: {} - /openapi-typescript@6.7.5: - resolution: {integrity: sha512-ZD6dgSZi0u1QCP55g8/2yS5hNJfIpgqsSGHLxxdOjvY7eIrXzj271FJEQw33VwsZ6RCtO/NOuhxa7GBWmEudyA==} - hasBin: true + openapi-typescript@7.10.1(typescript@5.9.3): dependencies: + '@redocly/openapi-core': 1.34.6(supports-color@10.2.2) ansi-colors: 4.1.3 - fast-glob: 3.3.2 - js-yaml: 4.1.0 - supports-color: 9.4.0 - undici: 5.28.4 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 5.9.3 yargs-parser: 21.1.1 - dev: true - /optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} + optionator@0.9.4: dependencies: deep-is: 0.1.4 fast-levenshtein: 2.0.6 @@ -10518,11 +8632,8 @@ packages: prelude-ls: 1.2.1 type-check: 0.4.0 word-wrap: 1.2.5 - dev: true - /ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} + ora@5.4.1: dependencies: bl: 4.1.0 chalk: 4.1.2 @@ -10533,755 +8644,373 @@ packages: log-symbols: 4.1.0 strip-ansi: 6.0.1 wcwidth: 1.0.1 - dev: true - /overlayscrollbars-react@0.5.6(overlayscrollbars@2.7.3)(react@18.3.1): - resolution: {integrity: sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==} - peerDependencies: - overlayscrollbars: ^2.0.0 - react: '>=16.8.0' + overlayscrollbars-react@0.5.6(overlayscrollbars@2.12.0)(react@19.2.6): dependencies: - overlayscrollbars: 2.7.3 - react: 18.3.1 - dev: false + overlayscrollbars: 2.12.0 + react: 19.2.6 - /overlayscrollbars@2.7.3: - resolution: {integrity: sha512-HmNo8RPtuGUjBhUbVpZBHH7SHci5iSAdg5zSekCZVsjzaM6z8MIr3F9RXrzf4y7m+fOY0nx0+y0emr1fqQmfoA==} - dev: false - - /p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} + overlayscrollbars-react@0.5.6(overlayscrollbars@2.13.0)(react@19.2.6): dependencies: - p-try: 2.2.0 - dev: true + overlayscrollbars: 2.13.0 + react: 19.2.6 - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: true + overlayscrollbars@2.12.0: {} - /p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - dependencies: - yocto-queue: 1.0.0 - dev: true + overlayscrollbars@2.13.0: {} - /p-locate@3.0.0: - resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} - engines: {node: '>=6'} + own-keys@1.0.1: dependencies: - p-limit: 2.3.0 - dev: true + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 - /p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} + oxc-resolver@11.16.2: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.16.2 + '@oxc-resolver/binding-android-arm64': 11.16.2 + '@oxc-resolver/binding-darwin-arm64': 11.16.2 + '@oxc-resolver/binding-darwin-x64': 11.16.2 + '@oxc-resolver/binding-freebsd-x64': 11.16.2 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.16.2 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.16.2 + '@oxc-resolver/binding-linux-arm64-gnu': 11.16.2 + '@oxc-resolver/binding-linux-arm64-musl': 11.16.2 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.16.2 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.16.2 + '@oxc-resolver/binding-linux-riscv64-musl': 11.16.2 + '@oxc-resolver/binding-linux-s390x-gnu': 11.16.2 + '@oxc-resolver/binding-linux-x64-gnu': 11.16.2 + '@oxc-resolver/binding-linux-x64-musl': 11.16.2 + '@oxc-resolver/binding-openharmony-arm64': 11.16.2 + '@oxc-resolver/binding-wasm32-wasi': 11.16.2 + '@oxc-resolver/binding-win32-arm64-msvc': 11.16.2 + '@oxc-resolver/binding-win32-ia32-msvc': 11.16.2 + '@oxc-resolver/binding-win32-x64-msvc': 11.16.2 + + p-limit@3.1.0: dependencies: - p-limit: 2.3.0 - dev: true + yocto-queue: 0.1.0 - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + p-locate@5.0.0: dependencies: p-limit: 3.1.0 - dev: true - /p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - dependencies: - aggregate-error: 3.1.0 - dev: true + package-json-from-dist@1.0.1: {} - /p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - dev: true + pako@1.0.11: {} - /pako@0.2.9: - resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} - dev: true + pako@2.1.0: {} - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + parent-module@1.0.1: dependencies: callsites: 3.1.0 - /parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.24.2 - error-ex: 1.3.2 + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - /parse-ms@4.0.0: - resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} - engines: {node: '>=18'} - dev: true - - /parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - dev: true - - /path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - dev: true - - /path-exists@3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - dev: true - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true - - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: true - - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true - - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - /path-scurry@1.10.2: - resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - lru-cache: 10.2.2 - minipass: 7.1.0 - dev: true - - /path-to-regexp@0.1.7: - resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} - dev: true - - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - /pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - dev: true - - /pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - dev: true - - /peek-stream@1.1.3: - resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} - dependencies: - buffer-from: 1.1.2 - duplexify: 3.7.1 - through2: 2.0.5 - dev: true - - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true - - /picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - dev: true - - /pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - dev: true - - /pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - dev: true - - /pkg-dir@3.0.0: - resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} - engines: {node: '>=6'} + parse-json@8.3.0: dependencies: - find-up: 3.0.0 - dev: true + '@babel/code-frame': 7.27.1 + index-to-position: 1.2.0 + type-fest: 4.41.0 - /pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - dependencies: - find-up: 4.1.0 - dev: true + path-exists@4.0.0: {} - /pkg-dir@5.0.0: - resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} - engines: {node: '>=10'} - dependencies: - find-up: 5.0.0 - dev: true + path-key@3.1.1: {} - /pkg-types@1.1.0: - resolution: {integrity: sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==} - dependencies: - confbox: 0.1.7 - mlly: 1.7.0 - pathe: 1.1.2 - dev: true + path-parse@1.0.7: {} - /polished@4.3.1: - resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} - engines: {node: '>=10'} + path-scurry@1.11.1: dependencies: - '@babel/runtime': 7.24.5 - dev: true - - /possible-typed-array-names@1.0.0: - resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} - engines: {node: '>= 0.4'} - dev: true + lru-cache: 10.4.3 + minipass: 7.1.2 - /postcss@8.4.38: - resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} - engines: {node: ^10 || ^12 || >=14} + path-scurry@2.0.2: dependencies: - nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.2.0 - dev: true + lru-cache: 11.2.4 + minipass: 7.1.3 - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true - - /prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} - engines: {node: '>=14'} - hasBin: true - dev: 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} - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - dev: true + path-type@4.0.0: {} - /pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - dev: true + pathe@2.0.3: {} - /pretty-hrtime@1.0.3: - resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} - engines: {node: '>= 0.8'} - dev: true + pathval@2.0.1: {} - /pretty-ms@9.0.0: - resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==} - engines: {node: '>=18'} - dependencies: - parse-ms: 4.0.0 - dev: true + perfect-freehand@1.2.2: {} - /process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: true + picocolors@1.1.1: {} - /process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - dev: true + picomatch@2.3.1: {} - /prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - dev: true + picomatch@4.0.3: {} - /prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 + picomatch@4.0.4: {} - /proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - dev: true + pluralize@8.0.0: {} - /proxy-compare@2.5.1: - resolution: {integrity: sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==} - dev: false + possible-typed-array-names@1.1.0: {} - /pump@2.0.1: - resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} + postcss@8.5.14: dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - dev: true + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 - /pump@3.0.0: - resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} - dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - dev: true + prelude-ls@1.2.1: {} - /pumpify@1.5.1: - resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} + prettier@3.8.3: {} + + pretty-format@27.5.1: dependencies: - duplexify: 3.7.1 - inherits: 2.0.4 - pump: 2.0.1 - dev: true + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 - /punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - dev: true + process-nextick-args@2.0.1: {} - /qs@6.11.0: - resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} - engines: {node: '>=0.6'} + prop-types@15.8.1: dependencies: - side-channel: 1.0.6 - dev: true + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 - /qs@6.12.1: - resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==} - engines: {node: '>=0.6'} - dependencies: - side-channel: 1.0.6 - dev: true + punycode@2.3.1: {} - /query-string@9.0.0: - resolution: {integrity: sha512-4EWwcRGsO2H+yzq6ddHcVqkCQ2EFUSfDMEjF8ryp8ReymyZhIuaFRGLomeOQLkrzacMHoyky2HW0Qe30UbzkKw==} - engines: {node: '>=18'} + query-string@9.3.1: dependencies: decode-uri-component: 0.4.1 filter-obj: 5.1.0 split-on-first: 3.0.0 - dev: false - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true + queue-microtask@1.2.3: {} - /railroad-diagrams@1.0.0: - resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==} - dev: false + raf-schd@4.0.3: {} - /ramda@0.29.0: - resolution: {integrity: sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==} - dev: true + raf-throttle@2.0.6: {} - /randexp@0.4.6: - resolution: {integrity: sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==} - engines: {node: '>=0.12'} + railroad-diagrams@1.0.0: {} + + randexp@0.4.6: dependencies: discontinuous-range: 1.0.0 ret: 0.1.15 - dev: false - - /range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - dev: true - /raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} + react-clientside-effect@1.2.8(react@19.2.6): dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - dev: true + '@babel/runtime': 7.28.4 + react: 19.2.6 - /react-clientside-effect@1.2.6(react@18.3.1): - resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} - peerDependencies: - react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@babel/runtime': 7.24.1 - react: 18.3.1 - dev: false - - /react-colorful@5.6.1(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' + react-colorful@5.7.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - /react-docgen-typescript@2.2.2(typescript@5.4.5): - resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} - peerDependencies: - typescript: '>= 4.3.x' + react-docgen-typescript@2.4.0(typescript@5.9.3): dependencies: - typescript: 5.4.5 - dev: true + typescript: 5.9.3 - /react-docgen@7.0.3: - resolution: {integrity: sha512-i8aF1nyKInZnANZ4uZrH49qn1paRgBZ7wZiCNBMnenlPzEv0mRl+ShpTVEI6wZNl8sSc79xZkivtgLKQArcanQ==} - engines: {node: '>=16.14.0'} + react-docgen@8.0.3: dependencies: - '@babel/core': 7.24.5 - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 + '@babel/core': 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.20.5 + '@types/babel__traverse': 7.28.0 '@types/doctrine': 0.0.9 '@types/resolve': 1.20.6 doctrine: 3.0.0 - resolve: 1.22.8 - strip-indent: 4.0.0 + resolve: 1.22.12 + strip-indent: 4.1.1 transitivePeerDependencies: - supports-color - dev: true - /react-dom@18.3.1(react@18.3.1): - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} - peerDependencies: - react: ^18.3.1 + react-dom@19.2.6(react@19.2.6): dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 + react: 19.2.6 + scheduler: 0.27.0 - /react-dropzone@14.2.3(react@18.3.1): - resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==} - engines: {node: '>= 10.13'} - peerDependencies: - react: '>= 16.8 || 18.0.0' + react-dropzone@14.3.8(react@19.2.6): dependencies: - attr-accept: 2.2.2 - file-selector: 0.6.0 + attr-accept: 2.2.5 + file-selector: 2.1.2 prop-types: 15.8.1 - react: 18.3.1 - dev: false - - /react-element-to-jsx-string@15.0.0(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==} - peerDependencies: - react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 - react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 - dependencies: - '@base2/pretty-print-object': 1.0.1 - is-plain-object: 5.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-is: 18.1.0 - dev: true + react: 19.2.6 - /react-error-boundary@4.0.13(react@18.3.1): - resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==} - peerDependencies: - react: '>=16.13.1' + react-error-boundary@6.0.0(react@19.2.6): dependencies: - '@babel/runtime': 7.24.5 - react: 18.3.1 - dev: false + '@babel/runtime': 7.28.4 + react: 19.2.6 - /react-fast-compare@3.2.2: - resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - dev: false + react-fast-compare@3.2.2: {} - /react-focus-lock@2.11.1(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true + react-focus-lock@2.13.7(@types/react@19.2.14)(react@19.2.6): dependencies: - '@babel/runtime': 7.23.9 - '@types/react': 18.3.1 - focus-lock: 1.3.3 + '@babel/runtime': 7.28.4 + focus-lock: 1.3.6 prop-types: 15.8.1 - react: 18.3.1 - react-clientside-effect: 1.2.6(react@18.3.1) - use-callback-ref: 1.3.1(@types/react@18.3.1)(react@18.3.1) - use-sidecar: 1.1.2(@types/react@18.3.1)(react@18.3.1) - dev: false + react: 19.2.6 + react-clientside-effect: 1.2.8(react@19.2.6) + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.6) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 - /react-hook-form@7.51.4(react@18.3.1): - resolution: {integrity: sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==} - engines: {node: '>=12.22.0'} - peerDependencies: - react: ^16.8.0 || ^17 || ^18 + react-hook-form@7.69.0(react@19.2.6): dependencies: - react: 18.3.1 - dev: false + react: 19.2.6 - /react-hotkeys-hook@4.5.0(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==} - peerDependencies: - react: '>=16.8.1' - react-dom: '>=16.8.1' + react-hotkeys-hook@4.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - /react-i18next@14.1.1(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-QSiKw+ihzJ/CIeIYWrarCmXJUySHDwQr5y8uaNIkbxoGRm/5DukkxZs+RPla79IKyyDPzC/DRlgQCABHtrQuQQ==} - peerDependencies: - i18next: '>= 23.2.3' - react: '>= 16.8.0' - react-dom: '*' - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true + react-i18next@15.7.4(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3): dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1 - i18next: 23.11.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false + i18next: 25.7.3(typescript@5.9.3) + react: 19.2.6 + optionalDependencies: + react-dom: 19.2.6(react@19.2.6) + typescript: 5.9.3 - /react-icons@5.2.0(react@18.3.1): - resolution: {integrity: sha512-n52Y7Eb4MgQZHsSZOhSXv1zs2668/hBYKfSRIvKh42yExjyhZu0d1IK2CLLZ3BZB1oo13lDfwx2vOh2z9FTV6Q==} - peerDependencies: - react: '*' + react-i18next@16.5.0(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3): dependencies: - react: 18.3.1 - dev: false + '@babel/runtime': 7.28.4 + html-parse-stringify: 3.0.1 + i18next: 25.7.3(typescript@5.9.3) + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + react-dom: 19.2.6(react@19.2.6) + typescript: 5.9.3 - /react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-icons@5.5.0(react@19.2.6): + dependencies: + react: 19.2.6 - /react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - dev: true + react-is@16.13.1: {} - /react-is@18.1.0: - resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==} - dev: true + react-is@17.0.2: {} - /react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - dev: true + react-redux@9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + redux: 5.0.1 - /react-konva@18.2.10(konva@9.3.6)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==} - peerDependencies: - konva: ^8.0.1 || ^7.2.5 || ^9.0.0 - react: '>=18.0.0' - react-dom: '>=18.0.0' - dependencies: - '@types/react-reconciler': 0.28.8 - its-fine: 1.2.5(react@18.3.1) - konva: 9.3.6 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-reconciler: 0.29.2(react@18.3.1) - scheduler: 0.23.2 - dev: false - - /react-reconciler@0.29.2(react@18.3.1): - resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==} - engines: {node: '>=0.10.0'} - peerDependencies: - react: ^18.3.1 + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.6): dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 - dev: false + react: 19.2.6 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.6) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 - /react-redux@9.1.2(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1): - resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==} - peerDependencies: - '@types/react': ^18.2.25 - react: ^18.0 - redux: ^5.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - redux: - optional: true + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.6): dependencies: - '@types/react': 18.3.1 - '@types/use-sync-external-store': 0.0.3 - react: 18.3.1 - redux: 5.0.1 - use-sync-external-store: 1.2.2(react@18.3.1) - dev: false + react: 19.2.6 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.6) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.6) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.6) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 - /react-remove-scroll-bar@2.3.5(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true + react-resizable-panels@3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@types/react': 18.3.1 - react: 18.3.1 - react-style-singleton: 2.2.1(@types/react@18.3.1)(react@18.3.1) - tslib: 2.6.2 - dev: false + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - /react-remove-scroll@2.5.7(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true + react-router-dom@7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@types/react': 18.3.1 - react: 18.3.1 - react-remove-scroll-bar: 2.3.5(@types/react@18.3.1)(react@18.3.1) - react-style-singleton: 2.2.1(@types/react@18.3.1)(react@18.3.1) - tslib: 2.6.2 - use-callback-ref: 1.3.1(@types/react@18.3.1)(react@18.3.1) - use-sidecar: 1.1.2(@types/react@18.3.1)(react@18.3.1) - dev: false + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-router: 7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - /react-resizable-panels@2.0.19(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-v3E41kfKSuCPIvJVb4nL4mIZjjKIn/gh6YqZF/gDfQDolv/8XnhJBek4EiV2gOr3hhc5A3kOGOayk3DhanpaQw==} - peerDependencies: - react: ^16.14.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 + react-router@7.15.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false + cookie: 1.1.1 + react: 19.2.6 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.6(react@19.2.6) - /react-select@5.7.7(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-select@5.10.2(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@babel/runtime': 7.24.5 - '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) - '@floating-ui/dom': 1.6.5 - '@types/react-transition-group': 4.4.10 + '@babel/runtime': 7.28.4 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.6) + '@floating-ui/dom': 1.7.4 + '@types/react-transition-group': 4.4.12(@types/react@19.2.14) 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) - use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.1)(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-transition-group: 4.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.6) transitivePeerDependencies: - '@types/react' - dev: false + - supports-color - /react-select@5.8.0(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-select@5.8.3(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@babel/runtime': 7.24.5 - '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) - '@floating-ui/dom': 1.6.5 - '@types/react-transition-group': 4.4.10 + '@babel/runtime': 7.28.4 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.6) + '@floating-ui/dom': 1.7.4 + '@types/react-transition-group': 4.4.12(@types/react@19.2.14) 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) - use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.1)(react@18.3.1) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-transition-group: 4.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.6) transitivePeerDependencies: - '@types/react' - dev: false + - supports-color - /react-style-singleton@2.2.1(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.6): dependencies: - '@types/react': 18.3.1 get-nonce: 1.0.1 - invariant: 2.2.4 - react: 18.3.1 - tslib: 2.6.2 - dev: false + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 - /react-transition-group@4.4.5(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} - peerDependencies: - react: '>=16.6.0' - react-dom: '>=16.6.0' + react-textarea-autosize@8.5.9(@types/react@19.2.14)(react@19.2.6): + dependencies: + '@babel/runtime': 7.28.4 + react: 19.2.6 + use-composed-ref: 1.4.0(@types/react@19.2.14)(react@19.2.6) + use-latest: 1.3.0(@types/react@19.2.14)(react@19.2.6) + transitivePeerDependencies: + - '@types/react' + + react-transition-group@4.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.28.4 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) - dev: false + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - /react-universal-interface@0.6.2(react@18.3.1)(tslib@2.6.2): - resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} - peerDependencies: - react: '*' - tslib: '*' + react-universal-interface@0.6.2(react@19.2.6)(tslib@2.8.1): dependencies: - react: 18.3.1 - tslib: 2.6.2 - dev: false + react: 19.2.6 + tslib: 2.8.1 - /react-use@17.5.0(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==} - peerDependencies: - react: '*' - react-dom: '*' + react-use@17.6.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@types/js-cookie': 2.2.7 '@xobotyi/scrollbar-width': 1.9.5 @@ -11289,75 +9018,25 @@ packages: fast-deep-equal: 3.1.3 fast-shallow-equal: 1.0.0 js-cookie: 2.2.1 - nano-css: 5.6.1(react-dom@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.6.2) + nano-css: 5.6.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-universal-interface: 0.6.2(react@19.2.6)(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.6.2 - dev: false - - /react-virtuoso@4.7.10(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-l+fnBf/G1Fp6pHCnhFq2Ra4lkZtT6c5XrS9rCS0OA6de7WGLZviCo0y61CUZZG79TeAw3L7O4czeNPiqh9CIrg==} - engines: {node: '>=10'} - peerDependencies: - react: '>=16 || >=17 || >= 18' - react-dom: '>=16 || >=17 || >= 18' - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} - engines: {node: '>=0.10.0'} - dependencies: - loose-envify: 1.4.0 - - /reactflow@11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-wusd1Xpn1wgsSEv7UIa4NNraCwH9syBtubBy4xVNXg3b+CDKM+sFaF3hnMx0tr0et4km9urIDdNvwm34QiZong==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - dependencies: - '@reactflow/background': 11.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - '@reactflow/controls': 11.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - '@reactflow/minimap': 11.7.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - '@reactflow/node-resizer': 2.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - '@reactflow/node-toolbar': 1.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - dev: false + tslib: 2.8.1 - /read-pkg-up@7.0.1: - resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} - engines: {node: '>=8'} + react-virtuoso@4.18.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - find-up: 4.1.0 - read-pkg: 5.2.0 - type-fest: 0.8.1 - dev: true + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) - /read-pkg@5.2.0: - resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} - engines: {node: '>=8'} - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 2.5.0 - parse-json: 5.2.0 - type-fest: 0.6.0 - dev: true + react@19.2.6: {} - /readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 inherits: 2.0.4 @@ -11366,1961 +9045,857 @@ packages: safe-buffer: 5.1.2 string_decoder: 1.1.1 util-deprecate: 1.0.2 - dev: true - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: true - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 - dev: true - - /recast@0.23.6: - resolution: {integrity: sha512-9FHoNjX1yjuesMwuthAmPKabxYQdOgihFYmT5ebXfYGBcnqXZf3WOVz+5foEZ8Y83P4ZY6yQD5GMmtV+pgCCAQ==} - engines: {node: '>= 4'} + 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.6.2 - dev: true + tslib: 2.8.1 - /redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} + redent@3.0.0: dependencies: indent-string: 4.0.0 strip-indent: 3.0.0 - dev: true - /redux-dynamic-middlewares@2.2.0: - resolution: {integrity: sha512-GHESQC+Y0PV98ZBoaC6br6cDOsNiM1Cu4UleGMqMWCXX03jIr3BoozYVrRkLVVAl4sC216chakMnZOu6SwNdGA==} - dev: false - - /redux-remember@5.1.0(redux@5.0.1): - resolution: {integrity: sha512-Z6/S/brpwflOsGpX8Az93eujJ5fytMcaefxDfx0iib5d0DkL804zlw/Fhh/4HzZ5nXsP67j1zPUeDNWO1rhfvA==} - peerDependencies: - redux: '>=5.0.0' + redux-remember@5.3.0(redux@5.0.1): dependencies: redux: 5.0.1 - dev: false - /redux-thunk@3.1.0(redux@5.0.1): - resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} - peerDependencies: - redux: ^5.0.0 + redux-thunk@3.1.0(redux@5.0.1): dependencies: redux: 5.0.1 - dev: false - /redux-undo@1.1.0: - resolution: {integrity: sha512-zzLFh2qeF0MTIlzDhDLm9NtkfBqCllQJ3OCuIl5RKlG/ayHw6GUdIFdMhzMS9NnrnWdBX5u//ExMOHpfudGGOg==} - dev: false + redux-undo@1.1.0: {} - /redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} - dev: false + redux@5.0.1: {} - /reflect.getprototypeof@1.0.6: - resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} - engines: {node: '>= 0.4'} + reflect.getprototypeof@1.0.10: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.3 + es-abstract: 1.24.1 es-errors: 1.3.0 - get-intrinsic: 1.2.4 - globalthis: 1.0.4 - which-builtin-type: 1.1.3 - dev: true + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 - /regenerate-unicode-properties@10.1.1: - resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} - engines: {node: '>=4'} - dependencies: - regenerate: 1.4.2 - dev: true - - /regenerate@1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - dev: true - - /regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - - /regenerator-transform@0.15.2: - resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} - dependencies: - '@babel/runtime': 7.24.5 - dev: true - - /regexp.prototype.flags@1.5.2: - resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} - engines: {node: '>= 0.4'} + regexp.prototype.flags@1.5.4: dependencies: - call-bind: 1.0.7 + 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 - dev: true - /regexpu-core@5.3.2: - resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} - engines: {node: '>=4'} - dependencies: - '@babel/regjsgen': 0.8.0 - regenerate: 1.4.2 - regenerate-unicode-properties: 10.1.1 - regjsparser: 0.9.1 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.1.0 - dev: true - - /regjsparser@0.9.1: - resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} - hasBin: true - dependencies: - jsesc: 0.5.0 - dev: true - - /rehype-external-links@3.0.0: - resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==} - dependencies: - '@types/hast': 3.0.4 - '@ungap/structured-clone': 1.2.0 - hast-util-is-element: 3.0.0 - is-absolute-url: 4.0.1 - space-separated-tokens: 2.0.2 - unist-util-visit: 5.0.0 - dev: true - - /rehype-slug@6.0.0: - resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} - dependencies: - '@types/hast': 3.0.4 - github-slugger: 2.0.0 - hast-util-heading-rank: 3.0.0 - hast-util-to-string: 3.0.0 - unist-util-visit: 5.0.0 - dev: true - - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - dev: true + require-directory@2.1.1: {} - /requireindex@1.1.0: - resolution: {integrity: sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==} - engines: {node: '>=0.10.5'} - dev: true - - /requireindex@1.2.0: - resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} - engines: {node: '>=0.10.5'} - dev: true + require-from-string@2.0.2: {} - /reselect@5.1.0: - resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==} - dev: false + requireindex@1.1.0: {} - /resize-observer-polyfill@1.5.1: - resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} - dev: false + reselect@5.1.1: {} - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} + resize-observer-polyfill@1.5.1: {} - /resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - dev: true + resolve-from@4.0.0: {} - /resolve@1.19.0: - resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} + resolve@1.22.11: dependencies: - is-core-module: 2.13.1 + is-core-module: 2.16.1 path-parse: 1.0.7 - dev: true + supports-preserve-symlinks-flag: 1.0.0 - /resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true + resolve@1.22.12: dependencies: - is-core-module: 2.13.1 + es-errors: 1.3.0 + is-core-module: 2.16.2 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - /resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} - hasBin: true + resolve@2.0.0-next.5: dependencies: - is-core-module: 2.13.1 + is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true - /restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} + restore-cursor@3.1.0: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 - dev: true - - /ret@0.1.15: - resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} - engines: {node: '>=0.12'} - dev: false - - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true - /reverse-arguments@1.0.0: - resolution: {integrity: sha512-/x8uIPdTafBqakK0TmPNJzgkLP+3H+yxpUJhCQHsLBg1rYEVNR2D8BRYNWQhVBjyOd7oo1dZRVzIkwMY2oqfYQ==} - dev: true + ret@0.1.15: {} - /rfdc@1.3.1: - resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} - dev: false + reusify@1.1.0: {} - /rimraf@2.6.3: - resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: true - - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: true + rfdc@1.4.1: {} - /roarr@7.21.1: - resolution: {integrity: sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ==} - engines: {node: '>=18.0'} + roarr@7.21.2: dependencies: - fast-printf: 1.6.9 - safe-stable-stringify: 2.4.3 + fast-printf: 1.6.10 + safe-stable-stringify: 2.5.0 semver-compare: 1.0.0 - dev: false - /rollup-plugin-visualizer@5.12.0: - resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==} - engines: {node: '>=14'} - hasBin: true - peerDependencies: - rollup: 2.x || 3.x || 4.x - peerDependenciesMeta: - rollup: - optional: true + rolldown@1.0.0-rc.18: + dependencies: + '@oxc-project/types': 0.128.0 + '@rolldown/pluginutils': 1.0.0-rc.18 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.18 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.18 + '@rolldown/binding-darwin-x64': 1.0.0-rc.18 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.18 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.18 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.18 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.18 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.18 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.18 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18 + + rollup-plugin-visualizer@6.0.5(rolldown@1.0.0-rc.18)(rollup@4.54.0): dependencies: open: 8.4.2 - picomatch: 2.3.1 - source-map: 0.7.4 + picomatch: 4.0.3 + source-map: 0.7.6 yargs: 17.7.2 - dev: true + optionalDependencies: + rolldown: 1.0.0-rc.18 + rollup: 4.54.0 - /rollup@2.79.1: - resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} - engines: {node: '>=10.0.0'} - hasBin: true + rollup@2.79.2: optionalDependencies: fsevents: 2.3.3 - dev: true - /rollup@4.17.2: - resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true + rollup@4.54.0: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.17.2 - '@rollup/rollup-android-arm64': 4.17.2 - '@rollup/rollup-darwin-arm64': 4.17.2 - '@rollup/rollup-darwin-x64': 4.17.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.17.2 - '@rollup/rollup-linux-arm-musleabihf': 4.17.2 - '@rollup/rollup-linux-arm64-gnu': 4.17.2 - '@rollup/rollup-linux-arm64-musl': 4.17.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.17.2 - '@rollup/rollup-linux-riscv64-gnu': 4.17.2 - '@rollup/rollup-linux-s390x-gnu': 4.17.2 - '@rollup/rollup-linux-x64-gnu': 4.17.2 - '@rollup/rollup-linux-x64-musl': 4.17.2 - '@rollup/rollup-win32-arm64-msvc': 4.17.2 - '@rollup/rollup-win32-ia32-msvc': 4.17.2 - '@rollup/rollup-win32-x64-msvc': 4.17.2 + '@rollup/rollup-android-arm-eabi': 4.54.0 + '@rollup/rollup-android-arm64': 4.54.0 + '@rollup/rollup-darwin-arm64': 4.54.0 + '@rollup/rollup-darwin-x64': 4.54.0 + '@rollup/rollup-freebsd-arm64': 4.54.0 + '@rollup/rollup-freebsd-x64': 4.54.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.54.0 + '@rollup/rollup-linux-arm-musleabihf': 4.54.0 + '@rollup/rollup-linux-arm64-gnu': 4.54.0 + '@rollup/rollup-linux-arm64-musl': 4.54.0 + '@rollup/rollup-linux-loong64-gnu': 4.54.0 + '@rollup/rollup-linux-ppc64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-musl': 4.54.0 + '@rollup/rollup-linux-s390x-gnu': 4.54.0 + '@rollup/rollup-linux-x64-gnu': 4.54.0 + '@rollup/rollup-linux-x64-musl': 4.54.0 + '@rollup/rollup-openharmony-arm64': 4.54.0 + '@rollup/rollup-win32-arm64-msvc': 4.54.0 + '@rollup/rollup-win32-ia32-msvc': 4.54.0 + '@rollup/rollup-win32-x64-gnu': 4.54.0 + '@rollup/rollup-win32-x64-msvc': 4.54.0 fsevents: 2.3.3 - dev: true + optional: true - /rtl-css-js@1.16.1: - resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} + rtl-css-js@1.16.1: dependencies: - '@babel/runtime': 7.24.5 - dev: false + '@babel/runtime': 7.28.4 - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + run-applescript@7.1.0: {} + + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - dev: true - /rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + rxjs@7.8.2: dependencies: - tslib: 2.6.2 - dev: true + tslib: 2.8.1 - /safe-array-concat@1.1.2: - resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} - engines: {node: '>=0.4'} + safe-array-concat@1.1.3: dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 isarray: 2.0.5 - dev: true - /safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - dev: true + safe-buffer@5.1.2: {} - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: true + safe-buffer@5.2.1: {} - /safe-regex-test@1.0.3: - resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} - engines: {node: '>= 0.4'} + safe-push-apply@1.0.0: dependencies: - call-bind: 1.0.7 es-errors: 1.3.0 - is-regex: 1.1.4 - dev: true - - /safe-stable-stringify@2.4.3: - resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} - engines: {node: '>=10'} - dev: false - - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true + isarray: 2.0.5 - /scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + safe-regex-test@1.1.0: dependencies: - loose-envify: 1.4.0 - - /screenfull@5.2.0: - resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} - engines: {node: '>=0.10.0'} - dev: false + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 - /semver-compare@1.0.0: - resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} - dev: false + safe-stable-stringify@2.5.0: {} - /semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - dev: true + scheduler@0.27.0: {} - /semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - dev: true + screenfull@5.2.0: {} - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true + semver-compare@1.0.0: {} - /semver@7.6.0: - resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true + semver@6.3.1: {} - /send@0.18.0: - resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} - engines: {node: '>= 0.8.0'} - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - dev: true + semver@7.7.3: {} - /serialize-error@11.0.3: - resolution: {integrity: sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==} - engines: {node: '>=14.16'} - dependencies: - type-fest: 2.19.0 - dev: false + semver@7.8.0: {} - /serve-static@1.15.0: - resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} - engines: {node: '>= 0.8.0'} + serialize-error@12.0.0: dependencies: - encodeurl: 1.0.2 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.18.0 - transitivePeerDependencies: - - supports-color - dev: true + type-fest: 4.41.0 - /set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} + 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.2.4 - gopd: 1.0.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 has-property-descriptors: 1.0.2 - dev: true - /set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} + 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 - dev: true - - /set-harmonic-interval@1.0.1: - resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} - engines: {node: '>=6.9'} - dev: false - /setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - dev: true + set-harmonic-interval@1.0.1: {} - /shallow-clone@3.0.1: - resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} - engines: {node: '>=8'} + set-proto@1.0.0: dependencies: - kind-of: 6.0.3 - dev: true + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + setimmediate@1.0.5: {} + + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 - dev: true - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: true + shebang-regex@3.0.0: {} - /shell-quote-word@1.0.1: - resolution: {integrity: sha512-lT297f1WLAdq0A4O+AknIFRP6kkiI3s8C913eJ0XqBxJbZPGWUNkRQk2u8zk4bEAjUJ5i+fSLwB6z1HzeT+DEg==} - dev: true + shell-quote@1.8.3: {} - /shell-quote@1.8.1: - resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} - dev: true + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 - /side-channel@1.0.6: - resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} - engines: {node: '>= 0.4'} + side-channel-map@1.0.1: dependencies: - call-bind: 1.0.7 + call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.2.4 - object-inspect: 1.13.1 - dev: true + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 - /siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - dev: true + 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 - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true + 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 - /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - dev: true + siginfo@2.0.0: {} - /sirv@2.0.4: - resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} - engines: {node: '>= 10'} - dependencies: - '@polka/url': 1.0.0-next.25 - mrmime: 2.0.0 - totalist: 3.0.1 - dev: true + signal-exit@3.0.7: {} - /sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - dev: true + signal-exit@4.1.0: {} - /slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - dev: true + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 - /smol-toml@1.1.4: - resolution: {integrity: sha512-Y0OT8HezWsTNeEOSVxDnKOW/AyNXHQ4BwJNbAXlLTF5wWsBvrcHhIkE5Rf8kQMLmgf7nDX3PVOlgC6/Aiggu3Q==} - engines: {node: '>= 18', pnpm: '>= 8'} - dev: true + smol-toml@1.6.0: {} - /socket.io-client@4.7.5: - resolution: {integrity: sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==} - engines: {node: '>=10.0.0'} + socket.io-client@4.8.3: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.4 - engine.io-client: 6.5.3 - socket.io-parser: 4.2.4 + debug: 4.4.3(supports-color@10.2.2) + engine.io-client: 6.6.4 + socket.io-parser: 4.2.5 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - dev: false - /socket.io-parser@4.2.4: - resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} - engines: {node: '>=10.0.0'} + socket.io-parser@4.2.5: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.4 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color - dev: false - - /source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - dev: true - - /source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - dev: true - /source-map@0.5.6: - resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} - engines: {node: '>=0.10.0'} - dev: false - - /source-map@0.5.7: - resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} - engines: {node: '>=0.10.0'} - dev: false - - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} + source-map-js@1.2.1: {} - /source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} - dev: true - - /space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - dev: true - - /spawn-command@0.0.2: - resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} - dev: true - - /spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.17 - dev: true + source-map@0.5.6: {} - /spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - dev: true + source-map@0.5.7: {} - /spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.17 - dev: true + source-map@0.6.1: {} - /spdx-license-ids@3.0.17: - resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} - dev: true + source-map@0.7.6: {} - /split-on-first@3.0.0: - resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} - engines: {node: '>=12'} - dev: false + split-on-first@3.0.0: {} - /sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: true + stable-hash@0.0.6: {} - /stack-generator@2.0.10: - resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + stack-generator@2.0.10: dependencies: stackframe: 1.3.4 - dev: false - /stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - dev: true + stackback@0.0.2: {} - /stackframe@1.3.4: - resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} - dev: false + stackframe@1.3.4: {} - /stacktrace-gps@3.1.2: - resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==} + stacktrace-gps@3.1.2: dependencies: source-map: 0.5.6 stackframe: 1.3.4 - dev: false - /stacktrace-js@2.0.2: - resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + stacktrace-js@2.0.2: dependencies: error-stack-parser: 2.1.4 stack-generator: 2.0.10 stacktrace-gps: 3.1.2 - dev: false - - /statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - dev: true - /std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} - dev: true + std-env@4.1.0: {} - /stop-iteration-iterator@1.0.0: - resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} - engines: {node: '>= 0.4'} + stop-iteration-iterator@1.1.0: dependencies: - internal-slot: 1.0.7 - dev: true - - /store2@2.14.3: - resolution: {integrity: sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==} - dev: true + es-errors: 1.3.0 + internal-slot: 1.1.0 - /storybook@8.0.10(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-9/4oxISopLyr5xz7Du27mmQgcIfB7UTLlNzkK4IklWTiSgsOgYgZpsmIwymoXNtkrvh+QsqskdcUP1C7nNiEtw==} - hasBin: true + storybook@10.3.6(@testing-library/dom@10.4.0)(prettier@3.8.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@storybook/cli': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/global': 5.0.0 + '@storybook/icons': 2.0.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + '@webcontainer/env': 1.1.1 + esbuild: 0.27.7 + open: 10.2.0 + recast: 0.23.11 + semver: 7.8.0 + use-sync-external-store: 1.6.0(react@19.2.6) + ws: 8.20.0 + optionalDependencies: + prettier: 3.8.3 transitivePeerDependencies: - - '@babel/preset-env' + - '@testing-library/dom' - bufferutil - - encoding - react - react-dom - - supports-color - utf-8-validate - dev: true - - /stream-shift@1.0.3: - resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} - dev: true - - /string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - dev: true - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true - /string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} + string-width@5.1.2: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - dev: true + strip-ansi: 7.1.2 - /string.fromcodepoint@0.2.1: - resolution: {integrity: sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg==} - dev: true - - /string.prototype.matchall@4.0.11: - resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} - engines: {node: '>= 0.4'} + string.prototype.matchall@4.0.12: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.3 + es-abstract: 1.24.1 es-errors: 1.3.0 - es-object-atoms: 1.0.0 - get-intrinsic: 1.2.4 - gopd: 1.0.1 - has-symbols: 1.0.3 - internal-slot: 1.0.7 - regexp.prototype.flags: 1.5.2 + 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.0.6 - dev: true + side-channel: 1.1.0 - /string.prototype.trim@1.2.9: - resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} - engines: {node: '>= 0.4'} + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.23.3 - es-object-atoms: 1.0.0 - dev: true + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 - /string.prototype.trimend@1.0.8: - resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + string.prototype.trimend@1.0.9: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.0.0 - dev: true + es-object-atoms: 1.1.1 - /string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} + string.prototype.trimstart@1.0.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-object-atoms: 1.0.0 - dev: true + es-object-atoms: 1.1.1 - /string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 - dev: true - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 - dev: true - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - dev: true - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} + strip-ansi@7.1.2: dependencies: - ansi-regex: 6.0.1 - dev: true - - /strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - dev: true - - /strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - dev: true + ansi-regex: 6.2.2 - /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: true - - /strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - dependencies: - min-indent: 1.0.1 - dev: true + strip-bom@3.0.0: {} - /strip-indent@4.0.0: - resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} - engines: {node: '>=12'} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 - dev: true - - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true - - /strip-json-comments@5.0.1: - resolution: {integrity: sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==} - engines: {node: '>=14.16'} - dev: true - - /strip-literal@2.1.0: - resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} - dependencies: - js-tokens: 9.0.0 - dev: true - - /stylis@4.2.0: - resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} - dev: false - - /stylis@4.3.2: - resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==} - dev: false - - /summary@2.1.0: - resolution: {integrity: sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==} - dev: true - - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - dependencies: - has-flag: 3.0.0 - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: true - - /supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - dependencies: - has-flag: 4.0.0 - dev: true - - /supports-color@9.4.0: - resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} - engines: {node: '>=12'} - dev: true - - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - /tabbable@6.2.0: - resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - dev: false - - /tar-fs@2.1.1: - resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.0 - tar-stream: 2.2.0 - dev: true - - /tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.4 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: true - - /tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - dev: true - - /telejson@7.2.0: - resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==} - dependencies: - memoizerific: 1.11.3 - dev: true - - /temp-dir@2.0.0: - resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} - engines: {node: '>=8'} - dev: true - - /temp@0.8.4: - resolution: {integrity: sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==} - engines: {node: '>=6.0.0'} - dependencies: - rimraf: 2.6.3 - dev: true - - /tempy@1.0.1: - resolution: {integrity: sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==} - engines: {node: '>=10'} - dependencies: - del: 6.1.1 - is-stream: 2.0.1 - temp-dir: 2.0.0 - type-fest: 0.16.0 - unique-string: 2.0.0 - dev: true - - /test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - dev: true - - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true - - /throttle-debounce@3.0.1: - resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} - engines: {node: '>=10'} - dev: false - - /through2@2.0.5: - resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} - dependencies: - readable-stream: 2.3.8 - xtend: 4.0.2 - dev: true - - /tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - - /tinybench@2.8.0: - resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} - dev: true - - /tinypool@0.8.4: - resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} - engines: {node: '>=14.0.0'} - dev: true - - /tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} - engines: {node: '>=14.0.0'} - dev: true - - /to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - - /to-no-case@1.0.2: - resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} - dev: true - - /to-pascal-case@1.0.0: - resolution: {integrity: sha512-QGMWHqM6xPrcQW57S23c5/3BbYb0Tbe9p+ur98ckRnGDwD4wbbtDiYI38CfmMKNB5Iv0REjs5SNDntTwvDxzZA==} - dependencies: - to-space-case: 1.0.0 - dev: true - - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: true - - /to-space-case@1.0.0: - resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} - dependencies: - to-no-case: 1.0.2 - dev: true - /tocbot@4.27.19: - resolution: {integrity: sha512-0yu8k0L3gCQ1OVNZnKqpbZp+kLd6qtlNEBxsb+e0G/bS0EXMl2tWqWi1Oy9knRX8rTPYfOxd/sI/OzAj3JowGg==} - dev: true + strip-indent@4.1.1: {} - /toggle-selection@1.0.6: - resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} - dev: false - - /toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - dev: true - - /totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} - engines: {node: '>=6'} - dev: true - - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - /tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - dev: true - - /ts-api-utils@1.3.0(typescript@5.4.5): - resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - dependencies: - typescript: 5.4.5 - dev: true - - /ts-dedent@2.2.0: - resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} - engines: {node: '>=6.10'} - dev: true - - /ts-easing@0.2.0: - resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} - dev: false - - /ts-error@1.0.6: - resolution: {integrity: sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==} - dev: false - - /ts-toolbelt@9.6.0: - resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} - dev: true - - /tsafe@1.6.6: - resolution: {integrity: sha512-gzkapsdbMNwBnTIjgO758GujLCj031IgHK/PKr2mrmkCSJMhSOR5FeOuSxKLMUoYc0vAA4RGEYYbjt/v6afD3g==} - dev: true - - /tsconfck@3.0.3(typescript@5.4.5): - resolution: {integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - dependencies: - typescript: 5.4.5 - dev: true - - /tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - dev: true - - /tsconfig-paths@4.2.0: - resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} - engines: {node: '>=6'} - dependencies: - json5: 2.2.3 - minimist: 1.2.8 - strip-bom: 3.0.0 - dev: true - - /tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true - - /tslib@2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - dev: false + strip-json-comments@3.1.1: {} - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + strip-json-comments@5.0.3: {} - /tsutils@3.21.0(typescript@5.4.5): - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - dependencies: - tslib: 1.14.1 - typescript: 5.4.5 - dev: true - - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - dev: true - - /type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - dev: true - - /type-fest@0.16.0: - resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} - engines: {node: '>=10'} - dev: true - - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true - - /type-fest@0.6.0: - resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} - engines: {node: '>=8'} - dev: true + stylis@4.2.0: {} - /type-fest@0.8.1: - resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} - engines: {node: '>=8'} - dev: true + stylis@4.3.6: {} - /type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} + supports-color@10.2.2: {} - /type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} + supports-color@7.2.0: dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - dev: true + has-flag: 4.0.0 - /typed-array-buffer@1.0.2: - resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} - engines: {node: '>= 0.4'} + supports-color@8.1.1: dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-typed-array: 1.1.13 - dev: true + has-flag: 4.0.0 - /typed-array-byte-length@1.0.1: - resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.0.1 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - dev: true + supports-preserve-symlinks-flag@1.0.0: {} - /typed-array-byte-offset@1.0.2: - resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.0.1 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - dev: true - - /typed-array-length@1.0.6: - resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.0.1 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - possible-typed-array-names: 1.0.0 - dev: true + throttle-debounce@3.0.1: {} - /typescript@5.4.2: - resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} - engines: {node: '>=14.17'} - hasBin: true - dev: true + tiny-invariant@1.3.3: {} - /typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} - engines: {node: '>=14.17'} - hasBin: true - dev: true + tinybench@2.9.0: {} - /ufo@1.5.3: - resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} - dev: true + tinyexec@1.1.2: {} - /uglify-js@3.17.4: - resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} - engines: {node: '>=0.8.0'} - hasBin: true - requiresBuild: true - dev: true - optional: true + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 - /unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + tinyglobby@0.2.16: dependencies: - call-bind: 1.0.7 - has-bigints: 1.0.2 - has-symbols: 1.0.3 - which-boxed-primitive: 1.0.2 - dev: true + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true + tinyrainbow@2.0.0: {} - /undici@5.28.4: - resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} - engines: {node: '>=14.0'} - dependencies: - '@fastify/busboy': 2.1.1 - dev: true + tinyrainbow@3.1.0: {} - /unescape-js@1.1.4: - resolution: {integrity: sha512-42SD8NOQEhdYntEiUQdYq/1V/YHwr1HLwlHuTJB5InVVdOSbgI6xu8jK5q65yIzuFCfczzyDF/7hbGzVbyCw0g==} + tinyspy@4.0.4: {} + + to-regex-range@5.0.1: dependencies: - string.fromcodepoint: 0.2.1 - dev: true + is-number: 7.0.0 - /unicode-canonical-property-names-ecmascript@2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} - engines: {node: '>=4'} - dev: true + toggle-selection@1.0.6: {} - /unicode-match-property-ecmascript@2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} - dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 - unicode-property-aliases-ecmascript: 2.1.0 - dev: true + totalist@3.0.1: {} - /unicode-match-property-value-ecmascript@2.1.0: - resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} - engines: {node: '>=4'} - dev: true + tr46@0.0.3: {} - /unicode-property-aliases-ecmascript@2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} - engines: {node: '>=4'} - dev: true + tree-kill@1.2.2: {} - /unique-string@2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} - engines: {node: '>=8'} + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: - crypto-random-string: 2.0.0 - dev: true + typescript: 5.9.3 - /unist-util-is@6.0.0: - resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: - '@types/unist': 3.0.2 - dev: true + typescript: 5.9.3 - /unist-util-visit-parents@6.0.1: - resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} - dependencies: - '@types/unist': 3.0.2 - unist-util-is: 6.0.0 - dev: true + ts-dedent@2.2.0: {} - /unist-util-visit@5.0.0: - resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - dependencies: - '@types/unist': 3.0.2 - unist-util-is: 6.0.0 - unist-util-visit-parents: 6.0.1 - dev: true + ts-easing@0.2.0: {} - /universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - dev: true + ts-error@1.0.6: {} - /universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - dev: true + tsafe@1.8.12: {} - /unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - dev: true + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 - /unplugin@1.10.1: - resolution: {integrity: sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg==} - engines: {node: '>=14.0.0'} + tsconfig-paths@4.2.0: dependencies: - acorn: 8.11.3 - chokidar: 3.6.0 - webpack-sources: 3.2.3 - webpack-virtual-modules: 0.6.1 - dev: true + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 - /untildify@4.0.0: - resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} - engines: {node: '>=8'} - dev: true + tslib@2.4.0: {} - /update-browserslist-db@1.0.15(browserslist@4.23.0): - resolution: {integrity: sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - dependencies: - browserslist: 4.23.0 - escalade: 3.1.2 - picocolors: 1.0.0 - dev: true + tslib@2.8.1: {} - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + type-check@0.4.0: dependencies: - punycode: 2.3.1 - dev: true + prelude-ls: 1.2.1 - /use-callback-ref@1.3.1(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.1 - react: 18.3.1 - tslib: 2.6.2 - dev: false + type-fest@4.41.0: {} - /use-debounce@10.0.0(react@18.3.1): - resolution: {integrity: sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==} - engines: {node: '>= 16.0.0'} - peerDependencies: - react: '>=16.8.0' + typed-array-buffer@1.0.3: dependencies: - react: 18.3.1 - dev: false + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 - /use-device-pixel-ratio@1.1.2(react@18.3.1): - resolution: {integrity: sha512-nFxV0HwLdRUt20kvIgqHYZe6PK/v4mU1X8/eLsT1ti5ck0l2ob0HDRziaJPx+YWzBo6dMm4cTac3mcyk68Gh+A==} - peerDependencies: - react: '>=16.8.0' + typed-array-byte-length@1.0.3: dependencies: - react: 18.3.1 - dev: false + 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 - /use-image@1.1.1(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-n4YO2k8AJG/BcDtxmBx8Aa+47kxY5m335dJiCQA5tTeVU4XdhrhqR6wT0WISRXwdMEOv5CSjqekDZkEMiiWaYQ==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' + typed-array-byte-offset@1.0.4: dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false + 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 - /use-isomorphic-layout-effect@1.1.2(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true + typed-array-length@1.0.7: dependencies: - '@types/react': 18.3.1 - react: 18.3.1 - dev: false + 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 - /use-sidecar@1.1.2(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.3.1 - detect-node-es: 1.1.0 - react: 18.3.1 - tslib: 2.6.2 - dev: false + typescript@5.9.3: {} - /use-sync-external-store@1.2.0(react@18.3.1): - resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + unbox-primitive@1.1.0: dependencies: - react: 18.3.1 - dev: false + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 - /use-sync-external-store@1.2.2(react@18.3.1): - resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - react: 18.3.1 - dev: false + undici-types@6.21.0: {} - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true + universalify@2.0.1: {} - /util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + unplugin@2.3.11: dependencies: - inherits: 2.0.4 - is-arguments: 1.1.1 - is-generator-function: 1.0.10 - is-typed-array: 1.1.13 - which-typed-array: 1.1.15 - dev: true - - /utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - dev: true - - /uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true + '@jridgewell/remapping': 2.3.5 + acorn: 8.16.0 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 - /validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - dev: true + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 - /validator@13.11.0: - resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} - engines: {node: '>= 0.10'} - dev: true + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 - /vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - dev: true + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 - /vite-node@1.6.0(@types/node@20.12.10): - resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.6): dependencies: - cac: 6.7.14 - debug: 4.3.4 - pathe: 1.1.2 - picocolors: 1.0.0 - vite: 5.2.11(@types/node@20.12.10) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - dev: true + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 - /vite-plugin-css-injected-by-js@3.5.1(vite@5.2.11): - resolution: {integrity: sha512-9ioqwDuEBxW55gNoWFEDhfLTrVKXEEZgl5adhWmmqa88EQGKfTmexy4v1Rh0pAS6RhKQs2bUYQArprB32JpUZQ==} - peerDependencies: - vite: '>2.0.0-0' + use-composed-ref@1.4.0(@types/react@19.2.14)(react@19.2.6): dependencies: - vite: 5.2.11(@types/node@20.12.10) - dev: true + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /vite-plugin-dts@3.9.1(@types/node@20.12.10)(typescript@5.4.5)(vite@5.2.11): - resolution: {integrity: sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - typescript: '*' - vite: '*' - peerDependenciesMeta: - vite: - optional: true + use-debounce@10.0.6(react@19.2.6): dependencies: - '@microsoft/api-extractor': 7.43.0(@types/node@20.12.10) - '@rollup/pluginutils': 5.1.0 - '@vue/language-core': 1.8.27(typescript@5.4.5) - debug: 4.3.4 - kolorist: 1.8.0 - magic-string: 0.30.10 - typescript: 5.4.5 - vite: 5.2.11(@types/node@20.12.10) - vue-tsc: 1.8.27(typescript@5.4.5) - transitivePeerDependencies: - - '@types/node' - - rollup - - supports-color - dev: true + react: 19.2.6 - /vite-plugin-eslint@1.8.1(eslint@8.57.0)(vite@5.2.11): - resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} - peerDependencies: - eslint: '>=7' - vite: '>=2' + use-device-pixel-ratio@1.1.2(react@19.2.6): dependencies: - '@rollup/pluginutils': 4.2.1 - '@types/eslint': 8.56.10 - eslint: 8.57.0 - rollup: 2.79.1 - vite: 5.2.11(@types/node@20.12.10) - dev: true + react: 19.2.6 - /vite-tsconfig-paths@4.3.2(typescript@5.4.5)(vite@5.2.11): - resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} - peerDependencies: - vite: '*' - peerDependenciesMeta: - vite: - optional: true + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.6): dependencies: - debug: 4.3.4 - globrex: 0.1.2 - tsconfck: 3.0.3(typescript@5.4.5) - vite: 5.2.11(@types/node@20.12.10) - transitivePeerDependencies: - - supports-color - - typescript - dev: true + react: 19.2.6 + optionalDependencies: + '@types/react': 19.2.14 - /vite@5.2.11(@types/node@20.12.10): - resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true + use-latest@1.3.0(@types/react@19.2.14)(react@19.2.6): dependencies: - '@types/node': 20.12.10 - esbuild: 0.20.2 - postcss: 8.4.38 - rollup: 4.17.2 + react: 19.2.6 + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.6) optionalDependencies: - fsevents: 2.3.3 - dev: true + '@types/react': 19.2.14 - /vitest@1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0): - resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.6.0 - '@vitest/ui': 1.6.0 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.6): dependencies: - '@types/node': 20.12.10 - '@vitest/expect': 1.6.0 - '@vitest/runner': 1.6.0 - '@vitest/snapshot': 1.6.0 - '@vitest/spy': 1.6.0 - '@vitest/ui': 1.6.0(vitest@1.6.0) - '@vitest/utils': 1.6.0 - acorn-walk: 8.3.2 - chai: 4.4.1 - debug: 4.3.4 - execa: 8.0.1 - local-pkg: 0.5.0 - magic-string: 0.30.10 - pathe: 1.1.2 - picocolors: 1.0.0 - std-env: 3.7.0 - strip-literal: 2.1.0 - tinybench: 2.8.0 - tinypool: 0.8.4 - vite: 5.2.11(@types/node@20.12.10) - vite-node: 1.6.0(@types/node@20.12.10) - why-is-node-running: 2.2.2 - transitivePeerDependencies: - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - dev: true + detect-node-es: 1.1.0 + react: 19.2.6 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 - /vlq@0.2.3: - resolution: {integrity: sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==} - dev: true + use-sync-external-store@1.6.0(react@19.2.6): + dependencies: + react: 19.2.6 - /void-elements@3.1.0: - resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} - engines: {node: '>=0.10.0'} - dev: false + util-deprecate@1.0.2: {} - /vue-template-compiler@2.7.16: - resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} - dependencies: - de-indent: 1.0.2 - he: 1.2.0 - dev: true + uuid@11.1.0: {} - /vue-tsc@1.8.27(typescript@5.4.5): - resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} - hasBin: true - peerDependencies: - typescript: '*' + vite-plugin-babel@1.6.0(@babel/core@7.28.5)(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)): dependencies: - '@volar/typescript': 1.11.1 - '@vue/language-core': 1.8.27(typescript@5.4.5) - semver: 7.6.0 - typescript: 5.4.5 - dev: true + '@babel/core': 7.28.5 + vite: 8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1) - /watchpack@2.4.1: - resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==} - engines: {node: '>=10.13.0'} + vite-plugin-eslint@1.8.1(eslint@9.39.2(jiti@2.6.1))(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)): dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - dev: true + '@rollup/pluginutils': 4.2.1 + '@types/eslint': 8.56.12 + eslint: 9.39.2(jiti@2.6.1) + rollup: 2.79.2 + vite: 8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1) + + vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.14 + rolldown: 1.0.0-rc.18 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 22.19.3 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 2.6.1 + + vitest@4.1.5(@types/node@22.19.3)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)): + dependencies: + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.11(@types/node@22.19.3)(esbuild@0.27.7)(jiti@2.6.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.3 + '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) + '@vitest/ui': 4.1.5(vitest@4.1.5) + transitivePeerDependencies: + - msw - /wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + void-elements@3.1.0: {} + + walk-up-path@4.0.0: {} + + wcwidth@1.0.1: dependencies: defaults: 1.0.4 - dev: true - - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - /webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} - engines: {node: '>=10.13.0'} - dev: true + webidl-conversions@3.0.1: {} - /webpack-virtual-modules@0.6.1: - resolution: {integrity: sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==} - dev: true + webpack-virtual-modules@0.6.2: {} - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - /which-boxed-primitive@1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + which-boxed-primitive@1.1.1: dependencies: - is-bigint: 1.0.4 - is-boolean-object: 1.1.2 - is-number-object: 1.0.7 - is-string: 1.0.7 - is-symbol: 1.0.4 - dev: true + 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.1.3: - resolution: {integrity: sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==} - engines: {node: '>= 0.4'} + which-builtin-type@1.2.1: dependencies: - function.prototype.name: 1.1.6 + call-bound: 1.0.4 + function.prototype.name: 1.1.8 has-tostringtag: 1.0.2 - is-async-function: 2.0.0 - is-date-object: 1.0.5 - is-finalizationregistry: 1.0.2 - is-generator-function: 1.0.10 - is-regex: 1.1.4 - is-weakref: 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.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 isarray: 2.0.5 - which-boxed-primitive: 1.0.2 + which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.15 - dev: true + which-typed-array: 1.1.19 - /which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} + 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.3 - dev: true + is-weakset: 2.0.4 - /which-typed-array@1.1.15: - resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} - engines: {node: '>= 0.4'} + which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.0.1 + 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 - dev: true - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true + which@2.0.2: dependencies: isexe: 2.0.0 - dev: true - /why-is-node-running@2.2.2: - resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} - engines: {node: '>=8'} - hasBin: true + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 - dev: true - - /word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - dev: true - /wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - dev: true + word-wrap@1.2.5: {} - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true - /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} + wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 - dev: true - - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true - - /write-file-atomic@2.4.3: - resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} - dependencies: - graceful-fs: 4.2.11 - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - dev: true + strip-ansi: 7.1.2 - /ws@8.11.0: - resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} - 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 - dev: false + ws@8.18.3: {} - /ws@8.17.0: - resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} - 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 - dev: true + ws@8.20.0: {} - /xmlhttprequest-ssl@2.0.0: - resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} - engines: {node: '>=0.4.0'} - dev: false + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 - /xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - dev: true + xmlhttprequest-ssl@2.1.2: {} - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: true + y18n@5.0.8: {} - /yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true + yallist@3.1.1: {} - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true + yaml-ast-parser@0.0.43: {} - /yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - dev: false + yaml@1.10.2: {} - /yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - dev: true + yargs-parser@21.1.1: {} - /yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + yargs@17.7.2: dependencies: cliui: 8.0.1 - escalade: 3.1.2 + 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 - dev: true - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true - /yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} - dev: true - - /z-schema@5.0.5: - resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} - engines: {node: '>=8.0.0'} - hasBin: true - dependencies: - lodash.get: 4.4.2 - lodash.isequal: 4.5.0 - validator: 13.11.0 - optionalDependencies: - commander: 9.5.0 - dev: true + yocto-queue@0.1.0: {} - /zod-validation-error@3.2.0(zod@3.23.6): - resolution: {integrity: sha512-cYlPR6zuyrgmu2wRTdumEAJGuwI7eHVHGT+VyneAQxmRAKtGRL1/7pjz4wfLhz4J05f5qoSZc3rGacswgyTjjw==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.18.0 + zod-validation-error@4.0.2(zod@4.2.1): dependencies: - zod: 3.23.6 + zod: 4.2.1 - /zod@3.23.6: - resolution: {integrity: sha512-RTHJlZhsRbuA8Hmp/iNL7jnfc4nZishjsanDAfEY1QpDQZCahUp3xDzl+zfweE9BklxMUcgBgS1b7Lvie/ZVwA==} + zod@4.2.1: {} - /zustand@4.5.2(@types/react@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} - 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 + zustand@4.5.7(@types/react@19.2.14)(immer@10.2.0)(react@19.2.6): dependencies: - '@types/react': 18.3.1 - react: 18.3.1 - use-sync-external-store: 1.2.0(react@18.3.1) - dev: false + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + immer: 10.2.0 + react: 19.2.6 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/locales/ar.json b/invokeai/frontend/web/public/locales/ar.json index ee370d1e421..0a03deb6472 100644 --- a/invokeai/frontend/web/public/locales/ar.json +++ b/invokeai/frontend/web/public/locales/ar.json @@ -5,7 +5,6 @@ "reportBugLabel": "بلغ عن خطأ", "settingsLabel": "إعدادات", "img2img": "صورة إلى صورة", - "unifiedCanvas": "لوحة موحدة", "nodes": "عقد", "upload": "رفع", "load": "تحميل", @@ -15,208 +14,7 @@ "gallery": { "galleryImageSize": "حجم الصورة", "gallerySettings": "إعدادات المعرض", - "autoSwitchNewImages": "التبديل التلقائي إلى الصور الجديدة", - "loadMore": "تحميل المزيد", - "noImagesInGallery": "لا توجد صور في المعرض" - }, - "hotkeys": { - "keyboardShortcuts": "مفاتيح الأزرار المختصرة", - "appHotkeys": "مفاتيح التطبيق", - "generalHotkeys": "مفاتيح عامة", - "galleryHotkeys": "مفاتيح المعرض", - "unifiedCanvasHotkeys": "مفاتيح اللوحةالموحدة ", - "invoke": { - "title": "أدعو", - "desc": "إنشاء صورة" - }, - "cancel": { - "title": "إلغاء", - "desc": "إلغاء إنشاء الصورة" - }, - "focusPrompt": { - "title": "تركيز الإشعار", - "desc": "تركيز منطقة الإدخال الإشعار" - }, - "toggleOptions": { - "title": "تبديل الخيارات", - "desc": "فتح وإغلاق لوحة الخيارات" - }, - "pinOptions": { - "title": "خيارات التثبيت", - "desc": "ثبت لوحة الخيارات" - }, - "toggleGallery": { - "title": "تبديل المعرض", - "desc": "فتح وإغلاق درابزين المعرض" - }, - "maximizeWorkSpace": { - "title": "تكبير مساحة العمل", - "desc": "إغلاق اللوحات وتكبير مساحة العمل" - }, - "changeTabs": { - "title": "تغيير الألسنة", - "desc": "التبديل إلى مساحة عمل أخرى" - }, - "consoleToggle": { - "title": "تبديل الطرفية", - "desc": "فتح وإغلاق الطرفية" - }, - "setPrompt": { - "title": "ضبط التشعب", - "desc": "استخدم تشعب الصورة الحالية" - }, - "setSeed": { - "title": "ضبط البذور", - "desc": "استخدم بذور الصورة الحالية" - }, - "setParameters": { - "title": "ضبط المعلمات", - "desc": "استخدم جميع المعلمات الخاصة بالصورة الحالية" - }, - "restoreFaces": { - "title": "استعادة الوجوه", - "desc": "استعادة الصورة الحالية" - }, - "upscale": { - "title": "تحسين الحجم", - "desc": "تحسين حجم الصورة الحالية" - }, - "showInfo": { - "title": "عرض المعلومات", - "desc": "عرض معلومات البيانات الخاصة بالصورة الحالية" - }, - "sendToImageToImage": { - "title": "أرسل إلى صورة إلى صورة", - "desc": "أرسل الصورة الحالية إلى صورة إلى صورة" - }, - "deleteImage": { - "title": "حذف الصورة", - "desc": "حذف الصورة الحالية" - }, - "closePanels": { - "title": "أغلق اللوحات", - "desc": "يغلق اللوحات المفتوحة" - }, - "previousImage": { - "title": "الصورة السابقة", - "desc": "عرض الصورة السابقة في الصالة" - }, - "nextImage": { - "title": "الصورة التالية", - "desc": "عرض الصورة التالية في الصالة" - }, - "increaseGalleryThumbSize": { - "title": "زيادة حجم صورة الصالة", - "desc": "يزيد حجم الصور المصغرة في الصالة" - }, - "decreaseGalleryThumbSize": { - "title": "انقاص حجم صورة الصالة", - "desc": "ينقص حجم الصور المصغرة في الصالة" - }, - "selectBrush": { - "title": "تحديد الفرشاة", - "desc": "يحدد الفرشاة على اللوحة" - }, - "selectEraser": { - "title": "تحديد الممحاة", - "desc": "يحدد الممحاة على اللوحة" - }, - "decreaseBrushSize": { - "title": "تصغير حجم الفرشاة", - "desc": "يصغر حجم الفرشاة/الممحاة على اللوحة" - }, - "increaseBrushSize": { - "title": "زيادة حجم الفرشاة", - "desc": "يزيد حجم فرشة اللوحة / الممحاة" - }, - "decreaseBrushOpacity": { - "title": "تخفيض شفافية الفرشاة", - "desc": "يخفض شفافية فرشة اللوحة" - }, - "increaseBrushOpacity": { - "title": "زيادة شفافية الفرشاة", - "desc": "يزيد شفافية فرشة اللوحة" - }, - "moveTool": { - "title": "أداة التحريك", - "desc": "يتيح التحرك في اللوحة" - }, - "fillBoundingBox": { - "title": "ملء الصندوق المحدد", - "desc": "يملأ الصندوق المحدد بلون الفرشاة" - }, - "eraseBoundingBox": { - "title": "محو الصندوق المحدد", - "desc": "يمحو منطقة الصندوق المحدد" - }, - "colorPicker": { - "title": "اختيار منتقي اللون", - "desc": "يختار منتقي اللون الخاص باللوحة" - }, - "toggleSnap": { - "title": "تبديل التأكيد", - "desc": "يبديل تأكيد الشبكة" - }, - "quickToggleMove": { - "title": "تبديل سريع للتحريك", - "desc": "يبديل مؤقتا وضع التحريك" - }, - "toggleLayer": { - "title": "تبديل الطبقة", - "desc": "يبديل إختيار الطبقة القناع / الأساسية" - }, - "clearMask": { - "title": "مسح القناع", - "desc": "مسح القناع بأكمله" - }, - "hideMask": { - "title": "إخفاء الكمامة", - "desc": "إخفاء وإظهار الكمامة" - }, - "showHideBoundingBox": { - "title": "إظهار / إخفاء علبة التحديد", - "desc": "تبديل ظهور علبة التحديد" - }, - "mergeVisible": { - "title": "دمج الطبقات الظاهرة", - "desc": "دمج جميع الطبقات الظاهرة في اللوحة" - }, - "saveToGallery": { - "title": "حفظ إلى صالة الأزياء", - "desc": "حفظ اللوحة الحالية إلى صالة الأزياء" - }, - "copyToClipboard": { - "title": "نسخ إلى الحافظة", - "desc": "نسخ اللوحة الحالية إلى الحافظة" - }, - "downloadImage": { - "title": "تنزيل الصورة", - "desc": "تنزيل اللوحة الحالية" - }, - "undoStroke": { - "title": "تراجع عن الخط", - "desc": "تراجع عن خط الفرشاة" - }, - "redoStroke": { - "title": "إعادة الخط", - "desc": "إعادة خط الفرشاة" - }, - "resetView": { - "title": "إعادة تعيين العرض", - "desc": "إعادة تعيين عرض اللوحة" - }, - "previousStagingImage": { - "title": "الصورة السابقة في المرحلة التجريبية", - "desc": "الصورة السابقة في منطقة المرحلة التجريبية" - }, - "nextStagingImage": { - "title": "الصورة التالية في المرحلة التجريبية", - "desc": "الصورة التالية في منطقة المرحلة التجريبية" - }, - "acceptStagingImage": { - "title": "قبول الصورة في المرحلة التجريبية", - "desc": "قبول الصورة الحالية في منطقة المرحلة التجريبية" - } + "autoSwitchNewImages": "التبديل التلقائي إلى الصور الجديدة" }, "modelManager": { "modelManager": "مدير النموذج", @@ -255,8 +53,6 @@ "type": "نوع", "strength": "قوة", "upscaling": "تصغير", - "upscale": "تصغير", - "upscaleImage": "تصغير الصورة", "scale": "مقياس", "imageFit": "ملائمة الصورة الأولية لحجم الخرج", "scaleBeforeProcessing": "تحجيم قبل المعالجة", @@ -264,21 +60,16 @@ "scaledHeight": "الارتفاع المحجوب", "infillMethod": "طريقة التعبئة", "tileSize": "حجم البلاطة", - "sendToImg2Img": "أرسل إلى صورة إلى صورة", - "sendToUnifiedCanvas": "أرسل إلى الخطوط الموحدة", "copyImage": "نسخ الصورة", - "downloadImage": "تحميل الصورة", "usePrompt": "استخدم المحث", "useSeed": "استخدام البذور", "useAll": "استخدام الكل", - "info": "معلومات", - "showOptionsPanel": "إظهار لوحة الخيارات" + "info": "معلومات" }, "settings": { "models": "موديلات", "displayInProgress": "عرض الصور المؤرشفة", "confirmOnDelete": "تأكيد عند الحذف", - "enableImageDebugging": "تمكين التصحيح عند التصوير", "resetWebUI": "إعادة تعيين واجهة الويب", "resetWebUIDesc1": "إعادة تعيين واجهة الويب يعيد فقط ذاكرة التخزين المؤقت للمتصفح لصورك وإعداداتك المذكورة. لا يحذف أي صور من القرص.", "resetWebUIDesc2": "إذا لم تظهر الصور في الصالة أو إذا كان شيء آخر غير ناجح، يرجى المحاولة إعادة تعيين قبل تقديم مشكلة على جيت هب.", @@ -287,71 +78,6 @@ "toast": { "uploadFailed": "فشل التحميل", "imageCopied": "تم نسخ الصورة", - "imageNotLoadedDesc": "لم يتم العثور على صورة لإرسالها إلى وحدة الصورة", - "canvasMerged": "تم دمج الخط", - "sentToImageToImage": "تم إرسال إلى صورة إلى صورة", - "sentToUnifiedCanvas": "تم إرسال إلى لوحة موحدة", - "parametersNotSet": "لم يتم تعيين المعلمات", - "metadataLoadFailed": "فشل تحميل البيانات الوصفية" - }, - "tooltip": { - "feature": { - "prompt": "هذا هو حقل التحذير. يشمل التحذير عناصر الإنتاج والمصطلحات الأسلوبية. يمكنك إضافة الأوزان (أهمية الرمز) في التحذير أيضًا، ولكن أوامر CLI والمعلمات لن تعمل.", - "gallery": "تعرض Gallery منتجات من مجلد الإخراج عندما يتم إنشاؤها. تخزن الإعدادات داخل الملفات ويتم الوصول إليها عن طريق قائمة السياق.", - "other": "ستمكن هذه الخيارات من وضع عمليات معالجة بديلة لـاستحضر الذكاء الصناعي. سيؤدي 'الزخرفة بلا جدران' إلى إنشاء أنماط تكرارية في الإخراج. 'دقة عالية' هي الإنتاج خلال خطوتين عبر صورة إلى صورة: استخدم هذا الإعداد عندما ترغب في توليد صورة أكبر وأكثر تجانبًا دون العيوب. ستستغرق الأشياء وقتًا أطول من نص إلى صورة المعتاد.", - "seed": "يؤثر قيمة البذور على الضوضاء الأولي الذي يتم تكوين الصورة منه. يمكنك استخدام البذور الخاصة بالصور السابقة. 'عتبة الضوضاء' يتم استخدامها لتخفيف العناصر الخللية في قيم CFG العالية (جرب مدى 0-10), و Perlin لإضافة ضوضاء Perlin أثناء الإنتاج: كلا منهما يعملان على إضافة التنوع إلى النتائج الخاصة بك.", - "upscale": "استخدم إي إس آر جان لتكبير الصورة على الفور بعد الإنتاج.", - "boundingBox": "مربع الحدود هو نفس الإعدادات العرض والارتفاع لنص إلى صورة أو صورة إلى صورة. فقط المنطقة في المربع سيتم معالجتها." - } - }, - "unifiedCanvas": { - "layer": "طبقة", - "base": "قاعدة", - "mask": "قناع", - "maskingOptions": "خيارات القناع", - "enableMask": "مكن القناع", - "preserveMaskedArea": "الحفاظ على المنطقة المقنعة", - "clearMask": "مسح القناع", - "brush": "فرشاة", - "eraser": "ممحاة", - "fillBoundingBox": "ملئ إطار الحدود", - "eraseBoundingBox": "مسح إطار الحدود", - "colorPicker": "اختيار اللون", - "brushOptions": "خيارات الفرشاة", - "brushSize": "الحجم", - "move": "تحريك", - "resetView": "إعادة تعيين العرض", - "mergeVisible": "دمج الظاهر", - "saveToGallery": "حفظ إلى المعرض", - "copyToClipboard": "نسخ إلى الحافظة", - "downloadAsImage": "تنزيل على شكل صورة", - "undo": "تراجع", - "redo": "إعادة", - "clearCanvas": "مسح سبيكة الكاملة", - "canvasSettings": "إعدادات سبيكة الكاملة", - "showIntermediates": "إظهار الوسطاء", - "showGrid": "إظهار الشبكة", - "snapToGrid": "الالتفاف إلى الشبكة", - "darkenOutsideSelection": "تعمية خارج التحديد", - "autoSaveToGallery": "حفظ تلقائي إلى المعرض", - "saveBoxRegionOnly": "حفظ منطقة الصندوق فقط", - "limitStrokesToBox": "تحديد عدد الخطوط إلى الصندوق", - "showCanvasDebugInfo": "إظهار معلومات تصحيح سبيكة الكاملة", - "clearCanvasHistory": "مسح تاريخ سبيكة الكاملة", - "clearHistory": "مسح التاريخ", - "clearCanvasHistoryMessage": "مسح تاريخ اللوحة تترك اللوحة الحالية عائمة، ولكن تمسح بشكل غير قابل للتراجع تاريخ التراجع والإعادة.", - "clearCanvasHistoryConfirm": "هل أنت متأكد من رغبتك في مسح تاريخ اللوحة؟", - "activeLayer": "الطبقة النشطة", - "canvasScale": "مقياس اللوحة", - "boundingBox": "صندوق الحدود", - "scaledBoundingBox": "صندوق الحدود المكبر", - "boundingBoxPosition": "موضع صندوق الحدود", - "canvasDimensions": "أبعاد اللوحة", - "canvasPosition": "موضع اللوحة", - "cursorPosition": "موضع المؤشر", - "previous": "السابق", - "next": "التالي", - "accept": "قبول", - "discardAll": "تجاهل الكل" + "parametersNotSet": "لم يتم تعيين المعلمات" } } diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 2da27264a1c..6562294631f 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -18,21 +18,18 @@ "postprocessing": "Nachbearbeitung", "t2iAdapter": "T2I Adapter", "communityLabel": "Gemeinschaft", - "dontAskMeAgain": "Frag mich nicht nochmal", - "areYouSure": "Bist du dir sicher?", + "dontAskMeAgain": "Nicht nochmal fragen", + "areYouSure": "Bist du sicher?", "on": "An", - "nodeEditor": "Knoten Editor", "ipAdapter": "IP Adapter", - "auto": "Automatisch", + "auto": "Auto", "controlNet": "ControlNet", - "imageFailedToLoad": "Kann Bild nicht laden", "modelManager": "Model Manager", - "learnMore": "Mehr lernen", + "learnMore": "Mehr erfahren", "loading": "Lade", "random": "Zufall", "batch": "Stapel-Manager", "advanced": "Erweitert", - "unifiedCanvas": "Leinwand", "openInNewTab": "In einem neuem Tab öffnen", "linear": "Linear", "checkpoint": "Checkpoint", @@ -42,7 +39,7 @@ "outputs": "Ausgabe", "data": "Daten", "safetensors": "Safe-Tensors", - "outpaint": "Outpaint (Außen ausmalen)", + "outpaint": "Outpaint", "details": "Details", "format": "Format", "unknown": "Unbekannt", @@ -54,7 +51,6 @@ "somethingWentWrong": "Etwas ist schief gelaufen", "copyError": "$t(gallery.copy) Fehler", "input": "Eingabe", - "notInstalled": "Nicht $t(common.installed)", "alpha": "Alpha", "red": "Rot", "green": "Grün", @@ -64,11 +60,8 @@ "direction": "Richtung", "save": "Speichern", "created": "Erstellt", - "prevPage": "Vorherige Seite", - "nextPage": "Nächste Seite", "unknownError": "Unbekannter Fehler", "aboutDesc": "Verwenden Sie Invoke für die Arbeit? Siehe hier:", - "localSystem": "Lokales System", "orderBy": "Ordnen nach", "saveAs": "Speichern als", "updated": "Aktualisiert", @@ -76,267 +69,451 @@ "aboutHeading": "Nutzen Sie Ihre kreative Energie", "toResolve": "Lösen", "add": "Hinzufügen", - "loglevel": "Protokoll Stufe", "selected": "Ausgewählt", - "beta": "Beta" + "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", - "loadMore": "Mehr laden", - "noImagesInGallery": "Keine Bilder in der Galerie", "loading": "Lade", "deleteImage_one": "Lösche Bild", "deleteImage_other": "Lösche {{count}} Bilder", "copy": "Kopieren", "download": "Runterladen", - "setCurrentImage": "Setze aktuelle Bild", "featuresWillReset": "Wenn Sie dieses Bild löschen, werden diese Funktionen sofort zurückgesetzt.", - "deleteImageBin": "Gelöschte Bilder werden an den Papierkorb Ihres Betriebssystems gesendet.", - "unableToLoad": "Galerie kann nicht geladen werden", "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", - "problemDeletingImagesDesc": "Ein oder mehrere Bilder konnten nicht gelöscht werden", "starImage": "Bild markieren", - "assets": "Ressourcen", "unstarImage": "Markierung entfernen", "image": "Bild", "deleteSelection": "Lösche Auswahl", "dropToUpload": "$t(gallery.drop) zum hochladen", "dropOrUpload": "$t(gallery.drop) oder hochladen", "drop": "Ablegen", - "problemDeletingImages": "Problem beim Löschen der Bilder", "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" + "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": { - "keyboardShortcuts": "Tastenkürzel", - "appHotkeys": "App", - "generalHotkeys": "Allgemein", - "galleryHotkeys": "Galerie", - "unifiedCanvasHotkeys": "Leinwand", - "invoke": { - "desc": "Ein Bild erzeugen", - "title": "Invoke" - }, - "cancel": { - "title": "Abbrechen", - "desc": "Aktuelle Bilderzeugung abbrechen" - }, - "focusPrompt": { - "title": "Fokussiere Prompt", - "desc": "Fokussieren des Eingabefeldes für den Prompt" - }, - "toggleOptions": { - "title": "Optionen umschalten", - "desc": "Öffnen und Schließen des Optionsfeldes" - }, - "pinOptions": { - "title": "Optionen anheften", - "desc": "Anheften des Optionsfeldes" - }, - "toggleGallery": { - "title": "Galerie umschalten", - "desc": "Öffnen und Schließen des Galerie-Schubfachs" - }, - "maximizeWorkSpace": { - "title": "Arbeitsbereich maximieren", - "desc": "Schließen Sie die Panels und maximieren Sie den Arbeitsbereich" - }, - "changeTabs": { - "title": "Tabs wechseln", - "desc": "Zu einem anderen Arbeitsbereich wechseln" - }, - "consoleToggle": { - "title": "Konsole Umschalten", - "desc": "Konsole öffnen und schließen" - }, - "setPrompt": { - "title": "Prompt setzen", - "desc": "Verwende den Prompt des aktuellen Bildes" - }, - "setSeed": { - "title": "Seed setzen", - "desc": "Verwende den Seed des aktuellen Bildes" - }, - "setParameters": { - "title": "Parameter setzen", - "desc": "Alle Parameter des aktuellen Bildes verwenden" - }, - "restoreFaces": { - "title": "Gesicht restaurieren", - "desc": "Das aktuelle Bild restaurieren" - }, - "upscale": { - "title": "Hochskalieren", - "desc": "Das aktuelle Bild hochskalieren" - }, - "showInfo": { - "title": "Info anzeigen", - "desc": "Metadaten des aktuellen Bildes anzeigen" - }, - "sendToImageToImage": { - "title": "An Bild zu Bild senden", - "desc": "Aktuelles Bild an Bild-zu-Bild senden" - }, - "deleteImage": { - "title": "Bild löschen", - "desc": "Aktuelles Bild löschen" - }, - "closePanels": { - "title": "Panels schließen", - "desc": "Schließt offene Panels" - }, - "previousImage": { - "title": "Vorheriges Bild", - "desc": "Vorheriges Bild in der Galerie anzeigen" - }, - "nextImage": { - "title": "Nächstes Bild", - "desc": "Nächstes Bild in Galerie anzeigen" - }, - "increaseGalleryThumbSize": { - "title": "Größe der Galeriebilder erhöhen", - "desc": "Vergrößert die Galerie-Miniaturansichten" - }, - "decreaseGalleryThumbSize": { - "title": "Größe der Galeriebilder verringern", - "desc": "Verringert die Größe der Galerie-Miniaturansichten" - }, - "selectBrush": { - "title": "Pinsel auswählen", - "desc": "Wählt den Leinwandpinsel aus" - }, - "selectEraser": { - "title": "Radiergummi auswählen", - "desc": "Wählt den Radiergummi aus" - }, - "decreaseBrushSize": { - "title": "Pinselgröße verkleinern", - "desc": "Verringert die Größe des Pinsels/Radiergummis" - }, - "increaseBrushSize": { - "title": "Pinselgröße erhöhen", - "desc": "Erhöht die Größe des Pinsels/Radiergummis" - }, - "decreaseBrushOpacity": { - "title": "Deckkraft des Pinsels vermindern", - "desc": "Verringert die Deckkraft des Pinsels" - }, - "increaseBrushOpacity": { - "title": "Deckkraft des Pinsels erhöhen", - "desc": "Erhöht die Deckkraft des Pinsels" - }, - "moveTool": { - "title": "Verschieben Werkzeug", - "desc": "Ermöglicht die Navigation auf der Leinwand" - }, - "fillBoundingBox": { - "title": "Begrenzungsrahmen füllen", - "desc": "Füllt den Begrenzungsrahmen mit Pinselfarbe" - }, - "eraseBoundingBox": { - "title": "Begrenzungsrahmen löschen", - "desc": "Löscht den Bereich des Begrenzungsrahmens" - }, - "colorPicker": { - "title": "Farbpipette", - "desc": "Farben aus dem Bild aufnehmen" - }, - "toggleSnap": { - "title": "Einrasten umschalten", - "desc": "Schaltet Einrasten am Raster ein und aus" - }, - "quickToggleMove": { - "title": "Schnell Verschiebemodus", - "desc": "Schaltet vorübergehend den Verschiebemodus um" - }, - "toggleLayer": { - "title": "Ebene umschalten", - "desc": "Schaltet die Auswahl von Maske/Basisebene um" - }, - "clearMask": { - "title": "Lösche Maske", - "desc": "Die gesamte Maske löschen" - }, - "hideMask": { - "title": "Maske ausblenden", - "desc": "Maske aus- und einblenden" - }, - "showHideBoundingBox": { - "title": "Begrenzungsrahmen ein-/ausblenden", - "desc": "Sichtbarkeit des Begrenzungsrahmens ein- und ausschalten" - }, - "mergeVisible": { - "title": "Sichtbares Zusammenführen", - "desc": "Alle sichtbaren Ebenen der Leinwand zusammenführen" - }, - "saveToGallery": { - "title": "In Galerie speichern", - "desc": "Aktuelle Leinwand in Galerie speichern" - }, - "copyToClipboard": { - "title": "In die Zwischenablage kopieren", - "desc": "Aktuelle Leinwand in die Zwischenablage kopieren" - }, - "downloadImage": { - "title": "Bild herunterladen", - "desc": "Aktuelles Bild herunterladen" - }, - "undoStroke": { - "title": "Pinselstrich rückgängig machen", - "desc": "Einen Pinselstrich rückgängig machen" - }, - "redoStroke": { - "title": "Pinselstrich wiederherstellen", - "desc": "Einen Pinselstrich wiederherstellen" - }, - "resetView": { - "title": "Ansicht zurücksetzen", - "desc": "Leinwandansicht zurücksetzen" - }, - "previousStagingImage": { - "title": "Vorheriges Staging-Bild", - "desc": "Bild des vorherigen Staging-Bereichs" - }, - "nextStagingImage": { - "title": "Nächstes Staging-Bild", - "desc": "Bild des nächsten Staging-Bereichs" - }, - "acceptStagingImage": { - "title": "Staging-Bild akzeptieren", - "desc": "Akzeptieren Sie das aktuelle Bild des Staging-Bereichs" - }, - "nodesHotkeys": "Knoten", - "addNodes": { - "title": "Knotenpunkt hinzufügen", - "desc": "Öffnet das Menü zum Hinzufügen von Knoten" - }, - "cancelAndClear": { - "title": "Abbruch und leeren", - "desc": "Aktuelle Berechnung abbrechen und alle wartenden löschen" - }, + "hotkeys": "Tastaturbefehle", "noHotkeysFound": "Kein Hotkey gefunden", "searchHotkeys": "Hotkeys durchsuchen", "clearSearch": "Suche leeren", - "resetOptionsAndGallery": { - "desc": "Optionen und Galerie-Panels zurücksetzen", - "title": "Optionen und Galerie zurücksetzen" - }, - "remixImage": { - "desc": "Alle Parameter außer Seed vom aktuellen Bild verwenden", - "title": "Remix des Bilds erstellen" - }, - "toggleOptionsAndGallery": { - "title": "Optionen und Galerie umschalten", - "desc": "Optionen und Galerie-Panels öffnen und schließen" + "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": { @@ -368,19 +545,16 @@ "manual": "Manuell", "modelManager": "Modell Manager", "model": "Modell", - "v2_base": "v2 (512px)", "name": "Name", - "v2_768": "v2 (768px)", "none": "Nix", "advanced": "Erweitert", "convertingModelBegin": "Konvertiere Modell. Bitte warten.", "baseModel": "Basis Modell", "convertToDiffusers": "Konvertiere zu Diffusers", "vae": "VAE", - "predictionType": "Vorhersagetyp (für Stable Diffusion 2.x-Modelle und gelegentliche Stable Diffusion 1.x-Modelle)", + "predictionType": "Vorhersagetyp", "selectModel": "Wählen Sie Modell aus", "repo_id": "Repo-ID", - "modelSyncFailed": "Modellsynchronisierung fehlgeschlagen", "modelDeleted": "Modell gelöscht", "modelUpdateFailed": "Modellaktualisierung fehlgeschlagen", "settings": "Einstellungen", @@ -388,7 +562,6 @@ "syncModels": "Modelle synchronisieren", "modelType": "Modelltyp", "convertToDiffusersHelpText1": "Dieses Modell wird in das 🧨 Diffusers-Format konvertiert.", - "modelsSynced": "Modelle synchronisiert", "vaePrecision": "VAE-Präzision", "variant": "Variante", "modelDeleteFailed": "Modell konnte nicht gelöscht werden", @@ -400,13 +573,78 @@ "defaultSettingsSaved": "Standardeinstellungen gespeichert", "addModels": "Model hinzufügen", "deleteModelImage": "Lösche Model Bild", - "hfTokenInvalidErrorMessage": "Falscher oder fehlender HuggingFace Schlüssel.", "huggingFaceRepoID": "HuggingFace Repo ID", - "hfToken": "HuggingFace Schlüssel", - "hfTokenInvalid": "Falscher oder fehlender HF Schlüssel", "huggingFacePlaceholder": "besitzer/model-name", - "hfTokenSaved": "HF Schlüssel gespeichert", - "hfTokenUnableToVerify": "Konnte den HF Schlüssel nicht validieren" + "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", @@ -420,8 +658,6 @@ "type": "Art", "strength": "Stärke", "upscaling": "Hochskalierung", - "upscale": "Hochskalieren (Shift + U)", - "upscaleImage": "Bild hochskalieren", "scale": "Maßstab", "imageFit": "Ausgangsbild an Ausgabegröße anpassen", "scaleBeforeProcessing": "Skalieren vor der Verarbeitung", @@ -429,13 +665,9 @@ "scaledHeight": "Skaliert H", "infillMethod": "Infill-Methode", "tileSize": "Kachelgröße", - "sendToImg2Img": "Senden an Bild-zu-Bild", - "sendToUnifiedCanvas": "Senden an Leinwand", - "downloadImage": "Bild herunterladen", "usePrompt": "Prompt verwenden", "useSeed": "Seed verwenden", "useAll": "Alle verwenden", - "showOptionsPanel": "Optionsleiste zeigen", "copyImage": "Bild kopieren", "denoisingStrength": "Stärke der Entrauschung", "symmetry": "Symmetrie", @@ -449,12 +681,40 @@ "setToOptimalSize": "Optimiere Größe für Modell", "useSize": "Maße übernehmen", "remixImage": "Remix des Bilds erstellen", - "imageActions": "Weitere Bildaktionen" + "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", - "enableImageDebugging": "Bild-Debugging aktivieren", "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.", @@ -463,7 +723,6 @@ "clearIntermediatesDesc1": "Das Löschen der Zwischenbilder setzt Leinwand und ControlNet zurück.", "generation": "Erzeugung", "enableInformationalPopovers": "Info-Popouts anzeigen", - "shouldLogToConsole": "Konsole loggen", "showProgressInViewer": "Zwischenbilder im Viewer anzeigen", "clearIntermediatesDesc3": "Ihre Bilder werden nicht gelöscht.", "clearIntermediatesWithCount_one": "Lösche {{count}} Zwischenbilder", @@ -486,116 +745,71 @@ "toast": { "uploadFailed": "Hochladen fehlgeschlagen", "imageCopied": "Bild kopiert", - "imageNotLoadedDesc": "Konnte kein Bild finden", - "canvasMerged": "Leinwand zusammengeführt", - "sentToImageToImage": "Gesendet an Bild zu Bild", - "sentToUnifiedCanvas": "Gesendet an Leinwand", - "parametersNotSet": "Parameter nicht festgelegt", - "metadataLoadFailed": "Metadaten konnten nicht geladen werden", - "setCanvasInitialImage": "Ausgangsbild setzen", - "problemMergingCanvas": "Problem bei Verschmelzung der Leinwand", - "canvasCopiedClipboard": "Leinwand in Zwischenablage kopiert", - "canvasSentControlnetAssets": "Leinwand an ControlNet & Sammlung geschickt", - "problemDownloadingCanvasDesc": "Kann Basis-Layer nicht exportieren", - "canvasDownloaded": "Leinwand heruntergeladen", - "problemSavingCanvasDesc": "Kann Basis-Layer nicht exportieren", - "canvasSavedGallery": "Leinwand in Galerie gespeichert", - "problemMergingCanvasDesc": "Kann Basis-Layer nicht exportieren", - "problemSavingCanvas": "Problem beim Speichern der Leinwand", - "problemCopyingCanvas": "Problem beim Kopieren der Leinwand", - "problemCopyingCanvasDesc": "Kann Basis-Layer nicht exportieren", - "problemDownloadingCanvas": "Problem beim Herunterladen der Leinwand", - "setAsCanvasInitialImage": "Als Ausgangsbild gesetzt", + "parametersNotSet": "Parameter nicht zurückgerufen", "addedToBoard": "Dem Board hinzugefügt", - "loadedWithWarnings": "Workflow mit Warnungen geladen" - }, - "tooltip": { - "feature": { - "prompt": "Dies ist das Prompt-Feld. Ein Prompt enthält Generierungsobjekte und stilistische Begriffe. Sie können auch Gewichtungen (Token-Bedeutung) dem Prompt hinzufügen, aber CLI-Befehle und Parameter funktionieren nicht.", - "gallery": "Die Galerie zeigt erzeugte Bilder aus dem Ausgabeordner an, sobald sie erstellt wurden. Die Einstellungen werden in den Dateien gespeichert und können über das Kontextmenü aufgerufen werden.", - "other": "Mit diesen Optionen werden alternative Verarbeitungsmodi für InvokeAI aktiviert. 'Nahtlose Kachelung' erzeugt sich wiederholende Muster in der Ausgabe. 'Hohe Auflösungen' werden in zwei Schritten mit img2img erzeugt: Verwenden Sie diese Einstellung, wenn Sie ein größeres und kohärenteres Bild ohne Artefakte wünschen. Es dauert länger als das normale txt2img.", - "seed": "Der Seed-Wert beeinflusst das Ausgangsrauschen, aus dem das Bild erstellt wird. Sie können die bereits vorhandenen Seeds von früheren Bildern verwenden. 'Der Rauschschwellenwert' wird verwendet, um Artefakte bei hohen CFG-Werten abzuschwächen (versuchen Sie es im Bereich 0-10), und Perlin, um während der Erzeugung Perlin-Rauschen hinzuzufügen: Beide dienen dazu, Ihre Ergebnisse zu variieren.", - "upscale": "Verwenden Sie ESRGAN, um das Bild unmittelbar nach der Erzeugung zu vergrößern.", - "boundingBox": "Der Begrenzungsrahmen ist derselbe wie die Einstellungen für Breite und Höhe bei Text-zu-Bild oder Bild-zu-Bild. Es wird nur der Bereich innerhalb des Rahmens verarbeitet." - } - }, - "unifiedCanvas": { - "layer": "Ebene", - "base": "Basis", - "mask": "Maske", - "maskingOptions": "Maskierungsoptionen", - "enableMask": "Maske aktivieren", - "preserveMaskedArea": "Maskierten Bereich bewahren", - "clearMask": "Maske löschen (Shift+C)", - "brush": "Pinsel", - "eraser": "Radierer", - "fillBoundingBox": "Begrenzungsrahmen füllen", - "eraseBoundingBox": "Begrenzungsrahmen löschen", - "colorPicker": "Pipette", - "brushOptions": "Pinseloptionen", - "brushSize": "Größe", - "move": "Bewegen", - "resetView": "Ansicht zurücksetzen", - "mergeVisible": "Sichtbare zusammenführen", - "saveToGallery": "In Galerie speichern", - "copyToClipboard": "In Zwischenablage kopieren", - "downloadAsImage": "Als Bild herunterladen", - "undo": "Rückgängig", - "redo": "Wiederherstellen", - "clearCanvas": "Leinwand löschen", - "canvasSettings": "Leinwand-Einstellungen", - "showIntermediates": "Zwischenbilder anzeigen", - "showGrid": "Gitternetz anzeigen", - "snapToGrid": "Am Gitternetz einrasten", - "darkenOutsideSelection": "Außerhalb der Auswahl verdunkeln", - "autoSaveToGallery": "Automatisch in Galerie speichern", - "saveBoxRegionOnly": "Nur Auswahlbox speichern", - "limitStrokesToBox": "Striche auf Auswahl beschränken", - "showCanvasDebugInfo": "Zusätzliche Informationen anzeigen", - "clearCanvasHistory": "Leinwand-Verlauf löschen", - "clearHistory": "Verlauf löschen", - "clearCanvasHistoryMessage": "Wenn Sie den Verlauf löschen, bleibt die aktuelle Leinwand intakt, aber der Verlauf der Rückgängig- und Wiederherstellung wird unwiderruflich gelöscht.", - "clearCanvasHistoryConfirm": "Sind Sie sicher, dass Sie den Verlauf löschen möchten?", - "activeLayer": "Aktive Ebene", - "canvasScale": "Leinwand Maßstab", - "boundingBox": "Begrenzungsrahmen", - "scaledBoundingBox": "Skalierter Begrenzungsrahmen", - "boundingBoxPosition": "Begrenzungsrahmen Position", - "canvasDimensions": "Maße der Leinwand", - "canvasPosition": "Leinwandposition", - "cursorPosition": "Position des Cursors", - "previous": "Vorherige", - "next": "Nächste", - "accept": "Akzeptieren", - "discardAll": "Alles verwerfen", - "antialiasing": "Kantenglättung", - "showResultsOn": "Zeige Ergebnisse (An)", - "showResultsOff": "Zeige Ergebnisse (Aus)" + "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", - "showOptionsPanel": "Seitenpanel anzeigen", "reset": "Zurücksetzten", "nextImage": "Nächstes Bild", - "showGalleryPanel": "Galerie-Panel anzeigen", "menu": "Menü", - "loadMore": "Mehr laden", "invokeProgressBar": "Invoke Fortschrittsanzeige", "mode": "Modus", "resetUI": "$t(accessibility.reset) von UI", "createIssue": "Ticket erstellen", - "about": "Über" + "about": "Über", + "submitSupportTicket": "Support-Ticket senden", + "toggleRightPanel": "Rechtes Bedienfeld umschalten (G)", + "toggleLeftPanel": "Linkes Bedienfeld umschalten (T)", + "uploadImages": "Bild(er) hochladen" }, "boards": { - "autoAddBoard": "Automatisches Hinzufügen zum Board", - "topMessage": "Dieser Ordner enthält Bilder die in den folgenden Funktionen verwendet werden:", + "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 aussuchen", + "selectBoard": "Ordner wählen", "cancel": "Abbrechen", "addBoard": "Board hinzufügen", "uncategorized": "Ohne Kategorie", @@ -603,107 +817,47 @@ "changeBoard": "Ordner wechseln", "loading": "Laden...", "clearSearch": "Suche leeren", - "bottomMessage": "Löschen des Boards und seiner Bilder setzt alle Funktionen zurück, die sie gerade verwenden.", + "bottomMessage": "Durch das Löschen von Bildern werden alle Funktionen zurückgesetzt, die diese Bilder derzeit verwenden.", "deleteBoardOnly": "Nur Ordner löschen", - "deleteBoard": "Löschen Ordner", - "deleteBoardAndImages": "Löschen Ordner und Bilder", - "deletedBoardsCannotbeRestored": "Gelöschte Ordner könnte nicht wiederhergestellt werden", - "movingImagesToBoard_one": "Verschiebe {{count}} Bild zu Ordner:", - "movingImagesToBoard_other": "Verschiebe {{count}} Bilder in Ordner:" - }, - "controlnet": { - "showAdvanced": "Zeige Erweitert", - "contentShuffleDescription": "Mischt den Inhalt von einem Bild", - "addT2IAdapter": "$t(common.t2iAdapter) hinzufügen", - "importImageFromCanvas": "Bild von Zeichenfläche importieren", - "lineartDescription": "Konvertiere Bild in Strichzeichnung", - "importMaskFromCanvas": "Importiere Maske von Zeichenfläche", - "hed": "HED", - "hideAdvanced": "Verstecke Erweitert", - "contentShuffle": "Inhalt mischen", - "beginEndStepPercent": "Start / Ende Step Prozent", - "duplicate": "Kopieren", - "f": "F", - "h": "H", - "depthMidasDescription": "Tiefenmap erstellen mit Midas", - "controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))", - "weight": "Einfluss", - "selectModel": "Wähle ein Modell", - "depthMidas": "Tiefe (Midas)", - "w": "W", - "addControlNet": "$t(common.controlNet) hinzufügen", - "none": "Kein", - "detectResolution": "Auflösung erkennen", - "ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))", - "fill": "Füllen", - "addIPAdapter": "$t(common.ipAdapter) hinzufügen", - "colorMapDescription": "Erstelle eine Farbkarte von diesem Bild", - "t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))", - "imageResolution": "Bild Auflösung", - "depthZoe": "Tiefe (Zoe)", - "colorMap": "Farbe", - "lowThreshold": "Niedrige Schwelle", - "highThreshold": "Hohe Schwelle", - "toggleControlNet": "Dieses ControlNet ein- oder ausschalten", - "delete": "Löschen", - "controlAdapter_one": "Control Adapter", - "controlAdapter_other": "Control Adapter", - "colorMapTileSize": "Kachelgröße", - "depthZoeDescription": "Tiefenmap erstellen mit Zoe", - "setControlImageDimensions": "Setze Control-Bild Auflösung auf Breite/Höhe", - "resize": "Größe ändern", - "resetControlImage": "Zurücksetzen vom Referenz Bild", - "balanced": "Ausgewogen", - "prompt": "Prompt", - "resizeMode": "Größe", - "processor": "Prozessor", - "saveControlImage": "Speichere Referenz Bild", - "safe": "Speichern", - "pidi": "PIDI", - "normalBae": "Normales BAE", - "mlsdDescription": "Minimalistischer Liniensegmentdetektor", - "control": "Kontrolle", - "coarse": "Grob", - "crop": "Zuschneiden", - "pidiDescription": "PIDI-Bildverarbeitung", - "mediapipeFace": "Mediapipe Gesichter", - "mlsd": "M-LSD", - "controlMode": "Steuermodus", - "cannyDescription": "Canny Umrisserkennung", - "lineart": "Linienzeichnung", - "lineartAnimeDescription": "Lineart-Verarbeitung im Anime-Stil", - "minConfidence": "Minimales Vertrauen", - "megaControl": "Mega-Kontrolle", - "autoConfigure": "Prozessor Auto-konfig", - "normalBaeDescription": "Normale BAE-Verarbeitung", - "noneDescription": "Es wurde keine Verarbeitung angewendet", - "lineartAnime": "Lineart Anime / \"Strichzeichnung Anime\"", - "mediapipeFaceDescription": "Gesichtserkennung mit Mediapipe", - "canny": "\"Canny\"", - "hedDescription": "Ganzheitlich verschachtelte Kantenerkennung", - "scribble": "Scribble", - "maxFaces": "Maximale Anzahl Gesichter", - "resizeSimple": "Größe ändern (einfach)", - "large": "Groß", - "modelSize": "Modellgröße", - "small": "Klein", - "base": "Basis", - "depthAnything": "Depth Anything", - "depthAnythingDescription": "Erstellung einer Tiefenkarte mit der Depth-Anything-Technik", - "face": "Gesicht", - "body": "Körper", - "hands": "Hände", - "dwOpenpose": "DW Openpose", - "dwOpenposeDescription": "Posenschätzung mit DW Openpose", - "selectCLIPVisionModel": "Wähle ein CLIP Vision Model aus", - "ipAdapterMethod": "Methode", - "composition": "Nur Komposition", - "full": "Voll", - "style": "Nur Style" + "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": "Aktuellen Aufgabe abbrechen", + "cancelTooltip": "Aufgabe abbrechen", "queueEmpty": "Warteschlange leer", "in_progress": "In Arbeit", "queueFront": "Am Anfang der Warteschlange einreihen", @@ -739,7 +893,7 @@ "clearQueueAlertDialog2": "Warteschlange wirklich leeren?", "pruneSucceeded": "{{item_count}} abgeschlossene Elemente aus der Warteschlange entfernt", "pauseSucceeded": "Prozess angehalten", - "cancelFailed": "Problem beim Stornieren des Auftrags", + "cancelFailed": "Problem beim Abbrechen", "pauseFailed": "Problem beim Anhalten des Prozesses", "front": "Vorne", "pruneTooltip": "Bereinigen Sie {{item_count}} abgeschlossene Aufträge", @@ -754,10 +908,25 @@ "batchQueuedDesc_other": "{{count}} Einträge an {{direction}} der Wartschlange hinzugefügt", "openQueue": "Warteschlange öffnen", "batchFailedToQueue": "Fehler beim Einreihen in die Stapelverarbeitung", - "batchFieldValues": "Stapelverarbeitungswerte", "batchQueued": "Stapelverarbeitung eingereiht", "graphQueued": "Graph eingereiht", - "graphFailedToQueue": "Fehler beim Einreihen des Graphen" + "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", @@ -767,18 +936,15 @@ "model": "Modell", "noImageDetails": "Keine Bild Details gefunden", "cfgScale": "CFG-Skala", - "fit": "Bild zu Bild anpassen", "height": "Höhe", "noMetaData": "Keine Meta-Daten gefunden", "width": "Breite", "createdBy": "Erstellt von", "steps": "Schritte", - "seamless": "Nahtlos", "positivePrompt": "Positiver Prompt", "generationMode": "Generierungsmodus", "Threshold": "Rauschen-Schwelle", "seed": "Seed", - "initImage": "Erstes Bild", "vae": "VAE", "workflow": "Workflow", "scheduler": "Planer", @@ -788,8 +954,10 @@ "allPrompts": "Alle Prompts", "imageDimensions": "Bilder Auslösungen", "parameterSet": "Parameter {{parameter}} setzen", - "recallParameter": "{{label}} Abrufen", - "parsingFailed": "Parsing Fehlgeschlagen" + "canvasV2Metadata": "Leinwand", + "guidance": "Führung", + "seamlessXAxis": "Nahtlose X Achse", + "seamlessYAxis": "Nahtlose Y Achse" }, "popovers": { "noiseUseCPU": { @@ -914,7 +1082,8 @@ }, "paramScheduler": { "paragraphs": [ - "\"Planer\" definiert, wie iterativ Rauschen zu einem Bild hinzugefügt wird, oder wie ein Sample bei der Ausgabe eines Modells aktualisiert wird." + "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" }, @@ -922,6 +1091,115 @@ "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": { @@ -955,22 +1233,18 @@ "cannotConnectToSelf": "Es kann keine Verbindung zu sich selbst hergestellt werden", "colorCodeEdges": "Farbkodierte Kanten", "addNodeToolTip": "Knoten hinzufügen (Umschalt+A, Leertaste)", - "collectionFieldType": "{{name}} Sammlung", + "collectionFieldType": "{{name}} (Sammlung)", "connectionWouldCreateCycle": "Verbindung würde einen Kreislauf/cycle schaffen", "inputMayOnlyHaveOneConnection": "Eingang darf nur eine Verbindung haben", - "hideLegendNodes": "Feldtyp-Legende ausblenden", "integer": "Ganze Zahl", - "addLinearView": "Zur linearen Ansicht hinzufügen", "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", - "noConnectionData": "Keine Verbindungsdaten", "reloadNodeTemplates": "Knoten-Vorlagen neu laden", "newWorkflow": "Neuer Arbeitsablauf / Workflow", "newWorkflowDesc": "Einen neuen Arbeitsablauf erstellen?", - "noFieldsLinearview": "Keine Felder zur linearen Ansicht hinzugefügt", "clearWorkflow": "Workflow löschen", "clearWorkflowDesc": "Diesen Arbeitsablauf löschen und neu starten?", "noConnectionInProgress": "Es besteht keine Verbindung", @@ -978,12 +1252,9 @@ "nodeVersion": "Knoten Version", "node": "Knoten", "nodeSearch": "Knoten suchen", - "removeLinearView": "Entfernen aus Linear View", "nodeOutputs": "Knoten-Ausgänge", "nodeTemplate": "Knoten-Vorlage", "nodeType": "Knotentyp", - "noFieldType": "Kein Feldtyp", - "noMatchingNodes": "Keine passenden Knoten", "noNodeSelected": "Kein Knoten gewählt", "nodeOpacity": "Knoten-Deckkraft", "noOutputRecorded": "Keine Ausgänge aufgezeichnet", @@ -991,7 +1262,6 @@ "clearWorkflowDesc2": "Ihr aktueller Arbeitsablauf hat ungespeicherte Änderungen.", "scheduler": "Planer", "showMinimapnodes": "MiniMap anzeigen", - "showLegendNodes": "Feldtyp-Legende anzeigen", "executionStateCompleted": "Erledigt", "downloadWorkflow": "Workflow JSON herunterladen", "executionStateInProgress": "In Bearbeitung", @@ -1001,51 +1271,94 @@ "fieldTypesMustMatch": "Feldtypen müssen übereinstimmen", "fitViewportNodes": "An Ansichtsgröße anpassen", "loadingNodes": "Lade Nodes...", - "mismatchedVersion": "Ungültiger Knoten: Knoten {{node}} vom Typ {{type}} hat keine passende Version (Update versuchen?)", "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", - "unknownOutput": "Unbekannte Ausgabe: {{name}}", "updateNode": "Knoten updaten", "edge": "Rand / Kante", "sourceNodeDoesNotExist": "Ungültiger Rand: Quell- / Ausgabe-Knoten {{node}} existiert nicht", "updateAllNodes": "Update Knoten", "allNodesUpdated": "Alle Knoten aktualisiert", - "unknownTemplate": "Unbekannte Vorlage", "updateApp": "Update App", - "unknownInput": "Unbekannte Eingabe: {{name}}", "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" + "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": { - "enableHrf": "Korrektur für hohe Auflösungen", - "upscaleMethod": "Vergrößerungsmethoden", "metadata": { - "strength": "Hochauflösender Fix Stärke", - "enabled": "Hochauflösender Fix aktiviert", - "method": "Hochauflösender Fix Methode" + "strength": "Auflösungs-Fix Stärke", + "enabled": "Auflösungs-Fix aktiviert", + "method": "Auflösungs-Fix Methode" }, - "hrf": "Hochauflösender Fix" + "hrf": "Hohe-Auflösung-Fix" }, "models": { "noMatchingModels": "Keine passenden Modelle", "loading": "lade", - "noMatchingLoRAs": "Keine passenden LoRAs", "noModelsAvailable": "Keine Modelle verfügbar", "selectModel": "Wählen ein Modell aus", "noRefinerModelsInstalled": "Keine SDXL Refiner-Modelle installiert", - "noLoRAsInstalled": "Keine LoRAs installiert", - "esrganModel": "ESRGAN Modell", "addLora": "LoRA hinzufügen", "defaultVAE": "Standard VAE", - "lora": "LoRA" + "lora": "LoRA", + "concepts": "Konzepte" }, "accordions": { "generation": { @@ -1063,7 +1376,7 @@ }, "compositing": { "coherenceTab": "Kohärenzpass", - "infillTab": "Füllung / Infill", + "infillTab": "Infill", "title": "Compositing" } }, @@ -1071,32 +1384,40 @@ "workflows": "Arbeitsabläufe", "workflowName": "Arbeitsablauf-Name", "saveWorkflowAs": "Arbeitsablauf speichern als", - "searchWorkflows": "Suche Arbeitsabläufe", "newWorkflowCreated": "Neuer Arbeitsablauf erstellt", "problemSavingWorkflow": "Problem beim Speichern des Arbeitsablaufs", - "problemLoading": "Problem beim Laden von Arbeitsabläufen", "downloadWorkflow": "Speichern als", "savingWorkflow": "Speichere Arbeitsablauf...", "saveWorkflow": "Arbeitsablauf speichern", "noWorkflows": "Keine Arbeitsabläufe", "workflowLibrary": "Bibliothek", "unnamedWorkflow": "Unbenannter Arbeitsablauf", - "noDescription": "Keine Beschreibung", - "clearWorkflowSearchFilter": "Suchfilter zurücksetzen", "workflowEditorMenu": "Arbeitsablauf-Editor Menü", "deleteWorkflow": "Arbeitsablauf löschen", "workflowSaved": "Arbeitsablauf gespeichert", "uploadWorkflow": "Aus Datei laden", - "openWorkflow": "Arbeitsablauf öffnen", "saveWorkflowToProject": "Arbeitsablauf in Projekt speichern", "workflowCleared": "Arbeitsablauf gelöscht", - "loading": "Lade Arbeitsabläufe" - }, - "app": { - "storeNotInitialized": "App-Store ist nicht initialisiert" + "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": { - "concatPromptStyle": "Verknüpfen von Prompt & Stil", "scheduler": "Planer", "steps": "Schritte" }, @@ -1104,8 +1425,345 @@ "showDynamicPrompts": "Dynamische Prompts anzeigen" }, "prompt": { - "noMatchingTriggers": "Keine passenden Auslöser", - "addPromptTrigger": "Auslöse Text hinzufügen", - "compatibleEmbeddings": "Kompatible Einbettungen" + "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 index 306151984a4..2ba5a4828d7 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -10,34 +10,168 @@ "previousImage": "Previous Image", "reset": "Reset", "resetUI": "$t(accessibility.reset) UI", - "showGalleryPanel": "Show Gallery Panel", - "showOptionsPanel": "Show Side Panel", + "toggleRightPanel": "Toggle Right Panel (G)", + "toggleLeftPanel": "Toggle Left Panel (T)", "uploadImage": "Upload Image", - "loadMore": "Load More" + "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", - "bottomMessage": "Deleting this board and its images will reset any features currently using them.", + "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 cannot be restored", + "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", - "topMessage": "This board contains images used in the following features:", + "shared": "Shared Boards", + "topMessage": "This selection contains images used in the following features:", + "unarchiveBoard": "Unarchive Board", "uncategorized": "Uncategorized", - "downloadBoard": "Download Board" + "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": { @@ -63,6 +197,7 @@ "aboutDesc": "Using Invoke for work? Check out:", "aboutHeading": "Own Your Creative Power", "accept": "Accept", + "apply": "Apply", "add": "Add", "advanced": "Advanced", "ai": "ai", @@ -71,11 +206,18 @@ "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", @@ -85,54 +227,86 @@ "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", - "loglevel": "Log Level", + "minimize": "Minimize", + "next": "Next", + "noMatchingItems": "No matching items", + "notifications": "Notifications", "learnMore": "Learn More", "modelManager": "Model Manager", - "nodeEditor": "Node Editor", + "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", - "unifiedCanvas": "Unified Canvas", "unknown": "Unknown", + "unpin": "Unpin", "upload": "Upload", + "userGuideLabel": "User Guide", + "zoomIn": "Zoom In", + "zoomOut": "Zoom Out", "updated": "Updated", "created": "Created", "prevPage": "Previous Page", @@ -143,109 +317,39 @@ "blue": "Blue", "alpha": "Alpha", "selected": "Selected", + "search": "Search", + "clear": "Clear", "tab": "Tab", - "viewing": "Viewing", - "viewingDesc": "Review images in a large gallery view", - "editing": "Editing", - "editingDesc": "Edit on the Control Layers canvas", - "comparing": "Comparing", - "comparingDesc": "Comparing two images", + "view": "View", + "edit": "Edit", "enabled": "Enabled", - "disabled": "Disabled" - }, - "controlnet": { - "controlAdapter_one": "Control Adapter", - "controlAdapter_other": "Control Adapters", - "controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))", - "ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))", - "t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))", - "addControlNet": "Add $t(common.controlNet)", - "addIPAdapter": "Add $t(common.ipAdapter)", - "addT2IAdapter": "Add $t(common.t2iAdapter)", - "amult": "a_mult", - "autoConfigure": "Auto configure processor", - "balanced": "Balanced", - "base": "Base", - "beginEndStepPercent": "Begin / End Step Percentage", - "beginEndStepPercentShort": "Begin/End %", - "bgth": "bg_th", - "canny": "Canny", - "cannyDescription": "Canny edge detection", - "colorMap": "Color", - "colorMapDescription": "Generates a color map from the image", - "coarse": "Coarse", - "contentShuffle": "Content Shuffle", - "contentShuffleDescription": "Shuffles the content in an image", - "control": "Control", - "controlMode": "Control Mode", - "crop": "Crop", - "delete": "Delete", - "depthAnything": "Depth Anything", - "depthAnythingDescription": "Depth map generation using the Depth Anything technique", - "depthMidas": "Depth (Midas)", - "depthMidasDescription": "Depth map generation using Midas", - "depthZoe": "Depth (Zoe)", - "depthZoeDescription": "Depth map generation using Zoe", - "detectResolution": "Detect Resolution", - "duplicate": "Duplicate", - "f": "F", - "fill": "Fill", - "h": "H", - "face": "Face", - "body": "Body", - "hands": "Hands", - "hed": "HED", - "hedDescription": "Holistically-Nested Edge Detection", - "hideAdvanced": "Hide Advanced", - "highThreshold": "High Threshold", - "imageResolution": "Image Resolution", - "colorMapTileSize": "Tile Size", - "importImageFromCanvas": "Import Image From Canvas", - "importMaskFromCanvas": "Import Mask From Canvas", - "large": "Large", - "lineart": "Lineart", - "lineartAnime": "Lineart Anime", - "lineartAnimeDescription": "Anime-style lineart processing", - "lineartDescription": "Converts image to lineart", - "lowThreshold": "Low Threshold", - "maxFaces": "Max Faces", - "mediapipeFace": "Mediapipe Face", - "mediapipeFaceDescription": "Face detection using Mediapipe", - "megaControl": "Mega Control", - "minConfidence": "Min Confidence", - "mlsd": "M-LSD", - "mlsdDescription": "Minimalist Line Segment Detector", - "modelSize": "Model Size", + "disabled": "Disabled", + "placeholderSelectAModel": "Select a model", + "reset": "Reset", "none": "None", - "noneDescription": "No processing applied", - "normalBae": "Normal BAE", - "normalBaeDescription": "Normal BAE processing", - "dwOpenpose": "DW Openpose", - "dwOpenposeDescription": "Human pose estimation using DW Openpose", - "pidi": "PIDI", - "pidiDescription": "PIDI image processing", - "processor": "Processor", - "prompt": "Prompt", - "resetControlImage": "Reset Control Image", - "resize": "Resize", - "resizeSimple": "Resize (Simple)", - "resizeMode": "Resize Mode", - "ipAdapterMethod": "Method", - "full": "Full", - "style": "Style Only", - "composition": "Composition Only", - "safe": "Safe", - "saveControlImage": "Save Control Image", - "scribble": "Scribble", - "selectModel": "Select a model", - "selectCLIPVisionModel": "Select a CLIP Vision model", - "setControlImageDimensions": "Copy size to W/H (optimize for model)", - "setControlImageDimensionsForce": "Copy size to W/H (ignore model)", - "showAdvanced": "Show Advanced", - "small": "Small", - "toggleControlNet": "Toggle this ControlNet", - "w": "W", - "weight": "Weight" + "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", @@ -260,13 +364,45 @@ "prompt": { "addPromptTrigger": "Add Prompt Trigger", "compatibleEmbeddings": "Compatible Embeddings", - "noMatchingTriggers": "No matching triggers" + "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", @@ -277,9 +413,17 @@ "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", @@ -288,25 +432,41 @@ "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.", + "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", + "gpu": "GPU #", "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", @@ -324,7 +484,14 @@ "iterations_one": "Iteration", "iterations_other": "Iterations", "generations_one": "Generation", - "generations_other": "Generations" + "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", @@ -343,296 +510,527 @@ "disableFailed": "Problem Disabling Invocation Cache", "useCache": "Use Cache" }, + "modelCache": { + "clear": "Clear Model Cache", + "clearSucceeded": "Model Cache Cleared", + "clearFailed": "Problem Clearing Model Cache" + }, "gallery": { - "alwaysShowImageSizeBadge": "Always Show Image Size Badge", + "gallery": "Gallery", + "images": "Images", "assets": "Assets", - "autoAssignBoardOnClick": "Auto-Assign Board on Click", + "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": "$t(gallery.drop) or Upload", + "dropOrUpload": "Drop or Upload", "dropToUpload": "$t(gallery.drop) to Upload", "deleteImage_one": "Delete Image", "deleteImage_other": "Delete {{count}} Images", - "deleteImageBin": "Deleted images will be sent to your operating system's Bin.", "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", - "loadMore": "Load More", + "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", - "setCurrentImage": "Set as Current Image", - "starImage": "Star Image", - "unstarImage": "Unstar Image", + "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", - "problemDeletingImages": "Problem Deleting Images", - "problemDeletingImagesDesc": "One or more images could not be deleted", "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", - "compareOptions": "Comparison Options", "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." + "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", - "acceptStagingImage": { - "desc": "Accept Current Staging Area Image", - "title": "Accept Staging Image" - }, - "addNodes": { - "desc": "Opens the add node menu", - "title": "Add Nodes" - }, - "appHotkeys": "App", - "cancel": { - "desc": "Cancel current queue item", - "title": "Cancel" - }, - "cancelAndClear": { - "desc": "Cancel current queue item and clear all pending items", - "title": "Cancel and Clear" - }, - "changeTabs": { - "desc": "Switch to another workspace", - "title": "Change Tabs" - }, - "clearMask": { - "desc": "Clear the entire mask", - "title": "Clear Mask" - }, - "closePanels": { - "desc": "Closes open panels", - "title": "Close Panels" - }, - "colorPicker": { - "desc": "Selects the canvas color picker", - "title": "Select Color Picker" - }, - "consoleToggle": { - "desc": "Open and close console", - "title": "Console Toggle" - }, - "copyToClipboard": { - "desc": "Copy current canvas to clipboard", - "title": "Copy to Clipboard" - }, - "decreaseBrushOpacity": { - "desc": "Decreases the opacity of the canvas brush", - "title": "Decrease Brush Opacity" - }, - "decreaseBrushSize": { - "desc": "Decreases the size of the canvas brush/eraser", - "title": "Decrease Brush Size" - }, - "decreaseGalleryThumbSize": { - "desc": "Decreases gallery thumbnails size", - "title": "Decrease Gallery Image Size" - }, - "deleteImage": { - "desc": "Delete the current image", - "title": "Delete Image" - }, - "downloadImage": { - "desc": "Download current canvas", - "title": "Download Image" - }, - "eraseBoundingBox": { - "desc": "Erases the bounding box area", - "title": "Erase Bounding Box" - }, - "fillBoundingBox": { - "desc": "Fills the bounding box with brush color", - "title": "Fill Bounding Box" - }, - "focusPrompt": { - "desc": "Focus the prompt input area", - "title": "Focus Prompt" - }, - "galleryHotkeys": "Gallery", - "generalHotkeys": "General", - "hideMask": { - "desc": "Hide and unhide mask", - "title": "Hide Mask" - }, - "increaseBrushOpacity": { - "desc": "Increases the opacity of the canvas brush", - "title": "Increase Brush Opacity" + "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" + } }, - "increaseBrushSize": { - "desc": "Increases the size of the canvas brush/eraser", - "title": "Increase Brush Size" + "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" + } }, - "increaseGalleryThumbSize": { - "desc": "Increases gallery thumbnails size", - "title": "Increase Gallery Image Size" + "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." + } }, - "invoke": { - "desc": "Generate an image", - "title": "Invoke" + "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." + } }, - "keyboardShortcuts": "Hotkeys", - "maximizeWorkSpace": { - "desc": "Close panels and maximize work area", - "title": "Maximize Workspace" - }, - "mergeVisible": { - "desc": "Merge all visible layers of canvas", - "title": "Merge Visible" - }, - "moveTool": { - "desc": "Allows canvas navigation", - "title": "Move Tool" - }, - "nextImage": { - "desc": "Display the next image in gallery", - "title": "Next Image" - }, - "nextStagingImage": { - "desc": "Next Staging Area Image", - "title": "Next Staging Image" - }, - "nodesHotkeys": "Nodes", - "pinOptions": { - "desc": "Pin the options panel", - "title": "Pin Options" - }, - "previousImage": { - "desc": "Display the previous image in gallery", - "title": "Previous Image" - }, - "previousStagingImage": { - "desc": "Previous Staging Area Image", - "title": "Previous Staging Image" - }, - "quickToggleMove": { - "desc": "Temporarily toggles Move mode", - "title": "Quick Toggle Move" - }, - "redoStroke": { - "desc": "Redo a brush stroke", - "title": "Redo Stroke" - }, - "resetView": { - "desc": "Reset Canvas View", - "title": "Reset View" - }, - "restoreFaces": { - "desc": "Restore the current image", - "title": "Restore Faces" - }, - "saveToGallery": { - "desc": "Save current canvas to gallery", - "title": "Save To Gallery" - }, - "selectBrush": { - "desc": "Selects the canvas brush", - "title": "Select Brush" - }, - "selectEraser": { - "desc": "Selects the canvas eraser", - "title": "Select Eraser" - }, - "sendToImageToImage": { - "desc": "Send current image to Image to Image", - "title": "Send To Image To Image" - }, - "remixImage": { - "desc": "Use all parameters except seed from the current image", - "title": "Remix image" - }, - "setParameters": { - "desc": "Use all parameters of the current image", - "title": "Set Parameters" - }, - "setPrompt": { - "desc": "Use the prompt of the current image", - "title": "Set Prompt" - }, - "setSeed": { - "desc": "Use the seed of the current image", - "title": "Set Seed" - }, - "showHideBoundingBox": { - "desc": "Toggle visibility of bounding box", - "title": "Show/Hide Bounding Box" - }, - "showInfo": { - "desc": "Show metadata info of the current image", - "title": "Show Info" - }, - "toggleGallery": { - "desc": "Open and close the gallery drawer", - "title": "Toggle Gallery" - }, - "toggleOptions": { - "desc": "Open and close the options panel", - "title": "Toggle Options" - }, - "toggleOptionsAndGallery": { - "desc": "Open and close the options and gallery panels", - "title": "Toggle Options and Gallery" - }, - "resetOptionsAndGallery": { - "desc": "Resets the options and gallery panels", - "title": "Reset Options and Gallery" - }, - "toggleLayer": { - "desc": "Toggles mask/base layer selection", - "title": "Toggle Layer" - }, - "toggleSnap": { - "desc": "Toggles Snap to Grid", - "title": "Toggle Snap" - }, - "undoStroke": { - "desc": "Undo a brush stroke", - "title": "Undo Stroke" - }, - "unifiedCanvasHotkeys": "Unified Canvas", - "upscale": { - "desc": "Upscale the current image", - "title": "Upscale" - }, - "toggleViewer": { - "desc": "Switches between the Image Viewer and workspace for the current tab.", - "title": "Toggle Image Viewer" + "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", - "fit": "Image to image fit", + "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", - "initImage": "Initial image", + "imageSize": "Image Size", "metadata": "Metadata", "model": "Model", "negativePrompt": "Negative Prompt", @@ -642,20 +1040,50 @@ "parameterSet": "Parameter {{parameter}} set", "parsingFailed": "Parsing Failed", "positivePrompt": "Positive Prompt", + "qwen3Encoder": "Qwen3 Encoder", + "qwen3Source": "Qwen3 Source", "recallParameters": "Recall Parameters", "recallParameter": "Recall {{label}}", "scheduler": "Scheduler", - "seamless": "Seamless", + "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" + "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", @@ -663,59 +1091,144 @@ "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 \ud83e\udde8 Diffusers format.", + "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 InvokeAI root folder. If it is in a custom location, then it WILL NOT be deleted.", + "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.", - "hfToken": "HuggingFace Token", - "hfTokenHelperText": "A HF token is required to use checkpoint models. Click here to create or get your token.", + "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 copying the files. When using the model, it will be loaded from its this location. If disabled, the model file(s) will be copied into the Invoke-managed models directory during installation.", + "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", @@ -723,22 +1236,51 @@ "modelManager": "Model Manager", "modelName": "Model Name", "modelSettings": "Model Settings", - "modelsSynced": "Models Synced", - "modelSyncFailed": "Model Sync Failed", + "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", - "noModelsInstalled": "No Models Installed", + "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", + "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", @@ -751,29 +1293,118 @@ "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_one": "Found {{count}} orphaned model directory", + "foundOrphanedModels_other": "Found {{count}} orphaned model directories", + "filesCount_one": "{{count}} file", + "filesCount_other": "{{count}} files", + "deleteSelected_one": "Delete {{count}} selected", + "deleteSelected_other": "Delete {{count}} selected", + "deselectAll": "Deselect All", + "orphanedModelsDeleted_one": "Successfully deleted {{count}} orphaned model", + "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.", - "useDefaultSettings": "Use Default Settings", - "v2_768": "v2 (768px)", - "v2_base": "v2 (512px)", "vae": "VAE", "vaePrecision": "VAE Precision", "variant": "Variant", - "width": "Width" + "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", - "esrganModel": "ESRGAN Model", "loading": "loading", "noMatchingLoRAs": "No matching LoRAs", "noMatchingModels": "No matching Models", @@ -782,9 +1413,27 @@ "selectModel": "Select a Model", "noLoRAsInstalled": "No LoRAs installed", "noRefinerModelsInstalled": "No SDXL Refiner models installed", - "defaultVAE": "Default VAE" + "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", @@ -799,6 +1448,8 @@ "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)", @@ -810,6 +1461,7 @@ "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", @@ -824,6 +1476,8 @@ "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", @@ -831,7 +1485,11 @@ "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", @@ -839,23 +1497,27 @@ "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}}", - "noConnectionData": "No connection data", + "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.", - "noFieldType": "No field type", - "noMatchingNodes": "No matching nodes", + "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", @@ -864,6 +1526,8 @@ "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.", @@ -890,9 +1554,13 @@ "unknownNodeType": "Unknown node type", "unknownTemplate": "Unknown Template", "unknownInput": "Unknown input: {{name}}", - "unknownOutput": "Unknown output: {{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", @@ -919,12 +1587,36 @@ "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" + "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", @@ -942,77 +1634,113 @@ "controlNetControlMode": "Control Mode", "copyImage": "Copy Image", "denoisingStrength": "Denoising Strength", + "disabledNoRasterContent": "Disabled (No Raster Content)", + "disabledNotSupported": "Not supported by model", "downloadImage": "Download Image", "general": "General", - "globalSettings": "Global Settings", + "guidance": "Guidance", "height": "Height", "imageFit": "Fit Initial Image To Output Size", "images": "Images", + "images_withCount_one": "Image", + "images_withCount_other": "Images", "infillMethod": "Infill Method", - "infillMosaicTileWidth": "Tile Width", - "infillMosaicTileHeight": "Tile Height", - "infillMosaicMinColor": "Min Color", - "infillMosaicMaxColor": "Max Color", "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": "{{nodeLabel}} -> {{fieldLabel}} missing input", + "missingInputForField": "missing input", "missingNodeTemplate": "Missing node template", - "noControlImageForControlAdapter": "Control Adapter #{{number}} has no control image", - "imageNotProcessedForControlAdapter": "Control Adapter #{{number}}'s image is not processed", - "noInitialImageSelected": "No initial image selected", - "noModelForControlAdapter": "Control Adapter #{{number}} has no model selected.", - "incompatibleBaseModelForControlAdapter": "Control Adapter #{{number}} model is incompatible with main model.", + "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", - "layer": { - "initialImageNoImageSelected": "no initial image selected", - "controlAdapterNoModelSelected": "no Control Adapter model selected", - "controlAdapterIncompatibleBaseModel": "incompatible Control Adapter base model", - "controlAdapterNoImageSelected": "no Control Adapter image selected", - "controlAdapterImageNotProcessed": "Control Adapter image not processed", - "t2iAdapterIncompatibleDimensions": "T2I Adapter requires image dimension to be multiples of {{multiple}}", - "ipAdapterNoModelSelected": "no IP adapter selected", - "ipAdapterIncompatibleBaseModel": "incompatible IP Adapter base model", - "ipAdapterNoImageSelected": "no IP Adapter image selected", - "rgNoPromptsOrIPAdapters": "no text prompts or IP Adapters", - "rgNoRegion": "no region selected" - } + "promptExpansionPending": "Prompt expansion in progress", + "promptExpansionResultPending": "Please accept or discard your prompt expansion result" }, "maskBlur": "Mask Blur", "negativePromptPlaceholder": "Negative Prompt", - "globalNegativePromptPlaceholder": "Global Negative Prompt", "noiseThreshold": "Noise Threshold", "patchmatchDownScaleSize": "Downscale", "perlinNoise": "Perlin Noise", "positivePromptPlaceholder": "Positive Prompt", - "globalPositivePromptPlaceholder": "Global Positive Prompt", + "recallMetadata": "Recall Metadata", "iterations": "Iterations", "scale": "Scale", "scaleBeforeProcessing": "Scale Before Processing", "scaledHeight": "Scaled H", "scaledWidth": "Scaled W", "scheduler": "Scheduler", - "seamlessXAxis": "Seamless Tiling X Axis", - "seamlessYAxis": "Seamless Tiling Y Axis", + "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", - "sendToImg2Img": "Send to Image to Image", - "sendToUnifiedCanvas": "Send To Unified Canvas", + "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", - "upscale": "Upscale (Shift + U)", - "upscaleImage": "Upscale Image", + "postProcessing": "Post-Processing (Shift + U)", + "processImage": "Process Image", "upscaling": "Upscaling", "useAll": "Use All", "useSize": "Use Size", @@ -1020,19 +1748,48 @@ "remixImage": "Remix Image", "usePrompt": "Use Prompt", "useSeed": "Use Seed", + "useClipSkip": "Use CLIP Skip", "width": "Width", - "isAllowedToUpscale": { - "useX2Model": "Image is too large to upscale with x4 model, use x2 model", - "tooLarge": "Image is too large to upscale, select smaller image" - } + "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", - "promptsWithCount_one": "{{count}} Prompt", - "promptsWithCount_other": "{{count}} Prompts", "seedBehaviour": { "label": "Seed Behaviour", "perIterationLabel": "Seed per Iteration", @@ -1040,7 +1797,9 @@ "perPromptLabel": "Seed per Image", "perPromptDesc": "Use a different seed for each image" }, - "loading": "Generating Dynamic Prompts..." + "loading": "Generating Dynamic Prompts...", + "problemGeneratingPrompts": "Problem generating prompts", + "promptsToGenerate": "Prompts to Generate" }, "sdxl": { "cfgScale": "CFG Scale", @@ -1064,20 +1823,43 @@ "antialiasProgressImages": "Antialias Progress Images", "beta": "Beta", "confirmOnDelete": "Confirm On Delete", + "confirmOnNewSession": "Confirm On New Session", "developer": "Developer", "displayInProgress": "Display Progress Images", - "enableImageDebugging": "Enable Image Debugging", "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", + "generationDevices": "Generation Devices", + "generationDevicesAuto": "Auto (all GPUs)", + "generationDevicesHelp": "Select which devices to use for parallel generation, one session per device. \"Auto\" uses every available GPU.", + "generationDevicesRestart": "Changes take effect after restarting InvokeAI.", + "generationDevicesSaveFailed": "Failed to save Generation Devices", + "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.", - "shouldLogToConsole": "Console Logging", + "showDetailedInvocationProgress": "Show Progress Details", "showProgressInViewer": "Show Progress Images in Viewer", "ui": "User Interface", "clearIntermediatesDisabled": "Queue must be empty to clear intermediates", @@ -1090,34 +1872,50 @@ "intermediatesCleared_one": "Cleared {{count}} Intermediate", "intermediatesCleared_other": "Cleared {{count}} Intermediates", "intermediatesClearedFailed": "Problem Clearing Intermediates", - "reloadingIn": "Reloading in" + "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", + "addedToBoard": "Added to board {{name}}'s assets", + "addedToUncategorized": "Added to board $t(boards.uncategorized)'s assets", "baseModelChanged": "Base Model Changed", - "baseModelChangedCleared_one": "Cleared or disabled {{count}} incompatible submodel", - "baseModelChangedCleared_other": "Cleared or disabled {{count}} incompatible submodels", + "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", - "canvasCopiedClipboard": "Canvas Copied to Clipboard", - "canvasDownloaded": "Canvas Downloaded", - "canvasMerged": "Canvas Merged", - "canvasSavedGallery": "Canvas Saved to Gallery", - "canvasSentControlnetAssets": "Canvas Sent to ControlNet & Assets", "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", - "maskSavedAssets": "Mask Saved to Assets", - "maskSentControlnetAssets": "Mask Sent to ControlNet & Assets", - "metadataLoadFailed": "Failed to load metadata", "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", @@ -1128,49 +1926,65 @@ "parametersSet": "Parameters Recalled", "parametersNotSet": "Parameters Not Recalled", "errorCopied": "Error Copied", - "problemCopyingCanvas": "Problem Copying Canvas", - "problemCopyingCanvasDesc": "Unable to export base layer", "problemCopyingImage": "Unable to Copy Image", + "problemCopyingLayer": "Unable to Copy Layer", + "problemSavingLayer": "Unable to Save Layer", "problemDownloadingImage": "Unable to Download Image", - "problemDownloadingCanvas": "Problem Downloading Canvas", - "problemDownloadingCanvasDesc": "Unable to export base layer", - "problemImportingMask": "Problem Importing Mask", - "problemImportingMaskDesc": "Unable to export mask", - "problemMergingCanvas": "Problem Merging Canvas", - "problemMergingCanvasDesc": "Unable to export base layer", - "problemSavingCanvas": "Problem Saving Canvas", - "problemSavingCanvasDesc": "Unable to export base layer", - "problemSavingMask": "Problem Saving Mask", - "problemSavingMaskDesc": "Unable to export mask", + "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", - "resetInitialImage": "Reset Initial Image", - "sentToImageToImage": "Sent To Image To Image", - "sentToUnifiedCanvas": "Sent to Unified Canvas", + "sentToCanvas": "Sent to Canvas", + "sentToUpscale": "Sent to Upscale", "serverError": "Server Error", "sessionRef": "Session: {{sessionId}}", - "setAsCanvasInitialImage": "Set as canvas initial image", - "setCanvasInitialImage": "Set canvas initial image", "setControlImage": "Set as control image", - "setInitialImage": "Set as initial image", "setNodeField": "Set as node field", "somethingWentWrong": "Something Went Wrong", "uploadFailed": "Upload failed", - "uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image", - "uploadInitialImage": "Upload Initial Image", + "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" - }, - "tooltip": { - "feature": { - "boundingBox": "The bounding box is the same as the Width and Height settings for Text to Image or Image to Image. Only the area in the box will be processed.", - "gallery": "Gallery displays generations from the outputs folder as they're created. Settings are stored within files and accesed by context menu.", - "other": "These options will enable alternative processing modes for Invoke. 'Seamless tiling' will create repeating patterns in the output. 'High resolution' is generation in two steps with img2img: use this setting when you want a larger and more coherent image without artifacts. It will take longer than usual txt2img.", - "prompt": "This is the prompt field. Prompt includes generation objects and stylistic terms. You can add weight (token importance) in the prompt as well, but CLI commands and parameters will not work.", - "seed": "Seed value affects the initial noise from which the image is formed. You can use the already existing seeds from previous images. 'Noise Threshold' is used to mitigate artifacts at high CFG values (try the 0-10 range), and Perlin to add Perlin noise during generation: both serve to add variation to your outputs.", - "upscale": "Use ESRGAN to enlarge the image immediately after generation." - } + "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": { @@ -1201,6 +2015,50 @@ "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."] @@ -1232,6 +2090,33 @@ "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": [ @@ -1241,8 +2126,9 @@ "controlNetBeginEnd": { "heading": "Begin / End Step Percentage", "paragraphs": [ - "The part of the of the denoising process that will have the Control Adapter applied.", - "Generally, Control Adapters applied at the start of the process guide composition, and Control Adapters applied at the end guide details." + "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": { @@ -1252,7 +2138,7 @@ "controlNetProcessor": { "heading": "Processor", "paragraphs": [ - "Method of processing the input image to guide the generation process. Different processors will providedifferent effects or styles in your generated images." + "Method of processing the input image to guide the generation process. Different processors will provide different effects or styles in your generated images." ] }, "controlNetResizeMode": { @@ -1260,13 +2146,15 @@ "paragraphs": ["Method to fit Control Adapter's input image size to the output generation size."] }, "ipAdapterMethod": { - "heading": "Method", - "paragraphs": ["Method by which to apply the current IP Adapter."] + "heading": "Mode", + "paragraphs": ["The mode defines how the reference image will guide the generation process."] }, "controlNetWeight": { "heading": "Weight", "paragraphs": [ - "Weight of the Control Adapter. Higher weight will lead to larger impacts on the final image." + "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": { @@ -1330,6 +2218,13 @@ "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": [ @@ -1340,8 +2235,9 @@ "paramDenoisingStrength": { "heading": "Denoising Strength", "paragraphs": [ - "How much noise is added to the input image.", - "0 will result in an identical image, while 1 will result in a completely new image." + "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": { @@ -1475,84 +2371,108 @@ "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." + ] } }, - "unifiedCanvas": { - "accept": "Accept", - "activeLayer": "Active Layer", - "antialiasing": "Antialiasing", - "autoSaveToGallery": "Auto Save to Gallery", - "base": "Base", - "boundingBox": "Bounding Box", - "boundingBoxPosition": "Bounding Box Position", - "brush": "Brush", - "brushOptions": "Brush Options", - "brushSize": "Size", - "canvasDimensions": "Canvas Dimensions", - "canvasPosition": "Canvas Position", - "canvasScale": "Canvas Scale", - "canvasSettings": "Canvas Settings", - "clearCanvas": "Clear Canvas", - "clearCanvasHistory": "Clear Canvas History", - "clearCanvasHistoryConfirm": "Are you sure you want to clear the canvas history?", - "clearCanvasHistoryMessage": "Clearing the canvas history leaves your current canvas intact, but irreversibly clears the undo and redo history.", - "clearHistory": "Clear History", - "clearMask": "Clear Mask (Shift+C)", - "coherenceModeGaussianBlur": "Gaussian Blur", - "coherenceModeBoxBlur": "Box Blur", - "coherenceModeStaged": "Staged", - "colorPicker": "Color Picker", - "copyToClipboard": "Copy to Clipboard", - "cursorPosition": "Cursor Position", - "darkenOutsideSelection": "Darken Outside Selection", - "discardAll": "Discard All", - "discardCurrent": "Discard Current", - "downloadAsImage": "Download As Image", - "enableMask": "Enable Mask", - "eraseBoundingBox": "Erase Bounding Box", - "eraser": "Eraser", - "fillBoundingBox": "Fill Bounding Box", - "hideBoundingBox": "Hide Bounding Box", - "initialFitImageSize": "Fit Image Size on Drop", - "invertBrushSizeScrollDirection": "Invert Scroll for Brush Size", - "layer": "Layer", - "limitStrokesToBox": "Limit Strokes to Box", - "mask": "Mask", - "maskingOptions": "Masking Options", - "mergeVisible": "Merge Visible", - "move": "Move", - "next": "Next", - "preserveMaskedArea": "Preserve Masked Area", - "previous": "Previous", - "redo": "Redo", - "resetView": "Reset View", - "saveBoxRegionOnly": "Save Box Region Only", - "saveMask": "Save $t(unifiedCanvas.mask)", - "saveToGallery": "Save To Gallery", - "scaledBoundingBox": "Scaled Bounding Box", - "showBoundingBox": "Show Bounding Box", - "showCanvasDebugInfo": "Show Additional Canvas Info", - "showGrid": "Show Grid", - "showResultsOn": "Show Results (On)", - "showResultsOff": "Show Results (Off)", - "showIntermediates": "Show Intermediates", - "snapToGrid": "Snap to Grid", - "undo": "Undo" - }, "workflows": { + "chooseWorkflowFromLibrary": "Choose Workflow from Library", + "defaultWorkflows": "Default Workflows", + "userWorkflows": "User Workflows", + "projectWorkflows": "Project Workflows", "ascending": "Ascending", "created": "Created", "descending": "Descending", "workflows": "Workflows", - "workflowLibrary": "Library", - "userWorkflows": "My Workflows", - "defaultWorkflows": "Default Workflows", - "projectWorkflows": "Project 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", @@ -1562,8 +2482,6 @@ "problemSavingWorkflow": "Problem Saving Workflow", "workflowSaved": "Workflow Saved", "name": "Name", - "noRecentWorkflows": "No Recent Workflows", - "noUserWorkflows": "No User Workflows", "noWorkflows": "No Workflows", "problemLoading": "Problem Loading Workflows", "loading": "Loading Workflows", @@ -1577,58 +2495,1053 @@ "loadFromGraph": "Load Workflow from Graph", "convertGraph": "Convert Graph", "loadWorkflow": "$t(common.load) Workflow", - "autoLayout": "Auto Layout" - }, - "app": { - "storeNotInitialized": "Store is not initialized" + "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": { - "deleteAll": "Delete All", + "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", - "brushSize": "Brush Size", - "controlLayers": "Control Layers", - "globalMaskOpacity": "Global Mask Opacity", + "width": "Width", "autoNegative": "Auto Negative", + "enableAutoNegative": "Enable Auto Negative", + "disableAutoNegative": "Disable Auto Negative", "deletePrompt": "Delete Prompt", - "resetRegion": "Reset Region", - "debugLayers": "Debug Layers", + "deleteReferenceImage": "Delete Reference Image", + "disableReferenceImage": "Disable Reference Image", + "enableReferenceImage": "Enable Reference Image", + "showHUD": "Show HUD", "rectangle": "Rectangle", - "maskPreviewColor": "Mask Preview Color", - "addPositivePrompt": "Add $t(common.positivePrompt)", - "addNegativePrompt": "Add $t(common.negativePrompt)", - "addIPAdapter": "Add $t(common.ipAdapter)", + "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", - "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", + "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", - "globalControlAdapter": "Global $t(controlnet.controlAdapter_one)", - "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", - "globalIPAdapter": "Global $t(common.ipAdapter)", - "globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)", - "globalInitialImage": "Global Initial Image", - "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", - "opacityFilter": "Opacity Filter", - "clearProcessor": "Clear Processor", - "resetProcessor": "Reset Processor to Defaults", - "noLayersAdded": "No Layers Added", - "layers_one": "Layer", - "layers_other": "Layers" + "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": { - "generation": "Generation", - "generationTab": "$t(ui.tabs.generation) $t(common.tab)", + "generate": "Generate", "canvas": "Canvas", - "canvasTab": "$t(ui.tabs.canvas) $t(common.tab)", "workflows": "Workflows", "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", "models": "Models", "modelsTab": "$t(ui.tabs.models) $t(common.tab)", "queue": "Queue", - "queueTab": "$t(ui.tabs.queue) $t(common.tab)" + "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", + "canvas-workflow-integration": "Canvas Workflow Integration", + "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 index 52ee3b5fe3f..8f68ea585c2 100644 --- a/invokeai/frontend/web/public/locales/es.json +++ b/invokeai/frontend/web/public/locales/es.json @@ -5,7 +5,6 @@ "reportBugLabel": "Reportar errores", "settingsLabel": "Ajustes", "img2img": "Imagen a Imagen", - "unifiedCanvas": "Lienzo Unificado", "nodes": "Flujos de trabajo", "upload": "Subir imagen", "load": "Cargar", @@ -14,7 +13,7 @@ "discordLabel": "Discord", "back": "Atrás", "loading": "Cargando", - "postprocessing": "Postprocesado", + "postprocessing": "Postprocesamiento", "txt2img": "De texto a imagen", "accept": "Aceptar", "cancel": "Cancelar", @@ -48,11 +47,8 @@ "editor": "Editor", "orderBy": "Ordenar por", "file": "Archivo", - "goTo": "Ir a", - "imageFailedToLoad": "No se puede cargar la imagen", "saveAs": "Guardar Como", "somethingWentWrong": "Algo salió mal", - "nextPage": "Página Siguiente", "selected": "Seleccionado", "tab": "Tabulador", "positivePrompt": "Prompt Positivo", @@ -61,13 +57,10 @@ "format": "formato", "unknown": "Desconocido", "input": "Entrada", - "nodeEditor": "Editor de nodos", "template": "Plantilla", - "prevPage": "Página Anterior", "red": "Rojo", "alpha": "Transparencia", - "outputs": "Salidas", - "editing": "Editando", + "outputs": "Resultados", "learnMore": "Aprende más", "enabled": "Activado", "disabled": "Desactivado", @@ -77,225 +70,92 @@ "save": "Guardar", "unknownError": "Error Desconocido", "blue": "Azul", - "viewingDesc": "Revisar imágenes en una vista de galería grande" + "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", - "loadMore": "Cargar más", - "noImagesInGallery": "No hay imágenes para mostrar", "deleteImage_one": "Eliminar Imagen", - "deleteImage_many": "", - "deleteImage_other": "", - "deleteImageBin": "Las imágenes eliminadas se enviarán a la papelera de tu sistema operativo.", + "deleteImage_many": "Eliminar {{count}} Imágenes", + "deleteImage_other": "Eliminar {{count}} Imágenes", "deleteImagePermanent": "Las imágenes eliminadas no se pueden restaurar.", - "assets": "Activos", - "autoAssignBoardOnClick": "Asignación automática de tableros al hacer clic" - }, - "hotkeys": { - "keyboardShortcuts": "Atajos de teclado", - "appHotkeys": "Atajos de applicación", - "generalHotkeys": "Atajos generales", - "galleryHotkeys": "Atajos de galería", - "unifiedCanvasHotkeys": "Atajos de lienzo unificado", - "invoke": { - "title": "Invocar", - "desc": "Generar una imagen" - }, - "cancel": { - "title": "Cancelar", - "desc": "Cancelar el proceso de generación de imagen" - }, - "focusPrompt": { - "title": "Mover foco a Entrada de texto", - "desc": "Mover foco hacia el campo de texto de la Entrada" - }, - "toggleOptions": { - "title": "Alternar opciones", - "desc": "Mostar y ocultar el panel de opciones" - }, - "pinOptions": { - "title": "Fijar opciones", - "desc": "Fijar el panel de opciones" - }, - "toggleGallery": { - "title": "Alternar galería", - "desc": "Mostar y ocultar la galería de imágenes" - }, - "maximizeWorkSpace": { - "title": "Maximizar espacio de trabajo", - "desc": "Cerrar otros páneles y maximizar el espacio de trabajo" - }, - "changeTabs": { - "title": "Cambiar", - "desc": "Cambiar entre áreas de trabajo" - }, - "consoleToggle": { - "title": "Alternar consola", - "desc": "Mostar y ocultar la consola" - }, - "setPrompt": { - "title": "Establecer Entrada", - "desc": "Usar el texto de entrada de la imagen actual" - }, - "setSeed": { - "title": "Establecer semilla", - "desc": "Usar la semilla de la imagen actual" - }, - "setParameters": { - "title": "Establecer parámetros", - "desc": "Usar todos los parámetros de la imagen actual" - }, - "restoreFaces": { - "title": "Restaurar rostros", - "desc": "Restaurar rostros en la imagen actual" - }, - "upscale": { - "title": "Aumentar resolución", - "desc": "Aumentar la resolución de la imagen actual" - }, - "showInfo": { - "title": "Mostrar información", - "desc": "Mostar metadatos de la imagen actual" - }, - "sendToImageToImage": { - "title": "Enviar hacia Imagen a Imagen", - "desc": "Enviar imagen actual hacia Imagen a Imagen" - }, - "deleteImage": { - "title": "Eliminar imagen", - "desc": "Eliminar imagen actual" - }, - "closePanels": { - "title": "Cerrar páneles", - "desc": "Cerrar los páneles abiertos" - }, - "previousImage": { - "title": "Imagen anterior", - "desc": "Muetra la imagen anterior en la galería" - }, - "nextImage": { - "title": "Imagen siguiente", - "desc": "Muetra la imagen siguiente en la galería" - }, - "increaseGalleryThumbSize": { - "title": "Aumentar imagen en galería", - "desc": "Aumenta el tamaño de las miniaturas de la galería" - }, - "decreaseGalleryThumbSize": { - "title": "Reducir imagen en galería", - "desc": "Reduce el tamaño de las miniaturas de la galería" - }, - "selectBrush": { - "title": "Seleccionar pincel", - "desc": "Selecciona el pincel en el lienzo" - }, - "selectEraser": { - "title": "Seleccionar borrador", - "desc": "Selecciona el borrador en el lienzo" - }, - "decreaseBrushSize": { - "title": "Disminuir tamaño de herramienta", - "desc": "Disminuye el tamaño del pincel/borrador en el lienzo" - }, - "increaseBrushSize": { - "title": "Aumentar tamaño del pincel", - "desc": "Aumenta el tamaño del pincel en el lienzo" - }, - "decreaseBrushOpacity": { - "title": "Disminuir opacidad del pincel", - "desc": "Disminuye la opacidad del pincel en el lienzo" - }, - "increaseBrushOpacity": { - "title": "Aumentar opacidad del pincel", - "desc": "Aumenta la opacidad del pincel en el lienzo" - }, - "moveTool": { - "title": "Herramienta de movimiento", - "desc": "Permite navegar por el lienzo" - }, - "fillBoundingBox": { - "title": "Rellenar Caja contenedora", - "desc": "Rellena la caja contenedora con el color seleccionado" - }, - "eraseBoundingBox": { - "title": "Borrar Caja contenedora", - "desc": "Borra el contenido dentro de la caja contenedora" - }, - "colorPicker": { - "title": "Selector de color", - "desc": "Selecciona un color del lienzo" - }, - "toggleSnap": { - "title": "Alternar ajuste de cuadrícula", - "desc": "Activa o desactiva el ajuste automático a la cuadrícula" - }, - "quickToggleMove": { - "title": "Alternar movimiento rápido", - "desc": "Activa momentáneamente la herramienta de movimiento" - }, - "toggleLayer": { - "title": "Alternar capa", - "desc": "Alterna entre las capas de máscara y base" - }, - "clearMask": { - "title": "Limpiar máscara", - "desc": "Limpia toda la máscara actual" - }, - "hideMask": { - "title": "Ocultar máscara", - "desc": "Oculta o muetre la máscara actual" - }, - "showHideBoundingBox": { - "title": "Alternar caja contenedora", - "desc": "Muestra u oculta la caja contenedora" - }, - "mergeVisible": { - "title": "Consolida capas visibles", - "desc": "Consolida todas las capas visibles en una sola" - }, - "saveToGallery": { - "title": "Guardar en galería", - "desc": "Guardar la imagen actual del lienzo en la galería" - }, - "copyToClipboard": { - "title": "Copiar al portapapeles", - "desc": "Copiar el lienzo actual al portapapeles" - }, - "downloadImage": { - "title": "Descargar imagen", - "desc": "Descargar la imagen actual del lienzo" - }, - "undoStroke": { - "title": "Deshar trazo", - "desc": "Desahacer el último trazo del pincel" - }, - "redoStroke": { - "title": "Rehacer trazo", - "desc": "Rehacer el último trazo del pincel" - }, - "resetView": { - "title": "Restablecer vista", - "desc": "Restablecer la vista del lienzo" - }, - "previousStagingImage": { - "title": "Imagen anterior", - "desc": "Imagen anterior en el área de preparación" - }, - "nextStagingImage": { - "title": "Imagen siguiente", - "desc": "Siguiente imagen en el área de preparación" - }, - "acceptStagingImage": { - "title": "Aceptar imagen", - "desc": "Aceptar la imagen actual en el área de preparación" - }, - "addNodes": { - "title": "Añadir Nodos", - "desc": "Abre el menú para añadir nodos" - }, - "nodesHotkeys": "Teclas de acceso rápido a los nodos" + "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", @@ -330,9 +190,7 @@ "alpha": "Alfa", "allModels": "Todos los modelos", "repo_id": "Identificador del repositorio", - "v2_base": "v2 (512px)", "none": "ninguno", - "v2_768": "v2 (768px)", "vae": "VAE", "variant": "Variante", "baseModel": "Modelo básico", @@ -344,8 +202,12 @@ "modelDeleteFailed": "Error al borrar el modelo", "settings": "Ajustes", "syncModels": "Sincronizar las plantillas", - "modelsSynced": "Plantillas sincronizadas", - "modelSyncFailed": "La sincronización de la plantilla falló" + "clipEmbed": "Incrustar CLIP", + "addModels": "Añadir modelos", + "advanced": "Avanzado", + "clipGEmbed": "Incrustar CLIP-G", + "cancel": "Cancelar", + "clipLEmbed": "Incrustar CLIP-L" }, "parameters": { "images": "Imágenes", @@ -360,8 +222,6 @@ "type": "Tipo", "strength": "Fuerza", "upscaling": "Aumento de resolución", - "upscale": "Aumentar resolución", - "upscaleImage": "Aumentar la resolución de la imagen", "scale": "Escala", "imageFit": "Ajuste tamaño de imagen inicial al tamaño objetivo", "scaleBeforeProcessing": "Redimensionar antes de procesar", @@ -369,26 +229,22 @@ "scaledHeight": "Alto escalado", "infillMethod": "Método de relleno", "tileSize": "Tamaño del mosaico", - "sendToImg2Img": "Enviar a Imagen a Imagen", - "sendToUnifiedCanvas": "Enviar a Lienzo Unificado", - "downloadImage": "Descargar imagen", "usePrompt": "Usar Entrada", "useSeed": "Usar Semilla", "useAll": "Usar Todo", "info": "Información", - "showOptionsPanel": "Mostrar panel de opciones", "symmetry": "Simetría", "copyImage": "Copiar la imagen", "general": "General", "denoisingStrength": "Intensidad de la eliminación del ruido", - "seamlessXAxis": "Eje x", - "seamlessYAxis": "Eje y", + "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": "Difuminar", + "maskBlur": "Desenfoque de máscara", "patchmatchDownScaleSize": "Reducir a escala", "coherenceMode": "Modo" }, @@ -396,98 +252,43 @@ "models": "Modelos", "displayInProgress": "Mostrar las imágenes del progreso", "confirmOnDelete": "Confirmar antes de eliminar", - "enableImageDebugging": "Habilitar depuración de imágenes", "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", - "shouldLogToConsole": "Registro de la consola", "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" + "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", - "imageNotLoadedDesc": "No se pudo encontrar la imagen", - "canvasMerged": "Lienzo consolidado", - "sentToImageToImage": "Enviar hacia Imagen a Imagen", - "sentToUnifiedCanvas": "Enviar hacia Lienzo Consolidado", "parametersNotSet": "Parámetros no recuperados", - "metadataLoadFailed": "Error al cargar metadatos", "serverError": "Error en el servidor", "canceled": "Procesando la cancelación", "connected": "Conectado al servidor", - "uploadFailedInvalidUploadDesc": "Debe ser una sola imagen PNG o JPEG", - "parameterSet": "Conjunto de parámetros", - "parameterNotSet": "Parámetro no configurado", + "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" - }, - "tooltip": { - "feature": { - "prompt": "Este campo tomará todo el texto de entrada, incluidos tanto los términos de contenido como los estilísticos. Si bien se pueden incluir pesos en la solicitud, los comandos/parámetros estándar de línea de comandos no funcionarán.", - "gallery": "Conforme se generan nuevas invocaciones, los archivos del directorio de salida se mostrarán aquí. Las generaciones tienen opciones adicionales para configurar nuevas generaciones.", - "other": "Estas opciones habilitarán modos de procesamiento alternativos para Invoke. 'Seamless mosaico' creará patrones repetitivos en la salida. 'Alta resolución' es la generación en dos pasos con img2img: use esta configuración cuando desee una imagen más grande y más coherente sin artefactos. tomar más tiempo de lo habitual txt2img.", - "seed": "Los valores de semilla proporcionan un conjunto inicial de ruido que guían el proceso de eliminación de ruido y se pueden aleatorizar o rellenar con una semilla de una invocación anterior. La función Umbral se puede usar para mitigar resultados indeseables a valores CFG más altos (intente entre 0-10), y Perlin se puede usar para agregar ruido Perlin al proceso de eliminación de ruido. Ambos sirven para agregar variación a sus salidas.", - "upscale": "Usando ESRGAN, puede aumentar la resolución de salida sin requerir un ancho/alto más alto en la generación inicial.", - "boundingBox": "La caja delimitadora es análoga a las configuraciones de Ancho y Alto para Texto a Imagen o Imagen a Imagen. Solo se procesará el área en la caja." - } - }, - "unifiedCanvas": { - "layer": "Capa", - "base": "Base", - "mask": "Máscara", - "maskingOptions": "Opciones de máscara", - "enableMask": "Habilitar Máscara", - "preserveMaskedArea": "Preservar área enmascarada", - "clearMask": "Limpiar máscara", - "brush": "Pincel", - "eraser": "Borrador", - "fillBoundingBox": "Rellenar Caja Contenedora", - "eraseBoundingBox": "Eliminar Caja Contenedora", - "colorPicker": "Selector de color", - "brushOptions": "Opciones de pincel", - "brushSize": "Tamaño", - "move": "Mover", - "resetView": "Restablecer vista", - "mergeVisible": "Consolidar vista", - "saveToGallery": "Guardar en galería", - "copyToClipboard": "Copiar al portapapeles", - "downloadAsImage": "Descargar como imagen", - "undo": "Deshacer", - "redo": "Rehacer", - "clearCanvas": "Limpiar lienzo", - "canvasSettings": "Ajustes de lienzo", - "showIntermediates": "Mostrar intermedios", - "showGrid": "Mostrar cuadrícula", - "snapToGrid": "Ajustar a cuadrícula", - "darkenOutsideSelection": "Oscurecer fuera de la selección", - "autoSaveToGallery": "Guardar automáticamente en galería", - "saveBoxRegionOnly": "Guardar solo región dentro de la caja", - "limitStrokesToBox": "Limitar trazos a la caja", - "showCanvasDebugInfo": "Mostrar la información adicional del lienzo", - "clearCanvasHistory": "Limpiar historial de lienzo", - "clearHistory": "Limpiar historial", - "clearCanvasHistoryMessage": "Limpiar el historial de lienzo también restablece completamente el lienzo unificado. Esto incluye todo el historial de deshacer/rehacer, las imágenes en el área de preparación y la capa base del lienzo.", - "clearCanvasHistoryConfirm": "¿Está seguro de que desea limpiar el historial del lienzo?", - "activeLayer": "Capa activa", - "canvasScale": "Escala de lienzo", - "boundingBox": "Caja contenedora", - "scaledBoundingBox": "Caja contenedora escalada", - "boundingBoxPosition": "Posición de caja contenedora", - "canvasDimensions": "Dimensiones de lienzo", - "canvasPosition": "Posición de lienzo", - "cursorPosition": "Posición del cursor", - "previous": "Anterior", - "next": "Siguiente", - "accept": "Aceptar", - "discardAll": "Descartar todo", - "antialiasing": "Suavizado" + "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", @@ -495,27 +296,26 @@ "uploadImage": "Cargar imagen", "previousImage": "Imagen anterior", "nextImage": "Siguiente imagen", - "showOptionsPanel": "Mostrar el panel lateral", "menu": "Menú", - "showGalleryPanel": "Mostrar panel de galería", - "loadMore": "Cargar más", "about": "Acerca de", "createIssue": "Crear un problema", "resetUI": "Interfaz de usuario $t(accessibility.reset)", "mode": "Modo", - "submitSupportTicket": "Enviar Ticket de Soporte" + "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", - "hideLegendNodes": "Ocultar la leyenda del tipo de campo", - "showLegendNodes": "Mostrar la leyenda del tipo de campo", "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" + "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", @@ -529,10 +329,10 @@ "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 eliminar este panel y las imágenes que contiene, se restablecerán las funciones que los estén utilizando actualmente.", + "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", + "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…", @@ -540,12 +340,43 @@ "downloadBoard": "Descargar panel", "deleteBoardOnly": "Borrar solo el panel", "myBoard": "Mi panel", - "noMatching": "No hay paneles que coincidan" + "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" + "infillTab": "Relleno", + "coherenceTab": "Parámetros de la coherencia" }, "generation": { "title": "Generación" @@ -563,33 +394,587 @@ }, "ui": { "tabs": { - "generationTab": "$t(ui.tabs.generation) $t(common.tab)", "canvas": "Lienzo", - "generation": "Generación", "queue": "Cola", - "queueTab": "$t(ui.tabs.queue) $t(common.tab)", "workflows": "Flujos de trabajo", "models": "Modelos", "modelsTab": "$t(ui.tabs.models) $t(common.tab)", - "canvasTab": "$t(ui.tabs.canvas) $t(common.tab)", - "workflowsTab": "$t(ui.tabs.workflows) $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": { - "layers_one": "Capa", - "layers_many": "Capas", - "layers_other": "Capas" + "layer_one": "Capa", + "layer_many": "Capas", + "layer_other": "Capas", + "copyToClipboard": "Copiar al portapapeles" }, - "controlnet": { - "crop": "Cortar", - "delete": "Eliminar", - "depthAnythingDescription": "Generación de mapa de profundidad usando la técnica de Depth Anything", - "duplicate": "Duplicar", - "colorMapDescription": "Genera un mapa de color desde la imagen", - "depthMidasDescription": "Crea un mapa de profundidad con Midas", - "balanced": "Equilibrado", - "beginEndStepPercent": "Inicio / Final Porcentaje de pasos", - "detectResolution": "Detectar resolución", - "beginEndStepPercentShort": "Inicio / Final %" + "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 index c46ac9a5367..54e5a666605 100644 --- a/invokeai/frontend/web/public/locales/fi.json +++ b/invokeai/frontend/web/public/locales/fi.json @@ -5,7 +5,7 @@ "invokeProgressBar": "Invoken edistymispalkki", "nextImage": "Seuraava kuva", "previousImage": "Edellinen kuva", - "showOptionsPanel": "Näytä asetukset" + "uploadImages": "Lähetä Kuva(t)" }, "common": { "languagePickerLabel": "Kielen valinta", @@ -24,28 +24,34 @@ "back": "Takaisin", "statusDisconnected": "Yhteys katkaistu", "loading": "Ladataan", - "txt2img": "Teksti kuvaksi", - "unifiedCanvas": "Yhdistetty kanvas" + "txt2img": "Teksti kuvaksi" }, "gallery": { "galleryImageSize": "Kuvan koko", "gallerySettings": "Gallerian asetukset", - "autoSwitchNewImages": "Vaihda uusiin kuviin automaattisesti", - "noImagesInGallery": "Ei kuvia galleriassa", - "loadMore": "Lataa lisää" + "autoSwitchNewImages": "Vaihda uusiin kuviin automaattisesti" }, - "hotkeys": { - "keyboardShortcuts": "näppäimistön pikavalinnat", - "appHotkeys": "Sovelluksen pikanäppäimet", - "generalHotkeys": "Yleiset pikanäppäimet", - "galleryHotkeys": "Gallerian pikanäppäimet", - "unifiedCanvasHotkeys": "Yhdistetyn kanvaan pikanäppäimet", - "cancel": { - "desc": "Peruuta kuvan luominen", - "title": "Peruuta" + "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." }, - "invoke": { - "desc": "Luo kuva" + "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 index b8f560e2650..4d5d9cfcb77 100644 --- a/invokeai/frontend/web/public/locales/fr.json +++ b/invokeai/frontend/web/public/locales/fr.json @@ -1,13 +1,12 @@ { "common": { "hotkeysLabel": "Raccourcis clavier", - "languagePickerLabel": "Sélecteur de langue", + "languagePickerLabel": "Langue", "reportBugLabel": "Signaler un bug", "settingsLabel": "Paramètres", - "img2img": "Image en image", - "unifiedCanvas": "Canvas unifié", - "nodes": "Nœuds", - "upload": "Télécharger", + "img2img": "Image vers Image", + "nodes": "Workflows", + "upload": "Importer", "load": "Charger", "back": "Retour", "statusDisconnected": "Hors ligne", @@ -16,358 +15,2248 @@ "accept": "Accepter", "cancel": "Annuler", "loading": "Chargement", - "txt2img": "Texte vers image", - "postprocessing": "Post-Traitement" + "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", - "loadMore": "Charger plus", - "noImagesInGallery": "Aucune image dans la galerie" + "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" }, - "hotkeys": { - "keyboardShortcuts": "Raccourcis clavier", - "appHotkeys": "Raccourcis de l'application", - "generalHotkeys": "Raccourcis généraux", - "galleryHotkeys": "Raccourcis de la galerie", - "unifiedCanvasHotkeys": "Raccourcis du canvas unifié", + "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": { - "title": "Invoquer", - "desc": "Générer une image" + "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": { - "title": "Annuler", - "desc": "Annuler la génération d'image" + "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." + ] }, - "focusPrompt": { - "title": "Prompt de focus", - "desc": "Mettre en focus la zone de saisie de la commande" + "controlNetControlMode": { + "paragraphs": [ + "Accordez plus de poids soit au prompt, soit au ControlNet." + ], + "heading": "Mode de Contrôle" }, - "toggleOptions": { - "title": "Affichage des options", - "desc": "Afficher et masquer le panneau d'options" + "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." + ] }, - "pinOptions": { - "title": "Epinglage des options", - "desc": "Epingler le panneau d'options" + "paramVAE": { + "heading": "VAE", + "paragraphs": [ + "Modèle utilisé pour convertir la sortie de l'IA en l'image finale." + ] }, - "toggleGallery": { - "title": "Affichage de la galerie", - "desc": "Afficher et masquer la galerie" + "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." + ] }, - "maximizeWorkSpace": { - "title": "Maximiser la zone de travail", - "desc": "Fermer les panneaux et maximiser la zone de travail" + "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." + ] }, - "changeTabs": { - "title": "Changer d'onglet", - "desc": "Passer à un autre espace de travail" + "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" }, - "consoleToggle": { - "title": "Affichage de la console", - "desc": "Afficher et masquer la console" + "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." + ] }, - "setPrompt": { - "title": "Définir le prompt", - "desc": "Utiliser le prompt de l'image actuelle" + "compositingCoherencePass": { + "heading": "Passe de cohérence", + "paragraphs": [ + "Un deuxième tour de débruitage aide à composer l'image remplie/étendue." + ] }, - "setSeed": { - "title": "Définir la graine", - "desc": "Utiliser la graine de l'image actuelle" + "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." + ] }, - "setParameters": { - "title": "Définir les paramètres", - "desc": "Utiliser tous les paramètres de l'image actuelle" + "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." + ] }, - "restoreFaces": { - "title": "Restaurer les visages", - "desc": "Restaurer l'image actuelle" + "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." + ] }, - "upscale": { - "title": "Agrandir", - "desc": "Agrandir l'image actuelle" + "compositingBlurMethod": { + "heading": "Méthode de flou", + "paragraphs": [ + "La méthode de flou appliquée à la zone masquée." + ] }, - "showInfo": { - "title": "Afficher les informations", - "desc": "Afficher les informations de métadonnées de l'image actuelle" + "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." + ] }, - "sendToImageToImage": { - "title": "Envoyer à l'image à l'image", - "desc": "Envoyer l'image actuelle à l'image à l'image" + "dynamicPromptsMaxPrompts": { + "heading": "Max Prompts", + "paragraphs": [ + "Limite le nombre de prompts pouvant être générés par les Prompts Dynamiques." + ] }, - "deleteImage": { - "title": "Supprimer l'image", - "desc": "Supprimer l'image actuelle" + "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é." + ] }, - "closePanels": { - "title": "Fermer les panneaux", - "desc": "Fermer les panneaux ouverts" + "lora": { + "heading": "LoRA", + "paragraphs": [ + "Modèles légers utilisés en conjonction avec des modèles de base." + ] }, - "previousImage": { - "title": "Image précédente", - "desc": "Afficher l'image précédente dans la galerie" + "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." + ] }, - "nextImage": { - "title": "Image suivante", - "desc": "Afficher l'image suivante dans la galerie" + "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. " + ] }, - "increaseGalleryThumbSize": { - "title": "Augmenter la taille des miniatures de la galerie", - "desc": "Augmente la taille des miniatures de la galerie" + "loraWeight": { + "heading": "Poids", + "paragraphs": [ + "Poids du LoRA. Un poids plus élevé aura un impact plus important sur l'image finale." + ] }, - "decreaseGalleryThumbSize": { - "title": "Diminuer la taille des miniatures de la galerie", - "desc": "Diminue la taille des miniatures de la galerie" + "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." + ] }, - "selectBrush": { - "title": "Sélectionner un pinceau", - "desc": "Sélectionne le pinceau de la toile" + "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." + ] }, - "selectEraser": { - "title": "Sélectionner un gomme", - "desc": "Sélectionne la gomme de la toile" + "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." + ] }, - "decreaseBrushSize": { - "title": "Diminuer la taille du pinceau", - "desc": "Diminue la taille du pinceau/gomme de la toile" + "paramUpscaleMethod": { + "paragraphs": [ + "Méthode utilisée pour améliorer l'image pour la correction de haute résolution." + ], + "heading": "Méthode d'agrandissement" }, - "increaseBrushSize": { - "title": "Augmenter la taille du pinceau", - "desc": "Augmente la taille du pinceau/gomme de la toile" + "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." + ] }, - "decreaseBrushOpacity": { - "title": "Diminuer l'opacité du pinceau", - "desc": "Diminue l'opacité du pinceau de la toile" + "paramWidth": { + "paragraphs": [ + "Largeur de l'image générée. Doit être un multiple de 8." + ], + "heading": "Largeur" }, - "increaseBrushOpacity": { - "title": "Augmenter l'opacité du pinceau", - "desc": "Augmente l'opacité du pinceau de la toile" + "paramHeight": { + "heading": "Hauteur", + "paragraphs": [ + "Hauteur de l'image générée. Doit être un multiple de 8." + ] }, - "moveTool": { - "title": "Outil de déplacement", - "desc": "Permet la navigation sur la toile" + "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." + ] }, - "fillBoundingBox": { - "title": "Remplir la boîte englobante", - "desc": "Remplit la boîte englobante avec la couleur du pinceau" + "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" }, - "eraseBoundingBox": { - "title": "Effacer la boîte englobante", - "desc": "Efface la zone de la boîte englobante" + "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" }, - "colorPicker": { - "title": "Sélectionnez le sélecteur de couleur", - "desc": "Sélectionne le sélecteur de couleur de la toile" + "refinerScheduler": { + "heading": "Planificateur", + "paragraphs": [ + "Planificateur utilisé pendant la partie de raffinage du processus de génération.", + "Semblable au Planificateur de Génération." + ] }, - "toggleSnap": { - "title": "Basculer Snap", - "desc": "Basculer Snap à la grille" + "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" }, - "quickToggleMove": { - "title": "Basculer rapidement déplacer", - "desc": "Basculer temporairement le mode Déplacer" + "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." + ] }, - "toggleLayer": { - "title": "Basculer la couche", - "desc": "Basculer la sélection de la couche masque/base" + "seamlessTilingYAxis": { + "paragraphs": [ + "Concaténer une image sans bord le long de l'axe vertical." + ], + "heading": "Concaténation sans bord axe Y" }, - "clearMask": { - "title": "Effacer le masque", - "desc": "Effacer entièrement le masque" + "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" }, - "hideMask": { - "title": "Masquer le masque", - "desc": "Masquer et démasquer le masque" + "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" }, - "showHideBoundingBox": { - "title": "Afficher/Masquer la boîte englobante", - "desc": "Basculer la visibilité de la boîte englobante" + "compositingMaskBlur": { + "heading": "Flou de masque", + "paragraphs": [ + "Le rayon de flou du masque." + ] }, - "mergeVisible": { - "title": "Fusionner visible", - "desc": "Fusionner toutes les couches visibles de la toile" + "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" }, - "saveToGallery": { - "title": "Enregistrer dans la galerie", - "desc": "Enregistrer la toile actuelle dans la galerie" + "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" }, - "copyToClipboard": { - "title": "Copier dans le presse-papiers", - "desc": "Copier la toile actuelle dans le presse-papiers" + "compositingCoherenceEdgeSize": { + "paragraphs": [ + "La taille de bord du passage de cohérence." + ], + "heading": "Taille de bord" }, - "downloadImage": { - "title": "Télécharger l'image", - "desc": "Télécharger la toile actuelle" + "seamlessTilingXAxis": { + "heading": "Concaténation sans bord axe X", + "paragraphs": [ + "Concaténer une image de manière fluide le long de l'axe horizontal." + ] }, - "undoStroke": { - "title": "Annuler le trait", - "desc": "Annuler un coup de pinceau" + "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é" }, - "redoStroke": { - "title": "Rétablir le trait", - "desc": "Rétablir un coup de pinceau" + "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." + ] }, - "resetView": { - "title": "Réinitialiser la vue", - "desc": "Réinitialiser la vue de la toile" + "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." + ] }, - "previousStagingImage": { - "title": "Image de mise en scène précédente", - "desc": "Image précédente de la zone de mise en scène" + "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." + ] }, - "nextStagingImage": { - "title": "Image de mise en scène suivante", - "desc": "Image suivante de la zone de mise en scène" + "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" }, - "acceptStagingImage": { - "title": "Accepter l'image de mise en scène", - "desc": "Accepter l'image actuelle de la zone de mise en scène" + "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." + ] } }, - "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 cette entrée de modèle dans InvokeAI ?", - "deleteMsg2": "Cela n'effacera pas le fichier de point de contrôle du modèle de votre disque. Vous pouvez les réajouter si vous le souhaitez." + "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..." }, - "parameters": { - "images": "Images", - "steps": "Etapes", - "cfgScale": "CFG Echelle", + "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", - "seed": "Graine", - "shuffle": "Mélanger", - "noiseThreshold": "Seuil de Bruit", - "perlinNoise": "Bruit de Perlin", - "type": "Type", - "strength": "Force", - "upscaling": "Agrandissement", - "upscale": "Agrandir", - "upscaleImage": "Image en Agrandissement", - "scale": "Echelle", - "imageFit": "Ajuster Image Initiale à la Taille de Sortie", - "scaleBeforeProcessing": "Echelle Avant Traitement", - "scaledWidth": "Larg. Échelle", - "scaledHeight": "Haut. Échelle", - "infillMethod": "Méthode de Remplissage", - "tileSize": "Taille des Tuiles", - "sendToImg2Img": "Envoyer à Image à Image", - "sendToUnifiedCanvas": "Envoyer au Canvas Unifié", - "copyImage": "Copier Image", - "downloadImage": "Télécharger Image", - "usePrompt": "Utiliser la suggestion", - "useSeed": "Utiliser la graine", - "useAll": "Tout utiliser", - "info": "Info", - "showOptionsPanel": "Afficher le panneau d'options" + "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" }, - "settings": { - "models": "Modèles", - "displayInProgress": "Afficher les images en cours", - "confirmOnDelete": "Confirmer la suppression", - "enableImageDebugging": "Activer le débogage d'image", - "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. Rafraîchissez la page pour recharger." + "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" }, - "toast": { - "uploadFailed": "Téléchargement échoué", - "imageCopied": "Image copiée", - "imageNotLoadedDesc": "Aucune image trouvée pour envoyer à module d'image", - "canvasMerged": "Canvas fusionné", - "sentToImageToImage": "Envoyé à Image à Image", - "sentToUnifiedCanvas": "Envoyé à Canvas unifié", - "parametersNotSet": "Paramètres non définis", - "metadataLoadFailed": "Échec du chargement des métadonnées" + "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" + } }, - "tooltip": { - "feature": { - "prompt": "Ceci est le champ prompt. Le prompt inclut des objets de génération et des termes stylistiques. Vous pouvez également ajouter un poids (importance du jeton) dans le prompt, mais les commandes CLI et les paramètres ne fonctionneront pas.", - "gallery": "La galerie affiche les générations à partir du dossier de sortie à mesure qu'elles sont créées. Les paramètres sont stockés dans des fichiers et accessibles via le menu contextuel.", - "other": "Ces options activent des modes de traitement alternatifs pour Invoke. 'Tuilage seamless' créera des motifs répétitifs dans la sortie. 'Haute résolution' est la génération en deux étapes avec img2img : utilisez ce paramètre lorsque vous souhaitez une image plus grande et plus cohérente sans artefacts. Cela prendra plus de temps que d'habitude txt2img.", - "seed": "La valeur de grain affecte le bruit initial à partir duquel l'image est formée. Vous pouvez utiliser les graines déjà existantes provenant d'images précédentes. 'Seuil de bruit' est utilisé pour atténuer les artefacts à des valeurs CFG élevées (essayez la plage de 0 à 10), et Perlin pour ajouter du bruit Perlin pendant la génération : les deux servent à ajouter de la variété à vos sorties.", - "upscale": "Utilisez ESRGAN pour agrandir l'image immédiatement après la génération.", - "boundingBox": "La boîte englobante est la même que les paramètres Largeur et Hauteur pour Texte à Image ou Image à Image. Seulement la zone dans la boîte sera traitée." + "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)" } }, - "unifiedCanvas": { - "layer": "Couche", - "base": "Base", - "mask": "Masque", - "maskingOptions": "Options de masquage", - "enableMask": "Activer le masque", - "preserveMaskedArea": "Préserver la zone masquée", - "clearMask": "Effacer le masque", - "brush": "Pinceau", - "eraser": "Gomme", - "fillBoundingBox": "Remplir la boîte englobante", - "eraseBoundingBox": "Effacer la boîte englobante", - "colorPicker": "Sélecteur de couleur", - "brushOptions": "Options de pinceau", - "brushSize": "Taille", - "move": "Déplacer", - "resetView": "Réinitialiser la vue", - "mergeVisible": "Fusionner les visibles", - "saveToGallery": "Enregistrer dans la galerie", - "copyToClipboard": "Copier dans le presse-papiers", - "downloadAsImage": "Télécharger en tant qu'image", - "undo": "Annuler", - "redo": "Refaire", - "clearCanvas": "Effacer le canvas", - "canvasSettings": "Paramètres du canvas", - "showIntermediates": "Afficher les intermédiaires", - "showGrid": "Afficher la grille", - "snapToGrid": "Aligner sur la grille", - "darkenOutsideSelection": "Assombrir à l'extérieur de la sélection", - "autoSaveToGallery": "Enregistrement automatique dans la galerie", - "saveBoxRegionOnly": "Enregistrer uniquement la région de la boîte", - "limitStrokesToBox": "Limiter les traits à la boîte", - "showCanvasDebugInfo": "Afficher les informations de débogage du canvas", - "clearCanvasHistory": "Effacer l'historique du canvas", + "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", - "clearCanvasHistoryMessage": "Effacer l'historique du canvas laisse votre canvas actuel intact, mais efface de manière irréversible l'historique annuler et refaire.", - "clearCanvasHistoryConfirm": "Voulez-vous vraiment effacer l'historique du canvas ?", - "activeLayer": "Calque actif", - "canvasScale": "Échelle du canevas", - "boundingBox": "Boîte englobante", - "scaledBoundingBox": "Boîte englobante mise à l'échelle", - "boundingBoxPosition": "Position de la boîte englobante", - "canvasDimensions": "Dimensions du canevas", - "canvasPosition": "Position du canevas", - "cursorPosition": "Position du curseur", - "previous": "Précédent", - "next": "Suivant", - "accept": "Accepter", - "discardAll": "Tout abandonner" + "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." }, - "accessibility": { - "uploadImage": "Charger une image", - "reset": "Réinitialiser", - "nextImage": "Image suivante", - "previousImage": "Image précédente", - "showOptionsPanel": "Montrer la page d'options", - "invokeProgressBar": "Barre de Progression Invoke", - "menu": "Menu" + "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 index dbbb3cbec44..01c2e9e51bf 100644 --- a/invokeai/frontend/web/public/locales/he.json +++ b/invokeai/frontend/web/public/locales/he.json @@ -39,7 +39,6 @@ "discordLabel": "דיסקורד", "settingsLabel": "הגדרות", "img2img": "תמונה לתמונה", - "unifiedCanvas": "קנבס מאוחד", "nodes": "צמתים", "statusDisconnected": "מנותק", "hotkeysLabel": "מקשים חמים", @@ -48,211 +47,10 @@ "load": "טעינה", "back": "אחורה" }, - "hotkeys": { - "toggleGallery": { - "desc": "פתח וסגור את מגירת הגלריה", - "title": "הצג את הגלריה" - }, - "keyboardShortcuts": "קיצורי מקלדת", - "appHotkeys": "קיצורי אפליקציה", - "generalHotkeys": "קיצורי דרך כלליים", - "galleryHotkeys": "קיצורי דרך של הגלריה", - "unifiedCanvasHotkeys": "קיצורי דרך לקנבס המאוחד", - "invoke": { - "title": "הפעל", - "desc": "צור תמונה" - }, - "focusPrompt": { - "title": "התמקדות על הבקשה", - "desc": "התמקדות על איזור הקלדת הבקשה" - }, - "toggleOptions": { - "desc": "פתח וסגור את פאנל ההגדרות", - "title": "הצג הגדרות" - }, - "pinOptions": { - "title": "הצמד הגדרות", - "desc": "הצמד את פאנל ההגדרות" - }, - "changeTabs": { - "title": "החלף לשוניות", - "desc": "החלף לאיזור עבודה אחר" - }, - "consoleToggle": { - "desc": "פתח וסגור את הקונסול", - "title": "הצג קונסול" - }, - "setPrompt": { - "title": "הגדרת בקשה", - "desc": "שימוש בבקשה של התמונה הנוכחית" - }, - "restoreFaces": { - "desc": "שחזור התמונה הנוכחית", - "title": "שחזור פרצופים" - }, - "upscale": { - "title": "הגדלת קנה מידה", - "desc": "הגדל את התמונה הנוכחית" - }, - "showInfo": { - "title": "הצג מידע", - "desc": "הצגת פרטי מטא-נתונים של התמונה הנוכחית" - }, - "sendToImageToImage": { - "title": "שלח לתמונה לתמונה", - "desc": "שלח תמונה נוכחית לתמונה לתמונה" - }, - "deleteImage": { - "title": "מחק תמונה", - "desc": "מחק את התמונה הנוכחית" - }, - "closePanels": { - "title": "סגור לוחות", - "desc": "סוגר לוחות פתוחים" - }, - "previousImage": { - "title": "תמונה קודמת", - "desc": "הצג את התמונה הקודמת בגלריה" - }, - "decreaseGalleryThumbSize": { - "title": "הקטנת גודל תמונת גלריה", - "desc": "מקטין את גודל התמונות הממוזערות של הגלריה" - }, - "selectBrush": { - "desc": "בוחר את מברשת הקנבס", - "title": "בחר מברשת" - }, - "selectEraser": { - "title": "בחר מחק", - "desc": "בוחר את מחק הקנבס" - }, - "decreaseBrushSize": { - "title": "הקטנת גודל המברשת", - "desc": "מקטין את גודל מברשת הקנבס/מחק" - }, - "increaseBrushSize": { - "desc": "מגדיל את גודל מברשת הקנבס/מחק", - "title": "הגדלת גודל המברשת" - }, - "decreaseBrushOpacity": { - "title": "הפחת את אטימות המברשת", - "desc": "מקטין את האטימות של מברשת הקנבס" - }, - "increaseBrushOpacity": { - "title": "הגדל את אטימות המברשת", - "desc": "מגביר את האטימות של מברשת הקנבס" - }, - "moveTool": { - "title": "כלי הזזה", - "desc": "מאפשר ניווט על קנבס" - }, - "fillBoundingBox": { - "desc": "ממלא את התיבה התוחמת בצבע מברשת", - "title": "מילוי תיבה תוחמת" - }, - "eraseBoundingBox": { - "desc": "מוחק את אזור התיבה התוחמת", - "title": "מחק תיבה תוחמת" - }, - "colorPicker": { - "title": "בחר בבורר צבעים", - "desc": "בוחר את בורר צבעי הקנבס" - }, - "toggleSnap": { - "title": "הפעל הצמדה", - "desc": "מפעיל הצמדה לרשת" - }, - "quickToggleMove": { - "title": "הפעלה מהירה להזזה", - "desc": "מפעיל זמנית את מצב ההזזה" - }, - "toggleLayer": { - "title": "הפעל שכבה", - "desc": "הפעל בחירת שכבת בסיס/מסיכה" - }, - "clearMask": { - "title": "נקה מסיכה", - "desc": "נקה את כל המסכה" - }, - "hideMask": { - "desc": "הסתרה והצגה של מסיכה", - "title": "הסתר מסיכה" - }, - "showHideBoundingBox": { - "title": "הצגה/הסתרה של תיבה תוחמת", - "desc": "הפעל תצוגה של התיבה התוחמת" - }, - "mergeVisible": { - "title": "מיזוג תוכן גלוי", - "desc": "מיזוג כל השכבות הגלויות של הקנבס" - }, - "saveToGallery": { - "title": "שמור לגלריה", - "desc": "שמור את הקנבס הנוכחי בגלריה" - }, - "copyToClipboard": { - "title": "העתק ללוח ההדבקה", - "desc": "העתק את הקנבס הנוכחי ללוח ההדבקה" - }, - "downloadImage": { - "title": "הורד תמונה", - "desc": "הורד את הקנבס הנוכחי" - }, - "undoStroke": { - "title": "בטל משיכה", - "desc": "בטל משיכת מברשת" - }, - "redoStroke": { - "title": "בצע שוב משיכה", - "desc": "ביצוע מחדש של משיכת מברשת" - }, - "resetView": { - "title": "איפוס תצוגה", - "desc": "אפס תצוגת קנבס" - }, - "previousStagingImage": { - "desc": "תמונת אזור ההערכות הקודמת", - "title": "תמונת הערכות קודמת" - }, - "nextStagingImage": { - "title": "תמנות הערכות הבאה", - "desc": "תמונת אזור ההערכות הבאה" - }, - "acceptStagingImage": { - "desc": "אשר את תמונת איזור ההערכות הנוכחית", - "title": "אשר תמונת הערכות" - }, - "cancel": { - "desc": "ביטול יצירת תמונה", - "title": "ביטול" - }, - "maximizeWorkSpace": { - "title": "מקסם את איזור העבודה", - "desc": "סגור פאנלים ומקסם את איזור העבודה" - }, - "setSeed": { - "title": "הגדר זרע", - "desc": "השתמש בזרע התמונה הנוכחית" - }, - "setParameters": { - "title": "הגדרת פרמטרים", - "desc": "שימוש בכל הפרמטרים של התמונה הנוכחית" - }, - "increaseGalleryThumbSize": { - "title": "הגדל את גודל תמונת הגלריה", - "desc": "מגדיל את התמונות הממוזערות של הגלריה" - }, - "nextImage": { - "title": "תמונה הבאה", - "desc": "הצג את התמונה הבאה בגלריה" - } - }, "gallery": { "galleryImageSize": "גודל תמונה", "gallerySettings": "הגדרות גלריה", - "autoSwitchNewImages": "החלף אוטומטית לתמונות חדשות", - "loadMore": "טען עוד", - "noImagesInGallery": "אין תמונות בגלריה" + "autoSwitchNewImages": "החלף אוטומטית לתמונות חדשות" }, "parameters": { "images": "תמונות", @@ -263,8 +61,6 @@ "seed": "זרע", "type": "סוג", "strength": "חוזק", - "upscale": "הגדלת קנה מידה", - "upscaleImage": "הגדלת קנה מידת התמונה", "denoisingStrength": "חוזק מנטרל הרעש", "scaleBeforeProcessing": "שנה קנה מידה לפני עיבוד", "scaledWidth": "קנה מידה לאחר שינוי W", @@ -273,14 +69,10 @@ "tileSize": "גודל אריח", "symmetry": "סימטריה", "copyImage": "העתקת תמונה", - "downloadImage": "הורדת תמונה", - "sendToImg2Img": "שליחה לתמונה לתמונה", - "sendToUnifiedCanvas": "שליחה אל קנבס מאוחד", "usePrompt": "שימוש בבקשה", "useSeed": "שימוש בזרע", "useAll": "שימוש בהכל", "info": "פרטים", - "showOptionsPanel": "הצג חלונית אפשרויות", "shuffle": "ערבוב", "noiseThreshold": "סף רעש", "perlinNoise": "רעש פרלין", @@ -296,77 +88,11 @@ "resetWebUI": "איפוס ממשק משתמש", "resetWebUIDesc1": "איפוס ממשק המשתמש האינטרנטי מאפס רק את המטמון המקומי של הדפדפן של התמונות וההגדרות שנשמרו. זה לא מוחק תמונות מהדיסק.", "resetComplete": "ממשק המשתמש אופס. יש לבצע רענון דף בכדי לטעון אותו מחדש.", - "enableImageDebugging": "הפעלת איתור באגים בתמונה", "resetWebUIDesc2": "אם תמונות לא מופיעות בגלריה או שמשהו אחר לא עובד, נא לנסות איפוס /או אתחול לפני שליחת תקלה ב-GitHub." }, "toast": { "uploadFailed": "העלאה נכשלה", "imageCopied": "התמונה הועתקה", - "imageNotLoadedDesc": "לא נמצאה תמונה לשליחה למודול תמונה לתמונה", - "canvasMerged": "קנבס מוזג", - "sentToImageToImage": "נשלח לתמונה לתמונה", - "sentToUnifiedCanvas": "נשלח אל קנבס מאוחד", - "parametersNotSet": "פרמטרים לא הוגדרו", - "metadataLoadFailed": "טעינת מטא-נתונים נכשלה" - }, - "tooltip": { - "feature": { - "gallery": "הגלריה מציגה יצירות מתיקיית הפלטים בעת יצירתם. ההגדרות מאוחסנות בתוך קבצים ונגישות באמצעות תפריט הקשר.", - "upscale": "השתמש ב-ESRGAN כדי להגדיל את התמונה מיד לאחר היצירה.", - "prompt": "זהו שדה הבקשה. הבקשה כוללת אובייקטי יצירה ומונחים סגנוניים. באפשרותך להוסיף משקל (חשיבות אסימון) גם בשורת הפקודה, אך פקודות ופרמטרים של CLI לא יפעלו.", - "other": "אפשרויות אלה יאפשרו מצבי עיבוד חלופיים עבור ההרצה. 'ריצוף חלק' ייצור תבניות חוזרות בפלט. 'רזולוציה גבוהה' נוצר בשני שלבים עם img2img: השתמש בהגדרה זו כאשר אתה רוצה תמונה גדולה וקוהרנטית יותר ללא חפצים. פעולה זאת תקח יותר זמן מפעולת טקסט לתמונה רגילה.", - "seed": "ערך הזרע משפיע על הרעש הראשוני שממנו נוצרת התמונה. אתה יכול להשתמש בזרעים שכבר קיימים מתמונות קודמות. 'סף רעש' משמש להפחתת חפצים בערכי CFG גבוהים (נסה את טווח 0-10), ופרלין כדי להוסיף רעשי פרלין במהלך היצירה: שניהם משמשים להוספת וריאציה לתפוקות שלך.", - "boundingBox": "התיבה התוחמת זהה להגדרות 'רוחב' ו'גובה' עבור 'טקסט לתמונה' או 'תמונה לתמונה'. רק האזור בתיבה יעובד." - } - }, - "unifiedCanvas": { - "layer": "שכבה", - "base": "בסיס", - "maskingOptions": "אפשרויות מסכות", - "enableMask": "הפעלת מסיכה", - "colorPicker": "בוחר הצבעים", - "preserveMaskedArea": "שימור איזור ממוסך", - "clearMask": "ניקוי מסיכה", - "brush": "מברשת", - "eraser": "מחק", - "fillBoundingBox": "מילוי תיבה תוחמת", - "eraseBoundingBox": "מחק תיבה תוחמת", - "copyToClipboard": "העתק ללוח ההדבקה", - "downloadAsImage": "הורדה כתמונה", - "undo": "ביטול", - "redo": "ביצוע מחדש", - "clearCanvas": "ניקוי קנבס", - "showGrid": "הצגת רשת", - "snapToGrid": "הצמדה לרשת", - "darkenOutsideSelection": "הכהיית בחירה חיצונית", - "saveBoxRegionOnly": "שמירת איזור תיבה בלבד", - "limitStrokesToBox": "הגבלת משיכות לקופסא", - "showCanvasDebugInfo": "הצגת מידע איתור באגים בקנבס", - "clearCanvasHistory": "ניקוי הסטוריית קנבס", - "clearHistory": "ניקוי היסטוריה", - "clearCanvasHistoryConfirm": "האם את/ה בטוח/ה שברצונך לנקות את היסטוריית הקנבס?", - "activeLayer": "שכבה פעילה", - "canvasScale": "קנה מידה של קנבס", - "canvasDimensions": "מידות קנבס", - "previous": "הקודם", - "next": "הבא", - "accept": "אישור", - "discardAll": "בטל הכל", - "boundingBox": "תיבה תוחמת", - "scaledBoundingBox": "תיבה תוחמת לאחר שינוי קנה מידה", - "brushOptions": "אפשרויות מברשת", - "brushSize": "גודל", - "mergeVisible": "מיזוג תוכן גלוי", - "move": "הזזה", - "resetView": "איפוס תצוגה", - "saveToGallery": "שמור לגלריה", - "canvasSettings": "הגדרות קנבס", - "showIntermediates": "הצגת מתווכים", - "autoSaveToGallery": "שמירה אוטומטית בגלריה", - "clearCanvasHistoryMessage": "ניקוי היסטוריית הקנבס משאיר את הקנבס הנוכחי ללא שינוי, אך מנקה באופן בלתי הפיך את היסטוריית הביטול והביצוע מחדש.", - "boundingBoxPosition": "מיקום תיבה תוחמת", - "canvasPosition": "מיקום קנבס", - "cursorPosition": "מיקום הסמן", - "mask": "מסכה" + "parametersNotSet": "פרמטרים לא הוגדרו" } } diff --git a/invokeai/frontend/web/public/locales/hu.json b/invokeai/frontend/web/public/locales/hu.json index 04993b010de..2624fa03fd8 100644 --- a/invokeai/frontend/web/public/locales/hu.json +++ b/invokeai/frontend/web/public/locales/hu.json @@ -4,8 +4,7 @@ "uploadImage": "Fénykép feltöltése", "nextImage": "Következő kép", "previousImage": "Előző kép", - "menu": "Menü", - "loadMore": "Több betöltése" + "menu": "Menü" }, "boards": { "cancel": "Mégsem", diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 3c0079de598..382bd8e5238 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -5,7 +5,6 @@ "reportBugLabel": "Segnala un errore", "settingsLabel": "Impostazioni", "img2img": "Immagine a Immagine", - "unifiedCanvas": "Tela", "nodes": "Flussi di lavoro", "upload": "Caricamento", "load": "Carica", @@ -26,9 +25,7 @@ "batch": "Gestione Lotto", "modelManager": "Gestione Modelli", "communityLabel": "Comunità", - "nodeEditor": "Editor dei nodi", "advanced": "Avanzate", - "imageFailedToLoad": "Impossibile caricare l'immagine", "learnMore": "Per saperne di più", "ipAdapter": "Adattatore IP", "t2iAdapter": "Adattatore T2I", @@ -47,81 +44,135 @@ "somethingWentWrong": "Qualcosa è andato storto", "copyError": "Errore $t(gallery.copy)", "input": "Ingresso", - "notInstalled": "Non $t(common.installed)", "unknownError": "Errore sconosciuto", "updated": "Aggiornato", "save": "Salva", "created": "Creato", - "prevPage": "Pagina precedente", "delete": "Elimina", "orderBy": "Ordina per", - "nextPage": "Pagina successiva", "saveAs": "Salva come", "direction": "Direzione", "or": "o", "red": "Rosso", "aboutHeading": "Possiedi il tuo potere creativo", "aboutDesc": "Utilizzi Invoke per lavoro? Guarda qui:", - "localSystem": "Sistema locale", "green": "Verde", "blue": "Blu", "alpha": "Alfa", "copy": "Copia", - "on": "Attivato", + "on": "Acceso", "checkpoint": "Checkpoint", "safetensors": "Safetensors", "ai": "ia", "file": "File", "toResolve": "Da risolvere", "add": "Aggiungi", - "loglevel": "Livello di log", "beta": "Beta", "positivePrompt": "Prompt positivo", "negativePrompt": "Prompt negativo", "selected": "Selezionato", - "goTo": "Vai a", "editor": "Editor", "tab": "Scheda", - "viewing": "Visualizza", - "viewingDesc": "Rivedi le immagini in un'ampia vista della galleria", - "editing": "Modifica", - "editingDesc": "Modifica nell'area Livelli di controllo", "enabled": "Abilitato", "disabled": "Disabilitato", - "comparingDesc": "Confronta due immagini", - "comparing": "Confronta" + "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", - "loadMore": "Carica altro", - "noImagesInGallery": "Nessuna immagine da visualizzare", "deleteImage_one": "Elimina l'immagine", "deleteImage_many": "Elimina {{count}} immagini", "deleteImage_other": "Elimina {{count}} immagini", "deleteImagePermanent": "Le immagini eliminate non possono essere ripristinate.", - "deleteImageBin": "Le immagini eliminate verranno spostate nel cestino del tuo sistema operativo.", - "assets": "Risorse", "autoAssignBoardOnClick": "Assegna automaticamente la bacheca al clic", "featuresWillReset": "Se elimini questa immagine, quelle funzionalità verranno immediatamente ripristinate.", "loading": "Caricamento in corso", - "unableToLoad": "Impossibile caricare la Galleria", "currentlyInUse": "Questa immagine è attualmente utilizzata nelle seguenti funzionalità:", "copy": "Copia", "download": "Scarica", - "setCurrentImage": "Imposta come immagine corrente", "downloadSelection": "Scarica gli elementi selezionati", "noImageSelected": "Nessuna immagine selezionata", "deleteSelection": "Elimina la selezione", "image": "immagine", "drop": "Rilascia", - "unstarImage": "Rimuovi preferenza immagine", - "dropOrUpload": "$t(gallery.drop) o carica", - "starImage": "Immagine preferita", + "unstarImage": "Rimuovi contrassegno", + "dropOrUpload": "Rilascia o carica", + "starImage": "Contrassegna", "dropToUpload": "$t(gallery.drop) per aggiornare", - "problemDeletingImagesDesc": "Impossibile eliminare una o più immagini", - "problemDeletingImages": "Problema durante l'eliminazione delle immagini", "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", @@ -129,247 +180,462 @@ "alwaysShowImageSizeBadge": "Mostra sempre le dimensioni dell'immagine", "openInViewer": "Apri nel visualizzatore", "selectForCompare": "Seleziona per il confronto", - "selectAnImageToCompare": "Seleziona un'immagine da confrontare", "slider": "Cursore", "sideBySide": "Fianco a Fianco", "compareImage": "Immagine di confronto", "viewerImage": "Immagine visualizzata", "hover": "Al passaggio del mouse", "swapImages": "Scambia le immagini", - "compareOptions": "Opzioni di confronto", "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." + "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": { - "keyboardShortcuts": "Tasti di scelta rapida", - "appHotkeys": "Applicazione", - "generalHotkeys": "Generale", - "galleryHotkeys": "Galleria", - "unifiedCanvasHotkeys": "Tela", - "invoke": { - "title": "Invoke", - "desc": "Genera un'immagine" - }, - "cancel": { - "title": "Annulla", - "desc": "Annulla l'elemento della coda corrente" - }, - "focusPrompt": { - "title": "Metti a fuoco il Prompt", - "desc": "Mette a fuoco l'area di immissione del prompt" - }, - "toggleOptions": { - "title": "Attiva/disattiva le opzioni", - "desc": "Apre e chiude il pannello delle opzioni" - }, - "pinOptions": { - "title": "Fissa le opzioni", - "desc": "Fissa il pannello delle opzioni" - }, - "toggleGallery": { - "title": "Attiva/disattiva galleria", - "desc": "Apre e chiude il pannello della galleria" - }, - "maximizeWorkSpace": { - "title": "Massimizza lo spazio di lavoro", - "desc": "Chiude i pannelli e massimizza l'area di lavoro" - }, - "changeTabs": { - "title": "Cambia scheda", - "desc": "Passa a un'altra area di lavoro" - }, - "consoleToggle": { - "title": "Attiva/disattiva console", - "desc": "Apre e chiude la console" - }, - "setPrompt": { - "title": "Imposta Prompt", - "desc": "Usa il prompt dell'immagine corrente" - }, - "setSeed": { - "title": "Imposta seme", - "desc": "Usa il seme dell'immagine corrente" - }, - "setParameters": { - "title": "Imposta parametri", - "desc": "Utilizza tutti i parametri dell'immagine corrente" - }, - "restoreFaces": { - "title": "Restaura volti", - "desc": "Restaura l'immagine corrente" - }, - "upscale": { - "title": "Amplia", - "desc": "Amplia l'immagine corrente" - }, - "showInfo": { - "title": "Mostra informazioni", - "desc": "Mostra le informazioni sui metadati dell'immagine corrente" - }, - "sendToImageToImage": { - "title": "Invia a Generazione da immagine", - "desc": "Invia l'immagine corrente a Generazione da immagine" - }, - "deleteImage": { - "title": "Elimina immagine", - "desc": "Elimina l'immagine corrente" - }, - "closePanels": { - "title": "Chiudi pannelli", - "desc": "Chiude i pannelli aperti" - }, - "previousImage": { - "title": "Immagine precedente", - "desc": "Visualizza l'immagine precedente nella galleria" - }, - "nextImage": { - "title": "Immagine successiva", - "desc": "Visualizza l'immagine successiva nella galleria" - }, - "increaseGalleryThumbSize": { - "title": "Aumenta dimensione immagini nella galleria", - "desc": "Aumenta la dimensione delle miniature della galleria" - }, - "decreaseGalleryThumbSize": { - "title": "Riduci dimensione immagini nella galleria", - "desc": "Riduce le dimensioni delle miniature della galleria" - }, - "selectBrush": { - "title": "Seleziona Pennello", - "desc": "Seleziona il pennello della tela" - }, - "selectEraser": { - "title": "Seleziona Cancellino", - "desc": "Seleziona il cancellino della tela" - }, - "decreaseBrushSize": { - "title": "Riduci la dimensione del pennello", - "desc": "Riduce la dimensione del pennello/cancellino della tela" - }, - "increaseBrushSize": { - "title": "Aumenta la dimensione del pennello", - "desc": "Aumenta la dimensione del pennello/cancellino della tela" - }, - "decreaseBrushOpacity": { - "title": "Riduci l'opacità del pennello", - "desc": "Diminuisce l'opacità del pennello della tela" - }, - "increaseBrushOpacity": { - "title": "Aumenta l'opacità del pennello", - "desc": "Aumenta l'opacità del pennello della tela" - }, - "moveTool": { - "title": "Strumento Sposta", - "desc": "Consente la navigazione nella tela" - }, - "fillBoundingBox": { - "title": "Riempi riquadro di selezione", - "desc": "Riempie il riquadro di selezione con il colore del pennello" - }, - "eraseBoundingBox": { - "title": "Cancella riquadro di selezione", - "desc": "Cancella l'area del riquadro di selezione" - }, - "colorPicker": { - "title": "Seleziona Selettore colore", - "desc": "Seleziona il selettore colore della tela" - }, - "toggleSnap": { - "title": "Attiva/disattiva Aggancia", - "desc": "Attiva/disattiva Aggancia alla griglia" - }, - "quickToggleMove": { - "title": "Attiva/disattiva Sposta rapido", - "desc": "Attiva/disattiva temporaneamente la modalità Sposta" - }, - "toggleLayer": { - "title": "Attiva/disattiva livello", - "desc": "Attiva/disattiva la selezione del livello base/maschera" - }, - "clearMask": { - "title": "Cancella maschera", - "desc": "Cancella l'intera maschera" - }, - "hideMask": { - "title": "Nascondi maschera", - "desc": "Nasconde e mostra la maschera" - }, - "showHideBoundingBox": { - "title": "Mostra/Nascondi riquadro di selezione", - "desc": "Attiva/disattiva la visibilità del riquadro di selezione" - }, - "mergeVisible": { - "title": "Fondi il visibile", - "desc": "Fonde tutti gli strati visibili della tela" - }, - "saveToGallery": { - "title": "Salva nella galleria", - "desc": "Salva la tela corrente nella galleria" - }, - "copyToClipboard": { - "title": "Copia negli appunti", - "desc": "Copia la tela corrente negli appunti" - }, - "downloadImage": { - "title": "Scarica l'immagine", - "desc": "Scarica la tela corrente" - }, - "undoStroke": { - "title": "Annulla tratto", - "desc": "Annulla una pennellata" - }, - "redoStroke": { - "title": "Ripeti tratto", - "desc": "Ripeti una pennellata" - }, - "resetView": { - "title": "Reimposta vista", - "desc": "Ripristina la visualizzazione della tela" - }, - "previousStagingImage": { - "title": "Immagine della sessione precedente", - "desc": "Immagine dell'area della sessione precedente" - }, - "nextStagingImage": { - "title": "Immagine della sessione successivo", - "desc": "Immagine dell'area della sessione successiva" - }, - "acceptStagingImage": { - "title": "Accetta l'immagine della sessione", - "desc": "Accetta l'immagine dell'area della sessione corrente" - }, - "nodesHotkeys": "Nodi", - "addNodes": { - "title": "Aggiungi Nodi", - "desc": "Apre il menu Aggiungi Nodi" - }, - "cancelAndClear": { - "desc": "Annulla l'elemento della coda corrente e cancella tutti gli elementi in sospeso", - "title": "Annulla e cancella" - }, - "resetOptionsAndGallery": { - "title": "Ripristina le opzioni e la galleria", - "desc": "Reimposta i pannelli delle opzioni e della galleria" - }, "searchHotkeys": "Cerca tasti di scelta rapida", "noHotkeysFound": "Nessun tasto di scelta rapida trovato", - "toggleOptionsAndGallery": { - "desc": "Apre e chiude le opzioni e i pannelli della galleria", - "title": "Attiva/disattiva le opzioni e la galleria" - }, "clearSearch": "Cancella ricerca", - "remixImage": { - "desc": "Utilizza tutti i parametri tranne il seme dell'immagine corrente", - "title": "Remixa l'immagine" + "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." + } }, - "toggleViewer": { - "title": "Attiva/disattiva il visualizzatore di immagini", - "desc": "Passa dal visualizzatore immagini all'area di lavoro per la scheda corrente." - } + "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", @@ -404,8 +670,6 @@ "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.", - "v2_base": "v2 (512px)", - "v2_768": "v2 (768px)", "none": "nessuno", "variant": "Variante", "baseModel": "Modello Base", @@ -416,8 +680,6 @@ "modelDeleted": "Modello eliminato", "modelDeleteFailed": "Impossibile eliminare il modello", "convertingModelBegin": "Conversione del modello. Attendere prego.", - "modelsSynced": "Modelli sincronizzati", - "modelSyncFailed": "Sincronizzazione modello non riuscita", "settings": "Impostazioni", "syncModels": "Sincronizza modelli", "predictionType": "Tipo di previsione", @@ -443,7 +705,6 @@ "defaultSettingsSaved": "Impostazioni predefinite salvate", "defaultSettings": "Impostazioni predefinite", "metadata": "Metadati", - "useDefaultSettings": "Usa le impostazioni predefinite", "triggerPhrases": "Frasi Trigger", "deleteModelImage": "Elimina l'immagine del modello", "localOnly": "solo locale", @@ -456,7 +717,7 @@ "loraTriggerPhrases": "Frasi Trigger LoRA", "mainModelTriggerPhrases": "Frasi Trigger del modello principale", "inplaceInstall": "Installazione sul posto", - "inplaceInstallDesc": "Installa i modelli senza copiare i file. Quando si utilizza il modello, verrà caricato da questa posizione. Se disabilitato, i file del modello verranno copiati nella directory dei modelli gestiti da Invoke durante l'installazione.", + "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", @@ -468,21 +729,230 @@ "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.", - "hfTokenHelperText": "Per utilizzare i modelli checkpoint è necessario un token HF. Clicca qui per creare o ottenere il tuo token.", - "hfTokenInvalid": "Token HF non valido o mancante", - "hfTokenInvalidErrorMessage": "Token HuggingFace non valido o mancante.", - "hfTokenUnableToVerify": "Impossibile verificare il token HF", - "hfTokenUnableToVerifyErrorMessage": "Impossibile verificare il token HuggingFace. Ciò è probabilmente dovuto a un errore di rete. Per favore riprova più tardi.", - "hfTokenSaved": "Token HF salvato", "loraModels": "LoRA", "starterModels": "Modelli iniziali", "textualInversions": "Inversioni Testuali", "noModelsInstalled": "Nessun modello installato", - "hfTokenInvalidErrorMessage2": "Aggiornalo in ", "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", - "noMatchingModels": "Nessun modello corrispondente" + "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", @@ -496,24 +966,18 @@ "perlinNoise": "Rumore Perlin", "type": "Tipo", "strength": "Forza", - "upscaling": "Ampliamento", - "upscale": "Amplia (Shift + U)", - "upscaleImage": "Amplia Immagine", + "upscaling": "Amplia", "scale": "Scala", "imageFit": "Adatta l'immagine iniziale alle dimensioni di output", "scaleBeforeProcessing": "Scala prima dell'elaborazione", - "scaledWidth": "Larghezza ridimensionata", - "scaledHeight": "Altezza ridimensionata", + "scaledWidth": "Larghezza scalata", + "scaledHeight": "Altezza scalata", "infillMethod": "Metodo di riempimento", "tileSize": "Dimensione piastrella", - "sendToImg2Img": "Invia a Generazione da immagine", - "sendToUnifiedCanvas": "Invia alla Tela", - "downloadImage": "Scarica l'immagine", "usePrompt": "Usa Prompt", "useSeed": "Usa Seme", "useAll": "Usa Tutto", "info": "Informazioni", - "showOptionsPanel": "Mostra il pannello laterale (O o T)", "general": "Generale", "denoisingStrength": "Forza di riduzione del rumore", "copyImage": "Copia immagine", @@ -521,8 +985,8 @@ "cancel": "Annulla" }, "symmetry": "Simmetria", - "seamlessXAxis": "Piastrella senza giunte Asse X", - "seamlessYAxis": "Piastrella senza giunte Asse Y", + "seamlessXAxis": "Asse X senza giunte", + "seamlessYAxis": "Asse Y senza giunte", "scheduler": "Campionatore", "positivePromptPlaceholder": "Prompt Positivo", "negativePromptPlaceholder": "Prompt Negativo", @@ -535,36 +999,61 @@ "noNodesInGraph": "Nessun nodo nel grafico", "noModelSelected": "Nessun modello selezionato", "noPrompts": "Nessun prompt generato", - "noInitialImageSelected": "Nessuna immagine iniziale selezionata", "addingImagesTo": "Aggiungi immagini a", "systemDisconnected": "Sistema disconnesso", - "noControlImageForControlAdapter": "L'adattatore di controllo #{{number}} non ha un'immagine di controllo", - "noModelForControlAdapter": "Nessun modello selezionato per l'adattatore di controllo #{{number}}.", - "incompatibleBaseModelForControlAdapter": "Il modello dell'adattatore di controllo #{{number}} non è compatibile con il modello principale.", "missingNodeTemplate": "Modello di nodo mancante", - "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} ingresso mancante", + "missingInputForField": "ingresso mancante", "missingFieldTemplate": "Modello di campo mancante", - "imageNotProcessedForControlAdapter": "L'immagine dell'adattatore di controllo #{{number}} non è stata elaborata", - "layer": { - "initialImageNoImageSelected": "Nessuna immagine iniziale selezionata", - "t2iAdapterIncompatibleDimensions": "L'adattatore T2I richiede che la dimensione dell'immagine sia un multiplo di {{multiple}}", - "controlAdapterNoModelSelected": "Nessun modello di adattatore di controllo selezionato", - "controlAdapterIncompatibleBaseModel": "Il modello base dell'adattatore di controllo non è compatibile", - "controlAdapterNoImageSelected": "Nessuna immagine dell'adattatore di controllo selezionata", - "controlAdapterImageNotProcessed": "Immagine dell'adattatore di controllo non elaborata", - "ipAdapterNoModelSelected": "Nessun adattatore IP selezionato", - "ipAdapterIncompatibleBaseModel": "Il modello base dell'adattatore IP non è compatibile", - "ipAdapterNoImageSelected": "Nessuna immagine dell'adattatore IP selezionata", - "rgNoPromptsOrIPAdapters": "Nessun prompt o adattatore IP", - "rgNoRegion": "Nessuna regione selezionata" - } + "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", - "isAllowedToUpscale": { - "useX2Model": "L'immagine è troppo grande per l'ampliamento con il modello x4, utilizza il modello x2", - "tooLarge": "L'immagine è troppo grande per l'ampliamento, seleziona un'immagine più piccola" - }, "imageActions": "Azioni Immagine", "cfgRescaleMultiplier": "Moltiplicatore riscala CFG", "useSize": "Usa Dimensioni", @@ -576,26 +1065,44 @@ "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (potrebbe essere troppo grande)", "remixImage": "Remixa l'immagine", "coherenceEdgeSize": "Dim. bordo", - "infillMosaicTileWidth": "Larghezza piastrella", - "infillMosaicMinColor": "Colore minimo", - "infillMosaicMaxColor": "Colore massimo", - "infillMosaicTileHeight": "Altezza piastrella", "infillColorValue": "Colore di riempimento", - "globalSettings": "Impostazioni globali", - "globalPositivePromptPlaceholder": "Prompt positivo globale", - "globalNegativePromptPlaceholder": "Prompt negativo globale" + "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", - "enableImageDebugging": "Abilita il debug dell'immagine", "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", - "shouldLogToConsole": "Registrazione della console", "developer": "Sviluppatore", "antialiasProgressImages": "Anti aliasing delle immagini di avanzamento", "showProgressInViewer": "Mostra le immagini di avanzamento nel visualizzatore", @@ -617,64 +1124,52 @@ "enableNSFWChecker": "Abilita controllo NSFW", "enableInvisibleWatermark": "Abilita filigrana invisibile", "enableInformationalPopovers": "Abilita testo informativo a comparsa", - "reloadingIn": "Ricaricando in" + "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", - "imageNotLoadedDesc": "Impossibile trovare l'immagine", - "canvasMerged": "Tela unita", - "sentToImageToImage": "Inviato a Generazione da immagine", - "sentToUnifiedCanvas": "Inviato alla Tela", "parametersNotSet": "Parametri non richiamati", - "metadataLoadFailed": "Impossibile caricare i metadati", "serverError": "Errore del Server", "connected": "Connesso al server", "canceled": "Elaborazione annullata", - "uploadFailedInvalidUploadDesc": "Deve essere una singola immagine PNG o JPEG", + "uploadFailedInvalidUploadDesc": "Devono essere immagini PNG, JPEG o WEBP.", "parameterSet": "Parametro richiamato", "parameterNotSet": "Parametro non richiamato", "problemCopyingImage": "Impossibile copiare l'immagine", - "baseModelChangedCleared_one": "Cancellato o disabilitato {{count}} sottomodello incompatibile", - "baseModelChangedCleared_many": "Cancellati o disabilitati {{count}} sottomodelli incompatibili", + "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", - "imageSavingFailed": "Salvataggio dell'immagine non riuscito", - "canvasSentControlnetAssets": "Tela inviata a ControlNet & Risorse", - "problemCopyingCanvasDesc": "Impossibile copiare la tela", "loadedWithWarnings": "Flusso di lavoro caricato con avvisi", - "canvasCopiedClipboard": "Tela copiata negli appunti", - "maskSavedAssets": "Maschera salvata nelle risorse", - "problemDownloadingCanvas": "Problema durante lo scarico della tela", - "problemMergingCanvas": "Problema nell'unione delle tele", "imageUploaded": "Immagine caricata", - "addedToBoard": "Aggiunto alla bacheca", + "addedToBoard": "Aggiunto alle risorse della bacheca {{name}}", "modelAddedSimple": "Modello aggiunto alla Coda", - "problemImportingMaskDesc": "Impossibile importare la maschera", - "problemCopyingCanvas": "Problema durante la copia della tela", - "problemSavingCanvas": "Problema nel salvataggio della tela", - "canvasDownloaded": "Tela scaricata", - "problemMergingCanvasDesc": "Impossibile unire le tele", - "problemDownloadingCanvasDesc": "Impossibile scaricare la tela", - "imageSaved": "Immagine salvata", - "maskSentControlnetAssets": "Maschera inviata a ControlNet & Risorse", - "canvasSavedGallery": "Tela salvata nella Galleria", "imageUploadFailed": "Caricamento immagine non riuscito", - "problemImportingMask": "Problema durante l'importazione della maschera", - "setInitialImage": "Imposta come immagine iniziale", - "setControlImage": "Imposta come immagine di controllo", - "setNodeField": "Imposta come campo nodo", - "problemSavingMask": "Problema nel salvataggio della maschera", - "problemSavingCanvasDesc": "Impossibile salvare la tela", - "setCanvasInitialImage": "Imposta l'immagine iniziale della tela", "workflowLoaded": "Flusso di lavoro caricato", - "problemSavingMaskDesc": "Impossibile salvare la maschera", - "setAsCanvasInitialImage": "Imposta come immagine iniziale della tela", - "invalidUpload": "Caricamento non valido", "problemDeletingWorkflow": "Problema durante l'eliminazione del flusso di lavoro", "workflowDeleted": "Flusso di lavoro eliminato", "problemRetrievingWorkflow": "Problema nel recupero del flusso di lavoro", - "resetInitialImage": "Reimposta l'immagine iniziale", - "uploadInitialImage": "Carica l'immagine iniziale", "problemDownloadingImage": "Impossibile scaricare l'immagine", "prunedQueue": "Coda ripulita", "modelImportCanceled": "Importazione del modello annullata", @@ -688,79 +1183,79 @@ "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." - }, - "tooltip": { - "feature": { - "prompt": "Questo è il campo del prompt. Il prompt include oggetti di generazione e termini stilistici. Puoi anche aggiungere il peso (importanza del token) nel prompt, ma i comandi e i parametri dell'interfaccia a linea di comando non funzioneranno.", - "gallery": "Galleria visualizza le generazioni dalla cartella degli output man mano che vengono create. Le impostazioni sono memorizzate all'interno di file e accessibili dal menu contestuale.", - "other": "Queste opzioni abiliteranno modalità di elaborazione alternative per Invoke. 'Piastrella senza giunte' creerà immagini piastrellabili senza giunture. 'Ottimizzazione Alta risoluzione' è la generazione in due passaggi con 'Immagine a Immagine': usa questa impostazione quando vuoi un'immagine più grande e più coerente senza artefatti. Ci vorrà più tempo del solito 'Testo a Immagine'.", - "seed": "Il valore del Seme influenza il rumore iniziale da cui è formata l'immagine. Puoi usare i semi già esistenti dalle immagini precedenti. 'Soglia del rumore' viene utilizzato per mitigare gli artefatti a valori CFG elevati (provare l'intervallo 0-10) e Perlin per aggiungere il rumore Perlin durante la generazione: entrambi servono per aggiungere variazioni ai risultati.", - "upscale": "Utilizza ESRGAN per ingrandire l'immagine subito dopo la generazione.", - "boundingBox": "Il riquadro di selezione è lo stesso delle impostazioni Larghezza e Altezza per da Testo a Immagine o da Immagine a Immagine. Verrà elaborata solo l'area nella casella." - } - }, - "unifiedCanvas": { - "layer": "Livello", - "base": "Base", - "mask": "Maschera", - "maskingOptions": "Opzioni maschera", - "enableMask": "Abilita maschera", - "preserveMaskedArea": "Mantieni area mascherata", - "clearMask": "Cancella maschera (Shift+C)", - "brush": "Pennello", - "eraser": "Cancellino", - "fillBoundingBox": "Riempi rettangolo di selezione", - "eraseBoundingBox": "Cancella rettangolo di selezione", - "colorPicker": "Selettore Colore", - "brushOptions": "Opzioni pennello", - "brushSize": "Dimensioni", - "move": "Sposta", - "resetView": "Reimposta vista", - "mergeVisible": "Fondi il visibile", - "saveToGallery": "Salva nella galleria", - "copyToClipboard": "Copia negli appunti", - "downloadAsImage": "Scarica come immagine", - "undo": "Annulla", - "redo": "Ripeti", - "clearCanvas": "Cancella la Tela", - "canvasSettings": "Impostazioni Tela", - "showIntermediates": "Mostra intermedi", - "showGrid": "Mostra griglia", - "snapToGrid": "Aggancia alla griglia", - "darkenOutsideSelection": "Scurisci l'esterno della selezione", - "autoSaveToGallery": "Salvataggio automatico nella Galleria", - "saveBoxRegionOnly": "Salva solo l'area di selezione", - "limitStrokesToBox": "Limita i tratti all'area di selezione", - "showCanvasDebugInfo": "Mostra ulteriori informazioni sulla Tela", - "clearCanvasHistory": "Cancella cronologia Tela", - "clearHistory": "Cancella la cronologia", - "clearCanvasHistoryMessage": "La cancellazione della cronologia della tela lascia intatta la tela corrente, ma cancella in modo irreversibile la cronologia degli annullamenti e dei ripristini.", - "clearCanvasHistoryConfirm": "Sei sicuro di voler cancellare la cronologia della Tela?", - "activeLayer": "Livello attivo", - "canvasScale": "Scala della Tela", - "boundingBox": "Rettangolo di selezione", - "scaledBoundingBox": "Rettangolo di selezione scalato", - "boundingBoxPosition": "Posizione del Rettangolo di selezione", - "canvasDimensions": "Dimensioni della Tela", - "canvasPosition": "Posizione Tela", - "cursorPosition": "Posizione del cursore", - "previous": "Precedente", - "next": "Successivo", - "accept": "Accetta", - "discardAll": "Scarta tutto", - "antialiasing": "Anti aliasing", - "showResultsOn": "Mostra i risultati (attivato)", - "showResultsOff": "Mostra i risultati (disattivato)", - "saveMask": "Salva $t(unifiedCanvas.mask)", - "coherenceModeGaussianBlur": "Sfocatura Gaussiana", - "coherenceModeBoxBlur": "Sfocatura Box", - "coherenceModeStaged": "Maschera espansa", - "invertBrushSizeScrollDirection": "Inverti scorrimento per dimensione pennello", - "discardCurrent": "Scarta l'attuale", - "initialFitImageSize": "Adatta dimensione immagine al rilascio", - "hideBoundingBox": "Nascondi il rettangolo di selezione", - "showBoundingBox": "Mostra il rettangolo di selezione" + "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", @@ -768,26 +1263,24 @@ "previousImage": "Immagine precedente", "nextImage": "Immagine successiva", "reset": "Reimposta", - "showOptionsPanel": "Mostra il pannello laterale", "menu": "Menu", - "showGalleryPanel": "Mostra il pannello Galleria", - "loadMore": "Carica altro", "mode": "Modalità", "resetUI": "$t(accessibility.reset) l'Interfaccia Utente", "createIssue": "Segnala un problema", "about": "Informazioni", - "submitSupportTicket": "Invia ticket di supporto" + "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", - "hideLegendNodes": "Nascondi la legenda del tipo di campo", - "showLegendNodes": "Mostra legenda del tipo di campo", "hideMinimapnodes": "Nascondi minimappa", "showMinimapnodes": "Mostra minimappa", "zoomInNodes": "Ingrandire", "fitViewportNodes": "Adatta vista", "reloadNodeTemplates": "Ricarica i modelli di nodo", - "loadWorkflow": "Importa flusso di lavoro JSON", + "loadWorkflow": "Importa flusso di lavoro", "downloadWorkflow": "Esporta flusso di lavoro JSON", "scheduler": "Campionatore", "addNode": "Aggiungi nodo", @@ -807,27 +1300,22 @@ "workflowSettings": "Impostazioni Editor del flusso di lavoro", "colorCodeEdges": "Bordi con codice colore", "noOutputRecorded": "Nessun output registrato", - "noFieldsLinearview": "Nessun campo aggiunto alla vista lineare", - "removeLinearView": "Rimuovi dalla vista lineare", "workflowDescription": "Breve descrizione", "workflowContact": "Contatto", "workflowVersion": "Versione", "workflow": "Flusso di lavoro", "noWorkflow": "Nessun flusso di lavoro", - "workflowTags": "Tag", + "workflowTags": "Etichette", "workflowValidation": "Errore di convalida del flusso di lavoro", "workflowAuthor": "Autore", "workflowName": "Nome", "workflowNotes": "Note", - "versionUnknown": " Versione sconosciuta", "unableToValidateWorkflow": "Impossibile convalidare il flusso di lavoro", "updateApp": "Aggiorna Applicazione", - "unableToLoadWorkflow": "Impossibile caricare il flusso di lavoro", "updateNode": "Aggiorna nodo", "version": "Versione", "notes": "Note", "problemSettingTitle": "Problema nell'impostazione del titolo", - "unknownTemplate": "Modello sconosciuto", "nodeType": "Tipo di nodo", "notesDescription": "Aggiunge note sul tuo flusso di lavoro", "unknownField": "Campo sconosciuto", @@ -839,20 +1327,16 @@ "nodeSearch": "Cerca nodi", "nodeOutputs": "Uscite del nodo", "noConnectionInProgress": "Nessuna connessione in corso", - "noConnectionData": "Nessun dato di connessione", "cannotDuplicateConnection": "Impossibile creare connessioni duplicate", - "noMatchingNodes": "Nessun nodo corrispondente", - "noFieldType": "Nessun tipo di campo", "boolean": "Booleani", "node": "Nodo", "collection": "Raccolta", - "cannotConnectInputToInput": "Impossibile collegare Input a Input", - "cannotConnectOutputToOutput": "Impossibile collegare Output ad Output", + "cannotConnectInputToInput": "Impossibile collegare ingresso a ingresso", + "cannotConnectOutputToOutput": "Impossibile collegare uscita ad uscita", "cannotConnectToSelf": "Impossibile connettersi a se stesso", - "mismatchedVersion": "Nodo non valido: il nodo {{node}} di tipo {{type}} ha una versione non corrispondente (provare ad aggiornare?)", "loadingNodes": "Caricamento nodi...", "enum": "Enumeratore", - "float": "In virgola mobile", + "float": "Decimale", "currentImageDescription": "Visualizza l'immagine corrente nell'editor dei nodi", "fieldTypesMustMatch": "I tipi di campo devono corrispondere", "edge": "Collegamento", @@ -866,7 +1350,6 @@ "unableToUpdateNodes_one": "Impossibile aggiornare {{count}} nodo", "unableToUpdateNodes_many": "Impossibile aggiornare {{count}} nodi", "unableToUpdateNodes_other": "Impossibile aggiornare {{count}} nodi", - "addLinearView": "Aggiungi alla vista Lineare", "unknownErrorValidatingWorkflow": "Errore sconosciuto durante la convalida del flusso di lavoro", "collectionFieldType": "{{name}} (Raccolta)", "collectionOrScalarFieldType": "{{name}} (Singola o Raccolta)", @@ -884,12 +1367,10 @@ "unableToGetWorkflowVersion": "Impossibile ottenere la versione dello schema del flusso di lavoro", "nodePack": "Pacchetto di nodi", "unableToExtractSchemaNameFromRef": "Impossibile estrarre il nome dello schema dal riferimento", - "unknownOutput": "Output sconosciuto: {{name}}", "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}}", - "unknownInput": "Input sconosciuto: {{name}}", "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", @@ -899,8 +1380,7 @@ "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": "Utilizzare nella vista lineare", - "reorderLinearView": "Riordina la vista lineare", + "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.", @@ -914,20 +1394,95 @@ "missingInvocationTemplate": "Modello di invocazione mancante", "missingFieldTemplate": "Modello di campo mancante", "singleFieldType": "{{name}} (Singola)", - "imageAccessError": "Impossibile trovare l'immagine {{image_name}}, ripristino delle impostazioni predefinite", + "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" + "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 di questa bacheca e delle sue immagini ripristinerà tutte le funzionalità che le stanno attualmente utilizzando.", + "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 bacheca contiene immagini utilizzate nelle seguenti funzionalità:", + "topMessage": "Questa selezione contiene immagini utilizzate nelle seguenti funzionalità:", "move": "Sposta", "myBoard": "Bacheca", "searchBoard": "Cerca bacheche ...", @@ -938,97 +1493,50 @@ "deleteBoardOnly": "solo la Bacheca", "deleteBoard": "Elimina Bacheca", "deleteBoardAndImages": "Bacheca e Immagini", - "deletedBoardsCannotbeRestored": "Le bacheche eliminate non possono essere ripristinate", + "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:" - }, - "controlnet": { - "contentShuffleDescription": "Rimescola il contenuto di un'immagine", - "contentShuffle": "Rimescola contenuto", - "beginEndStepPercent": "Percentuale passi Inizio / Fine", - "duplicate": "Duplica", - "balanced": "Bilanciato", - "depthMidasDescription": "Generazione di mappe di profondità usando Midas", - "control": "Controllo", - "crop": "Ritaglia", - "depthMidas": "Profondità (Midas)", - "detectResolution": "Rileva la risoluzione", - "controlMode": "Modalità di controllo", - "cannyDescription": "Canny rilevamento bordi", - "depthZoe": "Profondità (Zoe)", - "autoConfigure": "Configura automaticamente il processore", - "delete": "Elimina", - "depthZoeDescription": "Generazione di mappe di profondità usando Zoe", - "resize": "Ridimensiona", - "showAdvanced": "Mostra opzioni Avanzate", - "bgth": "Soglia rimozione sfondo", - "importImageFromCanvas": "Importa immagine dalla Tela", - "lineartDescription": "Converte l'immagine in linea", - "importMaskFromCanvas": "Importa maschera dalla Tela", - "hideAdvanced": "Nascondi opzioni avanzate", - "resetControlImage": "Reimposta immagine di controllo", - "f": "F", - "h": "A", - "prompt": "Prompt", - "resizeMode": "Ridimensionamento", - "weight": "Peso", - "selectModel": "Seleziona un modello", - "w": "L", - "processor": "Processore", - "none": "Nessuno", - "pidiDescription": "Elaborazione immagini PIDI", - "fill": "Riempie", - "colorMapDescription": "Genera una mappa dei colori dall'immagine", - "lineartAnimeDescription": "Elaborazione linea in stile anime", - "imageResolution": "Risoluzione dell'immagine", - "colorMap": "Colore", - "lowThreshold": "Soglia inferiore", - "highThreshold": "Soglia superiore", - "normalBaeDescription": "Elaborazione BAE normale", - "noneDescription": "Nessuna elaborazione applicata", - "saveControlImage": "Salva immagine di controllo", - "toggleControlNet": "Attiva/disattiva questo ControlNet", - "safe": "Sicuro", - "colorMapTileSize": "Dimensione piastrella", - "mediapipeFaceDescription": "Rilevamento dei volti tramite Mediapipe", - "hedDescription": "Rilevamento dei bordi nidificati olisticamente", - "setControlImageDimensions": "Copia le dimensioni in L/A (ottimizza per il modello)", - "maxFaces": "Numero massimo di volti", - "addT2IAdapter": "Aggiungi $t(common.t2iAdapter)", - "addControlNet": "Aggiungi $t(common.controlNet)", - "addIPAdapter": "Aggiungi $t(common.ipAdapter)", - "controlAdapter_one": "Adattatore di Controllo", - "controlAdapter_many": "Adattatori di Controllo", - "controlAdapter_other": "Adattatori di Controllo", - "megaControl": "Mega ControlNet", - "minConfidence": "Confidenza minima", - "scribble": "Scarabocchio", - "amult": "Angolo di illuminazione", - "coarse": "Approssimativo", - "resizeSimple": "Ridimensiona (semplice)", - "large": "Grande", - "small": "Piccolo", - "depthAnythingDescription": "Generazione di mappe di profondità utilizzando la tecnica Depth Anything", - "modelSize": "Dimensioni del modello", - "dwOpenposeDescription": "Stima della posa umana utilizzando DW Openpose", - "face": "Viso", - "body": "Corpo", - "hands": "Mani", - "lineartAnime": "Linea Anime", - "base": "Base", - "lineart": "Linea", - "controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))", - "mediapipeFace": "Mediapipe Volto", - "ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))", - "t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))", - "selectCLIPVisionModel": "Seleziona un modello CLIP Vision", - "ipAdapterMethod": "Metodo", - "full": "Completo", - "composition": "Solo la composizione", - "style": "Solo lo stile", - "beginEndStepPercentShort": "Inizio/Fine %", - "setControlImageDimensionsForce": "Copia le dimensioni in L/A (ignora il modello)" + "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", @@ -1057,7 +1565,7 @@ "batchQueuedDesc_other": "Aggiunte {{count}} sessioni a {{direction}} della coda", "graphQueued": "Grafico in coda", "batch": "Lotto", - "clearQueueAlertDialog": "Lo svuotamento della coda annulla immediatamente tutti gli elementi in elaborazione e cancella completamente la coda.", + "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", @@ -1085,7 +1593,6 @@ "clearQueueAlertDialog2": "Sei sicuro di voler cancellare la coda?", "item": "Elemento", "graphFailedToQueue": "Impossibile mettere in coda il grafico", - "batchFieldValues": "Valori Campi Lotto", "time": "Tempo", "openQueue": "Apri coda", "iterations_one": "Iterazione", @@ -1096,20 +1603,54 @@ "prompts_other": "Prompt", "generations_one": "Generazione", "generations_many": "Generazioni", - "generations_other": "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", - "noMatchingLoRAs": "Nessun LoRA corrispondente", "noModelsAvailable": "Nessun modello disponibile", "selectModel": "Seleziona un modello", "noRefinerModelsInstalled": "Nessun modello affinatore SDXL installato", - "noLoRAsInstalled": "Nessun LoRA installato", - "esrganModel": "Modello ESRGAN", "addLora": "Aggiungi LoRA", "defaultVAE": "VAE predefinito", - "concepts": "Concetti" + "concepts": "Concetti", + "lora": "LoRA", + "noCompatibleLoRAs": "Nessun LoRA compatibile", + "noMatchingLoRAs": "Nessun LoRA corrispondente", + "noLoRAsInstalled": "Nessun LoRA installato" }, "invocationCache": { "disable": "Disabilita", @@ -1137,13 +1678,12 @@ "label": "Comportamento del seme" }, "maxPrompts": "Numero massimo di prompt", - "promptsWithCount_one": "{{count}} Prompt", - "promptsWithCount_many": "{{count}} Prompt", - "promptsWithCount_other": "{{count}} Prompt", "dynamicPrompts": "Prompt dinamici", "promptsPreview": "Anteprima dei prompt", "showDynamicPrompts": "Mostra prompt dinamici", - "loading": "Generazione prompt dinamici..." + "loading": "Generazione prompt dinamici...", + "promptsToGenerate": "Prompt da generare", + "problemGeneratingPrompts": "Problema nella generazione dei prompt" }, "popovers": { "paramScheduler": { @@ -1169,7 +1709,8 @@ "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", @@ -1200,8 +1741,9 @@ "controlNetBeginEnd": { "heading": "Percentuale passi Inizio / Fine", "paragraphs": [ - "La parte del processo di rimozione del rumore in cui verrà applicato l'adattatore di controllo.", - "In genere, gli adattatori di controllo applicati all'inizio del processo guidano la composizione, mentre quelli applicati alla fine guidano i dettagli." + "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": { @@ -1284,8 +1826,9 @@ }, "paramDenoisingStrength": { "paragraphs": [ - "Quanto rumore viene aggiunto all'immagine in ingresso.", - "0 risulterà in un'immagine identica, mentre 1 risulterà in un'immagine completamente nuova." + "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" }, @@ -1297,14 +1840,16 @@ }, "infillMethod": { "paragraphs": [ - "Metodo di riempimento durante il processo di Outpainting o Inpainting." + "Metodo di riempimento durante il processo di Outpaint o Inpaint." ], "heading": "Metodo di riempimento" }, "controlNetWeight": { "heading": "Peso", "paragraphs": [ - "Peso dell'adattatore di controllo. Un peso maggiore porterà a impatti maggiori sull'immagine finale." + "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": { @@ -1330,7 +1875,7 @@ "lora": { "heading": "LoRA", "paragraphs": [ - "Modelli leggeri utilizzati insieme ai modelli base." + "Modelli concettuali utilizzati insieme ai modelli di base." ] }, "controlNet": { @@ -1386,7 +1931,7 @@ "paramUpscaleMethod": { "heading": "Metodo di ampliamento", "paragraphs": [ - "Metodo utilizzato per eseguire l'ampliamento dell'immagine per la correzione ad alta risoluzione." + "Metodo utilizzato per ampliare l'immagine per la correzione ad alta risoluzione." ] }, "patchmatchDownScaleSize": { @@ -1465,7 +2010,7 @@ "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'inpainting o l'outpainting" + "L'intensità minima di riduzione del rumore per la regione di coerenza durante l'inpaint o l'outpaint" ] }, "compositingMaskBlur": { @@ -1481,9 +2026,166 @@ ] }, "ipAdapterMethod": { - "heading": "Metodo", + "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": [ - "Metodo con cui applicare l'adattatore IP corrente." + "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." ] } }, @@ -1491,23 +2193,21 @@ "scheduler": "Campionatore", "noModelsAvailable": "Nessun modello disponibile", "denoisingStrength": "Forza di riduzione del rumore", - "concatPromptStyle": "Collega Prompt & Stile", "loading": "Caricamento...", "steps": "Passi", "refinerStart": "Inizio Affinamento", "cfgScale": "Scala CFG", - "negStylePrompt": "Prompt Stile negativo", "refiner": "Affinatore", "negAestheticScore": "Punteggio estetico negativo", "refinermodel": "Modello Affinatore", "posAestheticScore": "Punteggio estetico positivo", - "posStylePrompt": "Prompt Stile positivo", - "freePromptStyle": "Prompt di stile manuale", - "refinerSteps": "Passi Affinamento" + "refinerSteps": "Passi Affinamento", + "concatPromptStyle": "Collegamento di prompt e stile", + "freePromptStyle": "Prompt manuale Stile", + "negStylePrompt": "Prompt di stile negativo", + "posStylePrompt": "Prompt di stile positivo" }, "metadata": { - "initImage": "Immagine iniziale", - "seamless": "Senza giunture", "positivePrompt": "Prompt positivo", "negativePrompt": "Prompt negativo", "generationMode": "Modalità generazione", @@ -1519,7 +2219,6 @@ "model": "Modello", "noImageDetails": "Nessun dettaglio dell'immagine trovato", "cfgScale": "Scala CFG", - "fit": "Adatta Immagine a Immagine", "height": "Altezza", "noMetaData": "Nessun metadato trovato", "width": "Larghezza", @@ -1533,61 +2232,172 @@ "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}}" + "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": { - "enableHrf": "Abilita Correzione Alta Risoluzione", - "upscaleMethod": "Metodo di ampliamento", "metadata": { "strength": "Forza della Correzione Alta Risoluzione", "enabled": "Correzione Alta Risoluzione Abilitata", "method": "Metodo della Correzione Alta Risoluzione" }, - "hrf": "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", - "openWorkflow": "Apri flusso di lavoro", - "clearWorkflowSearchFilter": "Cancella il filtro di ricerca del flusso di lavoro", - "workflowLibrary": "Libreria", + "workflowLibrary": "Libreria flussi di lavoro", "workflowSaved": "Flusso di lavoro salvato", "unnamedWorkflow": "Flusso di lavoro senza nome", "savingWorkflow": "Salvataggio del flusso di lavoro...", - "problemLoading": "Problema durante il caricamento dei flussi di lavoro", "loading": "Caricamento dei flussi di lavoro", - "searchWorkflows": "Cerca flussi di lavoro", "problemSavingWorkflow": "Problema durante il salvataggio del flusso di lavoro", "deleteWorkflow": "Elimina flusso di lavoro", "workflows": "Flussi di lavoro", - "noDescription": "Nessuna descrizione", "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", - "noUserWorkflows": "Nessun flusso di lavoro utente", - "defaultWorkflows": "Flussi di lavoro predefiniti", - "userWorkflows": "I miei flussi di lavoro", "descending": "Discendente", "created": "Creato", "ascending": "Ascendente", - "noRecentWorkflows": "Nessun flusso di lavoro recente", "name": "Nome", "updated": "Aggiornato", - "projectWorkflows": "Flussi di lavoro del progetto", "opened": "Aperto", "convertGraph": "Converti grafico", "loadWorkflow": "$t(common.load) Flusso di lavoro", - "autoLayout": "Disposizione automatica", - "loadFromGraph": "Carica il flusso di lavoro dal grafico" - }, - "app": { - "storeNotInitialized": "Il negozio non è inizializzato" + "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": { @@ -1612,56 +2422,1079 @@ "prompt": { "compatibleEmbeddings": "Incorporamenti compatibili", "addPromptTrigger": "Aggiungi Trigger nel prompt", - "noMatchingTriggers": "Nessun Trigger corrispondente" + "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": { - "opacityFilter": "Filtro opacità", - "deleteAll": "Cancella tutto", "addLayer": "Aggiungi Livello", "moveToFront": "Sposta in primo piano", "moveToBack": "Sposta in fondo", "moveForward": "Sposta avanti", "moveBackward": "Sposta indietro", - "brushSize": "Dimensioni del pennello", - "globalMaskOpacity": "Opacità globale della maschera", "autoNegative": "Auto Negativo", - "deletePrompt": "Cancella il prompt", - "debugLayers": "Debug dei Livelli", "rectangle": "Rettangolo", - "maskPreviewColor": "Colore anteprima maschera", - "addPositivePrompt": "Aggiungi $t(common.positivePrompt)", - "addNegativePrompt": "Aggiungi $t(common.negativePrompt)", - "addIPAdapter": "Aggiungi $t(common.ipAdapter)", + "addPositivePrompt": "Aggiungi $t(controlLayers.prompt)", + "addNegativePrompt": "Aggiungi $t(controlLayers.negativePrompt)", "regionalGuidance": "Guida regionale", - "regionalGuidanceLayer": "$t(unifiedCanvas.layer) $t(controlLayers.regionalGuidance)", "opacity": "Opacità", - "globalControlAdapter": "$t(controlnet.controlAdapter_one) Globale", - "globalControlAdapterLayer": "$t(controlnet.controlAdapter_one) - $t(unifiedCanvas.layer) Globale", - "globalIPAdapter": "$t(common.ipAdapter) Globale", - "globalIPAdapterLayer": "$t(common.ipAdapter) - $t(unifiedCanvas.layer) Globale", - "globalInitialImage": "Immagine iniziale", - "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) - $t(unifiedCanvas.layer) Globale", - "clearProcessor": "Cancella processore", - "resetProcessor": "Ripristina il processore alle impostazioni predefinite", - "noLayersAdded": "Nessun livello aggiunto", - "resetRegion": "Reimposta la regione", - "controlLayers": "Livelli di controllo", - "layers_one": "Livello", - "layers_many": "Livelli", - "layers_other": "Livelli" + "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": { - "generation": "Generazione", - "generationTab": "$t(ui.tabs.generation) $t(common.tab)", "canvas": "Tela", - "canvasTab": "$t(ui.tabs.canvas) $t(common.tab)", "workflows": "Flussi di lavoro", "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", "models": "Modelli", "modelsTab": "$t(ui.tabs.models) $t(common.tab)", "queue": "Coda", - "queueTab": "$t(ui.tabs.queue) $t(common.tab)" + "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 index e953944c44c..74b490ffb0f 100644 --- a/invokeai/frontend/web/public/locales/ja.json +++ b/invokeai/frontend/web/public/locales/ja.json @@ -8,57 +8,53 @@ "back": "戻る", "statusDisconnected": "切断済", "cancel": "キャンセル", - "accept": "同意", + "accept": "採用", "img2img": "img2img", - "unifiedCanvas": "Unified Canvas", "loading": "ロード中", "githubLabel": "Github", - "hotkeysLabel": "ホットキー", + "hotkeysLabel": "ショートカットキー", "discordLabel": "Discord", "nodes": "ワークフロー", "txt2img": "txt2img", - "postprocessing": "Post Processing", + "postprocessing": "ポストプロセス", "t2iAdapter": "T2I アダプター", "communityLabel": "コミュニティ", "dontAskMeAgain": "次回から確認しない", "areYouSure": "本当によろしいですか?", "on": "オン", - "nodeEditor": "ノードエディター", "ipAdapter": "IPアダプター", "auto": "自動", "openInNewTab": "新しいタブで開く", "controlNet": "コントロールネット", "linear": "リニア", - "imageFailedToLoad": "画像が読み込めません", "modelManager": "モデルマネージャー", "learnMore": "もっと学ぶ", "random": "ランダム", "batch": "バッチマネージャー", - "advanced": "高度な設定", + "advanced": "高度", "created": "作成済", - "green": "緑", - "blue": "青", - "alpha": "アルファ", + "green": "G", + "blue": "B", + "alpha": "α", "outpaint": "アウトペイント", "unknown": "不明", "updated": "更新済", "add": "追加", - "ai": "AI", + "ai": "ai", "copyError": "$t(gallery.copy) エラー", "data": "データ", "template": "テンプレート", - "red": "赤", + "red": "R", "or": "または", - "checkpoint": "チェックポイント", - "direction": "方向", + "checkpoint": "Checkpoint", + "direction": "順序", "simple": "シンプル", "save": "保存", "saveAs": "名前をつけて保存", "somethingWentWrong": "何かの問題が発生しました", "details": "詳細", - "inpaint": "インペイント", + "inpaint": "inpaint", "delete": "削除", - "nextPage": "次のページ", "copy": "コピー", "error": "エラー", "file": "ファイル", @@ -66,243 +62,584 @@ "input": "インプット", "format": "形式", "installed": "インストール済み", - "localSystem": "ローカルシステム", "outputs": "アウトプット", - "prevPage": "前のページ", "unknownError": "未知のエラー", - "orderBy": "並び順:" + "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": "ギャラリーの設定", - "loadMore": "さらに読み込む", - "noImagesInGallery": "表示する画像がありません", "autoSwitchNewImages": "新しい画像に自動切替", "copy": "コピー", "image": "画像", - "setCurrentImage": "現在の画像としてセット", "autoAssignBoardOnClick": "クリックしたボードに自動追加", "featuresWillReset": "この画像を削除すると、これらの機能は即座にリセットされます。", "unstarImage": "スターを外す", "loading": "ロード中", - "assets": "アセット", "currentlyInUse": "この画像は現在下記の機能を使用しています:", - "problemDeletingImages": "画像の削除中に問題が発生", "drop": "ドロップ", - "dropOrUpload": "$t(gallery.drop) またはアップロード", - "deleteImage_other": "画像を削除", - "deleteImageBin": "削除された画像はOSのゴミ箱に送られます。", + "dropOrUpload": "ドロップまたはアップロード", + "deleteImage_other": "画像 {{count}} 枚を削除", "deleteImagePermanent": "削除された画像は復元できません。", "download": "ダウンロード", - "unableToLoad": "ギャラリーをロードできません", "bulkDownloadRequested": "ダウンロード準備中", "bulkDownloadRequestedDesc": "ダウンロードの準備中です。しばらくお待ちください。", "bulkDownloadRequestFailed": "ダウンロード準備中に問題が発生", "bulkDownloadFailed": "ダウンロード失敗", - "problemDeletingImagesDesc": "1つ以上の画像を削除できませんでした", "alwaysShowImageSizeBadge": "画像サイズバッジを常に表示", "dropToUpload": "$t(gallery.drop) してアップロード", "noImageSelected": "画像が選択されていません", "deleteSelection": "選択中のものを削除", "downloadSelection": "選択中のものをダウンロード", - "starImage": "スターをつける" + "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": { - "keyboardShortcuts": "ホットキー", - "appHotkeys": "アプリケーション", - "generalHotkeys": "一般", - "galleryHotkeys": "ギャラリー", - "unifiedCanvasHotkeys": "Unified Canvasのホットキー", - "invoke": { - "desc": "画像を生成", - "title": "Invoke" - }, - "cancel": { - "title": "キャンセル", - "desc": "現在のキューをキャンセル" - }, - "focusPrompt": { - "desc": "プロンプトテキストボックスにフォーカス", - "title": "プロンプトにフォーカス" - }, - "toggleOptions": { - "title": "オプションパネルのトグル", - "desc": "オプションパネルの開閉" - }, - "pinOptions": { - "title": "ピン", - "desc": "オプションパネルを固定" - }, - "toggleGallery": { - "title": "ギャラリーのトグル", - "desc": "ギャラリードロワーの開閉" - }, - "maximizeWorkSpace": { - "title": "作業領域の最大化", - "desc": "パネルを閉じて、作業領域を最大に" - }, - "changeTabs": { - "title": "タブの切替", - "desc": "他の作業領域と切替" - }, - "consoleToggle": { - "title": "コンソールのトグル", - "desc": "コンソールの開閉" - }, - "setPrompt": { - "title": "プロンプトをセット", - "desc": "現在の画像のプロンプトを使用" - }, - "setSeed": { - "title": "シード値をセット", - "desc": "現在の画像のシード値を使用" - }, - "setParameters": { - "title": "パラメータをセット", - "desc": "現在の画像のすべてのパラメータを使用" - }, - "restoreFaces": { - "title": "顔の修復", - "desc": "現在の画像を修復" - }, - "upscale": { - "title": "アップスケール", - "desc": "現在の画像をアップスケール" - }, - "showInfo": { - "title": "情報を見る", - "desc": "現在の画像のメタデータ情報を表示" - }, - "sendToImageToImage": { - "title": "Image To Imageに転送", - "desc": "現在の画像をImage to Imageに転送" - }, - "deleteImage": { - "title": "画像を削除", - "desc": "現在の画像を削除" - }, - "closePanels": { - "title": "パネルを閉じる", - "desc": "開いているパネルを閉じる" - }, - "previousImage": { - "title": "前の画像", - "desc": "ギャラリー内の1つ前の画像を表示" - }, - "nextImage": { - "title": "次の画像", - "desc": "ギャラリー内の1つ後の画像を表示" - }, - "increaseGalleryThumbSize": { - "title": "ギャラリーの画像を拡大", - "desc": "ギャラリーのサムネイル画像を拡大" - }, - "decreaseGalleryThumbSize": { - "title": "ギャラリーの画像サイズを縮小", - "desc": "ギャラリーのサムネイル画像を縮小" - }, - "selectBrush": { - "title": "ブラシを選択", - "desc": "ブラシを選択" - }, - "selectEraser": { - "title": "消しゴムを選択", - "desc": "消しゴムを選択" - }, - "decreaseBrushSize": { - "title": "ブラシサイズを縮小", - "desc": "ブラシ/消しゴムのサイズを縮小" - }, - "increaseBrushSize": { - "title": "ブラシサイズを拡大", - "desc": "ブラシ/消しゴムのサイズを拡大" - }, - "decreaseBrushOpacity": { - "title": "ブラシの不透明度を下げる", - "desc": "キャンバスブラシの不透明度を下げる" - }, - "increaseBrushOpacity": { - "title": "ブラシの不透明度を上げる", - "desc": "キャンバスブラシの不透明度を上げる" - }, - "fillBoundingBox": { - "title": "バウンディングボックスを塗りつぶす", - "desc": "ブラシの色でバウンディングボックス領域を塗りつぶす" - }, - "eraseBoundingBox": { - "title": "バウンディングボックスを消す", - "desc": "バウンディングボックス領域を消す" - }, - "colorPicker": { - "title": "カラーピッカーを選択", - "desc": "カラーピッカーを選択" - }, - "toggleLayer": { - "title": "レイヤーを切替", - "desc": "マスク/ベースレイヤの選択を切替" - }, - "clearMask": { - "title": "マスクを消す", - "desc": "マスク全体を消す" - }, - "hideMask": { - "title": "マスクを非表示", - "desc": "マスクを表示/非表示" - }, - "showHideBoundingBox": { - "title": "バウンディングボックスを表示/非表示", - "desc": "バウンディングボックスの表示/非表示を切替" - }, - "saveToGallery": { - "title": "ギャラリーに保存", - "desc": "現在のキャンバスをギャラリーに保存" - }, - "copyToClipboard": { - "title": "クリップボードにコピー", - "desc": "現在のキャンバスをクリップボードにコピー" - }, - "downloadImage": { - "title": "画像をダウンロード", - "desc": "現在の画像をダウンロード" - }, - "resetView": { - "title": "キャンバスをリセット", - "desc": "キャンバスをリセット" - }, - "acceptStagingImage": { - "title": "プレビュー画像の採用", - "desc": "現在のプレビュー画像を採用する" - }, - "addNodes": { - "desc": "ノード追加メニューを開く", - "title": "ノードを追加" + "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": "リミックス" + } }, - "moveTool": { - "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": "選択中の種別の表示レイヤーをマージします。" + } }, - "nextStagingImage": { - "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": "コピーされたノード,エッジ,コピーされたノードに付属した全てのエッジを貼り付けます." + } }, - "cancelAndClear": { - "desc": "生成をキャンセルしキューもクリアします", - "title": "キャンセルとクリア" + "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": "プロンプトのテキストが選択されている際に、選択されているプロンプトの重みを減らします。" + } }, - "mergeVisible": { - "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": "選択した画像にスターを付けたり、スターを外したりします。" + } }, - "searchHotkeys": "ホットキーを検索", - "clearSearch": "検索をクリア", - "noHotkeysFound": "ホットキーが見つかりません", - "remixImage": { - "title": "画像をリミックス", - "desc": "現在の画像のシード値を除く全パラメータを使用" - }, - "redoStroke": { - "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": "モデルマネージャ", @@ -313,14 +650,14 @@ "name": "名前", "description": "概要", "config": "コンフィグ", - "repo_id": "Repo ID", + "repo_id": "リポジトリID", "width": "幅", "height": "高さ", "addModel": "モデルを追加", "availableModels": "モデルを有効化", "search": "検索", - "load": "Load", - "active": "active", + "load": "ロード", + "active": "アクティブ", "selected": "選択済", "delete": "削除", "deleteModel": "モデルを削除", @@ -337,10 +674,9 @@ "convertToDiffusers": "ディフューザーに変換", "alpha": "アルファ", "modelConverted": "モデル変換が完了しました", - "predictionType": "予測タイプ(安定したディフュージョン 2.x モデルおよび一部の安定したディフュージョン 1.x モデル用)", + "predictionType": "予測タイプ(SD 2.x モデルおよび一部のSD 1.x モデル用)", "selectModel": "モデルを選択", - "modelSyncFailed": "モデルの同期に失敗しました", - "advanced": "高度な設定", + "advanced": "高度", "modelDeleted": "モデルが削除されました", "convertToDiffusersHelpText2": "このプロセスでは、モデルマネージャーのエントリーを同じモデルのディフューザーバージョンに置き換えます。", "modelUpdateFailed": "モデル更新が失敗しました", @@ -349,110 +685,530 @@ "syncModels": "モデルを同期", "modelType": "モデルタイプ", "convertToDiffusersHelpText1": "このモデルは 🧨 Diffusers フォーマットに変換されます。", - "modelsSynced": "モデルが同期されました", "convertToDiffusersHelpText3": "チェックポイントファイルは、InvokeAIルートフォルダ内にある場合、ディスクから削除されます。カスタムロケーションにある場合は、削除されません。", "convertToDiffusersHelpText4": "これは一回限りのプロセスです。コンピュータの仕様によっては、約30秒から60秒かかる可能性があります。", - "cancel": "キャンセル" + "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": "ステップ数", + "steps": "ステップ", "width": "幅", "height": "高さ", "seed": "シード値", "shuffle": "シャッフル", "strength": "強度", - "upscaling": "アップスケーリング", - "upscale": "アップスケール", - "upscaleImage": "画像をアップスケール", - "scale": "Scale", - "scaleBeforeProcessing": "処理前のスケール", + "upscaling": "アップスケール", + "scale": "スケール", + "scaleBeforeProcessing": "生成前のリサイズ", "scaledWidth": "幅のスケール", "scaledHeight": "高さのスケール", - "sendToImg2Img": "Image to Imageに転送", - "sendToUnifiedCanvas": "Unified Canvasに転送", - "downloadImage": "画像をダウンロード", "usePrompt": "プロンプトを使用", "useSeed": "シード値を使用", "useAll": "すべてを使用", "info": "情報", - "showOptionsPanel": "オプションパネルを表示", + "iterations": "生成回数", + "general": "基本設定", + "setToOptimalSize": "サイズをモデルに最適化", "invoke": { - "noControlImageForControlAdapter": "コントロールアダプター #{{number}} に画像がありません", - "noModelForControlAdapter": "コントロールアダプター #{{number}} のモデルが選択されていません。" + "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 が追加されました" }, - "iterations": "生成回数", - "general": "基本設定" + "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": "削除時に確認", - "enableImageDebugging": "画像のデバッグを有効化", + "confirmOnDelete": "削除時に確認する", "resetWebUI": "WebUIをリセット", - "resetWebUIDesc1": "WebUIのリセットは、画像と保存された設定のキャッシュをリセットするだけです。画像を削除するわけではありません。", + "resetWebUIDesc1": "WebUIのリセットは、ブラウザローカルキャッシュの画像と設定のリセットします。ディスク内の画像は削除されません。", "resetWebUIDesc2": "もしギャラリーに画像が表示されないなど、何か問題が発生した場合はGitHubにissueを提出する前にリセットを試してください。", - "resetComplete": "WebUIはリセットされました。F5を押して再読み込みしてください。" + "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": "画像をコピー", - "imageNotLoadedDesc": "Image To Imageに転送する画像が見つかりません。", - "canvasMerged": "Canvas Merged", - "sentToImageToImage": "Image To Imageに転送", - "sentToUnifiedCanvas": "Unified Canvasに転送", - "metadataLoadFailed": "メタデータの読み込みに失敗。" - }, - "tooltip": { - "feature": { - "prompt": "これはプロンプトフィールドです。プロンプトには生成オブジェクトや文法用語が含まれます。プロンプトにも重み(Tokenの重要度)を付けることができますが、CLIコマンドやパラメータは機能しません。", - "gallery": "ギャラリーは、出力先フォルダから生成物を表示します。設定はファイル内に保存され、コンテキストメニューからアクセスできます。.", - "seed": "シード値は、画像が形成される際の初期ノイズに影響します。以前の画像から既に存在するシードを使用することができます。ノイズしきい値は高いCFG値でのアーティファクトを軽減するために使用され、Perlinは生成中にPerlinノイズを追加します(0-10の範囲を試してみてください): どちらも出力にバリエーションを追加するのに役立ちます。", - "upscale": "生成直後の画像をアップスケールするには、ESRGANを使用します。", - "boundingBox": "バウンディングボックスは、Text To ImageまたはImage To Imageの幅/高さの設定と同じです。ボックス内の領域のみが処理されます。" - } - }, - "unifiedCanvas": { - "mask": "マスク", - "maskingOptions": "マスクのオプション", - "enableMask": "マスクを有効化", - "preserveMaskedArea": "マスク領域の保存", - "clearMask": "マスクを解除", - "brush": "ブラシ", - "eraser": "消しゴム", - "fillBoundingBox": "バウンディングボックスの塗りつぶし", - "eraseBoundingBox": "バウンディングボックスの消去", - "colorPicker": "カラーピッカー", - "brushOptions": "ブラシオプション", - "brushSize": "サイズ", - "saveToGallery": "ギャラリーに保存", - "copyToClipboard": "クリップボードにコピー", - "downloadAsImage": "画像としてダウンロード", - "undo": "取り消し", - "redo": "やり直し", - "clearCanvas": "キャンバスを片付ける", - "canvasSettings": "キャンバスの設定", - "showGrid": "グリッドを表示", - "darkenOutsideSelection": "外周を暗くする", - "autoSaveToGallery": "ギャラリーに自動保存", - "saveBoxRegionOnly": "ボックス領域のみ保存", - "showCanvasDebugInfo": "キャンバスのデバッグ情報を表示", - "clearCanvasHistory": "キャンバスの履歴を削除", - "clearHistory": "履歴を削除", - "clearCanvasHistoryMessage": "履歴を消去すると現在のキャンバスは残りますが、取り消しややり直しの履歴は不可逆的に消去されます。", - "clearCanvasHistoryConfirm": "履歴を削除しますか?", - "activeLayer": "Active Layer", - "canvasScale": "Canvas Scale", - "boundingBox": "バウンディングボックス", - "boundingBoxPosition": "バウンディングボックスの位置", - "canvasDimensions": "キャンバスの大きさ", - "canvasPosition": "キャンバスの位置", - "cursorPosition": "カーソルの位置", - "previous": "前", - "next": "次", - "accept": "同意", - "discardAll": "すべて破棄", - "snapToGrid": "グリッドにスナップ" + "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": "進捗バー", @@ -460,110 +1216,59 @@ "uploadImage": "画像をアップロード", "previousImage": "前の画像", "nextImage": "次の画像", - "showOptionsPanel": "サイドパネルを表示", - "showGalleryPanel": "ギャラリーパネルを表示", "menu": "メニュー", - "loadMore": "さらに読み込む", - "createIssue": "課題の作成", + "createIssue": "問題を報告", "resetUI": "$t(accessibility.reset) UI", - "mode": "モード:", - "about": "Invoke について" - }, - "controlnet": { - "resize": "リサイズ", - "showAdvanced": "高度な設定を表示", - "addT2IAdapter": "$t(common.t2iAdapter)を追加", - "importImageFromCanvas": "キャンバスから画像をインポート", - "lineartDescription": "画像を線画に変換", - "importMaskFromCanvas": "キャンバスからマスクをインポート", - "hideAdvanced": "高度な設定を非表示", - "resetControlImage": "コントロール画像をリセット", - "beginEndStepPercent": "開始 / 終了ステップパーセンテージ", - "duplicate": "複製", - "balanced": "バランス", - "prompt": "プロンプト", - "depthMidasDescription": "Midasを使用して深度マップを生成", - "control": "コントロール", - "resizeMode": "リサイズモード", - "weight": "重み", - "selectModel": "モデルを選択", - "crop": "切り抜き", - "w": "幅", - "processor": "プロセッサー", - "addControlNet": "$t(common.controlNet)を追加", - "none": "なし", - "detectResolution": "検出解像度", - "pidiDescription": "PIDI画像処理", - "controlMode": "コントロールモード", - "fill": "塗りつぶし", - "cannyDescription": "Canny 境界検出", - "addIPAdapter": "$t(common.ipAdapter)を追加", - "colorMapDescription": "画像からカラーマップを生成", - "lineartAnimeDescription": "アニメスタイルの線画処理", - "imageResolution": "画像解像度", - "megaControl": "メガコントロール", - "lowThreshold": "最低閾値", - "autoConfigure": "プロセッサーを自動設定", - "highThreshold": "最大閾値", - "saveControlImage": "コントロール画像を保存", - "toggleControlNet": "このコントロールネットを切り替え", - "delete": "削除", - "controlAdapter_other": "コントロールアダプター", - "colorMapTileSize": "タイルサイズ", - "mediapipeFaceDescription": "Mediapipeを使用して顔を検出", - "depthZoeDescription": "Zoeを使用して深度マップを生成", - "setControlImageDimensions": "コントロール画像のサイズを幅と高さにセット", - "amult": "a_mult", - "contentShuffleDescription": "画像の内容をシャッフルします", - "bgth": "bg_th", - "controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))", - "ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))", - "t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))", - "minConfidence": "最小確信度", - "colorMap": "Color", - "noneDescription": "処理は行われていません", - "canny": "Canny", - "hedDescription": "階層的エッジ検出", - "maxFaces": "顔の最大数", - "depthMidas": "深度 (Midas)", - "f": "F", - "h": "H", - "lineart": "線画", - "depthAnythingDescription": "Depth Anything 技術を使って深度マップを生成します", - "hed": "HED", - "normalBaeDescription": "法線 BAE 処理中", - "pidi": "PIDI", - "resizeSimple": "リサイズ(シンプル)", - "scribble": "らくがき", - "small": "小型", - "mediapipeFace": "Mediapipe 顔", - "mlsd": "M-LSD", - "normalBae": "法線 BAE", - "base": "ベース", - "contentShuffle": "シャッフル", - "modelSize": "モデルサイズ", - "safe": "セーフモード", - "depthZoe": "深度 (Zoe)", - "face": "顔", - "body": "体", - "hands": "手", - "large": "大型", - "lineartAnime": "アニメ線画", - "mlsdDescription": "最小線分検知", - "dwOpenpose": "DW オープンポーズ", - "dwOpenposeDescription": "DW オープンポーズによる人体ポーズの推定" + "mode": "モード", + "about": "Invoke について", + "submitSupportTicket": "サポート依頼を送信する", + "uploadImages": "画像をアップロード", + "toggleLeftPanel": "左パネルをトグル (T)", + "toggleRightPanel": "右パネルをトグル (G)" }, "metadata": { - "seamless": "シームレス", "Threshold": "ノイズ閾値", "seed": "シード", "width": "幅", "workflow": "ワークフロー", "steps": "ステップ", - "scheduler": "スケジューラー", + "scheduler": "スケジューラ", "positivePrompt": "ポジティブプロンプト", "strength": "Image to Image 強度", - "recallParameters": "パラメータを呼び出す" + "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": "キューが空です", @@ -595,7 +1300,7 @@ "batchQueuedDesc_other": "{{count}} セッションをキューの{{direction}}に追加しました", "graphQueued": "グラフをキューに追加しました", "batch": "バッチ", - "clearQueueAlertDialog": "キューをクリアすると、処理中のアイテムは直ちにキャンセルされ、キューは完全にクリアされます。", + "clearQueueAlertDialog": "キューをクリアすると、処理中の項目は直ちにキャンセルされ、キューは完全にクリアされます。保留中のフィルターもキャンセルされ、ステージングエリアもリセットされます。", "pending": "保留中", "resumeFailed": "処理の再開に問題があります", "clear": "クリア", @@ -613,29 +1318,70 @@ "enqueueing": "バッチをキューに追加", "cancelBatchFailed": "バッチのキャンセルに問題があります", "clearQueueAlertDialog2": "キューをクリアしてもよろしいですか?", - "item": "アイテム", + "item": "項目", "graphFailedToQueue": "グラフをキューに追加できませんでした", - "batchFieldValues": "バッチの詳細", "openQueue": "キューを開く", "time": "時間", "completedIn": "完了まで", "back": "戻る", - "prune": "刈り込み" + "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": "読み込み中", - "noMatchingLoRAs": "一致するLoRAがありません", "noModelsAvailable": "使用可能なモデルがありません", - "selectModel": "モデルを選択してください" + "selectModel": "モデルを選択してください", + "concepts": "コンセプト", + "addLora": "LoRAを追加", + "lora": "LoRA", + "defaultVAE": "デフォルトVAE", + "noRefinerModelsInstalled": "インストールされているSDXLリファイナーモデルはありません", + "noCompatibleLoRAs": "互換性のあるLoRAはありません", + "noMatchingLoRAs": "一致するLoRAがありません", + "noLoRAsInstalled": "LoRAがインストールされていません" }, "nodes": { "addNode": "ノードを追加", "boolean": "ブーリアン", "addNodeToolTip": "ノードを追加 (Shift+A, Space)", - "missingTemplate": "テンプレートが見つかりません", + "missingTemplate": "Invalid node: タイプ {{type}} のノード {{node}} にテンプレートがありません(未インストール?)", "loadWorkflow": "ワークフローを読み込み", - "hideLegendNodes": "フィールドタイプの凡例を非表示", "float": "浮動小数点", "integer": "整数", "nodeTemplate": "ノードテンプレート", @@ -644,11 +1390,11 @@ "currentImageDescription": "ノードエディタ内の現在の画像を表示", "downloadWorkflow": "ワークフローのJSONをダウンロード", "fieldTypesMustMatch": "フィールドタイプが一致している必要があります", - "edge": "輪郭", - "animatedEdgesHelp": "選択したエッジおよび選択したノードに接続されたエッジをアニメーション化します", + "edge": "エッジ", + "animatedEdgesHelp": "選択したエッジ、選択したノードに接続されたエッジをアニメーション表示", "cannotDuplicateConnection": "重複した接続は作れません", "noWorkflow": "ワークフローがありません", - "fullyContainNodesHelp": "ノードは選択ボックス内に完全に存在する必要があります", + "fullyContainNodesHelp": "選択ボックス内に完全に含まれているノードだけが選択されます", "nodeType": "ノードタイプ", "executionStateInProgress": "処理中", "executionStateError": "エラー", @@ -661,8 +1407,166 @@ "cannotConnectInputToInput": "入力から入力には接続できません", "cannotConnectOutputToOutput": "出力から出力には接続できません", "cannotConnectToSelf": "自身のノードには接続できません", - "colorCodeEdges": "カラー-Code Edges", - "loadingNodes": "ノードを読み込み中..." + "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": "自動追加するボード", @@ -678,14 +1582,50 @@ "downloadBoard": "ボードをダウンロード", "changeBoard": "ボードを変更", "loading": "ロード中...", - "topMessage": "このボードには、以下の機能で使用されている画像が含まれています:", - "bottomMessage": "このボードおよび画像を削除すると、現在これらを利用している機能はリセットされます。", + "topMessage": "この選択には、次の機能で使用される画像が含まれています:", + "bottomMessage": "この画像を削除すると、現在利用している機能はリセットされます。", "clearSearch": "検索をクリア", "deleteBoard": "ボードの削除", "deleteBoardAndImages": "ボードと画像の削除", "deleteBoardOnly": "ボードのみ削除", - "deletedBoardsCannotbeRestored": "削除されたボードは復元できません", - "movingImagesToBoard_other": "{{count}} の画像をボードに移動:" + "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": "呼び出しキャッシュ", @@ -708,18 +1648,458 @@ "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": "コヒーレンスパス" + "title": "コンポジット", + "coherenceTab": "境界のなじませ" }, "advanced": { - "title": "高度な設定" + "title": "高度", + "options": "$t(accordions.advanced.title) オプション" }, "control": { "title": "コントロール" @@ -737,13 +2117,1196 @@ "strength": "高解像修復の強度", "enabled": "高解像修復が有効" }, - "enableHrf": "高解像修復を有効", "hrf": "高解像修復", + "enableHrf": "高解像度修正を有効にする", "upscaleMethod": "アップスケール手法" }, "prompt": { - "addPromptTrigger": "プロンプトトリガーを追加", + "addPromptTrigger": "トリガーワードを追加", "compatibleEmbeddings": "互換性のある埋め込み", - "noMatchingTriggers": "一致するトリガーがありません" + "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 index db9cd0ca673..6ead01bd1ba 100644 --- a/invokeai/frontend/web/public/locales/ko.json +++ b/invokeai/frontend/web/public/locales/ko.json @@ -4,7 +4,6 @@ "reportBugLabel": "버그 리포트", "githubLabel": "Github", "settingsLabel": "설정", - "unifiedCanvas": "통합 캔버스", "nodes": "Workflow Editor", "upload": "업로드", "load": "불러오기", @@ -27,9 +26,7 @@ "on": "켜기", "save": "저장", "created": "생성됨", - "nodeEditor": "Node Editor", "error": "에러", - "prevPage": "이전 페이지", "ipAdapter": "IP 어댑터", "installed": "설치됨", "accept": "수락", @@ -44,7 +41,6 @@ "outputs": "결과물", "unknownError": "알려지지 않은 에러", "linear": "선형", - "imageFailedToLoad": "이미지를 로드할 수 없음", "direction": "방향", "data": "데이터", "somethingWentWrong": "뭔가 잘못됐어요", @@ -54,7 +50,6 @@ "orderBy": "정렬 기준", "copyError": "$t(gallery.copy) 에러", "learnMore": "더 알아보기", - "nextPage": "다음 페이지", "saveAs": "다른 이름으로 저장", "loading": "불러오는 중", "random": "랜덤", @@ -62,25 +57,17 @@ "postprocessing": "후처리", "advanced": "고급", "input": "입력", - "details": "세부사항", - "notInstalled": "설치되지 않음" + "details": "세부사항" }, "gallery": { "galleryImageSize": "이미지 크기", "gallerySettings": "갤러리 설정", "deleteSelection": "선택 항목 삭제", "featuresWillReset": "이 이미지를 삭제하면 해당 기능이 즉시 재설정됩니다.", - "deleteImageBin": "삭제된 이미지는 운영 체제의 Bin으로 전송됩니다.", - "assets": "자산", - "problemDeletingImagesDesc": "하나 이상의 이미지를 삭제할 수 없습니다", - "noImagesInGallery": "보여줄 이미지가 없음", "autoSwitchNewImages": "새로운 이미지로 자동 전환", "loading": "불러오는 중", - "unableToLoad": "갤러리를 로드할 수 없음", "image": "이미지", - "loadMore": "더 불러오기", "drop": "드랍", - "problemDeletingImages": "이미지 삭제 중 발생한 문제", "downloadSelection": "선택 항목 다운로드", "deleteImage_other": "이미지 삭제", "currentlyInUse": "이 이미지는 현재 다음 기능에서 사용되고 있습니다:", @@ -90,7 +77,6 @@ "deleteImagePermanent": "삭제된 이미지는 복원할 수 없습니다.", "noImageSelected": "선택된 이미지 없음", "autoAssignBoardOnClick": "클릭 시 Board로 자동 할당", - "setCurrentImage": "현재 이미지로 설정", "dropToUpload": "업로드를 위해 $t(gallery.drop)" }, "accessibility": { @@ -99,10 +85,7 @@ "mode": "모드", "menu": "메뉴", "uploadImage": "이미지 업로드", - "showGalleryPanel": "갤러리 패널 표시", - "reset": "리셋", - "loadMore": "더 불러오기", - "showOptionsPanel": "사이드 패널 표시" + "reset": "리셋" }, "modelManager": { "availableModels": "사용 가능한 모델", @@ -118,7 +101,6 @@ "predictionType": "예측 유형(안정 확산 2.x 모델 및 간혹 안정 확산 1.x 모델의 경우)", "selectModel": "모델 선택", "repo_id": "Repo ID", - "modelSyncFailed": "모델 동기화 실패", "convertToDiffusersHelpText6": "이 모델을 변환하시겠습니까?", "config": "구성", "selected": "선택된", @@ -137,10 +119,8 @@ "syncModels": "동기화 모델", "modelType": "모델 유형", "convertingModelBegin": "모델 변환 중입니다. 잠시만 기다려 주십시오.", - "v2_base": "v2 (512px)", "name": "이름", "convertToDiffusersHelpText1": "이 모델은 🧨 Diffusers 형식으로 변환됩니다.", - "modelsSynced": "동기화된 모델", "vaePrecision": "VAE 정밀도", "deleteMsg2": "모델이 InvokeAI root 폴더에 있으면 디스크에서 모델이 삭제됩니다. 사용자 지정 위치를 사용하는 경우 모델이 디스크에서 삭제되지 않습니다.", "baseModel": "기본 모델", @@ -153,283 +133,10 @@ "allModels": "모든 모델", "alpha": "Alpha", "noModelSelected": "선택한 모델 없음", - "v2_768": "v2 (768px)", "convertToDiffusersHelpText4": "이것은 한 번의 과정일 뿐입니다. 컴퓨터 사양에 따라 30-60초 정도 소요될 수 있습니다.", "model": "모델", "delete": "삭제" }, - "controlnet": { - "amult": "a_mult", - "resize": "크기 조정", - "showAdvanced": "고급 표시", - "contentShuffleDescription": "이미지에서 content 섞기", - "bgth": "bg_th", - "addT2IAdapter": "$t(common.t2iAdapter) 추가", - "pidi": "PIDI", - "importImageFromCanvas": "캔버스에서 이미지 가져오기", - "lineartDescription": "이미지->lineart 변환", - "normalBae": "Normal BAE", - "importMaskFromCanvas": "캔버스에서 Mask 가져오기", - "hed": "HED", - "contentShuffle": "Content Shuffle", - "resetControlImage": "Control Image 재설정", - "beginEndStepPercent": "Begin / End Step Percentage", - "mlsdDescription": "Minimalist Line Segment Detector", - "duplicate": "복제", - "balanced": "Balanced", - "f": "F", - "h": "H", - "prompt": "프롬프트", - "depthMidasDescription": "Midas를 사용하여 Depth map 생성하기", - "control": "Control", - "resizeMode": "크기 조정 모드", - "coarse": "Coarse", - "weight": "Weight", - "selectModel": "모델 선택", - "crop": "Crop", - "depthMidas": "Depth (Midas)", - "w": "W", - "processor": "프로세서", - "addControlNet": "$t(common.controlNet) 추가", - "none": "해당없음", - "detectResolution": "해상도 탐지", - "pidiDescription": "PIDI image 처리", - "mediapipeFace": "Mediapipe Face", - "mlsd": "M-LSD", - "controlMode": "Control Mode", - "fill": "채우기", - "cannyDescription": "Canny 모서리 삭제", - "addIPAdapter": "$t(common.ipAdapter) 추가", - "lineart": "Lineart", - "colorMapDescription": "이미지에서 color map을 생성합니다", - "lineartAnimeDescription": "Anime-style lineart 처리", - "minConfidence": "Min Confidence", - "imageResolution": "이미지 해상도", - "megaControl": "Mega Control", - "depthZoe": "Depth (Zoe)", - "colorMap": "색", - "lowThreshold": "Low Threshold", - "autoConfigure": "프로세서 자동 구성", - "highThreshold": "High Threshold", - "normalBaeDescription": "Normal BAE 처리", - "noneDescription": "처리되지 않음", - "saveControlImage": "Control Image 저장", - "toggleControlNet": "해당 ControlNet으로 전환", - "delete": "삭제", - "controlAdapter_other": "Control Adapter(s)", - "safe": "Safe", - "colorMapTileSize": "타일 크기", - "lineartAnime": "Lineart Anime", - "mediapipeFaceDescription": "Mediapipe를 사용하여 Face 탐지", - "canny": "Canny", - "depthZoeDescription": "Zoe를 사용하여 Depth map 생성하기", - "hedDescription": "Holistically-Nested 모서리 탐지", - "setControlImageDimensions": "Control Image Dimensions를 W/H로 설정", - "scribble": "scribble", - "maxFaces": "Max Faces" - }, - "hotkeys": { - "toggleSnap": { - "desc": "Snap을 Grid로 전환", - "title": "Snap 전환" - }, - "setSeed": { - "title": "시드 설정", - "desc": "현재 이미지의 시드 사용" - }, - "keyboardShortcuts": "키보드 바로 가기", - "decreaseGalleryThumbSize": { - "desc": "갤러리 미리 보기 크기 축소", - "title": "갤러리 이미지 크기 축소" - }, - "previousStagingImage": { - "title": "이전 스테이징 이미지", - "desc": "이전 스테이징 영역 이미지" - }, - "decreaseBrushSize": { - "title": "브러시 크기 줄이기", - "desc": "캔버스 브러시/지우개 크기 감소" - }, - "consoleToggle": { - "desc": "콘솔 열고 닫기", - "title": "콘솔 전환" - }, - "selectBrush": { - "desc": "캔버스 브러시를 선택", - "title": "브러시 선택" - }, - "upscale": { - "desc": "현재 이미지를 업스케일", - "title": "업스케일" - }, - "previousImage": { - "title": "이전 이미지", - "desc": "갤러리에 이전 이미지 표시" - }, - "unifiedCanvasHotkeys": "Unified Canvas Hotkeys", - "toggleOptions": { - "desc": "옵션 패널을 열고 닫기", - "title": "옵션 전환" - }, - "selectEraser": { - "title": "지우개 선택", - "desc": "캔버스 지우개를 선택" - }, - "setPrompt": { - "title": "프롬프트 설정", - "desc": "현재 이미지의 프롬프트 사용" - }, - "acceptStagingImage": { - "desc": "현재 준비 영역 이미지 허용", - "title": "준비 이미지 허용" - }, - "resetView": { - "desc": "Canvas View 초기화", - "title": "View 초기화" - }, - "hideMask": { - "title": "Mask 숨김", - "desc": "mask 숨김/숨김 해제" - }, - "pinOptions": { - "title": "옵션 고정", - "desc": "옵션 패널을 고정" - }, - "toggleGallery": { - "desc": "gallery drawer 열기 및 닫기", - "title": "Gallery 전환" - }, - "quickToggleMove": { - "title": "빠른 토글 이동", - "desc": "일시적으로 이동 모드 전환" - }, - "generalHotkeys": "General Hotkeys", - "showHideBoundingBox": { - "desc": "bounding box 표시 전환", - "title": "Bounding box 표시/숨김" - }, - "showInfo": { - "desc": "현재 이미지의 metadata 정보 표시", - "title": "정보 표시" - }, - "copyToClipboard": { - "title": "클립보드로 복사", - "desc": "현재 캔버스를 클립보드로 복사" - }, - "restoreFaces": { - "title": "Faces 복원", - "desc": "현재 이미지 복원" - }, - "fillBoundingBox": { - "title": "Bounding Box 채우기", - "desc": "bounding box를 브러시 색으로 채웁니다" - }, - "closePanels": { - "desc": "열린 panels 닫기", - "title": "panels 닫기" - }, - "downloadImage": { - "desc": "현재 캔버스 다운로드", - "title": "이미지 다운로드" - }, - "setParameters": { - "title": "매개 변수 설정", - "desc": "현재 이미지의 모든 매개 변수 사용" - }, - "maximizeWorkSpace": { - "desc": "패널을 닫고 작업 면적을 극대화", - "title": "작업 공간 극대화" - }, - "galleryHotkeys": "Gallery Hotkeys", - "cancel": { - "desc": "이미지 생성 취소", - "title": "취소" - }, - "saveToGallery": { - "title": "갤러리에 저장", - "desc": "현재 캔버스를 갤러리에 저장" - }, - "eraseBoundingBox": { - "desc": "bounding box 영역을 지웁니다", - "title": "Bounding Box 지우기" - }, - "nextImage": { - "title": "다음 이미지", - "desc": "갤러리에 다음 이미지 표시" - }, - "colorPicker": { - "desc": "canvas color picker 선택", - "title": "Color Picker 선택" - }, - "invoke": { - "desc": "이미지 생성", - "title": "불러오기" - }, - "sendToImageToImage": { - "desc": "현재 이미지를 이미지로 보내기" - }, - "toggleLayer": { - "desc": "mask/base layer 선택 전환", - "title": "Layer 전환" - }, - "increaseBrushSize": { - "title": "브러시 크기 증가", - "desc": "캔버스 브러시/지우개 크기 증가" - }, - "appHotkeys": "App Hotkeys", - "deleteImage": { - "title": "이미지 삭제", - "desc": "현재 이미지 삭제" - }, - "moveTool": { - "desc": "캔버스 탐색 허용", - "title": "툴 옮기기" - }, - "clearMask": { - "desc": "전체 mask 제거", - "title": "Mask 제거" - }, - "increaseGalleryThumbSize": { - "title": "갤러리 이미지 크기 증가", - "desc": "갤러리 미리 보기 크기를 늘립니다" - }, - "increaseBrushOpacity": { - "desc": "캔버스 브러시의 불투명도를 높입니다", - "title": "브러시 불투명도 증가" - }, - "focusPrompt": { - "desc": "프롬프트 입력 영역에 초점을 맞춥니다", - "title": "프롬프트에 초점 맞추기" - }, - "decreaseBrushOpacity": { - "desc": "캔버스 브러시의 불투명도를 줄입니다", - "title": "브러시 불투명도 감소" - }, - "nextStagingImage": { - "desc": "다음 스테이징 영역 이미지", - "title": "다음 스테이징 이미지" - }, - "redoStroke": { - "title": "Stroke 다시 실행", - "desc": "brush stroke 다시 실행" - }, - "nodesHotkeys": "Nodes Hotkeys", - "addNodes": { - "desc": "노드 추가 메뉴 열기", - "title": "노드 추가" - }, - "undoStroke": { - "title": "Stroke 실행 취소", - "desc": "brush stroke 실행 취소" - }, - "changeTabs": { - "desc": "다른 workspace으로 전환", - "title": "탭 바꾸기" - }, - "mergeVisible": { - "desc": "캔버스의 보이는 모든 레이어 병합" - } - }, "nodes": { "missingTemplate": "잘못된 노드: {{type}} 유형의 {{node}} 템플릿 누락(설치되지 않으셨나요?)", "noNodeSelected": "선택한 노드 없음", @@ -438,8 +145,6 @@ "loadWorkflow": "Workflow 불러오기", "noOutputRecorded": "기록된 출력 없음", "colorCodeEdgesHelp": "연결된 필드에 따른 색상 코드 선", - "hideLegendNodes": "필드 유형 범례 숨기기", - "addLinearView": "Linear View에 추가", "float": "실수", "targetNodeFieldDoesNotExist": "잘못된 모서리: 대상/입력 필드 {{node}}. {{field}}이(가) 없습니다", "animatedEdges": "애니메이션 모서리", @@ -447,7 +152,6 @@ "nodeTemplate": "노드 템플릿", "nodeOpacity": "노드 불투명도", "sourceNodeDoesNotExist": "잘못된 모서리: 소스/출력 노드 {{node}}이(가) 없습니다", - "noFieldsLinearview": "Linear View에 추가된 필드 없음", "nodeSearch": "노드 검색", "inputMayOnlyHaveOneConnection": "입력에 하나의 연결만 있을 수 있습니다", "notes": "메모", @@ -456,7 +160,6 @@ "downloadWorkflow": "Workflow JSON 다운로드", "ipAdapter": "IP-Adapter", "noConnectionInProgress": "진행중인 연결이 없습니다", - "noConnectionData": "연결 데이터 없음", "fieldTypesMustMatch": "필드 유형은 일치해야 합니다", "edge": "Edge", "sourceNodeFieldDoesNotExist": "잘못된 모서리: 소스/출력 필드 {{node}}. {{field}}이(가) 없습니다", @@ -466,10 +169,8 @@ "fullyContainNodesHelp": "선택하려면 노드가 선택 상자 안에 완전히 있어야 합니다", "nodePack": "Node pack", "nodeType": "노드 유형", - "noMatchingNodes": "일치하는 노드 없음", "fullyContainNodes": "선택할 노드 전체 포함", "executionStateInProgress": "진행중", - "noFieldType": "필드 유형 없음", "executionStateError": "에러", "boolean": "Booleans", "hideMinimapnodes": "미니맵 숨기기", @@ -485,7 +186,6 @@ "notesDescription": "Workflow에 대한 메모 추가", "colorCodeEdges": "색상-코드 선", "targetNodeDoesNotExist": "잘못된 모서리: 대상/입력 노드 {{node}}이(가) 없습니다", - "mismatchedVersion": "잘못된 노드: {{type}} 유형의 {{node}} 노드에 일치하지 않는 버전이 있습니다(업데이트 해보시겠습니까?)", "addNodeToolTip": "노드 추가(Shift+A, Space)", "collectionOrScalarFieldType": "{{name}} 컬렉션|Scalar", "nodeVersion": "노드 버전", @@ -532,7 +232,6 @@ "next": "다음", "cancelBatch": "Batch 취소", "back": "back", - "batchFieldValues": "Batch 필드 값들", "cancel": "취소", "session": "세션", "time": "시간", @@ -556,7 +255,6 @@ "model": "모델", "noImageDetails": "이미지 세부 정보를 찾을 수 없습니다", "cfgScale": "CFG scale", - "initImage": "초기이미지", "recallParameters": "매개변수 호출", "height": "Height", "noMetaData": "metadata를 찾을 수 없습니다", @@ -587,8 +285,6 @@ "cacheSize": "캐시 크기" }, "hrf": { - "enableHrf": "이용 가능한 고해상도 고정", - "upscaleMethod": "업스케일 방법", "metadata": { "strength": "고해상도 고정 강도", "enabled": "고해상도 고정 사용", @@ -598,14 +294,11 @@ }, "models": { "noMatchingModels": "일치하는 모델 없음", - "esrganModel": "ESRGAN 모델", "loading": "로딩중", - "noMatchingLoRAs": "일치하는 LoRA 없음", "noModelsAvailable": "사용 가능한 모델이 없음", "addLora": "LoRA 추가", "selectModel": "모델 선택", - "noRefinerModelsInstalled": "SDXL Refiner 모델이 설치되지 않음", - "noLoRAsInstalled": "설치된 LoRA 없음" + "noRefinerModelsInstalled": "SDXL Refiner 모델이 설치되지 않음" }, "boards": { "autoAddBoard": "자동 추가 Board", diff --git a/invokeai/frontend/web/public/locales/nl.json b/invokeai/frontend/web/public/locales/nl.json index afcce621638..2da6709348e 100644 --- a/invokeai/frontend/web/public/locales/nl.json +++ b/invokeai/frontend/web/public/locales/nl.json @@ -5,7 +5,6 @@ "reportBugLabel": "Meld bug", "settingsLabel": "Instellingen", "img2img": "Afbeelding naar afbeelding", - "unifiedCanvas": "Centraal canvas", "nodes": "Werkstromen", "upload": "Upload", "load": "Laad", @@ -28,16 +27,13 @@ "communityLabel": "Gemeenschap", "t2iAdapter": "T2I-adapter", "on": "Aan", - "nodeEditor": "Knooppunteditor", "ipAdapter": "IP-adapter", "auto": "Autom.", "controlNet": "ControlNet", - "imageFailedToLoad": "Kan afbeelding niet laden", "learnMore": "Meer informatie", "advanced": "Uitgebreid", "file": "Bestand", "installed": "Geïnstalleerd", - "notInstalled": "Niet $t(common.installed)", "simple": "Eenvoudig", "somethingWentWrong": "Er ging iets mis", "add": "Voeg toe", @@ -45,17 +41,14 @@ "details": "Details", "outputs": "Uitvoeren", "save": "Bewaar", - "nextPage": "Volgende pagina", "blue": "Blauw", "alpha": "Alfa", "red": "Rood", "editor": "Editor", "folder": "Map", "format": "structuur", - "goTo": "Ga naar", "template": "Sjabloon", "input": "Invoer", - "loglevel": "Logboekniveau", "safetensors": "Safetensors", "saveAs": "Bewaar als", "created": "Gemaakt", @@ -65,7 +58,6 @@ "negativePrompt": "Negatieve prompt", "selected": "Geselecteerd", "orderBy": "Sorteer op", - "prevPage": "Vorige pagina", "beta": "Bèta", "copyError": "$t(gallery.copy) Fout", "toResolve": "Op te lossen", @@ -76,243 +68,28 @@ "or": "of", "updated": "Bijgewerkt", "outpaint": "outpainten", - "viewing": "Bekijken", - "viewingDesc": "Beoordeel afbeelding in een grote galerijweergave", - "editing": "Bewerken", - "editingDesc": "Bewerk op het canvas Stuurlagen", "ai": "ai", "inpaint": "inpainten", "unknown": "Onbekend", "delete": "Verwijder", "direction": "Richting", "error": "Fout", - "localSystem": "Lokaal systeem", "unknownError": "Onbekende fout" }, "gallery": { "galleryImageSize": "Afbeeldingsgrootte", "gallerySettings": "Instellingen galerij", "autoSwitchNewImages": "Wissel autom. naar nieuwe afbeeldingen", - "loadMore": "Laad meer", - "noImagesInGallery": "Geen afbeeldingen om te tonen", "deleteImage_one": "Verwijder afbeelding", "deleteImage_other": "", - "deleteImageBin": "Verwijderde afbeeldingen worden naar de prullenbak van je besturingssysteem gestuurd.", "deleteImagePermanent": "Verwijderde afbeeldingen kunnen niet worden hersteld.", - "assets": "Eigen onderdelen", "autoAssignBoardOnClick": "Ken automatisch bord toe bij klikken", "featuresWillReset": "Als je deze afbeelding verwijdert, dan worden deze functies onmiddellijk teruggezet.", "loading": "Bezig met laden", - "unableToLoad": "Kan galerij niet laden", "downloadSelection": "Download selectie", "currentlyInUse": "Deze afbeelding is momenteel in gebruik door de volgende functies:", "copy": "Kopieer", - "download": "Download", - "setCurrentImage": "Stel in als huidige afbeelding" - }, - "hotkeys": { - "keyboardShortcuts": "Sneltoetsen", - "appHotkeys": "Appsneltoetsen", - "generalHotkeys": "Algemene sneltoetsen", - "galleryHotkeys": "Sneltoetsen galerij", - "unifiedCanvasHotkeys": "Sneltoetsen centraal canvas", - "invoke": { - "title": "Genereer", - "desc": "Genereert een afbeelding" - }, - "cancel": { - "title": "Annuleer", - "desc": "Annuleert het genereren van een afbeelding" - }, - "focusPrompt": { - "title": "Focus op invoer", - "desc": "Legt de focus op het invoertekstvak" - }, - "toggleOptions": { - "title": "Open/sluit Opties", - "desc": "Opent of sluit het deelscherm Opties" - }, - "pinOptions": { - "title": "Zet Opties vast", - "desc": "Zet het deelscherm Opties vast" - }, - "toggleGallery": { - "title": "Zet Galerij vast", - "desc": "Opent of sluit het deelscherm Galerij" - }, - "maximizeWorkSpace": { - "title": "Maximaliseer werkgebied", - "desc": "Sluit deelschermen en maximaliseer het werkgebied" - }, - "changeTabs": { - "title": "Wissel van tabblad", - "desc": "Wissel naar een ander werkgebied" - }, - "consoleToggle": { - "title": "Open/sluit console", - "desc": "Opent of sluit de console" - }, - "setPrompt": { - "title": "Stel invoertekst in", - "desc": "Gebruikt de invoertekst van de huidige afbeelding" - }, - "setSeed": { - "title": "Stel seed in", - "desc": "Gebruikt de seed van de huidige afbeelding" - }, - "setParameters": { - "title": "Stel parameters in", - "desc": "Gebruikt alle parameters van de huidige afbeelding" - }, - "restoreFaces": { - "title": "Herstel gezichten", - "desc": "Herstelt de huidige afbeelding" - }, - "upscale": { - "title": "Schaal op", - "desc": "Schaalt de huidige afbeelding op" - }, - "showInfo": { - "title": "Toon info", - "desc": "Toont de metagegevens van de huidige afbeelding" - }, - "sendToImageToImage": { - "title": "Stuur naar Afbeelding naar afbeelding", - "desc": "Stuurt de huidige afbeelding naar Afbeelding naar afbeelding" - }, - "deleteImage": { - "title": "Verwijder afbeelding", - "desc": "Verwijdert de huidige afbeelding" - }, - "closePanels": { - "title": "Sluit deelschermen", - "desc": "Sluit geopende deelschermen" - }, - "previousImage": { - "title": "Vorige afbeelding", - "desc": "Toont de vorige afbeelding in de galerij" - }, - "nextImage": { - "title": "Volgende afbeelding", - "desc": "Toont de volgende afbeelding in de galerij" - }, - "increaseGalleryThumbSize": { - "title": "Vergroot afbeeldingsgrootte galerij", - "desc": "Vergroot de grootte van de galerijminiaturen" - }, - "decreaseGalleryThumbSize": { - "title": "Verklein afbeeldingsgrootte galerij", - "desc": "Verkleint de grootte van de galerijminiaturen" - }, - "selectBrush": { - "title": "Kies penseel", - "desc": "Kiest de penseel op het canvas" - }, - "selectEraser": { - "title": "Kies gum", - "desc": "Kiest de gum op het canvas" - }, - "decreaseBrushSize": { - "title": "Verklein penseelgrootte", - "desc": "Verkleint de grootte van het penseel/gum op het canvas" - }, - "increaseBrushSize": { - "title": "Vergroot penseelgrootte", - "desc": "Vergroot de grootte van het penseel/gum op het canvas" - }, - "decreaseBrushOpacity": { - "title": "Verlaag ondoorzichtigheid penseel", - "desc": "Verlaagt de ondoorzichtigheid van de penseel op het canvas" - }, - "increaseBrushOpacity": { - "title": "Verhoog ondoorzichtigheid penseel", - "desc": "Verhoogt de ondoorzichtigheid van de penseel op het canvas" - }, - "moveTool": { - "title": "Verplaats canvas", - "desc": "Maakt canvasnavigatie mogelijk" - }, - "fillBoundingBox": { - "title": "Vul tekenvak", - "desc": "Vult het tekenvak met de penseelkleur" - }, - "eraseBoundingBox": { - "title": "Wis tekenvak", - "desc": "Wist het gebied van het tekenvak" - }, - "colorPicker": { - "title": "Kleurkiezer", - "desc": "Opent de kleurkiezer op het canvas" - }, - "toggleSnap": { - "title": "Zet uitlijnen aan/uit", - "desc": "Zet uitlijnen op raster aan/uit" - }, - "quickToggleMove": { - "title": "Verplaats canvas even", - "desc": "Verplaats kortstondig het canvas" - }, - "toggleLayer": { - "title": "Zet laag aan/uit", - "desc": "Wisselt tussen de masker- en basislaag" - }, - "clearMask": { - "title": "Wis masker", - "desc": "Wist het volledig masker" - }, - "hideMask": { - "title": "Toon/verberg masker", - "desc": "Toont of verbegt het masker" - }, - "showHideBoundingBox": { - "title": "Toon/verberg tekenvak", - "desc": "Wisselt de zichtbaarheid van het tekenvak" - }, - "mergeVisible": { - "title": "Voeg lagen samen", - "desc": "Voegt alle zichtbare lagen op het canvas samen" - }, - "saveToGallery": { - "title": "Bewaar in galerij", - "desc": "Bewaart het huidige canvas in de galerij" - }, - "copyToClipboard": { - "title": "Kopieer naar klembord", - "desc": "Kopieert het huidige canvas op het klembord" - }, - "downloadImage": { - "title": "Download afbeelding", - "desc": "Downloadt het huidige canvas" - }, - "undoStroke": { - "title": "Maak streek ongedaan", - "desc": "Maakt een penseelstreek ongedaan" - }, - "redoStroke": { - "title": "Herhaal streek", - "desc": "Voert een ongedaan gemaakte penseelstreek opnieuw uit" - }, - "resetView": { - "title": "Herstel weergave", - "desc": "Herstelt de canvasweergave" - }, - "previousStagingImage": { - "title": "Vorige sessie-afbeelding", - "desc": "Bladert terug naar de vorige afbeelding in het sessiegebied" - }, - "nextStagingImage": { - "title": "Volgende sessie-afbeelding", - "desc": "Bladert vooruit naar de volgende afbeelding in het sessiegebied" - }, - "acceptStagingImage": { - "title": "Accepteer sessie-afbeelding", - "desc": "Accepteert de huidige sessie-afbeelding" - }, - "addNodes": { - "title": "Voeg knooppunten toe", - "desc": "Opent het menu Voeg knooppunt toe" - }, - "nodesHotkeys": "Sneltoetsen knooppunten" + "download": "Download" }, "modelManager": { "modelManager": "Modelonderhoud", @@ -347,8 +124,6 @@ "convertToDiffusersHelpText5": "Zorg ervoor dat je genoeg schijfruimte hebt. Modellen nemen gewoonlijk ongeveer 2 tot 7 GB ruimte in beslag.", "modelConverted": "Model omgezet", "alpha": "Alfa", - "v2_base": "v2 (512px)", - "v2_768": "v2 (768px)", "none": "geen", "baseModel": "Basismodel", "vae": "VAE", @@ -359,8 +134,6 @@ "settings": "Instellingen", "modelDeleted": "Model verwijderd", "syncModels": "Synchroniseer Modellen", - "modelsSynced": "Modellen Gesynchroniseerd", - "modelSyncFailed": "Synchronisatie modellen mislukt", "modelDeleteFailed": "Model kon niet verwijderd worden", "convertingModelBegin": "Model aan het converteren. Even geduld.", "predictionType": "Soort voorspelling", @@ -373,7 +146,6 @@ "path": "Pad", "triggerPhrases": "Triggerzinnen", "typePhraseHere": "Typ zin hier in", - "useDefaultSettings": "Gebruik standaardinstellingen", "modelImageDeleteFailed": "Fout bij verwijderen modelafbeelding", "modelImageUpdated": "Modelafbeelding bijgewerkt", "modelImageUpdateFailed": "Fout bij bijwerken modelafbeelding", @@ -412,8 +184,6 @@ "type": "Soort", "strength": "Sterkte", "upscaling": "Opschalen", - "upscale": "Vergroot (Shift + U)", - "upscaleImage": "Schaal afbeelding op", "scale": "Schaal", "imageFit": "Pas initiële afbeelding in uitvoergrootte", "scaleBeforeProcessing": "Schalen voor verwerking", @@ -421,14 +191,10 @@ "scaledHeight": "Geschaalde H", "infillMethod": "Infill-methode", "tileSize": "Grootte tegel", - "sendToImg2Img": "Stuur naar Afbeelding naar afbeelding", - "sendToUnifiedCanvas": "Stuur naar Centraal canvas", - "downloadImage": "Download afbeelding", "usePrompt": "Hergebruik invoertekst", "useSeed": "Hergebruik seed", "useAll": "Hergebruik alles", "info": "Info", - "showOptionsPanel": "Toon deelscherm Opties (O of T)", "symmetry": "Symmetrie", "cancel": { "cancel": "Annuleer" @@ -449,33 +215,11 @@ "noModelSelected": "Geen model ingesteld", "invoke": "Start", "noPrompts": "Geen prompts gegenereerd", - "noInitialImageSelected": "Geen initiële afbeelding gekozen", "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} invoer ontbreekt", - "noControlImageForControlAdapter": "Controle-adapter #{{number}} heeft geen controle-afbeelding", - "noModelForControlAdapter": "Control-adapter #{{number}} heeft geen model ingesteld staan.", - "incompatibleBaseModelForControlAdapter": "Model van controle-adapter #{{number}} is niet compatibel met het hoofdmodel.", "systemDisconnected": "Systeem is niet verbonden", "missingNodeTemplate": "Knooppuntsjabloon ontbreekt", "missingFieldTemplate": "Veldsjabloon ontbreekt", - "addingImagesTo": "Bezig met toevoegen van afbeeldingen aan", - "layer": { - "initialImageNoImageSelected": "geen initiële afbeelding geselecteerd", - "controlAdapterNoModelSelected": "geen controle-adaptermodel geselecteerd", - "controlAdapterIncompatibleBaseModel": "niet-compatibele basismodel voor controle-adapter", - "controlAdapterNoImageSelected": "geen afbeelding voor controle-adapter geselecteerd", - "controlAdapterImageNotProcessed": "Afbeelding voor controle-adapter niet verwerkt", - "ipAdapterIncompatibleBaseModel": "niet-compatibele basismodel voor IP-adapter", - "ipAdapterNoImageSelected": "geen afbeelding voor IP-adapter geselecteerd", - "rgNoRegion": "geen gebied geselecteerd", - "rgNoPromptsOrIPAdapters": "geen tekstprompts of IP-adapters", - "t2iAdapterIncompatibleDimensions": "T2I-adapter vereist een afbeelding met afmetingen met een veelvoud van 64", - "ipAdapterNoModelSelected": "geen IP-adapter geselecteerd" - }, - "imageNotProcessedForControlAdapter": "De afbeelding van controle-adapter #{{number}} is niet verwerkt" - }, - "isAllowedToUpscale": { - "useX2Model": "Afbeelding is te groot om te vergroten met het x4-model. Gebruik hiervoor het x2-model", - "tooLarge": "Afbeelding is te groot om te vergoten. Kies een kleinere afbeelding" + "addingImagesTo": "Bezig met toevoegen van afbeeldingen aan" }, "patchmatchDownScaleSize": "Verklein", "useCpuNoise": "Gebruik CPU-ruis", @@ -487,31 +231,22 @@ "setToOptimalSize": "Optimaliseer grootte voor het model", "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (is mogelijk te klein)", "aspect": "Beeldverhouding", - "infillMosaicTileWidth": "Breedte tegel", "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (is mogelijk te groot)", "lockAspectRatio": "Zet beeldverhouding vast", - "infillMosaicTileHeight": "Hoogte tegel", - "globalNegativePromptPlaceholder": "Globale negatieve prompt", - "globalPositivePromptPlaceholder": "Globale positieve prompt", "useSize": "Gebruik grootte", "swapDimensions": "Wissel afmetingen om", - "globalSettings": "Globale instellingen", "coherenceEdgeSize": "Randgrootte", "coherenceMinDenoise": "Min. ontruising", - "infillMosaicMinColor": "Min. kleur", - "infillMosaicMaxColor": "Max. kleur", "cfgRescaleMultiplier": "Vermenigvuldiger voor CFG-herschaling" }, "settings": { "models": "Modellen", "displayInProgress": "Toon voortgangsafbeeldingen", "confirmOnDelete": "Bevestig bij verwijderen", - "enableImageDebugging": "Schakel foutopsporing afbeelding in", "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.", - "shouldLogToConsole": "Schrijf logboek naar console", "developer": "Ontwikkelaar", "general": "Algemeen", "showProgressInViewer": "Toon voortgangsafbeeldingen in viewer", @@ -537,12 +272,7 @@ "toast": { "uploadFailed": "Upload mislukt", "imageCopied": "Afbeelding gekopieerd", - "imageNotLoadedDesc": "Geen afbeeldingen gevonden", - "canvasMerged": "Canvas samengevoegd", - "sentToImageToImage": "Gestuurd naar Afbeelding naar afbeelding", - "sentToUnifiedCanvas": "Gestuurd naar Centraal canvas", "parametersNotSet": "Parameters niet ingesteld", - "metadataLoadFailed": "Fout bij laden metagegevens", "serverError": "Serverfout", "connected": "Verbonden met server", "canceled": "Verwerking geannuleerd", @@ -552,110 +282,19 @@ "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", - "imageSavingFailed": "Fout bij bewaren afbeelding", - "canvasSentControlnetAssets": "Canvas gestuurd naar ControlNet en Assets", - "problemCopyingCanvasDesc": "Kan basislaag niet exporteren", "loadedWithWarnings": "Werkstroom geladen met waarschuwingen", - "setInitialImage": "Ingesteld als initiële afbeelding", - "canvasCopiedClipboard": "Canvas gekopieerd naar klembord", - "setControlImage": "Ingesteld als controle-afbeelding", - "setNodeField": "Ingesteld als knooppuntveld", - "problemSavingMask": "Fout bij bewaren masker", - "problemSavingCanvasDesc": "Kan basislaag niet exporteren", - "maskSavedAssets": "Masker bewaard in Assets", - "problemDownloadingCanvas": "Fout bij downloaden van canvas", - "problemMergingCanvas": "Fout bij samenvoegen canvas", - "setCanvasInitialImage": "Initiële canvasafbeelding ingesteld", "imageUploaded": "Afbeelding geüpload", "addedToBoard": "Toegevoegd aan bord", "workflowLoaded": "Werkstroom geladen", "modelAddedSimple": "Model toegevoegd aan wachtrij", - "problemImportingMaskDesc": "Kan masker niet exporteren", - "problemCopyingCanvas": "Fout bij kopiëren canvas", - "problemSavingCanvas": "Fout bij bewaren canvas", - "canvasDownloaded": "Canvas gedownload", - "problemMergingCanvasDesc": "Kan basislaag niet exporteren", - "problemDownloadingCanvasDesc": "Kan basislaag niet exporteren", - "problemSavingMaskDesc": "Kan masker niet exporteren", - "imageSaved": "Afbeelding bewaard", - "maskSentControlnetAssets": "Masker gestuurd naar ControlNet en Assets", - "canvasSavedGallery": "Canvas bewaard in galerij", "imageUploadFailed": "Fout bij uploaden afbeelding", - "problemImportingMask": "Fout bij importeren masker", "workflowDeleted": "Werkstroom verwijderd", - "invalidUpload": "Ongeldige upload", - "uploadInitialImage": "Initiële afbeelding uploaden", - "setAsCanvasInitialImage": "Ingesteld als initiële afbeelding voor canvas", "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", - "resetInitialImage": "Initiële afbeelding hersteld" - }, - "tooltip": { - "feature": { - "prompt": "Dit is het invoertekstvak. De invoertekst bevat de te genereren voorwerpen en stylistische termen. Je kunt hiernaast in de invoertekst ook het gewicht (het belang van een trefwoord) toekennen. Opdrachten en parameters voor op de opdrachtregelinterface werken hier niet.", - "gallery": "De galerij toont gegenereerde afbeeldingen uit de uitvoermap nadat ze gegenereerd zijn. Instellingen worden opgeslagen binnen de bestanden zelf en zijn toegankelijk via het contextmenu.", - "other": "Deze opties maken alternative werkingsstanden voor Invoke mogelijk. De optie 'Naadloze tegels' maakt herhalende patronen in de uitvoer. 'Hoge resolutie' genereert in twee stappen via Afbeelding naar afbeelding: gebruik dit als je een grotere en coherentere afbeelding wilt zonder artifacten. Dit zal meer tijd in beslag nemen t.o.v. Tekst naar afbeelding.", - "seed": "Seedwaarden hebben invloed op de initiële ruis op basis waarvan de afbeelding wordt gevormd. Je kunt de al bestaande seeds van eerdere afbeeldingen gebruiken. De waarde 'Drempelwaarde ruis' wordt gebruikt om de hoeveelheid artifacten te verkleinen bij hoge CFG-waarden (beperk je tot 0 - 10). De Perlinruiswaarde wordt gebruikt om Perlinruis toe te voegen bij het genereren: beide dienen als variatie op de uitvoer.", - "upscale": "Gebruik ESRGAN om de afbeelding direct na het genereren te vergroten.", - "boundingBox": "Het tekenvak is gelijk aan de instellingen Breedte en Hoogte voor de functies Tekst naar afbeelding en Afbeelding naar afbeelding. Alleen het gebied in het tekenvak wordt verwerkt." - } - }, - "unifiedCanvas": { - "layer": "Laag", - "base": "Basis", - "mask": "Masker", - "maskingOptions": "Maskeropties", - "enableMask": "Schakel masker in", - "preserveMaskedArea": "Behoud gemaskeerd gebied", - "clearMask": "Wis masker", - "brush": "Penseel", - "eraser": "Gum", - "fillBoundingBox": "Vul tekenvak", - "eraseBoundingBox": "Wis tekenvak", - "colorPicker": "Kleurenkiezer", - "brushOptions": "Penseelopties", - "brushSize": "Grootte", - "move": "Verplaats", - "resetView": "Herstel weergave", - "mergeVisible": "Voeg lagen samen", - "saveToGallery": "Bewaar in galerij", - "copyToClipboard": "Kopieer naar klembord", - "downloadAsImage": "Download als afbeelding", - "undo": "Maak ongedaan", - "redo": "Herhaal", - "clearCanvas": "Wis canvas", - "canvasSettings": "Canvasinstellingen", - "showIntermediates": "Toon tussenafbeeldingen", - "showGrid": "Toon raster", - "snapToGrid": "Lijn uit op raster", - "darkenOutsideSelection": "Verduister buiten selectie", - "autoSaveToGallery": "Bewaar automatisch naar galerij", - "saveBoxRegionOnly": "Bewaar alleen tekengebied", - "limitStrokesToBox": "Beperk streken tot tekenvak", - "showCanvasDebugInfo": "Toon aanvullende canvasgegevens", - "clearCanvasHistory": "Wis canvasgeschiedenis", - "clearHistory": "Wis geschiedenis", - "clearCanvasHistoryMessage": "Het wissen van de canvasgeschiedenis laat het huidige canvas ongemoeid, maar wist onherstelbaar de geschiedenis voor het ongedaan maken en herhalen.", - "clearCanvasHistoryConfirm": "Weet je zeker dat je de canvasgeschiedenis wilt wissen?", - "activeLayer": "Actieve laag", - "canvasScale": "Schaal canvas", - "boundingBox": "Tekenvak", - "scaledBoundingBox": "Geschaalde tekenvak", - "boundingBoxPosition": "Positie tekenvak", - "canvasDimensions": "Afmetingen canvas", - "canvasPosition": "Positie canvas", - "cursorPosition": "Positie cursor", - "previous": "Vorige", - "next": "Volgende", - "accept": "Accepteer", - "discardAll": "Gooi alles weg", - "antialiasing": "Anti-aliasing", - "showResultsOn": "Toon resultaten (aan)", - "showResultsOff": "Toon resultaten (uit)" + "problemDownloadingImage": "Fout bij downloaden afbeelding" }, "accessibility": { "invokeProgressBar": "Voortgangsbalk Invoke", @@ -663,10 +302,7 @@ "uploadImage": "Upload afbeelding", "previousImage": "Vorige afbeelding", "nextImage": "Volgende afbeelding", - "showOptionsPanel": "Toon zijscherm", "menu": "Menu", - "showGalleryPanel": "Toon deelscherm Galerij", - "loadMore": "Laad meer", "about": "Over", "mode": "Modus", "resetUI": "$t(accessibility.reset) UI", @@ -676,17 +312,14 @@ "zoomOutNodes": "Uitzoomen", "fitViewportNodes": "Aanpassen aan beeld", "hideMinimapnodes": "Minimap verbergen", - "showLegendNodes": "Typelegende veld tonen", "zoomInNodes": "Inzoomen", "showMinimapnodes": "Minimap tonen", - "hideLegendNodes": "Typelegende veld verbergen", "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", - "versionUnknown": " Versie onbekend", "noNodeSelected": "Geen knooppunt gekozen", "addNode": "Voeg knooppunt toe", "unableToValidateWorkflow": "Kan werkstroom niet valideren", @@ -700,9 +333,7 @@ "integer": "Geheel getal", "nodeTemplate": "Sjabloon knooppunt", "nodeOpacity": "Dekking knooppunt", - "unableToLoadWorkflow": "Fout bij laden werkstroom", "snapToGrid": "Lijn uit op raster", - "noFieldsLinearview": "Geen velden toegevoegd aan lineaire weergave", "nodeSearch": "Zoek naar knooppunten", "updateNode": "Werk knooppunt bij", "version": "Versie", @@ -716,23 +347,18 @@ "ipAdapter": "IP-adapter", "noConnectionInProgress": "Geen verbinding bezig te maken", "workflowVersion": "Versie", - "noConnectionData": "Geen verbindingsgegevens", "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", - "unknownTemplate": "Onbekend sjabloon", "noWorkflow": "Geen werkstroom", - "removeLinearView": "Verwijder uit lineaire weergave", "workflowTags": "Labels", "fullyContainNodesHelp": "Knooppunten moeten zich volledig binnen het keuzevak bevinden om te worden gekozen", "workflowValidation": "Validatiefout werkstroom", "nodeType": "Soort knooppunt", - "noMatchingNodes": "Geen overeenkomende knooppunten", "fullyContainNodes": "Omvat knooppunten volledig om ze te kiezen", "executionStateInProgress": "Bezig", - "noFieldType": "Geen soort veld", "executionStateError": "Fout", "boolean": "Booleaanse waarden", "executionStateCompleted": "Voltooid", @@ -751,14 +377,11 @@ "unknownField": "Onbekend veld", "colorCodeEdges": "Kleurgecodeerde randen", "unknownNode": "Onbekend knooppunt", - "mismatchedVersion": "Ongeldig knooppunt: knooppunt {{node}} van het soort {{type}} heeft een niet-overeenkomende versie (probeer het bij te werken?)", "addNodeToolTip": "Voeg knooppunt toe (Shift+A, spatie)", "loadingNodes": "Bezig met laden van knooppunten...", "snapToGridHelp": "Lijn knooppunten uit op raster bij verplaatsing", "workflowSettings": "Instellingen werkstroomeditor", - "addLinearView": "Voeg toe aan lineaire weergave", "nodePack": "Knooppuntpakket", - "unknownInput": "Onbekende invoer: {{name}}", "sourceNodeFieldDoesNotExist": "Ongeldige rand: bron-/uitvoerveld {{node}}.{{field}} bestaat niet", "collectionFieldType": "Verzameling {{name}}", "deletedInvalidEdge": "Ongeldige hoek {{source}} -> {{target}} verwijderd", @@ -773,13 +396,11 @@ "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", - "reorderLinearView": "Herorden lineaire weergave", "newWorkflowDesc": "Een nieuwe werkstroom aanmaken?", "collectionOrScalarFieldType": "Verzameling|scalair {{name}}", "newWorkflow": "Nieuwe werkstroom", "unknownErrorValidatingWorkflow": "Onbekende fout bij valideren werkstroom", "unsupportedAnyOfLength": "te veel union-leden ({{count}})", - "unknownOutput": "Onbekende uitvoer: {{name}}", "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}}", @@ -802,100 +423,6 @@ "unableToUpdateNodes_one": "Fout bij bijwerken van {{count}} knooppunt", "unableToUpdateNodes_other": "Fout bij bijwerken van {{count}} knooppunten" }, - "controlnet": { - "amult": "a_mult", - "resize": "Schaal", - "showAdvanced": "Toon uitgebreide opties", - "contentShuffleDescription": "Verschuift het materiaal in de afbeelding", - "bgth": "bg_th", - "addT2IAdapter": "Voeg $t(common.t2iAdapter) toe", - "pidi": "PIDI", - "importImageFromCanvas": "Importeer afbeelding uit canvas", - "lineartDescription": "Zet afbeelding om naar line-art", - "normalBae": "Normale BAE", - "importMaskFromCanvas": "Importeer masker uit canvas", - "hed": "HED", - "hideAdvanced": "Verberg uitgebreid", - "contentShuffle": "Verschuif materiaal", - "resetControlImage": "Herstel controle-afbeelding", - "beginEndStepPercent": "Percentage begin-/eindstap", - "mlsdDescription": "Minimalistische herkenning lijnsegmenten", - "duplicate": "Maak kopie", - "balanced": "Gebalanceerd", - "f": "F", - "h": "H", - "prompt": "Prompt", - "depthMidasDescription": "Genereer diepteblad via Midas", - "controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))", - "control": "Controle", - "resizeMode": "Modus schaling", - "coarse": "Grof", - "weight": "Gewicht", - "selectModel": "Kies een model", - "crop": "Snij bij", - "depthMidas": "Diepte (Midas)", - "w": "B", - "processor": "Verwerker", - "addControlNet": "Voeg $t(common.controlNet) toe", - "none": "Geen", - "detectResolution": "Herken resolutie", - "ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))", - "pidiDescription": "PIDI-afbeeldingsverwerking", - "mediapipeFace": "Mediapipe - Gezicht", - "mlsd": "M-LSD", - "controlMode": "Controlemodus", - "fill": "Vul", - "cannyDescription": "Herkenning Canny-rand", - "addIPAdapter": "Voeg $t(common.ipAdapter) toe", - "lineart": "Line-art", - "colorMapDescription": "Genereert een kleurenblad van de afbeelding", - "lineartAnimeDescription": "Lineartverwerking in anime-stijl", - "t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))", - "minConfidence": "Min. vertrouwensniveau", - "imageResolution": "Resolutie afbeelding", - "megaControl": "Zeer veel controle", - "depthZoe": "Diepte (Zoe)", - "colorMap": "Kleur", - "lowThreshold": "Lage drempelwaarde", - "autoConfigure": "Configureer verwerker automatisch", - "highThreshold": "Hoge drempelwaarde", - "normalBaeDescription": "Normale BAE-verwerking", - "noneDescription": "Geen verwerking toegepast", - "saveControlImage": "Bewaar controle-afbeelding", - "toggleControlNet": "Zet deze ControlNet aan/uit", - "delete": "Verwijder", - "controlAdapter_one": "Control-adapter", - "controlAdapter_other": "Control-adapters", - "safe": "Veilig", - "colorMapTileSize": "Grootte tegel", - "lineartAnime": "Line-art voor anime", - "mediapipeFaceDescription": "Gezichtsherkenning met Mediapipe", - "canny": "Canny", - "depthZoeDescription": "Genereer diepteblad via Zoe", - "hedDescription": "Herkenning van holistisch-geneste randen", - "setControlImageDimensions": "Kopieer grootte naar B/H (optimaliseer voor model)", - "scribble": "Krabbel", - "maxFaces": "Max. gezichten", - "dwOpenpose": "DW Openpose", - "depthAnything": "Depth Anything", - "base": "Basis", - "hands": "Handen", - "selectCLIPVisionModel": "Selecteer een CLIP Vision-model", - "modelSize": "Modelgrootte", - "small": "Klein", - "large": "Groot", - "resizeSimple": "Wijzig grootte (eenvoudig)", - "beginEndStepPercentShort": "Begin-/eind-%", - "depthAnythingDescription": "Genereren dieptekaart d.m.v. de techniek Depth Anything", - "face": "Gezicht", - "body": "Lichaam", - "dwOpenposeDescription": "Schatting menselijke pose d.m.v. DW Openpose", - "ipAdapterMethod": "Methode", - "full": "Volledig", - "style": "Alleen stijl", - "composition": "Alleen samenstelling", - "setControlImageDimensionsForce": "Kopieer grootte naar B/H (negeer model)" - }, "dynamicPrompts": { "seedBehaviour": { "perPromptDesc": "Gebruik een verschillende seedwaarde per afbeelding", @@ -905,8 +432,6 @@ "label": "Gedrag seedwaarde" }, "maxPrompts": "Max. prompts", - "promptsWithCount_one": "{{count}} prompt", - "promptsWithCount_other": "{{count}} prompts", "dynamicPrompts": "Dynamische prompts", "showDynamicPrompts": "Toon dynamische prompts", "loading": "Genereren van dynamische prompts...", @@ -1105,7 +630,6 @@ } }, "metadata": { - "seamless": "Naadloos", "positivePrompt": "Positieve prompt", "negativePrompt": "Negatieve prompt", "generationMode": "Genereermodus", @@ -1117,8 +641,6 @@ "model": "Model", "noImageDetails": "Geen afbeeldingsdetails gevonden", "cfgScale": "CFG-schaal", - "fit": "Schaal aanpassen in Afbeelding naar afbeelding", - "initImage": "Initiële afbeelding", "recallParameters": "Opnieuw aan te roepen parameters", "height": "Hoogte", "noMetaData": "Geen metagegevens gevonden", @@ -1188,31 +710,24 @@ "refinerStart": "Startwaarde verfijning", "scheduler": "Planner", "cfgScale": "CFG-schaal", - "negStylePrompt": "Negatieve-stijlprompt", "noModelsAvailable": "Geen modellen beschikbaar", "refiner": "Verfijning", "negAestheticScore": "Negatieve esthetische score", "denoisingStrength": "Sterkte ontruising", "refinermodel": "Verfijningsmodel", "posAestheticScore": "Positieve esthetische score", - "concatPromptStyle": "Koppelen van prompt en stijl", "loading": "Bezig met laden...", "steps": "Stappen", - "posStylePrompt": "Positieve-stijlprompt", - "freePromptStyle": "Handmatige stijlprompt", "refinerSteps": "Aantal stappen verfijner" }, "models": { "noMatchingModels": "Geen overeenkomend modellen", "loading": "bezig met laden", - "noMatchingLoRAs": "Geen overeenkomende LoRA's", "noModelsAvailable": "Geen modellen beschikbaar", "selectModel": "Kies een model", - "noLoRAsInstalled": "Geen LoRA's geïnstalleerd", "noRefinerModelsInstalled": "Geen SDXL-verfijningsmodellen geïnstalleerd", "defaultVAE": "Standaard-VAE", "lora": "LoRA", - "esrganModel": "ESRGAN-model", "addLora": "Voeg LoRA toe", "concepts": "Concepten" }, @@ -1277,14 +792,12 @@ } }, "hrf": { - "upscaleMethod": "Opschaalmethode", "metadata": { "strength": "Sterkte oplossing voor hoge resolutie", "method": "Methode oplossing voor hoge resolutie", "enabled": "Oplossing voor hoge resolutie ingeschakeld" }, - "hrf": "Oplossing voor hoge resolutie", - "enableHrf": "Schakel oplossing in voor hoge resolutie" + "hrf": "Oplossing voor hoge resolutie" }, "prompt": { "addPromptTrigger": "Voeg prompttrigger toe", diff --git a/invokeai/frontend/web/public/locales/pl.json b/invokeai/frontend/web/public/locales/pl.json index b7592c3faec..49cf5a24c43 100644 --- a/invokeai/frontend/web/public/locales/pl.json +++ b/invokeai/frontend/web/public/locales/pl.json @@ -1,223 +1,105 @@ { "common": { "hotkeysLabel": "Skróty klawiszowe", - "languagePickerLabel": "Wybór języka", + "languagePickerLabel": "Język", "reportBugLabel": "Zgłoś błąd", "settingsLabel": "Ustawienia", "img2img": "Obraz na obraz", - "unifiedCanvas": "Tryb uniwersalny", "nodes": "Węzły", "upload": "Prześlij", "load": "Załaduj", - "statusDisconnected": "Odłączono od serwera", + "statusDisconnected": "Odłączono", "githubLabel": "GitHub", - "discordLabel": "Discord" + "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", - "loadMore": "Wczytaj więcej", - "noImagesInGallery": "Brak obrazów w galerii" - }, - "hotkeys": { - "keyboardShortcuts": "Skróty klawiszowe", - "appHotkeys": "Podstawowe", - "generalHotkeys": "Pomocnicze", - "galleryHotkeys": "Galeria", - "unifiedCanvasHotkeys": "Tryb uniwersalny", - "invoke": { - "title": "Wywołaj", - "desc": "Generuje nowy obraz" - }, - "cancel": { - "title": "Anuluj", - "desc": "Zatrzymuje generowanie obrazu" - }, - "focusPrompt": { - "title": "Aktywuj pole tekstowe", - "desc": "Aktywuje pole wprowadzania sugestii" - }, - "toggleOptions": { - "title": "Przełącz panel opcji", - "desc": "Wysuwa lub chowa panel opcji" - }, - "pinOptions": { - "title": "Przypnij opcje", - "desc": "Przypina panel opcji" - }, - "toggleGallery": { - "title": "Przełącz galerię", - "desc": "Wysuwa lub chowa galerię" - }, - "maximizeWorkSpace": { - "title": "Powiększ obraz roboczy", - "desc": "Chowa wszystkie panele, zostawia tylko podgląd obrazu" - }, - "changeTabs": { - "title": "Przełącznie trybu", - "desc": "Przełącza na n-ty tryb pracy" - }, - "consoleToggle": { - "title": "Przełącz konsolę", - "desc": "Otwiera lub chowa widok konsoli" - }, - "setPrompt": { - "title": "Skopiuj sugestie", - "desc": "Kopiuje sugestie z aktywnego obrazu" - }, - "setSeed": { - "title": "Skopiuj inicjator", - "desc": "Kopiuje inicjator z aktywnego obrazu" - }, - "setParameters": { - "title": "Skopiuj wszystko", - "desc": "Kopiuje wszystkie parametry z aktualnie aktywnego obrazu" - }, - "restoreFaces": { - "title": "Popraw twarze", - "desc": "Uruchamia proces poprawiania twarzy dla aktywnego obrazu" - }, - "upscale": { - "title": "Powiększ", - "desc": "Uruchamia proces powiększania aktywnego obrazu" - }, - "showInfo": { - "title": "Pokaż informacje", - "desc": "Pokazuje metadane zapisane w aktywnym obrazie" - }, - "sendToImageToImage": { - "title": "Użyj w trybie \"Obraz na obraz\"", - "desc": "Ustawia aktywny obraz jako źródło w trybie \"Obraz na obraz\"" - }, - "deleteImage": { - "title": "Usuń obraz", - "desc": "Usuwa aktywny obraz" - }, - "closePanels": { - "title": "Zamknij panele", - "desc": "Zamyka wszystkie otwarte panele" - }, - "previousImage": { - "title": "Poprzedni obraz", - "desc": "Aktywuje poprzedni obraz z galerii" - }, - "nextImage": { - "title": "Następny obraz", - "desc": "Aktywuje następny obraz z galerii" - }, - "increaseGalleryThumbSize": { - "title": "Powiększ obrazy", - "desc": "Powiększa rozmiar obrazów w galerii" - }, - "decreaseGalleryThumbSize": { - "title": "Pomniejsz obrazy", - "desc": "Pomniejsza rozmiar obrazów w galerii" - }, - "selectBrush": { - "title": "Aktywuj pędzel", - "desc": "Aktywuje narzędzie malowania" - }, - "selectEraser": { - "title": "Aktywuj gumkę", - "desc": "Aktywuje narzędzie usuwania" - }, - "decreaseBrushSize": { - "title": "Zmniejsz rozmiar narzędzia", - "desc": "Zmniejsza rozmiar aktywnego narzędzia" - }, - "increaseBrushSize": { - "title": "Zwiększ rozmiar narzędzia", - "desc": "Zwiększa rozmiar aktywnego narzędzia" - }, - "decreaseBrushOpacity": { - "title": "Zmniejsz krycie", - "desc": "Zmniejsza poziom krycia pędzla" - }, - "increaseBrushOpacity": { - "title": "Zwiększ", - "desc": "Zwiększa poziom krycia pędzla" - }, - "moveTool": { - "title": "Aktywuj przesunięcie", - "desc": "Włącza narzędzie przesuwania" - }, - "fillBoundingBox": { - "title": "Wypełnij zaznaczenie", - "desc": "Wypełnia zaznaczony obszar aktualnym kolorem pędzla" - }, - "eraseBoundingBox": { - "title": "Wyczyść zaznaczenia", - "desc": "Usuwa całą zawartość zaznaczonego obszaru" - }, - "colorPicker": { - "title": "Aktywuj pipetę", - "desc": "Włącza narzędzie kopiowania koloru" - }, - "toggleSnap": { - "title": "Przyciąganie do siatki", - "desc": "Włącza lub wyłącza opcje przyciągania do siatki" - }, - "quickToggleMove": { - "title": "Szybkie przesunięcie", - "desc": "Tymczasowo włącza tryb przesuwania obszaru roboczego" - }, - "toggleLayer": { - "title": "Przełącz wartwę", - "desc": "Przełącza pomiędzy warstwą bazową i maskowania" - }, - "clearMask": { - "title": "Wyczyść maskę", - "desc": "Usuwa całą zawartość warstwy maskowania" - }, - "hideMask": { - "title": "Przełącz maskę", - "desc": "Pokazuje lub ukrywa podgląd maski" - }, - "showHideBoundingBox": { - "title": "Przełącz zaznaczenie", - "desc": "Pokazuje lub ukrywa podgląd zaznaczenia" - }, - "mergeVisible": { - "title": "Połącz widoczne", - "desc": "Łączy wszystkie widoczne maski w jeden obraz" - }, - "saveToGallery": { - "title": "Zapisz w galerii", - "desc": "Zapisuje całą zawartość płótna w galerii" - }, - "copyToClipboard": { - "title": "Skopiuj do schowka", - "desc": "Zapisuje zawartość płótna w schowku systemowym" - }, - "downloadImage": { - "title": "Pobierz obraz", - "desc": "Zapisuje zawartość płótna do pliku obrazu" - }, - "undoStroke": { - "title": "Cofnij", - "desc": "Cofa ostatnie pociągnięcie pędzlem" - }, - "redoStroke": { - "title": "Ponawia", - "desc": "Ponawia cofnięte pociągnięcie pędzlem" - }, - "resetView": { - "title": "Resetuj widok", - "desc": "Centruje widok płótna" - }, - "previousStagingImage": { - "title": "Poprzedni obraz tymczasowy", - "desc": "Pokazuje poprzedni obraz tymczasowy" - }, - "nextStagingImage": { - "title": "Następny obraz tymczasowy", - "desc": "Pokazuje następny obraz tymczasowy" - }, - "acceptStagingImage": { - "title": "Akceptuj obraz tymczasowy", - "desc": "Akceptuje aktualnie wybrany obraz tymczasowy" - } + "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", @@ -232,8 +114,6 @@ "type": "Metoda", "strength": "Siła", "upscaling": "Powiększanie", - "upscale": "Powiększ", - "upscaleImage": "Powiększ obraz", "scale": "Skala", "imageFit": "Przeskaluj oryginalny obraz", "scaleBeforeProcessing": "Tryb skalowania", @@ -241,20 +121,15 @@ "scaledHeight": "Sk. do wys.", "infillMethod": "Metoda wypełniania", "tileSize": "Rozmiar kafelka", - "sendToImg2Img": "Użyj w trybie \"Obraz na obraz\"", - "sendToUnifiedCanvas": "Użyj w trybie uniwersalnym", - "downloadImage": "Pobierz obraz", "usePrompt": "Skopiuj sugestie", "useSeed": "Skopiuj inicjator", "useAll": "Skopiuj wszystko", - "info": "Informacje", - "showOptionsPanel": "Pokaż panel ustawień" + "info": "Informacje" }, "settings": { "models": "Modele", "displayInProgress": "Podgląd generowanego obrazu", "confirmOnDelete": "Potwierdzaj usuwanie", - "enableImageDebugging": "Włącz debugowanie obrazu", "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.", @@ -263,72 +138,7 @@ "toast": { "uploadFailed": "Błąd przesyłania obrazu", "imageCopied": "Skopiowano obraz", - "imageNotLoadedDesc": "Nie znaleziono obrazu, który można użyć w Obraz na obraz", - "canvasMerged": "Scalono widoczne warstwy", - "sentToImageToImage": "Wysłano do Obraz na obraz", - "sentToUnifiedCanvas": "Wysłano do trybu uniwersalnego", - "parametersNotSet": "Nie ustawiono parametrów", - "metadataLoadFailed": "Błąd wczytywania metadanych" - }, - "tooltip": { - "feature": { - "prompt": "To pole musi zawierać cały tekst sugestii, w tym zarówno opis oczekiwanej zawartości, jak i terminy stylistyczne. Chociaż wagi mogą być zawarte w sugestiach, inne parametry znane z linii poleceń nie będą działać.", - "gallery": "W miarę generowania nowych wywołań w tym miejscu będą wyświetlane pliki z katalogu wyjściowego. Obrazy mają dodatkowo opcje konfiguracji nowych wywołań.", - "other": "Opcje umożliwią alternatywne tryby przetwarzania. Płynne scalanie będzie pomocne przy generowaniu powtarzających się wzorów. Optymalizacja wysokiej rozdzielczości wykonuje dwuetapowy cykl generowania i powinna być używana przy wyższych rozdzielczościach, gdy potrzebny jest bardziej spójny obraz/kompozycja.", - "seed": "Inicjator określa początkowy zestaw szumów, który kieruje procesem odszumiania i może być losowy lub pobrany z poprzedniego wywołania. Funkcja \"Poziom szumu\" może być użyta do złagodzenia saturacji przy wyższych wartościach CFG (spróbuj między 0-10), a Perlin może być użyty w celu dodania wariacji do twoich wyników.", - "upscale": "Korzystając z ESRGAN, możesz zwiększyć rozdzielczość obrazu wyjściowego bez konieczności zwiększania szerokości/wysokości w ustawieniach początkowych.", - "boundingBox": "Zaznaczony obszar odpowiada ustawieniom wysokości i szerokości w trybach Tekst na obraz i Obraz na obraz. Jedynie piksele znajdujące się w obszarze zaznaczenia zostaną uwzględnione podczas wywoływania nowego obrazu." - } - }, - "unifiedCanvas": { - "layer": "Warstwa", - "base": "Główna", - "mask": "Maska", - "maskingOptions": "Opcje maski", - "enableMask": "Włącz maskę", - "preserveMaskedArea": "Zachowaj obszar", - "clearMask": "Wyczyść maskę", - "brush": "Pędzel", - "eraser": "Gumka", - "fillBoundingBox": "Wypełnij zaznaczenie", - "eraseBoundingBox": "Wyczyść zaznaczenie", - "colorPicker": "Pipeta", - "brushOptions": "Ustawienia pędzla", - "brushSize": "Rozmiar", - "move": "Przesunięcie", - "resetView": "Resetuj widok", - "mergeVisible": "Scal warstwy", - "saveToGallery": "Zapisz w galerii", - "copyToClipboard": "Skopiuj do schowka", - "downloadAsImage": "Zapisz do pliku", - "undo": "Cofnij", - "redo": "Ponów", - "clearCanvas": "Wyczyść obraz", - "canvasSettings": "Ustawienia obrazu", - "showIntermediates": "Pokazuj stany pośrednie", - "showGrid": "Pokazuj siatkę", - "snapToGrid": "Przyciągaj do siatki", - "darkenOutsideSelection": "Przyciemnij poza zaznaczeniem", - "autoSaveToGallery": "Zapisuj automatycznie do galerii", - "saveBoxRegionOnly": "Zapisuj tylko zaznaczony obszar", - "limitStrokesToBox": "Rysuj tylko wewnątrz zaznaczenia", - "showCanvasDebugInfo": "Informacje dla developera", - "clearCanvasHistory": "Wyczyść historię operacji", - "clearHistory": "Wyczyść historię", - "clearCanvasHistoryMessage": "Wyczyszczenie historii nie będzie miało wpływu na sam obraz, ale niemożliwe będzie cofnięcie i otworzenie wszystkich wykonanych do tej pory operacji.", - "clearCanvasHistoryConfirm": "Czy na pewno chcesz wyczyścić historię operacji?", - "activeLayer": "Warstwa aktywna", - "canvasScale": "Poziom powiększenia", - "boundingBox": "Rozmiar zaznaczenia", - "scaledBoundingBox": "Rozmiar po skalowaniu", - "boundingBoxPosition": "Pozycja zaznaczenia", - "canvasDimensions": "Rozmiar płótna", - "canvasPosition": "Pozycja płótna", - "cursorPosition": "Pozycja kursora", - "previous": "Poprzedni", - "next": "Następny", - "accept": "Zaakceptuj", - "discardAll": "Odrzuć wszystkie" + "parametersNotSet": "Nie ustawiono parametrów" }, "accessibility": { "invokeProgressBar": "Pasek postępu", @@ -336,7 +146,170 @@ "uploadImage": "Wgrywanie obrazu", "previousImage": "Poprzedni obraz", "nextImage": "Następny obraz", - "showOptionsPanel": "Pokaż panel opcji", - "menu": "Menu" + "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 index 3003a1732b9..f24022363ad 100644 --- a/invokeai/frontend/web/public/locales/pt.json +++ b/invokeai/frontend/web/public/locales/pt.json @@ -5,7 +5,6 @@ "languagePickerLabel": "Seletor de Idioma", "hotkeysLabel": "Hotkeys", "img2img": "Imagem para Imagem", - "unifiedCanvas": "Tela Unificada", "nodes": "Nós", "upload": "Upload", "load": "Abrir", @@ -18,209 +17,8 @@ "gallery": { "gallerySettings": "Configurações de Galeria", "autoSwitchNewImages": "Trocar para Novas Imagens Automaticamente", - "loadMore": "Carregar Mais", - "noImagesInGallery": "Sem Imagens na Galeria", "galleryImageSize": "Tamanho da Imagem" }, - "hotkeys": { - "generalHotkeys": "Atalhos Gerais", - "galleryHotkeys": "Atalhos da Galeria", - "maximizeWorkSpace": { - "desc": "Fechar painéis e maximixar área de trabalho", - "title": "Maximizar a Área de Trabalho" - }, - "changeTabs": { - "title": "Mudar Guias", - "desc": "Trocar para outra área de trabalho" - }, - "consoleToggle": { - "desc": "Abrir e fechar console", - "title": "Ativar Console" - }, - "setPrompt": { - "title": "Definir Prompt", - "desc": "Usar o prompt da imagem atual" - }, - "sendToImageToImage": { - "desc": "Manda a imagem atual para Imagem Para Imagem", - "title": "Mandar para Imagem Para Imagem" - }, - "previousImage": { - "desc": "Mostra a imagem anterior na galeria", - "title": "Imagem Anterior" - }, - "nextImage": { - "title": "Próxima Imagem", - "desc": "Mostra a próxima imagem na galeria" - }, - "decreaseGalleryThumbSize": { - "desc": "Diminui o tamanho das thumbs na galeria", - "title": "Diminuir Tamanho da Galeria de Imagem" - }, - "selectBrush": { - "title": "Selecionar Pincel", - "desc": "Seleciona o pincel" - }, - "selectEraser": { - "title": "Selecionar Apagador", - "desc": "Seleciona o apagador" - }, - "decreaseBrushSize": { - "title": "Diminuir Tamanho do Pincel", - "desc": "Diminui o tamanho do pincel/apagador" - }, - "increaseBrushOpacity": { - "desc": "Aumenta a opacidade do pincel", - "title": "Aumentar Opacidade do Pincel" - }, - "moveTool": { - "title": "Ferramenta Mover", - "desc": "Permite navegar pela tela" - }, - "decreaseBrushOpacity": { - "desc": "Diminui a opacidade do pincel", - "title": "Diminuir Opacidade do Pincel" - }, - "toggleSnap": { - "title": "Ativar Encaixe", - "desc": "Ativa Encaixar na Grade" - }, - "quickToggleMove": { - "title": "Ativar Mover Rapidamente", - "desc": "Temporariamente ativa o modo Mover" - }, - "toggleLayer": { - "title": "Ativar Camada", - "desc": "Ativa a seleção de camada de máscara/base" - }, - "clearMask": { - "title": "Limpar Máscara", - "desc": "Limpa toda a máscara" - }, - "hideMask": { - "title": "Esconder Máscara", - "desc": "Esconde e Revela a máscara" - }, - "mergeVisible": { - "title": "Fundir Visível", - "desc": "Fundir todas as camadas visíveis das telas" - }, - "downloadImage": { - "desc": "Descarregar a tela atual", - "title": "Descarregar Imagem" - }, - "undoStroke": { - "title": "Desfazer Traço", - "desc": "Desfaz um traço de pincel" - }, - "redoStroke": { - "title": "Refazer Traço", - "desc": "Refaz o traço de pincel" - }, - "keyboardShortcuts": "Atalhos de Teclado", - "appHotkeys": "Atalhos do app", - "invoke": { - "title": "Invocar", - "desc": "Gerar uma imagem" - }, - "cancel": { - "title": "Cancelar", - "desc": "Cancelar geração de imagem" - }, - "focusPrompt": { - "title": "Foco do Prompt", - "desc": "Foco da área de texto do prompt" - }, - "toggleOptions": { - "title": "Ativar Opções", - "desc": "Abrir e fechar o painel de opções" - }, - "pinOptions": { - "title": "Fixar Opções", - "desc": "Fixar o painel de opções" - }, - "closePanels": { - "title": "Fechar Painéis", - "desc": "Fecha os painéis abertos" - }, - "unifiedCanvasHotkeys": "Atalhos da Tela Unificada", - "toggleGallery": { - "title": "Ativar Galeria", - "desc": "Abrir e fechar a gaveta da galeria" - }, - "setSeed": { - "title": "Definir Seed", - "desc": "Usar seed da imagem atual" - }, - "setParameters": { - "title": "Definir Parâmetros", - "desc": "Usar todos os parâmetros da imagem atual" - }, - "restoreFaces": { - "title": "Restaurar Rostos", - "desc": "Restaurar a imagem atual" - }, - "upscale": { - "title": "Redimensionar", - "desc": "Redimensionar a imagem atual" - }, - "showInfo": { - "title": "Mostrar Informações", - "desc": "Mostrar metadados de informações da imagem atual" - }, - "deleteImage": { - "title": "Apagar Imagem", - "desc": "Apaga a imagem atual" - }, - "increaseGalleryThumbSize": { - "title": "Aumentar Tamanho da Galeria de Imagem", - "desc": "Aumenta o tamanho das thumbs na galeria" - }, - "increaseBrushSize": { - "title": "Aumentar Tamanho do Pincel", - "desc": "Aumenta o tamanho do pincel/apagador" - }, - "fillBoundingBox": { - "title": "Preencher Caixa Delimitadora", - "desc": "Preenche a caixa delimitadora com a cor do pincel" - }, - "eraseBoundingBox": { - "title": "Apagar Caixa Delimitadora", - "desc": "Apaga a área da caixa delimitadora" - }, - "colorPicker": { - "title": "Selecionar Seletor de Cor", - "desc": "Seleciona o seletor de cores" - }, - "showHideBoundingBox": { - "title": "Mostrar/Esconder Caixa Delimitadora", - "desc": "Ativa a visibilidade da caixa delimitadora" - }, - "saveToGallery": { - "title": "Gravara Na Galeria", - "desc": "Grava a tela atual na galeria" - }, - "copyToClipboard": { - "title": "Copiar para a Área de Transferência", - "desc": "Copia a tela atual para a área de transferência" - }, - "resetView": { - "title": "Resetar Visualização", - "desc": "Reseta Visualização da Tela" - }, - "previousStagingImage": { - "title": "Imagem de Preparação Anterior", - "desc": "Área de Imagem de Preparação Anterior" - }, - "nextStagingImage": { - "title": "Próxima Imagem de Preparação Anterior", - "desc": "Próxima Área de Imagem de Preparação Anterior" - }, - "acceptStagingImage": { - "title": "Aceitar Imagem de Preparação Anterior", - "desc": "Aceitar Área de Imagem de Preparação Anterior" - } - }, "modelManager": { "modelUpdated": "Modelo Atualizado", "description": "Descrição", @@ -231,7 +29,6 @@ "convertToDiffusersHelpText6": "Deseja converter este modelo?", "alpha": "Alpha", "config": "Configuração", - "v2_768": "v2 (768px)", "modelConverted": "Modelo Convertido", "manual": "Manual", "name": "Nome", @@ -245,7 +42,6 @@ "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.", - "v2_base": "v2 (512px)", "none": "nenhum", "modelManager": "Gerente de Modelo", "model": "Modelo", @@ -272,11 +68,8 @@ "tileSize": "Tamanho do Ladrilho", "symmetry": "Simetria", "usePrompt": "Usar Prompt", - "showOptionsPanel": "Mostrar Painel de Opções", "strength": "Força", "upscaling": "Redimensionando", - "upscale": "Redimensionar", - "upscaleImage": "Redimensionar Imagem", "scaleBeforeProcessing": "Escala Antes do Processamento", "images": "Imagems", "steps": "Passos", @@ -285,17 +78,13 @@ "scaledWidth": "L Escalada", "scaledHeight": "A Escalada", "infillMethod": "Método de Preenchimento", - "sendToImg2Img": "Mandar para Imagem Para Imagem", - "sendToUnifiedCanvas": "Mandar para Tela Unificada", "copyImage": "Copiar imagem", - "downloadImage": "Descarregar Imagem", "useSeed": "Usar Seed", "useAll": "Usar Todos", "info": "Informações" }, "settings": { "confirmOnDelete": "Confirmar Antes de Apagar", - "enableImageDebugging": "Ativar Depuração de Imagem", "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", @@ -305,80 +94,30 @@ }, "toast": { "uploadFailed": "Envio Falhou", - "imageNotLoadedDesc": "Nenhuma imagem encontrada a enviar para o módulo de imagem para imagem", "imageCopied": "Imagem Copiada", - "canvasMerged": "Tela Fundida", - "sentToImageToImage": "Mandar Para Imagem Para Imagem", - "sentToUnifiedCanvas": "Enviada para a Tela Unificada", - "parametersNotSet": "Parâmetros Não Definidos", - "metadataLoadFailed": "Falha ao tentar carregar metadados" - }, - "tooltip": { - "feature": { - "prompt": "Este é o campo de prompt. O prompt inclui objetos de geração e termos estilísticos. Também pode adicionar peso (importância do token) no prompt, mas comandos e parâmetros de CLI não funcionarão.", - "other": "Essas opções ativam modos alternativos de processamento para o Invoke. 'Seamless tiling' criará padrões repetidos na saída. 'High resolution' é uma geração em duas etapas com img2img: use essa configuração quando desejar uma imagem maior e mais coerente sem artefatos. Levará mais tempo do que o txt2img usual.", - "seed": "O valor da semente afeta o ruído inicial a partir do qual a imagem é formada. Pode usar as sementes já existentes de imagens anteriores. 'Limiar de ruído' é usado para mitigar artefatos em valores CFG altos (experimente a faixa de 0-10) e o Perlin para adicionar ruído Perlin durante a geração: ambos servem para adicionar variação às suas saídas.", - "gallery": "A galeria exibe as gerações da pasta de saída conforme elas são criadas. As configurações são armazenadas em ficheiros e acessadas pelo menu de contexto.", - "upscale": "Use o ESRGAN para ampliar a imagem imediatamente após a geração.", - "boundingBox": "A caixa delimitadora é a mesma que as configurações de largura e altura para Texto para Imagem ou Imagem para Imagem. Apenas a área na caixa será processada." - } - }, - "unifiedCanvas": { - "scaledBoundingBox": "Caixa Delimitadora Escalada", - "boundingBoxPosition": "Posição da Caixa Delimitadora", - "next": "Próximo", - "accept": "Aceitar", - "discardAll": "Descartar Todos", - "base": "Base", - "brush": "Pincel", - "showIntermediates": "Mostrar Intermediários", - "showGrid": "Mostrar Grade", - "clearCanvasHistoryConfirm": "Tem certeza que quer limpar o histórico de tela?", - "boundingBox": "Caixa Delimitadora", - "canvasDimensions": "Dimensões da Tela", - "canvasPosition": "Posição da Tela", - "cursorPosition": "Posição do cursor", - "previous": "Anterior", - "layer": "Camada", - "mask": "Máscara", - "maskingOptions": "Opções de Mascaramento", - "enableMask": "Ativar Máscara", - "preserveMaskedArea": "Preservar Área da Máscara", - "clearMask": "Limpar Máscara", - "eraser": "Apagador", - "fillBoundingBox": "Preencher Caixa Delimitadora", - "eraseBoundingBox": "Apagar Caixa Delimitadora", - "colorPicker": "Seletor de Cor", - "brushOptions": "Opções de Pincel", - "brushSize": "Tamanho", - "move": "Mover", - "resetView": "Resetar Visualização", - "mergeVisible": "Fundir Visível", - "saveToGallery": "Gravar na Galeria", - "copyToClipboard": "Copiar para a Área de Transferência", - "downloadAsImage": "Descarregar Como Imagem", - "undo": "Desfazer", - "redo": "Refazer", - "clearCanvas": "Limpar Tela", - "canvasSettings": "Configurações de Tela", - "snapToGrid": "Encaixar na Grade", - "darkenOutsideSelection": "Escurecer Seleção Externa", - "autoSaveToGallery": "Gravar Automaticamente na Galeria", - "saveBoxRegionOnly": "Gravar Apenas a Região da Caixa", - "limitStrokesToBox": "Limitar Traços à Caixa", - "showCanvasDebugInfo": "Mostrar Informações de Depuração daTela", - "clearCanvasHistory": "Limpar o Histórico da Tela", - "clearHistory": "Limpar Históprico", - "clearCanvasHistoryMessage": "Limpar o histórico de tela deixa a sua tela atual intacta, mas limpa de forma irreversível o histórico de desfazer e refazer.", - "activeLayer": "Camada Ativa", - "canvasScale": "Escala da Tela" + "parametersNotSet": "Parâmetros Não Definidos" }, "accessibility": { "invokeProgressBar": "Invocar barra de progresso", - "reset": "Repôr", + "reset": "Reiniciar", "nextImage": "Próxima imagem", - "showOptionsPanel": "Mostrar painel de opções", "uploadImage": "Enviar imagem", - "previousImage": "Imagem anterior" + "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/pt_BR.json b/invokeai/frontend/web/public/locales/pt_BR.json deleted file mode 100644 index c966c6db50d..00000000000 --- a/invokeai/frontend/web/public/locales/pt_BR.json +++ /dev/null @@ -1,373 +0,0 @@ -{ - "common": { - "hotkeysLabel": "Teclas de atalho", - "languagePickerLabel": "Seletor de Idioma", - "reportBugLabel": "Relatar Bug", - "settingsLabel": "Configurações", - "img2img": "Imagem Para Imagem", - "unifiedCanvas": "Tela Unificada", - "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", - "loadMore": "Carregar Mais", - "noImagesInGallery": "Sem Imagens na Galeria" - }, - "hotkeys": { - "keyboardShortcuts": "Atalhos de Teclado", - "appHotkeys": "Atalhos do app", - "generalHotkeys": "Atalhos Gerais", - "galleryHotkeys": "Atalhos da Galeria", - "unifiedCanvasHotkeys": "Atalhos da Tela Unificada", - "invoke": { - "title": "Invoke", - "desc": "Gerar uma imagem" - }, - "cancel": { - "title": "Cancelar", - "desc": "Cancelar geração de imagem" - }, - "focusPrompt": { - "title": "Foco do Prompt", - "desc": "Foco da área de texto do prompt" - }, - "toggleOptions": { - "title": "Ativar Opções", - "desc": "Abrir e fechar o painel de opções" - }, - "pinOptions": { - "title": "Fixar Opções", - "desc": "Fixar o painel de opções" - }, - "toggleGallery": { - "title": "Ativar Galeria", - "desc": "Abrir e fechar a gaveta da galeria" - }, - "maximizeWorkSpace": { - "title": "Maximizar a Área de Trabalho", - "desc": "Fechar painéis e maximixar área de trabalho" - }, - "changeTabs": { - "title": "Mudar Abas", - "desc": "Trocar para outra área de trabalho" - }, - "consoleToggle": { - "title": "Ativar Console", - "desc": "Abrir e fechar console" - }, - "setPrompt": { - "title": "Definir Prompt", - "desc": "Usar o prompt da imagem atual" - }, - "setSeed": { - "title": "Definir Seed", - "desc": "Usar seed da imagem atual" - }, - "setParameters": { - "title": "Definir Parâmetros", - "desc": "Usar todos os parâmetros da imagem atual" - }, - "restoreFaces": { - "title": "Restaurar Rostos", - "desc": "Restaurar a imagem atual" - }, - "upscale": { - "title": "Redimensionar", - "desc": "Redimensionar a imagem atual" - }, - "showInfo": { - "title": "Mostrar Informações", - "desc": "Mostrar metadados de informações da imagem atual" - }, - "sendToImageToImage": { - "title": "Mandar para Imagem Para Imagem", - "desc": "Manda a imagem atual para Imagem Para Imagem" - }, - "deleteImage": { - "title": "Apagar Imagem", - "desc": "Apaga a imagem atual" - }, - "closePanels": { - "title": "Fechar Painéis", - "desc": "Fecha os painéis abertos" - }, - "previousImage": { - "title": "Imagem Anterior", - "desc": "Mostra a imagem anterior na galeria" - }, - "nextImage": { - "title": "Próxima Imagem", - "desc": "Mostra a próxima imagem na galeria" - }, - "increaseGalleryThumbSize": { - "title": "Aumentar Tamanho da Galeria de Imagem", - "desc": "Aumenta o tamanho das thumbs na galeria" - }, - "decreaseGalleryThumbSize": { - "title": "Diminuir Tamanho da Galeria de Imagem", - "desc": "Diminui o tamanho das thumbs na galeria" - }, - "selectBrush": { - "title": "Selecionar Pincel", - "desc": "Seleciona o pincel" - }, - "selectEraser": { - "title": "Selecionar Apagador", - "desc": "Seleciona o apagador" - }, - "decreaseBrushSize": { - "title": "Diminuir Tamanho do Pincel", - "desc": "Diminui o tamanho do pincel/apagador" - }, - "increaseBrushSize": { - "title": "Aumentar Tamanho do Pincel", - "desc": "Aumenta o tamanho do pincel/apagador" - }, - "decreaseBrushOpacity": { - "title": "Diminuir Opacidade do Pincel", - "desc": "Diminui a opacidade do pincel" - }, - "increaseBrushOpacity": { - "title": "Aumentar Opacidade do Pincel", - "desc": "Aumenta a opacidade do pincel" - }, - "moveTool": { - "title": "Ferramenta Mover", - "desc": "Permite navegar pela tela" - }, - "fillBoundingBox": { - "title": "Preencher Caixa Delimitadora", - "desc": "Preenche a caixa delimitadora com a cor do pincel" - }, - "eraseBoundingBox": { - "title": "Apagar Caixa Delimitadora", - "desc": "Apaga a área da caixa delimitadora" - }, - "colorPicker": { - "title": "Selecionar Seletor de Cor", - "desc": "Seleciona o seletor de cores" - }, - "toggleSnap": { - "title": "Ativar Encaixe", - "desc": "Ativa Encaixar na Grade" - }, - "quickToggleMove": { - "title": "Ativar Mover Rapidamente", - "desc": "Temporariamente ativa o modo Mover" - }, - "toggleLayer": { - "title": "Ativar Camada", - "desc": "Ativa a seleção de camada de máscara/base" - }, - "clearMask": { - "title": "Limpar Máscara", - "desc": "Limpa toda a máscara" - }, - "hideMask": { - "title": "Esconder Máscara", - "desc": "Esconde e Revela a máscara" - }, - "showHideBoundingBox": { - "title": "Mostrar/Esconder Caixa Delimitadora", - "desc": "Ativa a visibilidade da caixa delimitadora" - }, - "mergeVisible": { - "title": "Fundir Visível", - "desc": "Fundir todas as camadas visíveis em tela" - }, - "saveToGallery": { - "title": "Salvara Na Galeria", - "desc": "Salva a tela atual na galeria" - }, - "copyToClipboard": { - "title": "Copiar para a Área de Transferência", - "desc": "Copia a tela atual para a área de transferência" - }, - "downloadImage": { - "title": "Baixar Imagem", - "desc": "Baixa a tela atual" - }, - "undoStroke": { - "title": "Desfazer Traço", - "desc": "Desfaz um traço de pincel" - }, - "redoStroke": { - "title": "Refazer Traço", - "desc": "Refaz o traço de pincel" - }, - "resetView": { - "title": "Resetar Visualização", - "desc": "Reseta Visualização da Tela" - }, - "previousStagingImage": { - "title": "Imagem de Preparação Anterior", - "desc": "Área de Imagem de Preparação Anterior" - }, - "nextStagingImage": { - "title": "Próxima Imagem de Preparação Anterior", - "desc": "Próxima Área de Imagem de Preparação Anterior" - }, - "acceptStagingImage": { - "title": "Aceitar Imagem de Preparação Anterior", - "desc": "Aceitar Área de Imagem de Preparação Anterior" - } - }, - "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", - "upscale": "Redimensionar", - "upscaleImage": "Redimensionar Imagem", - "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", - "sendToImg2Img": "Mandar para Imagem Para Imagem", - "sendToUnifiedCanvas": "Mandar para Tela Unificada", - "downloadImage": "Baixar Imagem", - "usePrompt": "Usar Prompt", - "useSeed": "Usar Seed", - "useAll": "Usar Todos", - "info": "Informações", - "showOptionsPanel": "Mostrar Painel de Opçõ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", - "enableImageDebugging": "Ativar Depuração de Imagem", - "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", - "imageNotLoadedDesc": "Nenhuma imagem encontrar para mandar para o módulo de imagem para imagem", - "canvasMerged": "Tela Fundida", - "sentToImageToImage": "Mandar Para Imagem Para Imagem", - "sentToUnifiedCanvas": "Enviada para a Tela Unificada", - "parametersNotSet": "Parâmetros Não Definidos", - "metadataLoadFailed": "Falha ao tentar carregar metadados" - }, - "unifiedCanvas": { - "layer": "Camada", - "base": "Base", - "mask": "Máscara", - "maskingOptions": "Opções de Mascaramento", - "enableMask": "Ativar Máscara", - "preserveMaskedArea": "Preservar Área da Máscara", - "clearMask": "Limpar Máscara", - "brush": "Pincel", - "eraser": "Apagador", - "fillBoundingBox": "Preencher Caixa Delimitadora", - "eraseBoundingBox": "Apagar Caixa Delimitadora", - "colorPicker": "Seletor de Cor", - "brushOptions": "Opções de Pincel", - "brushSize": "Tamanho", - "move": "Mover", - "resetView": "Resetar Visualização", - "mergeVisible": "Fundir Visível", - "saveToGallery": "Salvar na Galeria", - "copyToClipboard": "Copiar para a Área de Transferência", - "downloadAsImage": "Baixar Como Imagem", - "undo": "Desfazer", - "redo": "Refazer", - "clearCanvas": "Limpar Tela", - "canvasSettings": "Configurações de Tela", - "showIntermediates": "Mostrar Intermediários", - "showGrid": "Mostrar Grade", - "snapToGrid": "Encaixar na Grade", - "darkenOutsideSelection": "Escurecer Seleção Externa", - "autoSaveToGallery": "Salvar Automaticamente na Galeria", - "saveBoxRegionOnly": "Salvar Apenas a Região da Caixa", - "limitStrokesToBox": "Limitar Traços para a Caixa", - "showCanvasDebugInfo": "Mostrar Informações de Depuração daTela", - "clearCanvasHistory": "Limpar o Histórico da Tela", - "clearHistory": "Limpar Históprico", - "clearCanvasHistoryMessage": "Limpar o histórico de tela deixa sua tela atual intacta, mas limpa de forma irreversível o histórico de desfazer e refazer.", - "clearCanvasHistoryConfirm": "Tem certeza que quer limpar o histórico de tela?", - "activeLayer": "Camada Ativa", - "canvasScale": "Escala da Tela", - "boundingBox": "Caixa Delimitadora", - "scaledBoundingBox": "Caixa Delimitadora Escalada", - "boundingBoxPosition": "Posição da Caixa Delimitadora", - "canvasDimensions": "Dimensões da Tela", - "canvasPosition": "Posição da Tela", - "cursorPosition": "Posição do cursor", - "previous": "Anterior", - "next": "Próximo", - "accept": "Aceitar", - "discardAll": "Descartar Todos" - }, - "tooltip": { - "feature": { - "seed": "O valor da semente afeta o ruído inicial a partir do qual a imagem é formada. Você pode usar as sementes já existentes de imagens anteriores. 'Limiar de ruído' é usado para mitigar artefatos em valores CFG altos (experimente a faixa de 0-10), e o Perlin para adicionar ruído Perlin durante a geração: ambos servem para adicionar variação às suas saídas.", - "gallery": "A galeria exibe as gerações da pasta de saída conforme elas são criadas. As configurações são armazenadas em arquivos e acessadas pelo menu de contexto.", - "other": "Essas opções ativam modos alternativos de processamento para o Invoke. 'Seamless tiling' criará padrões repetidos na saída. 'High resolution' é uma geração em duas etapas com img2img: use essa configuração quando desejar uma imagem maior e mais coerente sem artefatos. Levará mais tempo do que o txt2img usual.", - "boundingBox": "A caixa delimitadora é a mesma que as configurações de largura e altura para Texto para Imagem ou Imagem para Imagem. Apenas a área na caixa será processada.", - "upscale": "Use o ESRGAN para ampliar a imagem imediatamente após a geração.", - "prompt": "Este é o campo de prompt. O prompt inclui objetos de geração e termos estilísticos. Você também pode adicionar peso (importância do token) no prompt, mas comandos e parâmetros de CLI não funcionarão." - } - } -} diff --git a/invokeai/frontend/web/public/locales/ro.json b/invokeai/frontend/web/public/locales/ro.json index 0967ef424bc..9fb4068a93f 100644 --- a/invokeai/frontend/web/public/locales/ro.json +++ b/invokeai/frontend/web/public/locales/ro.json @@ -1 +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 index 2f7c711bf24..279b1b0eabe 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -5,8 +5,7 @@ "reportBugLabel": "Сообщить об ошибке", "settingsLabel": "Настройки", "img2img": "Изображение в изображение (img2img)", - "unifiedCanvas": "Единый холст", - "nodes": "Рабочие процессы", + "nodes": "Схемы", "upload": "Загрузить", "load": "Загрузить", "statusDisconnected": "Отключен", @@ -26,23 +25,21 @@ "communityLabel": "Сообщество", "batch": "Пакетный менеджер", "modelManager": "Менеджер моделей", - "nodeEditor": "Редактор Нодов (Узлов)", - "controlNet": "Controlnet", + "controlNet": "ControlNet", "advanced": "Расширенные", - "t2iAdapter": "T2I Adapter", + "t2iAdapter": "T2I адаптер", "checkpoint": "Checkpoint", "format": "Формат", "unknown": "Неизвестно", "folder": "Папка", "inpaint": "Перерисовать", "updated": "Обновлен", - "on": "На", + "on": "Вкл", "save": "Сохранить", "created": "Создано", "error": "Ошибка", - "prevPage": "Предыдущая страница", "simple": "Простой", - "ipAdapter": "IP Adapter", + "ipAdapter": "IP адаптер", "installed": "Установлено", "ai": "ИИ", "auto": "Авто", @@ -51,7 +48,6 @@ "template": "Шаблон", "outputs": "результаты", "unknownError": "Неизвестная ошибка", - "imageFailedToLoad": "Невозможно загрузить изображение", "direction": "Направление", "data": "Данные", "somethingWentWrong": "Что-то пошло не так", @@ -60,68 +56,72 @@ "orderBy": "Сортировать по", "copyError": "Ошибка $t(gallery.copy)", "learnMore": "Узнать больше", - "nextPage": "Следущая страница", "saveAs": "Сохранить как", "input": "Вход", "details": "Детали", - "notInstalled": "Нет $t(common.installed)", "or": "или", - "aboutHeading": "Владей своей творческой силой", + "aboutHeading": "Управляй своей творческой силой", "red": "Красный", "green": "Зеленый", "blue": "Синий", "alpha": "Альфа", "toResolve": "Чтоб решить", "copy": "Копировать", - "localSystem": "Локальная система", - "aboutDesc": "Используя Invoke для работы? Проверьте это:", + "aboutDesc": "Используете Invoke в работе? Ознакомьтесь:", "add": "Добавить", - "loglevel": "Уровень логов", "beta": "Бета", "selected": "Выбрано", - "positivePrompt": "Позитивный запрос", - "negativePrompt": "Негативный запрос", + "positivePrompt": "Позитивный промпт", + "negativePrompt": "Негативный промпт", "editor": "Редактор", - "goTo": "Перейти к", "tab": "Вкладка", - "viewing": "Просмотр", - "editing": "Редактирование", - "viewingDesc": "Просмотр изображений в режиме большой галереи", - "editingDesc": "Редактировать на холсте слоёв управления", "enabled": "Включено", "disabled": "Отключено", - "comparingDesc": "Сравнение двух изображений", - "comparing": "Сравнение" + "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": "Автоматически выбирать новые", - "loadMore": "Показать больше", - "noImagesInGallery": "Изображений нет", "deleteImagePermanent": "Удаленные изображения невозможно восстановить.", - "deleteImageBin": "Удаленные изображения будут отправлены в корзину вашей операционной системы.", "deleteImage_one": "Удалить изображение", "deleteImage_few": "Удалить {{count}} изображения", "deleteImage_many": "Удалить {{count}} изображений", - "assets": "Ресурсы", "autoAssignBoardOnClick": "Авто-назначение доски по клику", "deleteSelection": "Удалить выделенное", "featuresWillReset": "Если вы удалите это изображение, эти функции будут немедленно сброшены.", - "problemDeletingImagesDesc": "Не удалось удалить одно или несколько изображений", "loading": "Загрузка", - "unableToLoad": "Невозможно загрузить галерею", "image": "изображение", "drop": "перебросить", - "problemDeletingImages": "Проблема с удалением изображений", "downloadSelection": "Скачать выделенное", "currentlyInUse": "В настоящее время это изображение используется в следующих функциях:", "unstarImage": "Удалить из избранного", - "dropOrUpload": "$t(gallery.drop) или загрузить", + "dropOrUpload": "Перетащите или загрузите", "copy": "Копировать", "download": "Скачать", "noImageSelected": "Изображение не выбрано", - "setCurrentImage": "Установить как текущее изображение", "starImage": "Добавить в избранное", "dropToUpload": "$t(gallery.drop) чтоб загрузить", "bulkDownloadFailed": "Загрузка не удалась", @@ -138,239 +138,337 @@ "compareHelp4": "Нажмите Z или Esc для выхода.", "compareImage": "Сравнить изображение", "viewerImage": "Изображение просмотрщика", - "selectAnImageToCompare": "Выберите изображение для сравнения", "slider": "Слайдер", "sideBySide": "Бок о бок", - "compareOptions": "Варианты сравнения", "compareHelp1": "Удерживайте Alt при нажатии на изображение в галерее или при помощи клавиш со стрелками, чтобы изменить сравниваемое изображение.", "compareHelp2": "Нажмите M, чтобы переключиться между режимами сравнения.", - "compareHelp3": "Нажмите C, чтобы поменять местами сравниваемые изображения." + "compareHelp3": "Нажмите C, чтобы поменять местами сравниваемые изображения.", + "newestFirst": "Сначала новые", + "sortDirection": "Направление сортировки", + "oldestFirst": "Сначала старые", + "showStarredImagesFirst": "Сначала избранные изображения", + "selectAllOnPage": "Выбрать все на странице", + "showArchivedBoards": "Показать архивированные доски", + "searchImages": "Поиск по метаданным", + "displayBoardSearch": "Поиск доски", + "displaySearch": "Поиск изображений", + "exitBoardSearch": "Выйти из поиска досок", + "go": "Перейти", + "exitSearch": "Выйти из поиска изображений", + "move": "Двигать", + "gallery": "Галерея", + "imagesTab": "Изображения, созданные и сохраненные в Invoke.", + "assetsTab": "Файлы, которые вы загрузили для использования в своих проектах.", + "boardsSettings": "Настройки доски", + "imagesSettings": "Настройки галереи изображений" }, "hotkeys": { - "keyboardShortcuts": "Горячие клавиши", - "appHotkeys": "Приложение", - "generalHotkeys": "Общее", - "galleryHotkeys": "Галлерея", - "unifiedCanvasHotkeys": "Единый холст", - "invoke": { - "title": "Invoke", - "desc": "Сгенерировать изображение" - }, - "cancel": { - "title": "Отменить", - "desc": "Отменить текущий элемент" - }, - "focusPrompt": { - "title": "Переключиться на ввод запроса", - "desc": "Переключение на область ввода запроса" - }, - "toggleOptions": { - "title": "Показать/скрыть параметры", - "desc": "Открывать и закрывать панель параметров" - }, - "pinOptions": { - "title": "Закрепить параметры", - "desc": "Закрепить панель параметров" - }, - "toggleGallery": { - "title": "Показать галерею", - "desc": "Открывать и закрывать ящик галереи" - }, - "maximizeWorkSpace": { - "title": "Максимизировать рабочее пространство", - "desc": "Скрыть панели и максимизировать рабочую область" - }, - "changeTabs": { - "title": "Переключить вкладку", - "desc": "Переключиться на другую рабочую область" - }, - "consoleToggle": { - "title": "Показать консоль", - "desc": "Открывать и закрывать консоль" - }, - "setPrompt": { - "title": "Использовать запрос", - "desc": "Использовать запрос из текущего изображения" - }, - "setSeed": { - "title": "Использовать сид", - "desc": "Использовать сид текущего изображения" - }, - "setParameters": { - "title": "Использовать все параметры", - "desc": "Использовать все параметры текущего изображения" - }, - "restoreFaces": { - "title": "Восстановить лица", - "desc": "Восстановить лица на текущем изображении" - }, - "upscale": { - "title": "Увеличение", - "desc": "Увеличить текущеее изображение" - }, - "showInfo": { - "title": "Показать метаданные", - "desc": "Показать метаданные из текущего изображения" - }, - "sendToImageToImage": { - "title": "Отправить в img2img", - "desc": "Отправить текущее изображение в Image To Image" - }, - "deleteImage": { - "title": "Удалить изображение", - "desc": "Удалить текущее изображение" - }, - "closePanels": { - "title": "Закрыть панели", - "desc": "Закрывает открытые панели" - }, - "previousImage": { - "title": "Предыдущее изображение", - "desc": "Отображать предыдущее изображение в галерее" - }, - "nextImage": { - "title": "Следующее изображение", - "desc": "Отображение следующего изображения в галерее" - }, - "increaseGalleryThumbSize": { - "title": "Увеличить размер миниатюр галереи", - "desc": "Увеличивает размер миниатюр галереи" - }, - "decreaseGalleryThumbSize": { - "title": "Уменьшает размер миниатюр галереи", - "desc": "Уменьшает размер миниатюр галереи" - }, - "selectBrush": { - "title": "Выбрать кисть", - "desc": "Выбирает кисть для холста" - }, - "selectEraser": { - "title": "Выбрать ластик", - "desc": "Выбирает ластик для холста" - }, - "decreaseBrushSize": { - "title": "Уменьшить размер кисти", - "desc": "Уменьшает размер кисти/ластика холста" - }, - "increaseBrushSize": { - "title": "Увеличить размер кисти", - "desc": "Увеличивает размер кисти/ластика холста" - }, - "decreaseBrushOpacity": { - "title": "Уменьшить непрозрачность кисти", - "desc": "Уменьшает непрозрачность кисти холста" - }, - "increaseBrushOpacity": { - "title": "Увеличить непрозрачность кисти", - "desc": "Увеличивает непрозрачность кисти холста" - }, - "moveTool": { - "title": "Инструмент перемещения", - "desc": "Позволяет перемещаться по холсту" - }, - "fillBoundingBox": { - "title": "Заполнить ограничивающую рамку", - "desc": "Заполняет ограничивающую рамку цветом кисти" - }, - "eraseBoundingBox": { - "title": "Стереть ограничивающую рамку", - "desc": "Стирает область ограничивающей рамки" - }, - "colorPicker": { - "title": "Выбрать цвет", - "desc": "Выбирает средство выбора цвета холста" - }, - "toggleSnap": { - "title": "Включить привязку", - "desc": "Включает/выключает привязку к сетке" - }, - "quickToggleMove": { - "title": "Быстрое переключение перемещения", - "desc": "Временно переключает режим перемещения" - }, - "toggleLayer": { - "title": "Переключить слой", - "desc": "Переключение маски/базового слоя" - }, - "clearMask": { - "title": "Очистить маску", - "desc": "Очистить всю маску" - }, - "hideMask": { - "title": "Скрыть маску", - "desc": "Скрывает/показывает маску" - }, - "showHideBoundingBox": { - "title": "Показать/скрыть ограничивающую рамку", - "desc": "Переключить видимость ограничивающей рамки" - }, - "mergeVisible": { - "title": "Объединить видимые", - "desc": "Объединить все видимые слои холста" - }, - "saveToGallery": { - "title": "Сохранить в галерею", - "desc": "Сохранить текущий холст в галерею" - }, - "copyToClipboard": { - "title": "Копировать в буфер обмена", - "desc": "Копировать текущий холст в буфер обмена" - }, - "downloadImage": { - "title": "Скачать изображение", - "desc": "Скачать содержимое холста" - }, - "undoStroke": { - "title": "Отменить кисть", - "desc": "Отменить мазок кисти" - }, - "redoStroke": { - "title": "Повторить кисть", - "desc": "Повторить мазок кисти" - }, - "resetView": { - "title": "Вид по умолчанию", - "desc": "Сбросить вид холста" - }, - "previousStagingImage": { - "title": "Предыдущее изображение", - "desc": "Предыдущая область изображения" - }, - "nextStagingImage": { - "title": "Следующее изображение", - "desc": "Следующая область изображения" - }, - "acceptStagingImage": { - "title": "Принять изображение", - "desc": "Принять текущее изображение" - }, - "addNodes": { - "desc": "Открывает меню добавления узла", - "title": "Добавление узлов" - }, - "nodesHotkeys": "Узлы", - "cancelAndClear": { - "desc": "Отмена текущего элемента очереди и очистка всех ожидающих элементов", - "title": "Отменить и очистить" - }, - "resetOptionsAndGallery": { - "title": "Сброс настроек и галереи", - "desc": "Сброс панелей галереи и настроек" - }, "searchHotkeys": "Поиск горячих клавиш", "noHotkeysFound": "Горячие клавиши не найдены", - "toggleOptionsAndGallery": { - "desc": "Открытие и закрытие панели опций и галереи", - "title": "Переключить опции и галерею" - }, "clearSearch": "Очистить поиск", - "remixImage": { - "desc": "Используйте все параметры, кроме сида из текущего изображения", - "title": "Ремикс изображения" + "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." + } }, - "toggleViewer": { - "title": "Переключить просмотр изображений", - "desc": "Переключение между средством просмотра изображений и рабочей областью для текущей вкладки." + "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": { @@ -395,7 +493,7 @@ "deleteMsg1": "Вы точно хотите удалить модель из InvokeAI?", "deleteMsg2": "Это приведет К УДАЛЕНИЮ модели С ДИСКА, если она находится в корневой папке Invoke. Если вы используете пользовательское расположение, то модель НЕ будет удалена с диска.", "convertToDiffusersHelpText5": "Пожалуйста, убедитесь, что у вас достаточно места на диске. Модели обычно занимают 2–7 Гб.", - "convertToDiffusersHelpText3": "Ваш файл контрольной точки НА ДИСКЕ будет УДАЛЕН, если он находится в корневой папке InvokeAI. Если он находится в пользовательском расположении, то он НЕ будет удален.", + "convertToDiffusersHelpText3": "Файл чекпоинта будет удалён с диска, если он находится в корневой папке InvokeAI. Если файл расположен в пользовательской папке, он удалён не будет.", "allModels": "Все модели", "repo_id": "ID репозитория", "convert": "Преобразовать", @@ -407,13 +505,9 @@ "alpha": "Альфа", "none": "пусто", "convertToDiffusersHelpText2": "Этот процесс заменит вашу запись в менеджере моделей на версию той же модели в Diffusers.", - "v2_768": "v2 (768px)", - "v2_base": "v2 (512px)", "modelDeleted": "Модель удалена", "variant": "Вариант", "baseModel": "Базовая модель", - "modelsSynced": "Модели синхронизированы", - "modelSyncFailed": "Не удалось синхронизировать модели", "vae": "VAE", "modelDeleteFailed": "Не удалось удалить модель", "convertingModelBegin": "Конвертация модели. Пожалуйста, подождите.", @@ -443,7 +537,6 @@ "scanResults": "Результаты сканирования", "source": "Источник", "triggerPhrases": "Триггерные фразы", - "useDefaultSettings": "Использовать стандартные настройки", "modelName": "Название модели", "modelSettings": "Настройки модели", "upcastAttention": "Внимание", @@ -458,7 +551,7 @@ "pathToConfig": "Путь к конфигурации", "loraTriggerPhrases": "Триггерные фразы LoRA", "mainModelTriggerPhrases": "Триггерные фразы основной модели", - "inplaceInstallDesc": "Устанавливайте модели без копирования файлов. При использовании модели она будет загружаться из этого места. Если этот параметр отключен, файлы модели будут скопированы в каталог моделей, управляемых Invoke, во время установки.", + "inplaceInstallDesc": "Устанавливать модели без перемещения файлов. В этом случае модель будет загружаться из исходной папки. Если опция отключена, файлы модели при установке будут перемещены в каталог моделей Invoke.", "huggingFaceRepoID": "ID репозитория HuggingFace", "installQueue": "Очередь установки", "installAll": "Установить все", @@ -472,22 +565,28 @@ "simpleModelPlaceholder": "URL или путь к локальному файлу или папке diffusers", "urlOrLocalPath": "URL или локальный путь", "urlOrLocalPathHelper": "URL-адреса должны указывать на один файл. Локальные пути могут указывать на один файл или папку для одной модели диффузоров.", - "hfToken": "Токен HuggingFace", - "hfTokenInvalid": "Недействительный или отсутствующий HF-токен", - "hfTokenInvalidErrorMessage2": "Обновите его в . ", - "hfTokenUnableToVerify": "Невозможно проверить HF-токен", - "hfTokenSaved": "HF-токен сохранен", "starterModels": "Стартовые модели", "textualInversions": "Текстовые инверсии", - "hfTokenHelperText": "Для использования моделей контрольных точек требуется токен HF. Нажмите здесь, чтобы создать или получить свой токен.", - "hfTokenInvalidErrorMessage": "Недействительный или отсутствующий HuggingFace токен.", - "hfTokenUnableToVerifyErrorMessage": "Невозможно проверить токен HuggingFace. Вероятно, это связано с сетевой ошибкой. Пожалуйста, повторите попытку позже.", "loraModels": "LoRAs", "main": "Основные", "noModelsInstalled": "Нет установленных моделей", "noModelsInstalledDesc1": "Установите модели с помощью", "noMatchingModels": "Нет подходящих моделей", - "ipAdapters": "IP адаптеры" + "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": "Изображения", @@ -502,8 +601,6 @@ "type": "Тип", "strength": "Сила", "upscaling": "Увеличение", - "upscale": "Увеличить", - "upscaleImage": "Увеличить изображение", "scale": "Масштаб", "imageFit": "Уместить изображение", "scaleBeforeProcessing": "Масштабировать", @@ -511,14 +608,10 @@ "scaledHeight": "Масштаб В", "infillMethod": "Способ заполнения", "tileSize": "Размер области", - "sendToImg2Img": "Отправить в img2img", - "sendToUnifiedCanvas": "Отправить на Единый холст", - "downloadImage": "Скачать", "usePrompt": "Использовать запрос", "useSeed": "Использовать сид", "useAll": "Использовать все", "info": "Метаданные", - "showOptionsPanel": "Показать панель настроек", "cancel": { "cancel": "Отмена" }, @@ -526,8 +619,8 @@ "symmetry": "Симметрия", "denoisingStrength": "Сила зашумления", "copyImage": "Скопировать изображение", - "seamlessXAxis": "Бесшовность по оси X", - "seamlessYAxis": "Бесшовность по оси Y", + "seamlessXAxis": "Бесшовная ось X", + "seamlessYAxis": "Бесшовная ось Y", "scheduler": "Планировщик", "positivePromptPlaceholder": "Запрос", "negativePromptPlaceholder": "Исключающий запрос", @@ -538,34 +631,19 @@ "noNodesInGraph": "Нет узлов в графе", "noModelSelected": "Модель не выбрана", "noPrompts": "Подсказки не создаются", - "noInitialImageSelected": "Исходное изображение не выбрано", "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} отсутствует ввод", - "noControlImageForControlAdapter": "Адаптер контроля #{{number}} не имеет изображения", - "noModelForControlAdapter": "Не выбрана модель адаптера контроля #{{number}}.", - "incompatibleBaseModelForControlAdapter": "Адаптер контроля №{{number}} несовместим с основной моделью.", "systemDisconnected": "Система отключена", "missingNodeTemplate": "Отсутствует шаблон узла", "missingFieldTemplate": "Отсутствует шаблон поля", "addingImagesTo": "Добавление изображений в", "invoke": "Создать", - "imageNotProcessedForControlAdapter": "Изображение адаптера контроля №{{number}} не обрабатывается", - "layer": { - "controlAdapterImageNotProcessed": "Изображение адаптера контроля не обработано", - "ipAdapterNoModelSelected": "IP адаптер не выбран", - "controlAdapterNoModelSelected": "не выбрана модель адаптера контроля", - "controlAdapterIncompatibleBaseModel": "несовместимая базовая модель адаптера контроля", - "controlAdapterNoImageSelected": "не выбрано изображение контрольного адаптера", - "initialImageNoImageSelected": "начальное изображение не выбрано", - "rgNoRegion": "регион не выбран", - "rgNoPromptsOrIPAdapters": "нет текстовых запросов или IP-адаптеров", - "ipAdapterIncompatibleBaseModel": "несовместимая базовая модель IP-адаптера", - "t2iAdapterIncompatibleDimensions": "Адаптер T2I требует, чтобы размеры изображения были кратны {{multiple}}", - "ipAdapterNoImageSelected": "изображение IP-адаптера не выбрано" - } - }, - "isAllowedToUpscale": { - "useX2Model": "Изображение слишком велико для увеличения с помощью модели x4. Используйте модель x2", - "tooLarge": "Изображение слишком велико для увеличения. Выберите изображение меньшего размера" + "noFLUXVAEModelSelected": "Для генерации FLUX не выбрана модель VAE", + "noT5EncoderModelSelected": "Для генерации FLUX не выбрана модель T5 энкодера", + "canvasIsFiltering": "Холст фильтруется", + "canvasIsTransforming": "Холст трансформируется", + "noCLIPEmbedModelSelected": "Для генерации FLUX не выбрана модель CLIP Embed", + "canvasIsRasterizing": "Холст занят (идёт растеризация)", + "canvasIsCompositing": "Холст занят (идёт компоновка)" }, "cfgRescaleMultiplier": "Множитель масштабирования CFG", "patchmatchDownScaleSize": "уменьшить", @@ -583,25 +661,28 @@ "remixImage": "Ремикс изображения", "coherenceMinDenoise": "Мин. шумоподавление", "coherenceEdgeSize": "Размер края", - "infillMosaicTileWidth": "Ширина плиток", - "infillMosaicTileHeight": "Высота плиток", - "infillMosaicMinColor": "Мин цвет", - "infillMosaicMaxColor": "Макс цвет", "infillColorValue": "Цвет заливки", - "globalSettings": "Глобальные настройки", - "globalNegativePromptPlaceholder": "Глобальный негативный запрос", - "globalPositivePromptPlaceholder": "Глобальный запрос" + "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": "Подтверждать удаление", - "enableImageDebugging": "Включить отладку", "resetWebUI": "Сброс настроек веб-интерфейса", "resetWebUIDesc1": "Сброс настроек веб-интерфейса удаляет только локальный кэш браузера с вашими изображениями и настройками. Он не удаляет изображения с диска.", "resetWebUIDesc2": "Если изображения не отображаются в галерее или не работает что-то еще, пожалуйста, попробуйте сбросить настройки, прежде чем сообщать о проблеме на GitHub.", "resetComplete": "Настройки веб-интерфейса были сброшены.", - "shouldLogToConsole": "Логи в консоль", "developer": "Разработчик", "general": "Основное", "showProgressInViewer": "Показывать процесс генерации в Просмотрщике", @@ -622,67 +703,37 @@ "intermediatesCleared_one": "Очищено {{count}} промежуточное", "intermediatesCleared_few": "Очищено {{count}} промежуточных", "intermediatesCleared_many": "Очищено {{count}} промежуточных", - "clearIntermediatesDesc1": "Очистка промежуточных элементов приведет к сбросу состояния Canvas и ControlNet.", + "clearIntermediatesDesc1": "Очистка промежуточных данных приведёт к сбросу состояния холста и ControlNet.", "intermediatesClearedFailed": "Проблема очистки промежуточных", - "reloadingIn": "Перезагрузка через" + "reloadingIn": "Перезагрузка через", + "informationalPopoversDisabled": "Информационные всплывающие окна отключены", + "informationalPopoversDisabledDesc": "Информационные всплывающие окна были отключены. Включите их в Настройках.", + "confirmOnNewSession": "Подтверждение нового сеанса" }, "toast": { "uploadFailed": "Загрузка не удалась", "imageCopied": "Изображение скопировано", - "imageNotLoadedDesc": "Не удалось найти изображение", - "canvasMerged": "Холст объединен", - "sentToImageToImage": "Отправить в img2img", - "sentToUnifiedCanvas": "Отправлено на Единый холст", "parametersNotSet": "Параметры не заданы", - "metadataLoadFailed": "Не удалось загрузить метаданные", "serverError": "Ошибка сервера", "connected": "Подключено к серверу", "canceled": "Обработка отменена", - "uploadFailedInvalidUploadDesc": "Должно быть одно изображение в формате PNG или JPEG", + "uploadFailedInvalidUploadDesc": "Допускаются только изображения в формате PNG, JPEG или WEBP.", "parameterNotSet": "Параметр не задан", "parameterSet": "Параметр задан", "problemCopyingImage": "Не удается скопировать изображение", "baseModelChangedCleared_one": "Очищена или отключена {{count}} несовместимая подмодель", - "baseModelChangedCleared_few": "Очищены или отключены {{count}} несовместимые подмодели", - "baseModelChangedCleared_many": "Очищены или отключены {{count}} несовместимых подмоделей", - "imageSavingFailed": "Не удалось сохранить изображение", - "canvasSentControlnetAssets": "Холст отправлен в ControlNet и ресурсы", - "problemCopyingCanvasDesc": "Невозможно экспортировать базовый слой", + "baseModelChangedCleared_few": "Очищено или отключено {{count}} несовместимых подмодели", + "baseModelChangedCleared_many": "Очищено или отключено {{count}} несовместимых подмоделей", "loadedWithWarnings": "Рабочий процесс загружен с предупреждениями", - "setInitialImage": "Установить как исходное изображение", - "canvasCopiedClipboard": "Холст скопирован в буфер обмена", - "setControlImage": "Установить как контрольное изображение", - "setNodeField": "Установить как поле узла", - "problemSavingMask": "Проблема с сохранением маски", - "problemSavingCanvasDesc": "Невозможно экспортировать базовый слой", - "invalidUpload": "Неверная загрузка", - "maskSavedAssets": "Маска сохранена в ресурсах", - "problemDownloadingCanvas": "Проблема с скачиванием холста", - "setAsCanvasInitialImage": "Установить в качестве исходного изображения холста", - "problemMergingCanvas": "Проблема с объединением холста", - "setCanvasInitialImage": "Установить исходное изображение холста", "imageUploaded": "Изображение загружено", - "addedToBoard": "Добавлено на доску", + "addedToBoard": "Добавлено в активы доски {{name}}", "workflowLoaded": "Рабочий процесс загружен", "problemDeletingWorkflow": "Проблема с удалением рабочего процесса", "modelAddedSimple": "Модель добавлена в очередь", - "problemImportingMaskDesc": "Невозможно экспортировать маску", - "problemCopyingCanvas": "Проблема с копированием холста", "workflowDeleted": "Рабочий процесс удален", - "problemSavingCanvas": "Проблема с сохранением холста", - "canvasDownloaded": "Холст скачан", - "problemMergingCanvasDesc": "Невозможно экспортировать базовый слой", - "problemDownloadingCanvasDesc": "Невозможно экспортировать базовый слой", - "problemSavingMaskDesc": "Невозможно экспортировать маску", "problemRetrievingWorkflow": "Проблема с получением рабочего процесса", - "imageSaved": "Изображение сохранено", - "maskSentControlnetAssets": "Маска отправлена в ControlNet и ресурсы", - "canvasSavedGallery": "Холст сохранен в галерею", "imageUploadFailed": "Не удалось загрузить изображение", - "problemImportingMask": "Проблема с импортом маски", "problemDownloadingImage": "Не удается скачать изображение", - "uploadInitialImage": "Загрузить начальное изображение", - "resetInitialImage": "Сбросить начальное изображение", "prunedQueue": "Урезанная очередь", "modelImportCanceled": "Импорт модели отменен", "parameters": "Параметры", @@ -695,109 +746,53 @@ "sessionRef": "Сессия: {{sessionId}}", "outOfMemoryError": "Ошибка нехватки памяти", "outOfMemoryErrorDesc": "Ваши текущие настройки генерации превышают возможности системы. Пожалуйста, измените настройки и повторите попытку.", - "somethingWentWrong": "Что-то пошло не так" - }, - "tooltip": { - "feature": { - "prompt": "Это поле для текста запроса, включая объекты генерации и стилистические термины. В запрос можно включить и коэффициенты веса (значимости токена), но консольные команды и параметры не будут работать.", - "gallery": "Здесь отображаются генерации из папки outputs по мере их появления.", - "other": "Эти опции включают альтернативные режимы обработки для Invoke. 'Бесшовный узор' создаст повторяющиеся узоры на выходе. 'Высокое разрешение' это генерация в два этапа с помощью img2img: используйте эту настройку, когда хотите получить цельное изображение большего размера без артефактов.", - "seed": "Значение сида влияет на начальный шум, из которого сформируется изображение. Можно использовать уже имеющийся сид из предыдущих изображений. 'Порог шума' используется для смягчения артефактов при высоких значениях CFG (попробуйте в диапазоне 0-10), а Перлин для добавления шума Перлина в процессе генерации: оба параметра служат для большей вариативности результатов.", - "upscale": "Используйте ESRGAN, чтобы увеличить изображение сразу после генерации.", - "boundingBox": "'Ограничительная рамка' аналогична настройкам Ширина и Высота для 'Избражения из текста' или 'Изображения в изображение'. Будет обработана только область в рамке." - } - }, - "unifiedCanvas": { - "layer": "Слой", - "base": "Базовый", - "mask": "Маска", - "maskingOptions": "Параметры маски", - "enableMask": "Включить маску", - "preserveMaskedArea": "Сохранять маскируемую область", - "clearMask": "Очистить маску", - "brush": "Кисть", - "eraser": "Ластик", - "fillBoundingBox": "Заполнить ограничивающую рамку", - "eraseBoundingBox": "Стереть ограничивающую рамку", - "colorPicker": "Пипетка", - "brushOptions": "Параметры кисти", - "brushSize": "Размер", - "move": "Переместить", - "resetView": "Сбросить вид", - "mergeVisible": "Объединить видимые", - "saveToGallery": "Сохранить в галерею", - "copyToClipboard": "Копировать в буфер обмена", - "downloadAsImage": "Скачать как изображение", - "undo": "Отменить", - "redo": "Повторить", - "clearCanvas": "Очистить холст", - "canvasSettings": "Настройки холста", - "showIntermediates": "Показывать процесс", - "showGrid": "Показать сетку", - "snapToGrid": "Привязать к сетке", - "darkenOutsideSelection": "Затемнить холст снаружи", - "autoSaveToGallery": "Автосохранение в галерее", - "saveBoxRegionOnly": "Сохранять только выделение", - "limitStrokesToBox": "Ограничить штрихи выделением", - "showCanvasDebugInfo": "Показать доп. информацию о холсте", - "clearCanvasHistory": "Очистить историю холста", - "clearHistory": "Очистить историю", - "clearCanvasHistoryMessage": "Очистка истории холста оставляет текущий холст нетронутым, но удаляет историю отмен и повторов.", - "clearCanvasHistoryConfirm": "Вы уверены, что хотите очистить историю холста?", - "activeLayer": "Активный слой", - "canvasScale": "Масштаб холста", - "boundingBox": "Ограничивающая рамка", - "scaledBoundingBox": "Масштабирование рамки", - "boundingBoxPosition": "Позиция ограничивающей рамки", - "canvasDimensions": "Размеры холста", - "canvasPosition": "Положение холста", - "cursorPosition": "Положение курсора", - "previous": "Предыдущее", - "next": "Следующее", - "accept": "Принять", - "discardAll": "Отменить все", - "antialiasing": "Не удалось скопировать ссылку на изображение", - "saveMask": "Сохранить $t(unifiedCanvas.mask)", - "showResultsOn": "Показывать результаты (вкл)", - "showResultsOff": "Показывать результаты (вЫкл)", - "coherenceModeStaged": "Постановка", - "coherenceModeGaussianBlur": "Размытие по Гауссу", - "coherenceModeBoxBlur": "коробчатое размытие", - "discardCurrent": "Отбросить текущее", - "invertBrushSizeScrollDirection": "Инвертировать прокрутку для размера кисти", - "initialFitImageSize": "Подогнать размер изображения при перебросе", - "hideBoundingBox": "Скрыть ограничительную рамку", - "showBoundingBox": "Показать ограничительную рамку" + "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": "Предыдущее изображение", - "showOptionsPanel": "Показать боковую панель", "invokeProgressBar": "Индикатор выполнения", "reset": "Сброс", "menu": "Меню", - "showGalleryPanel": "Показать панель галереи", "mode": "Режим", - "loadMore": "Загрузить больше", "resetUI": "$t(accessibility.reset) интерфейс", "createIssue": "Сообщить о проблеме", - "about": "Об этом", - "submitSupportTicket": "Отправить тикет в службу поддержки" + "about": "О программе", + "submitSupportTicket": "Отправить тикет в службу поддержки", + "toggleRightPanel": "Показать / скрыть правую панель (G)", + "toggleLeftPanel": "Показать / скрыть левую панель (T)", + "uploadImages": "Загрузить изображения" }, "nodes": { "zoomInNodes": "Увеличьте масштаб", "zoomOutNodes": "Уменьшите масштаб", "fitViewportNodes": "Уместить вид", - "showLegendNodes": "Показать тип поля", "hideMinimapnodes": "Скрыть миникарту", - "hideLegendNodes": "Скрыть тип поля", "showMinimapnodes": "Показать миникарту", "loadWorkflow": "Загрузить рабочий процесс", "reloadNodeTemplates": "Перезагрузить шаблоны узлов", "downloadWorkflow": "Скачать JSON рабочего процесса", "addNode": "Добавить узел", - "addLinearView": "Добавить в линейный вид", "animatedEdges": "Анимированные ребра", "animatedEdgesHelp": "Анимация выбранных ребер и ребер, соединенных с выбранными узлами", "boolean": "Логические значения", @@ -809,7 +804,6 @@ "workflowDescription": "Краткое описание", "inputFieldTypeParseError": "Невозможно разобрать тип поля ввода {{node}}.{{field}} ({{message}})", "unsupportedAnyOfLength": "слишком много элементов объединения ({{count}})", - "versionUnknown": " Версия неизвестна", "unsupportedArrayItemType": "неподдерживаемый тип элемента массива \"{{type}}\"", "noNodeSelected": "Узел не выбран", "unableToValidateWorkflow": "Невозможно проверить рабочий процесс", @@ -827,10 +821,8 @@ "nodeTemplate": "Шаблон узла", "nodeOpacity": "Непрозрачность узла", "sourceNodeDoesNotExist": "Недопустимое ребро: исходный/выходной узел {{node}} не существует", - "unableToLoadWorkflow": "Невозможно загрузить рабочий процесс", "unableToExtractEnumOptions": "невозможно извлечь параметры перечисления", "snapToGrid": "Привязка к сетке", - "noFieldsLinearview": "Нет полей, добавленных в линейный вид", "unableToParseFieldType": "невозможно проанализировать тип поля", "nodeSearch": "Поиск узлов", "updateNode": "Обновить узел", @@ -846,29 +838,23 @@ "ipAdapter": "IP-адаптер", "noConnectionInProgress": "Соединение не выполняется", "workflowVersion": "Версия", - "noConnectionData": "Нет данных о соединении", "fieldTypesMustMatch": "Типы полей должны совпадать", "workflow": "Рабочий процесс", "edge": "Край", "sourceNodeFieldDoesNotExist": "Неверный край: поле источника/вывода {{node}}.{{field}} не существует", "cannotDuplicateConnection": "Невозможно создать дубликаты соединений", - "unknownTemplate": "Неизвестный шаблон", "noWorkflow": "Нет рабочего процесса", - "removeLinearView": "Удалить из линейного вида", "workflowTags": "Теги", "fullyContainNodesHelp": "Чтобы узлы были выбраны, они должны полностью находиться в поле выбора", "unableToGetWorkflowVersion": "Не удалось получить версию схемы рабочего процесса", "workflowValidation": "Ошибка проверки рабочего процесса", "nodePack": "Пакет узлов", "nodeType": "Тип узла", - "noMatchingNodes": "Нет соответствующих узлов", "fullyContainNodes": "Выбор узлов с полным содержанием", "executionStateInProgress": "В процессе", "unableToExtractSchemaNameFromRef": "невозможно извлечь имя схемы из ссылки", - "noFieldType": "Нет типа поля", "executionStateError": "Ошибка", "prototypeDesc": "Этот вызов является прототипом. Он может претерпевать изменения при обновлении приложения и может быть удален в любой момент.", - "unknownOutput": "Неизвестный вывод: {{name}}", "executionStateCompleted": "Выполнено", "node": "Узел", "workflowAuthor": "Автор", @@ -890,7 +876,6 @@ "colorCodeEdges": "Ребра с цветовой кодировкой", "unknownNode": "Неизвестный узел", "targetNodeDoesNotExist": "Недопустимое ребро: целевой/входной узел {{node}} не существует", - "mismatchedVersion": "Недопустимый узел: узел {{node}} типа {{type}} имеет несоответствующую версию (попробовать обновить?)", "unknownFieldType": "$t(nodes.unknownField) тип: {{type}}", "collectionOrScalarFieldType": "{{name}} (Один или коллекция)", "betaDesc": "Этот вызов находится в бета-версии. Пока он не станет стабильным, в нем могут происходить изменения при обновлении приложений. Мы планируем поддерживать этот вызов в течение длительного времени.", @@ -899,14 +884,12 @@ "snapToGridHelp": "Привязка узлов к сетке при перемещении", "workflowSettings": "Настройки редактора рабочих процессов", "deletedInvalidEdge": "Удалено недопустимое ребро {{source}} -> {{target}}", - "unknownInput": "Неизвестный вход: {{name}}", "newWorkflow": "Новый рабочий процесс", "newWorkflowDesc": "Создать новый рабочий процесс?", "clearWorkflow": "Очистить рабочий процесс", "newWorkflowDesc2": "Текущий рабочий процесс имеет несохраненные изменения.", "clearWorkflowDesc": "Очистить этот рабочий процесс и создать новый?", "clearWorkflowDesc2": "Текущий рабочий процесс имеет несохраненные измерения.", - "reorderLinearView": "Изменить порядок линейного просмотра", "viewMode": "Использовать в линейном представлении", "editMode": "Открыть в редакторе узлов", "resetToDefaultValue": "Сбросить к стандартному значкнию", @@ -923,127 +906,76 @@ "noGraph": "Нет графика", "imageAccessError": "Невозможно найти изображение {{image_name}}, сбрасываем на значение по умолчанию", "boardAccessError": "Невозможно найти доску {{board_id}}, сбрасываем на значение по умолчанию", - "modelAccessError": "Невозможно найти модель {{key}}, сброс на модель по умолчанию" - }, - "controlnet": { - "amult": "a_mult", - "contentShuffleDescription": "Перетасовывает содержимое изображения", - "bgth": "bg_th", - "contentShuffle": "Перетасовка содержимого", - "beginEndStepPercent": "Процент начала/конца шага", - "duplicate": "Дублировать", - "balanced": "Сбалансированный", - "f": "F", - "depthMidasDescription": "Генерация карты глубины с использованием Midas", - "control": "Контроль", - "coarse": "Грубость обработки", - "crop": "Обрезка", - "depthMidas": "Глубина (Midas)", - "detectResolution": "Определить разрешение", - "controlMode": "Режим контроля", - "cannyDescription": "Детектор границ Canny", - "depthZoe": "Глубина (Zoe)", - "autoConfigure": "Автонастройка процессора", - "delete": "Удалить", - "canny": "Canny", - "depthZoeDescription": "Генерация карты глубины с использованием Zoe", - "resize": "Изменить размер", - "showAdvanced": "Показать расширенные", - "addT2IAdapter": "Добавить $t(common.t2iAdapter)", - "importImageFromCanvas": "Импортировать изображение с холста", - "lineartDescription": "Конвертация изображения в контурный рисунок", - "normalBae": "Обычный BAE", - "importMaskFromCanvas": "Импортировать маску с холста", - "hideAdvanced": "Скрыть расширенные", - "resetControlImage": "Сбросить контрольное изображение", - "prompt": "Запрос", - "controlnet": "$t(controlnet.controlAdapter_one) №{{number}} $t(common.controlNet)", - "resizeMode": "Режим изменения размера", - "weight": "Вес", - "selectModel": "Выберите модель", - "w": "В", - "processor": "Процессор", - "addControlNet": "Добавить $t(common.controlNet)", - "none": "ничего", - "ip_adapter": "$t(controlnet.controlAdapter_one) №{{number}} $t(common.ipAdapter)", - "pidiDescription": "PIDI-обработка изображений", - "mediapipeFace": "Лицо Mediapipe", - "fill": "Заполнить", - "addIPAdapter": "Добавить $t(common.ipAdapter)", - "lineart": "Контурный рисунок", - "colorMapDescription": "Создает карту цветов из изображения", - "lineartAnimeDescription": "Создание контурных рисунков в стиле аниме", - "t2i_adapter": "$t(controlnet.controlAdapter_one) №{{number}} $t(common.t2iAdapter)", - "minConfidence": "Минимальная уверенность", - "imageResolution": "Разрешение изображения", - "colorMap": "Цвет", - "lowThreshold": "Низкий порог", - "highThreshold": "Высокий порог", - "normalBaeDescription": "Обычная обработка BAE", - "noneDescription": "Обработка не применяется", - "saveControlImage": "Сохранить контрольное изображение", - "toggleControlNet": "Переключить эту ControlNet", - "controlAdapter_one": "Адаптер контроля", - "controlAdapter_few": "Адаптера контроля", - "controlAdapter_many": "Адаптеров контроля", - "safe": "Безопасный", - "colorMapTileSize": "Размер плитки", - "lineartAnime": "Контурный рисунок в стиле аниме", - "mediapipeFaceDescription": "Обнаружение лиц с помощью Mediapipe", - "hedDescription": "Целостное обнаружение границ", - "setControlImageDimensions": "Скопируйте размер в Ш/В (оптимизируйте для модели)", - "scribble": "Штрихи", - "maxFaces": "Макс Лица", - "mlsdDescription": "Минималистичный детектор отрезков линии", - "resizeSimple": "Изменить размер (простой)", - "megaControl": "Mega контроль", - "base": "Базовый", - "depthAnything": "Глубина всего", - "depthAnythingDescription": "Создание карты глубины с использованием метода Depth Anything", - "face": "Лицо", - "dwOpenposeDescription": "Оценка позы человека с помощью DW Openpose", - "large": "Большой", - "modelSize": "Размер модели", - "small": "Маленький", - "body": "Тело", - "hands": "Руки", - "selectCLIPVisionModel": "Выбрать модель CLIP Vision", - "ipAdapterMethod": "Метод", - "full": "Всё", - "mlsd": "M-LSD", - "h": "H", - "style": "Только стиль", - "dwOpenpose": "DW Openpose", - "pidi": "PIDI", - "composition": "Только композиция", - "hed": "HED", - "beginEndStepPercentShort": "Начало/конец %", - "setControlImageDimensionsForce": "Скопируйте размер в Ш/В (игнорируйте модель)" + "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": "Эта доска содержит изображения, используемые в следующих функциях:", + "autoAddBoard": "Коллекция для автодобавления", + "topMessage": "Этот выбор содержит изображения, используемые в следующих функциях:", "move": "Перемещение", - "menuItemAutoAdd": "Авто добавление на эту доску", - "myBoard": "Моя Доска", - "searchBoard": "Поиск Доски...", - "noMatching": "Нет подходящих Досок", - "selectBoard": "Выбрать Доску", + "menuItemAutoAdd": "Авто добавление в эту коллекцию", + "myBoard": "Моя коллекция", + "searchBoard": "Поиск коллекции...", + "noMatching": "Нет подходящих коллекций", + "selectBoard": "Выбрать коллекцию", "cancel": "Отменить", - "addBoard": "Добавить Доску", - "bottomMessage": "Удаление этой доски и ее изображений приведет к сбросу всех функций, использующихся их в данный момент.", + "addBoard": "Добавить коллекцию", + "bottomMessage": "Удаление изображений приведёт к сбросу всех функций, которые их используют.", "uncategorized": "Без категории", - "changeBoard": "Изменить Доску", + "changeBoard": "Сменить коллекцию", "loading": "Загрузка...", "clearSearch": "Очистить поиск", - "deleteBoardOnly": "Удалить только доску", - "movingImagesToBoard_one": "Перемещаем {{count}} изображение на доску:", - "movingImagesToBoard_few": "Перемещаем {{count}} изображения на доску:", - "movingImagesToBoard_many": "Перемещаем {{count}} изображений на доску:", - "downloadBoard": "Скачать доску", - "deleteBoard": "Удалить доску", - "deleteBoardAndImages": "Удалить доску и изображения", - "deletedBoardsCannotbeRestored": "Удаленные доски не подлежат восстановлению" + "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": { @@ -1055,9 +987,6 @@ }, "maxPrompts": "Максимум запросов", "promptsPreview": "Предпросмотр запросов", - "promptsWithCount_one": "{{count}} Запрос", - "promptsWithCount_few": "{{count}} Запроса", - "promptsWithCount_many": "{{count}} Запросов", "dynamicPrompts": "Динамические запросы", "loading": "Создание динамических запросов...", "showDynamicPrompts": "Показать динамические запросы" @@ -1142,19 +1071,19 @@ "controlNetResizeMode": { "heading": "Режим изменения размера", "paragraphs": [ - "Метод подгонки размера входного изображения Control Adaptor к размеру выходного изображения." + "Метод подгонки размера входного изображения Control Adapter под размер выходного изображения." ] }, "controlNetBeginEnd": { "paragraphs": [ - "Часть процесса шумоподавления, к которой будет применен адаптер контроля.", - "ControlNet, применяемые в начале процесса, направляют композицию, а ControlNet, применяемые в конце, направляют детали." + "Эта настройка определяет, на каком этапе денойзинга (генерации) используется влияние данного слоя.", + "• Начальный шаг (%): Определяет, с какого момента в процессе генерации начинает учитываться влияние данного слоя." ], "heading": "Процент начала/конца шага" }, "dynamicPromptsSeedBehaviour": { "paragraphs": [ - "Управляет использованием сида при создании запросов.", + "Определяет, как используется сид при генерации промптов.", "Для каждой итерации будет использоваться уникальный сид. Используйте это, чтобы изучить варианты запросов для одного сида.", "Например, если у вас 5 запросов, каждое изображение будет использовать один и то же сид.", "для каждого изображения будет использоваться уникальный сид. Это обеспечивает большую вариативность." @@ -1182,8 +1111,8 @@ }, "paramDenoisingStrength": { "paragraphs": [ - "Количество шума, добавляемого к входному изображению.", - "0 приведет к идентичному изображению, а 1 - к совершенно новому." + "Определяет, насколько сгенерированное изображение отличается от растрового слоя (слоёв).", + "Меньшее значение сохраняет больше сходства с объединёнными видимыми растровыми слоями. Большее значение усиливает влияние глобального промпта." ], "heading": "Шумоподавление" }, @@ -1222,7 +1151,7 @@ "controlNetWeight": { "heading": "Вес", "paragraphs": [ - "Вес адаптера управления. Более высокий вес приведет к большему воздействию на окончательное изображение." + "Определяет, насколько сильно слой влияет на процесс генерации." ] }, "controlNet": { @@ -1234,13 +1163,13 @@ "paramCFGScale": { "heading": "Шкала точности (CFG)", "paragraphs": [ - "Контролирует, насколько запрос влияет на процесс генерации.", + "Определяет, насколько сильно промпт влияет на процесс генерации.", "Высокие значения шкалы CFG могут привести к перенасыщению и искажению результатов генерации. " ] }, "controlNetControlMode": { "paragraphs": [ - "Придает больший вес либо запросу, либо ControlNet." + "Смещает приоритет в сторону промпта или ControlNet." ], "heading": "Режим управления" }, @@ -1292,7 +1221,7 @@ "refinerCfgScale": { "heading": "Шкала CFG", "paragraphs": [ - "Контролирует, насколько сильно запрос влияет на процесс генерации.", + "Определяет, насколько сильно промпт влияет на процесс генерации.", "Аналогично CFG шкале генерации." ] }, @@ -1401,12 +1330,54 @@ "ipAdapterMethod": { "heading": "Метод", "paragraphs": [ - "Метод, с помощью которого применяется текущий IP-адаптер." + "Метод определяет, как референсное изображение будет влиять на процесс генерации." + ] + }, + "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": { - "seamless": "Бесшовность", "positivePrompt": "Запрос", "negativePrompt": "Негативный запрос", "generationMode": "Режим генерации", @@ -1418,8 +1389,6 @@ "model": "Модель", "noImageDetails": "Детали изображения не найдены", "cfgScale": "Шкала точности", - "fit": "Соответствие изображения к изображению", - "initImage": "Исходное изображение", "recallParameters": "Вызов параметров", "height": "Высота", "noMetaData": "Метаданные не найдены", @@ -1432,10 +1401,10 @@ "noRecallParameters": "Параметры для вызова не найдены", "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)", "parameterSet": "Параметр {{parameter}} установлен", - "parsingFailed": "Не удалось выполнить синтаксический анализ", - "recallParameter": "Отозвать {{label}}", "allPrompts": "Все запросы", - "imageDimensions": "Размеры изображения" + "imageDimensions": "Размеры изображения", + "canvasV2Metadata": "Слои холста", + "guidance": "Точность" }, "queue": { "status": "Статус", @@ -1464,7 +1433,7 @@ "graphQueued": "График поставлен в очередь", "queue": "Очередь", "batch": "Пакет", - "clearQueueAlertDialog": "Очистка очереди немедленно отменяет все элементы обработки и полностью очищает очередь.", + "clearQueueAlertDialog": "Очистка очереди немедленно отменит все текущие задачи и очистит очередь. Ожидающие фильтры будут отменены, а область предпросмотра на холсте сброшена.", "pending": "В ожидании", "completedIn": "Завершено за", "resumeFailed": "Проблема с возобновлением рендеринга", @@ -1482,7 +1451,6 @@ "next": "Следующий", "cancelBatch": "Отменить пакет", "back": "задний", - "batchFieldValues": "Пакетные значения полей", "cancel": "Отмена", "session": "Сессия", "time": "Время", @@ -1503,24 +1471,28 @@ "iterations_many": "Итераций", "generations_one": "Генерация", "generations_few": "Генерации", - "generations_many": "Генераций" + "generations_many": "Генераций", + "other": "Другое", + "gallery": "Галерея", + "upscaling": "Увеличение", + "canvas": "Холст", + "generation": "Генерация", + "workflows": "Рабочие процессы", + "origin": "Источник", + "destination": "Назначение" }, "sdxl": { "refinerStart": "Запуск доработчика", "scheduler": "Планировщик", "cfgScale": "Шкала точности (CFG)", - "negStylePrompt": "Негативный запрос стиля", "noModelsAvailable": "Нет доступных моделей", "refiner": "Доработчик", "negAestheticScore": "Отрицательная эстетическая оценка", "denoisingStrength": "Шумоподавление", "refinermodel": "Дорабатывающая модель", "posAestheticScore": "Положительная эстетическая оценка", - "concatPromptStyle": "Связывание запроса и стиля", "loading": "Загрузка...", "steps": "Шаги", - "posStylePrompt": "Запрос стиля", - "freePromptStyle": "Ручной запрос стиля", "refinerSteps": "Шаги доработчика" }, "invocationCache": { @@ -1545,20 +1517,15 @@ "workflowEditorMenu": "Меню редактора рабочего процесса", "workflowName": "Имя рабочего процесса", "saveWorkflow": "Сохранить рабочий процесс", - "openWorkflow": "Открытый рабочий процесс", - "clearWorkflowSearchFilter": "Очистить фильтр поиска рабочих процессов", - "workflowLibrary": "Библиотека", + "workflowLibrary": "Библиотека схем генерации", "downloadWorkflow": "Сохранить в файл", "workflowSaved": "Рабочий процесс сохранен", "unnamedWorkflow": "Безымянный рабочий процесс", "savingWorkflow": "Сохранение рабочего процесса...", - "problemLoading": "Проблема с загрузкой рабочих процессов", "loading": "Загрузка рабочих процессов", - "searchWorkflows": "Поиск рабочих процессов", "problemSavingWorkflow": "Проблема с сохранением рабочего процесса", "deleteWorkflow": "Удалить рабочий процесс", "workflows": "Рабочие процессы", - "noDescription": "Без описания", "uploadWorkflow": "Загрузить из файла", "newWorkflowCreated": "Создан новый рабочий процесс", "saveWorkflowToProject": "Сохранить рабочий процесс в проект", @@ -1566,23 +1533,23 @@ "noWorkflows": "Нет рабочих процессов", "opened": "Открыто", "updated": "Обновлено", - "noUserWorkflows": "Нет рабочих процессов пользователя", "ascending": "Восходящий", "created": "Создано", "descending": "Спуск", - "userWorkflows": "Мои рабочие процессы", - "projectWorkflows": "Рабочие процессы проекта", - "defaultWorkflows": "Стандартные рабочие процессы", "name": "Имя", - "noRecentWorkflows": "Нет последних рабочих процессов", "loadWorkflow": "Рабочий процесс $t(common.load)", "convertGraph": "Конвертировать график", "loadFromGraph": "Загрузка рабочего процесса из графика", - "autoLayout": "Автоматическое расположение" + "autoLayout": "Автоматическое расположение", + "deleteWorkflow2": "Вы уверены, что хотите удалить этот рабочий процесс? Это нельзя отменить.", + "chooseWorkflowFromLibrary": "Выбрать рабочий процесс из библиотеки", + "edit": "Редактировать", + "download": "Скачать", + "copyShareLink": "Скопировать ссылку на общий доступ", + "copyShareLinkForWorkflow": "Скопировать ссылку на общий доступ для рабочего процесса", + "delete": "Удалить" }, "hrf": { - "enableHrf": "Включить исправление высокого разрешения", - "upscaleMethod": "Метод увеличения", "metadata": { "strength": "Сила исправления высокого разрешения", "enabled": "Исправление высокого разрешения включено", @@ -1592,21 +1559,15 @@ }, "models": { "noMatchingModels": "Нет подходящих моделей", - "esrganModel": "Модель ESRGAN", "loading": "загрузка", - "noMatchingLoRAs": "Нет подходящих LoRA", "noModelsAvailable": "Нет доступных моделей", "addLora": "Добавить LoRA", "selectModel": "Выберите модель", "noRefinerModelsInstalled": "Дорабатывающие модели SDXL не установлены", - "noLoRAsInstalled": "Нет установленных LoRA", "lora": "LoRA", "defaultVAE": "Стандартное VAE", "concepts": "LoRA" }, - "app": { - "storeNotInitialized": "Магазин не инициализирован" - }, "accordions": { "compositing": { "infillTab": "Заполнение", @@ -1636,50 +1597,519 @@ "moveToBack": "На задний план", "moveForward": "Переместить вперёд", "moveBackward": "Переместить назад", - "brushSize": "Размер кисти", - "controlLayers": "Слои управления", - "globalMaskOpacity": "Глобальная непрозрачность маски", "autoNegative": "Авто негатив", - "deletePrompt": "Удалить запрос", - "resetRegion": "Сбросить регион", - "debugLayers": "Слои отладки", "rectangle": "Прямоугольник", - "maskPreviewColor": "Цвет предпросмотра маски", - "addNegativePrompt": "Добавить $t(common.negativePrompt)", - "regionalGuidance": "Региональная точность", + "addNegativePrompt": "Добавить $t(controlLayers.negativePrompt)", + "regionalGuidance": "Региональное влияние", "opacity": "Непрозрачность", - "globalControlAdapter": "Глобальный $t(controlnet.controlAdapter_one)", - "globalControlAdapterLayer": "Глобальный $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", - "globalIPAdapter": "Глобальный $t(common.ipAdapter)", - "globalIPAdapterLayer": "Глобальный $t(common.ipAdapter) $t(unifiedCanvas.layer)", - "opacityFilter": "Фильтр непрозрачности", - "deleteAll": "Удалить всё", "addLayer": "Добавить слой", "moveToFront": "На передний план", - "addPositivePrompt": "Добавить $t(common.positivePrompt)", - "addIPAdapter": "Добавить $t(common.ipAdapter)", - "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", - "resetProcessor": "Сброс процессора по умолчанию", - "clearProcessor": "Чистый процессор", - "globalInitialImage": "Глобальное исходное изображение", - "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", - "noLayersAdded": "Без слоев", - "layers_one": "Слой", - "layers_few": "Слоя", - "layers_many": "Слоев" + "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": { - "generation": "Генерация", "canvas": "Холст", "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", "models": "Модели", - "generationTab": "$t(ui.tabs.generation) $t(common.tab)", "workflows": "Рабочие процессы", - "canvasTab": "$t(ui.tabs.canvas) $t(common.tab)", - "queueTab": "$t(ui.tabs.queue) $t(common.tab)", "modelsTab": "$t(ui.tabs.models) $t(common.tab)", - "queue": "Очередь" + "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 index 2c51027244a..512626f1e2f 100644 --- a/invokeai/frontend/web/public/locales/sv.json +++ b/invokeai/frontend/web/public/locales/sv.json @@ -4,8 +4,7 @@ "invokeProgressBar": "Invoke förloppsmätare", "nextImage": "Nästa bild", "reset": "Starta om", - "previousImage": "Föregående bild", - "showOptionsPanel": "Visa inställningspanelen" + "previousImage": "Föregående bild" }, "common": { "hotkeysLabel": "Snabbtangenter", @@ -13,7 +12,6 @@ "githubLabel": "Github", "discordLabel": "Discord", "settingsLabel": "Inställningar", - "unifiedCanvas": "Förenad kanvas", "upload": "Ladda upp", "cancel": "Avbryt", "accept": "Acceptera", @@ -29,139 +27,7 @@ }, "gallery": { "galleryImageSize": "Bildstorlek", - "loadMore": "Ladda mer", "gallerySettings": "Galleriinställningar", - "noImagesInGallery": "Inga bilder i galleriet", "autoSwitchNewImages": "Ändra automatiskt till nya bilder" - }, - "hotkeys": { - "generalHotkeys": "Allmänna snabbtangenter", - "galleryHotkeys": "Gallerisnabbtangenter", - "unifiedCanvasHotkeys": "Snabbtangenter för sammanslagskanvas", - "invoke": { - "title": "Anropa", - "desc": "Genererar en bild" - }, - "cancel": { - "title": "Avbryt", - "desc": "Avbryt bildgenerering" - }, - "focusPrompt": { - "desc": "Fokusera området för promptinmatning", - "title": "Fokusprompt" - }, - "pinOptions": { - "desc": "Nåla fast alternativpanelen", - "title": "Nåla fast alternativ" - }, - "toggleOptions": { - "title": "Växla inställningar", - "desc": "Öppna och stäng alternativpanelen" - }, - "toggleGallery": { - "title": "Växla galleri", - "desc": "Öppna eller stäng galleribyrån" - }, - "maximizeWorkSpace": { - "title": "Maximera arbetsyta", - "desc": "Stäng paneler och maximera arbetsyta" - }, - "changeTabs": { - "title": "Växla flik", - "desc": "Byt till en annan arbetsyta" - }, - "consoleToggle": { - "title": "Växla konsol", - "desc": "Öppna och stäng konsol" - }, - "setSeed": { - "desc": "Använd seed för nuvarande bild", - "title": "välj seed" - }, - "setParameters": { - "title": "Välj parametrar", - "desc": "Använd alla parametrar från nuvarande bild" - }, - "setPrompt": { - "desc": "Använd prompt för nuvarande bild", - "title": "Välj prompt" - }, - "restoreFaces": { - "title": "Återskapa ansikten", - "desc": "Återskapa nuvarande bild" - }, - "upscale": { - "title": "Skala upp", - "desc": "Skala upp nuvarande bild" - }, - "showInfo": { - "title": "Visa info", - "desc": "Visa metadata för nuvarande bild" - }, - "sendToImageToImage": { - "title": "Skicka till Bild till bild", - "desc": "Skicka nuvarande bild till Bild till bild" - }, - "deleteImage": { - "title": "Radera bild", - "desc": "Radera nuvarande bild" - }, - "closePanels": { - "title": "Stäng paneler", - "desc": "Stäng öppna paneler" - }, - "previousImage": { - "title": "Föregående bild", - "desc": "Visa föregående bild" - }, - "nextImage": { - "title": "Nästa bild", - "desc": "Visa nästa bild" - }, - "increaseGalleryThumbSize": { - "title": "Förstora galleriets bildstorlek", - "desc": "Förstora miniatyrbildernas storlek" - }, - "decreaseGalleryThumbSize": { - "title": "Minska gelleriets bildstorlek", - "desc": "Minska miniatyrbildernas storlek i galleriet" - }, - "decreaseBrushSize": { - "desc": "Förminska storleken på kanvas- pensel eller suddgummi", - "title": "Minska penselstorlek" - }, - "increaseBrushSize": { - "title": "Öka penselstorlek", - "desc": "Öka stoleken på kanvas- pensel eller suddgummi" - }, - "increaseBrushOpacity": { - "title": "Öka penselns opacitet", - "desc": "Öka opaciteten för kanvaspensel" - }, - "decreaseBrushOpacity": { - "desc": "Minska kanvaspenselns opacitet", - "title": "Minska penselns opacitet" - }, - "moveTool": { - "title": "Flytta", - "desc": "Tillåt kanvasnavigation" - }, - "fillBoundingBox": { - "title": "Fyll ram", - "desc": "Fyller ramen med pensels färg" - }, - "keyboardShortcuts": "Snabbtangenter", - "appHotkeys": "Appsnabbtangenter", - "selectBrush": { - "desc": "Välj kanvaspensel", - "title": "Välj pensel" - }, - "selectEraser": { - "desc": "Välj kanvassuddgummi", - "title": "Välj suddgummi" - }, - "eraseBoundingBox": { - "title": "Ta bort ram" - } } } diff --git a/invokeai/frontend/web/public/locales/tr.json b/invokeai/frontend/web/public/locales/tr.json index 415bd2d7443..a18da1dca59 100644 --- a/invokeai/frontend/web/public/locales/tr.json +++ b/invokeai/frontend/web/public/locales/tr.json @@ -2,7 +2,6 @@ "accessibility": { "invokeProgressBar": "Invoke durum çubuğu", "nextImage": "Sonraki Görsel", - "showOptionsPanel": "Yan Paneli Göster", "reset": "Resetle", "uploadImage": "Görsel Yükle", "previousImage": "Önceki Görsel", @@ -10,8 +9,6 @@ "about": "Hakkında", "mode": "Kip", "resetUI": "$t(accessibility.reset)Arayüz", - "showGalleryPanel": "Galeri Panelini Göster", - "loadMore": "Daha Getir", "createIssue": "Sorun Bildir" }, "common": { @@ -26,7 +23,6 @@ "linear": "Doğrusal", "nodes": "İş Akışı Düzenleyici", "postprocessing": "Rötuş", - "unifiedCanvas": "Tuval", "batch": "Toplu İş Yöneticisi", "accept": "Onayla", "cancel": "Vazgeç", @@ -40,19 +36,16 @@ "communityLabel": "Topluluk", "back": "Geri", "areYouSure": "Emin misiniz?", - "notInstalled": "$t(common.installed) Değil", "openInNewTab": "Yeni Sekmede Aç", "aboutHeading": "Yaratıcı Gücünüzün Sahibi Olun", "load": "Yükle", "loading": "Yükleniyor", - "localSystem": "Yerel Sistem", "inpaint": "içboyama", "modelManager": "Model Yöneticisi", "orderBy": "Sırala", "outpaint": "dışboyama", "outputs": "Çıktılar", "learnMore": "Bilgi Edin", - "nodeEditor": "Çizge Düzenleyici", "save": "Kaydet", "random": "Rastgele", "simple": "Basit", @@ -70,11 +63,8 @@ "format": "biçim", "details": "Ayrıntılar", "error": "Hata", - "imageFailedToLoad": "Görsel Yüklenemedi", "safetensors": "Safetensors", "upload": "Yükle", - "nextPage": "Sonraki Sayfa", - "prevPage": "Önceki Sayfa", "dontAskMeAgain": "Bir daha sorma", "delete": "Kaldır", "direction": "Yön", @@ -132,85 +122,6 @@ "changeBoard": "Panoyu Değiştir", "bottomMessage": "Bu panoyu ve görselleri silmek, bunları kullanan özelliklerin resetlemesine neden olacaktır." }, - "controlnet": { - "balanced": "Dengeli", - "contentShuffle": "İçerik Karıştırma", - "contentShuffleDescription": "Görselin içeriğini karıştırır", - "depthZoe": "Derinlik (Zoe)", - "depthZoeDescription": "Zoe kullanarak derinlik haritası oluşturma", - "resizeMode": "Boyutlandırma Kipi", - "addControlNet": "$t(common.controlNet) Ekle", - "addIPAdapter": "$t(common.ipAdapter) Ekle", - "addT2IAdapter": "$t(common.t2iAdapter) Ekle", - "colorMap": "Renk", - "crop": "Kırpma", - "delete": "Kaldır", - "depthMidas": "Derinlik (Midas)", - "depthMidasDescription": "Midas kullanarak derinlik haritası oluşturma", - "detectResolution": "Çözünürlüğü Bul", - "none": "Hiçbiri", - "noneDescription": "Hiçbir işlem uygulanmamış", - "selectModel": "Model seçin", - "showAdvanced": "Gelişmiş Seçenekleri Göster", - "canny": "Canny", - "colorMapDescription": "Görselden renk haritası oluşturur", - "processor": "İşlemci", - "prompt": "İstem", - "duplicate": "Kopyala", - "large": "Büyük", - "modelSize": "Model Boyutu", - "resize": "Boyutlandır", - "resizeSimple": "Boyutlandır (Basit)", - "safe": "Güvenli", - "small": "Küçük", - "weight": "Etki", - "cannyDescription": "Canny kenar algılama", - "fill": "Doldur", - "highThreshold": "Üst Eşik", - "imageResolution": "Görsel Çözünürlüğü", - "colorMapTileSize": "Döşeme Boyutu", - "importImageFromCanvas": "Tuvaldeki Görseli Al", - "importMaskFromCanvas": "Tuvalden Maskeyi İçe Aktar", - "lowThreshold": "Alt Eşik", - "base": "Taban", - "depthAnythingDescription": "Depth Anything yöntemi ile derinlik haritası oluşturma", - "controlAdapter_one": "Yönetim Aracı", - "controlAdapter_other": "Yönetim Aracı", - "beginEndStepPercent": "Başlangıç / Bitiş Yüzdesi", - "control": "Yönetim", - "controlnet": "{{number}}. $t(controlnet.controlAdapter_one) ($t(common.controlNet))", - "amult": "a_mult", - "hideAdvanced": "Gelişmiş Seçenekleri Gizle", - "hed": "HED", - "ip_adapter": "{{number}}. $t(controlnet.controlAdapter_one) ($t(common.ipAdapter))", - "t2i_adapter": "{{number}}. $t(controlnet.controlAdapter_one) ($t(common.t2iAdapter))", - "autoConfigure": "Aracı otomatik yapılandır", - "bgth": "bg_th", - "coarse": "İri", - "controlMode": "Yönetim Kipi", - "depthAnything": "Depth Anything", - "hedDescription": "Holistically-Nested Edge Detection (Bütünsel Yuvalanmış Kenar Tespiti)", - "lineartAnime": "Çizim (Anime)", - "lineartAnimeDescription": "Anime stili çizim işleme", - "minConfidence": "Özgüven Alt Limiti", - "pidiDescription": "PIDI görsel işleme", - "mediapipeFace": "Mediapipe Yüz", - "megaControl": "Aşırı Yönetim", - "mlsd": "M-LSD", - "setControlImageDimensions": "Yönetim Görseli Boyutlarını En/Boydan Al", - "pidi": "PIDI", - "scribble": "çiziktirme", - "mediapipeFaceDescription": "Mediapipe kullanarak yüz algılama", - "saveControlImage": "Yönetim Görselini Kaydet", - "w": "En", - "lineartDescription": "Görseli çizime dönüştürür", - "maxFaces": "Yüz Üst Limiti", - "mlsdDescription": "Minimalist Line Segment Detector (Kolay Çizgi Parçası Algılama)", - "normalBae": "Normal BAE", - "normalBaeDescription": "Normal BAE işleme", - "resetControlImage": "Yönetim Görselini Kaldır", - "lineart": "Çizim" - }, "queue": { "resumeSucceeded": "İşlem Sürdürüldü", "openQueue": "Sırayı Göster", @@ -265,7 +176,6 @@ "session": "Oturum", "batchQueued": "Toplu İş Sıraya Alındı", "notReady": "Sıraya Alınamadı", - "batchFieldValues": "Toplu İş Değişkenleri", "graphFailedToQueue": "Çizge sıraya alınamadı", "graphQueued": "Çizge sıraya alındı" }, @@ -278,31 +188,23 @@ "enable": "Aç" }, "gallery": { - "deleteImageBin": "Silinen görseller işletim sisteminin çöp kutusuna gönderilir.", "deleteImagePermanent": "Silinen görseller geri getirilemez.", - "assets": "Özkaynaklar", "autoAssignBoardOnClick": "Tıklanan Panoya Otomatik Atama", "loading": "Yükleniyor", "starImage": "Yıldız Koy", "download": "İndir", "deleteSelection": "Seçileni Sil", - "problemDeletingImages": "Görsel Silmede Sorun", "featuresWillReset": "Bu görseli silerseniz, o özellikler resetlenecektir.", "noImageSelected": "Görsel Seçilmedi", "unstarImage": "Yıldızı Kaldır", - "problemDeletingImagesDesc": "Bir ya da daha çok görsel silinemedi", "gallerySettings": "Galeri Düzeni", "image": "görsel", "galleryImageSize": "Görsel Boyutu", "copy": "Kopyala", - "noImagesInGallery": "Gösterilecek Görsel Yok", "autoSwitchNewImages": "Yeni Görseli Biter Bitmez Gör", "currentlyInUse": "Bu görsel şurada kullanımda:", "deleteImage_one": "Görseli Sil", "deleteImage_other": "", - "loadMore": "Daha Getir", - "setCurrentImage": "Çalışma Görseli Yap", - "unableToLoad": "Galeri Yüklenemedi", "downloadSelection": "Seçileni İndir", "dropOrUpload": "$t(gallery.drop) ya da Yükle", "dropToUpload": "Yüklemek için $t(gallery.drop)", @@ -310,240 +212,16 @@ }, "hrf": { "hrf": "Yüksek Çözünürlük Kürü", - "enableHrf": "Yüksek Çözünürlük Kürünü Aç", "metadata": { "enabled": "Yüksek Çözünürlük Kürü Açık", "strength": "Yüksek Çözünürlük Kürü Etkisi", "method": "Yüksek Çözünürlük Kürü Yöntemi" - }, - "upscaleMethod": "Büyütme Yöntemi" + } }, "hotkeys": { "noHotkeysFound": "Kısayol Tuşu Bulanamadı", "searchHotkeys": "Kısayol Tuşlarında Ara", - "clearSearch": "Aramayı Sil", - "colorPicker": { - "title": "Renk Seçici", - "desc": "Tuvalde renk seçiciye geçer" - }, - "consoleToggle": { - "title": "Konsolu Aç-Kapat", - "desc": "Konsolu aç-kapat" - }, - "hideMask": { - "desc": "Maskeyi gizle-göster", - "title": "Maskeyi Gizle" - }, - "focusPrompt": { - "title": "İsteme Odaklan", - "desc": "Görsel istemi alanına odaklanır" - }, - "keyboardShortcuts": "Kısayol Tuşları", - "nextImage": { - "title": "Sonraki Görsel", - "desc": "Galerideki sonraki görseli göster" - }, - "maximizeWorkSpace": { - "desc": "Panelleri kapatıp çalışma alanını genişlet", - "title": "Çalışma Alanını Genişlet" - }, - "pinOptions": { - "desc": "Seçenekler panelini iğnele", - "title": "Seçenekleri İğnele" - }, - "nodesHotkeys": "Çizgeler", - "quickToggleMove": { - "desc": "Geçici olarak Kayma Aracına geçer", - "title": "Geçici Kayma" - }, - "showHideBoundingBox": { - "title": "Sınırlayıcı Kutuyu Gizle/Göster", - "desc": "Sınırlayıcı kutunun görünürlüğünü değiştir" - }, - "showInfo": { - "desc": "Seçili görselin üstverisini göster", - "title": "Bilgileri Göster" - }, - "nextStagingImage": { - "desc": "Sonraki Görsel Parçayı Göster", - "title": "Sonraki Görsel Parça" - }, - "acceptStagingImage": { - "desc": "Geçiçi Görsel Parçasını Onayla", - "title": "Geçiçi Görsel Parçasını Onayla" - }, - "changeTabs": { - "desc": "Çalışma alanını değiştir", - "title": "Sekmeyi değiştir" - }, - "closePanels": { - "title": "Panelleri Kapat", - "desc": "Açık panelleri kapat" - }, - "decreaseBrushOpacity": { - "title": "Fırça Saydamlığını Artır", - "desc": "Tuval fırçasının saydamlığını artırır" - }, - "clearMask": { - "title": "Maskeyi Sil", - "desc": "Tüm maskeyi sil" - }, - "decreaseGalleryThumbSize": { - "desc": "Galerideki küçük görsel boyutunu düşürür", - "title": "Küçük Görsel Boyutunu Düşür" - }, - "deleteImage": { - "desc": "Seçili görseli sil", - "title": "Görseli Sil" - }, - "invoke": { - "desc": "Görsel Oluştur", - "title": "Invoke" - }, - "increaseGalleryThumbSize": { - "title": "Küçük Görsel Boyutunu Artır", - "desc": "Galerideki küçük görsel boyutunu artırır" - }, - "setParameters": { - "title": "Değişkenleri Kullan", - "desc": "Seçili görselin tüm değişkenlerini kullan" - }, - "setPrompt": { - "desc": "Seçili görselin istemini kullan", - "title": "İstemi Kullan" - }, - "toggleLayer": { - "desc": "Maske/Taban katmanları arasında geçiş yapar", - "title": "Katmanı Gizle-Göster" - }, - "upscale": { - "title": "Büyüt", - "desc": "Seçili görseli büyüt" - }, - "setSeed": { - "title": "Tohumu Kullan", - "desc": "Seçili görselin tohumunu kullan" - }, - "appHotkeys": "Uygulama", - "cancel": { - "desc": "Geçerli İşi Sil", - "title": "Vazgeç" - }, - "sendToImageToImage": { - "title": "Görselden Görsel'e Gönder", - "desc": "Seçili görseli Görselden Görsel'e gönder" - }, - "fillBoundingBox": { - "title": "Sınırlayıcı Kutuyu Doldur", - "desc": "Sınırlayıcı kutuyu fırçadaki renkle doldurur" - }, - "moveTool": { - "desc": "Tuvalde kaymayı sağlar", - "title": "Kayma Aracı" - }, - "redoStroke": { - "desc": "Fırça vuruşunu yinele", - "title": "Vuruşu Yinele" - }, - "increaseBrushOpacity": { - "title": "Fırçanın Saydamlığını Düşür", - "desc": "Tuval fırçasının saydamlığını düşürür" - }, - "selectEraser": { - "desc": "Tuval silgisini kullan", - "title": "Silgiyi Kullan" - }, - "toggleOptions": { - "desc": "Seçenekler panelini aç-kapat", - "title": "Seçenekleri Aç-Kapat" - }, - "copyToClipboard": { - "desc": "Tuval içeriğini kopyala", - "title": "Kopyala" - }, - "galleryHotkeys": "Galeri", - "generalHotkeys": "Genel", - "mergeVisible": { - "desc": "Tuvalin görünür tüm katmanlarını birleştir", - "title": "Katmanları Birleştir" - }, - "toggleGallery": { - "title": "Galeriyi Aç-Kapat", - "desc": "Galeri panelini aç-kapat" - }, - "downloadImage": { - "title": "Görseli İndir", - "desc": "Tuval içeriğini indir" - }, - "previousStagingImage": { - "title": "Önceki Görsel Parça", - "desc": "Önceki Görsel Parçayı Göster" - }, - "increaseBrushSize": { - "title": "Fırça Boyutunu Artır", - "desc": "Tuval fırçasının/silgisinin boyutunu artırır" - }, - "previousImage": { - "desc": "Galerideki önceki görseli göster", - "title": "Önceki Görsel" - }, - "toggleOptionsAndGallery": { - "title": "Seçenekleri ve Galeriyi Aç-Kapat", - "desc": "Seçenekler ve galeri panellerini aç-kapat" - }, - "toggleSnap": { - "desc": "Kılavuza Uydur", - "title": "Kılavuza Uydur" - }, - "resetView": { - "desc": "Tuval Görüşünü Resetle", - "title": "Görüşü Resetle" - }, - "cancelAndClear": { - "desc": "Geçerli işi geri çek ve sıradaki tüm işleri sil", - "title": "Vazgeç ve Sil" - }, - "decreaseBrushSize": { - "title": "Fırça Boyutunu Düşür", - "desc": "Tuval fırçasının/silgisinin boyutunu düşürür" - }, - "resetOptionsAndGallery": { - "desc": "Seçenekler ve galeri panellerini resetler", - "title": "Seçenekleri ve Galeriyi Resetle" - }, - "remixImage": { - "desc": "Seçili görselin tohumu hariç tüm değişkenlerini kullan", - "title": "Benzerini Türet" - }, - "undoStroke": { - "title": "Vuruşu Geri Al", - "desc": "Fırça vuruşunu geri al" - }, - "saveToGallery": { - "title": "Galeriye Gönder", - "desc": "Tuval içeriğini galeriye gönder" - }, - "unifiedCanvasHotkeys": "Tuval", - "addNodes": { - "desc": "Çizge ekleme menüsünü açar", - "title": "Çizge Ekle" - }, - "eraseBoundingBox": { - "desc": "Sınırlayıcı kutunun içini boşaltır", - "title": "Sınırlayıcı Kutuyu Boşalt" - }, - "selectBrush": { - "desc": "Tuval fırçasını kullan", - "title": "Fırçayı Kullan" - }, - "restoreFaces": { - "title": "Yüzleri Onar", - "desc": "Geçerli görseli onar" - } - }, - "unifiedCanvas": { - "accept": "Onayla", - "clearCanvasHistoryMessage": "Tuval geçmişini silmek tuvale dokunmaz, ancak yineleme ve geri alma geçmişini geri dönülemez bir biçimde siler." + "clearSearch": "Aramayı Sil" }, "nodes": { "unableToValidateWorkflow": "İş Akışı Doğrulanamadı", @@ -568,7 +246,6 @@ "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.", - "unableToLoadWorkflow": "İş Akışı Yüklenemedi", "cannotConnectInputToInput": "Giriş girişe bağlanamaz", "zoomInNodes": "Yakınlaştır", "boolean": "Boole Değeri", @@ -579,16 +256,12 @@ "cannotDuplicateConnection": "Kopya bağlantılar yaratılamaz" }, "workflows": { - "searchWorkflows": "İş Akışlarında Ara", "workflowName": "İş Akışı Adı", "problemSavingWorkflow": "İş Akışını Kaydetmede Sorun", "saveWorkflow": "İş Akışını Kaydet", "uploadWorkflow": "Dosyadan Yükle", "newWorkflowCreated": "Yeni İş Akışı Yaratıldı", - "problemLoading": "İş Akışlarını Yüklemede Sorun", "loading": "İş Akışları Yükleniyor", - "noDescription": "Tanımsız", - "clearWorkflowSearchFilter": "İş Akışı Aramasını Resetle", "workflowEditorMenu": "İş Akışı Düzenleyici Menüsü", "downloadWorkflow": "İndir", "saveWorkflowAs": "İş Akışını Farklı Kaydet", @@ -601,15 +274,9 @@ "workflowSaved": "İş Akışı Kaydedildi" }, "toast": { - "problemDownloadingCanvasDesc": "Taban katman indirilemedi", - "problemSavingMaskDesc": "Maske kaydedilemedi", - "problemSavingCanvasDesc": "Taban katman kaydedilemedi", "problemRetrievingWorkflow": "İş Akışını Getirmede Sorun", "workflowDeleted": "İş Akışı Silindi", "loadedWithWarnings": "İş Akışı Yüklendi Ancak Uyarılar Var", - "problemImportingMaskDesc": "Maske aktarılamadı", - "problemMergingCanvasDesc": "Taban katman aktarılamadı", - "problemCopyingCanvasDesc": "Taban katman aktarılamadı", "workflowLoaded": "İş Akışı Yüklendi", "problemDeletingWorkflow": "İş Akışını Silmede Sorun" }, @@ -617,7 +284,6 @@ "invoke": { "noPrompts": "İstem oluşturulmadı", "noModelSelected": "Model seçilmedi", - "incompatibleBaseModelForControlAdapter": "{{number}}. yönetim aracı, ana model ile uyumlu değil.", "systemDisconnected": "Sistem bağlantısı kesildi", "invoke": "Invoke" }, @@ -626,10 +292,6 @@ "controlNetControlMode": "Yönetim Kipi", "general": "Genel", "seamlessYAxis": "Dikişsiz Döşeme Y Ekseni", - "isAllowedToUpscale": { - "tooLarge": "Görsel, büyütme işlemi için çok büyük, daha küçük bir boyut seçin", - "useX2Model": "Görsel 4 kat büyütme işlemi için çok geniş, 2 kat büyütmeyi kullanın" - }, "maskBlur": "Bulandırma", "images": "Görseller", "info": "Bilgi", @@ -641,7 +303,6 @@ "copyImage": "Görseli Kopyala", "height": "Boy", "width": "En", - "upscale": "Büyüt (Shift + U)", "useSize": "Boyutu Kullan", "symmetry": "Bakışım", "tileSize": "Döşeme Boyutu", @@ -652,12 +313,8 @@ "noiseThreshold": "Gürültü Eşiği", "seed": "Tohum", "imageActions": "Görsel İşlemleri", - "sendToImg2Img": "Görselden Görsele Aktar", - "sendToUnifiedCanvas": "Tuvale Aktar", - "showOptionsPanel": "Yan Paneli Göster (O ya da T)", "shuffle": "Kar", "usePrompt": "İstemi Kullan", - "upscaleImage": "Görseli Büyüt", "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (çok küçük olabilir)", "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (çok büyük olabilir)", "cfgRescaleMultiplier": "CFG Rescale Çarpanı", @@ -673,7 +330,6 @@ "perlinNoise": "Perlin Gürültüsü", "scaledWidth": "Ölçekli En", "seamlessXAxis": "Dikişsiz Döşeme X Ekseni", - "downloadImage": "Görseli İndir", "type": "Tür" }, "modelManager": { @@ -691,9 +347,7 @@ "vaePrecision": "VAE Kesinliği", "convertToDiffusersHelpText6": "Bu modeli dönüştürmek istiyor musunuz?", "deleteMsg1": "Bu modeli InvokeAI'dan silmek istediğinize emin misiniz?", - "modelSyncFailed": "Modeller Senkronize Edilemedi", "settings": "Seçenekler", - "v2_768": "v2 (768pks)", "vae": "VAE", "width": "En", "delete": "Sil", @@ -716,30 +370,21 @@ "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ı", - "modelsSynced": "Modeller Senkronize Edildi", "noModelSelected": "Model Seçilmedi", "predictionType": "Saptama Türü", "selectModel": "Model Seç", - "v2_base": "v2 (512pks)", "modelConversionFailed": "Model Dönüşümü Başarısız", "modelConverted": "Model Dönüştürüldü", "description": "Tanım" }, - "dynamicPrompts": { - "promptsWithCount_one": "{{count}} İstem", - "promptsWithCount_other": "{{count}} İstem" - }, "models": { "addLora": "LoRA Ekle", - "esrganModel": "ESRGAN Modeli", "defaultVAE": "Varsayılan VAE", "lora": "LoRA", "noModelsAvailable": "Model yok", - "noMatchingLoRAs": "Uygun LoRA Yok", "noMatchingModels": "Uygun Model Yok", "loading": "yükleniyor", - "selectModel": "Model Seçin", - "noLoRAsInstalled": "LoRA Yok" + "selectModel": "Model Seçin" }, "settings": { "generation": "Oluşturma" @@ -747,7 +392,6 @@ "sdxl": { "cfgScale": "CFG Ölçeği", "loading": "Yükleniyor...", - "denoisingStrength": "Arındırma Ölçüsü", - "concatPromptStyle": "İstem ve Stili Bitiştir" + "denoisingStrength": "Arındırma Ölçüsü" } } diff --git a/invokeai/frontend/web/public/locales/uk.json b/invokeai/frontend/web/public/locales/uk.json index 9bb38c21b3e..9d530068c2e 100644 --- a/invokeai/frontend/web/public/locales/uk.json +++ b/invokeai/frontend/web/public/locales/uk.json @@ -5,7 +5,6 @@ "reportBugLabel": "Повідомити про помилку", "settingsLabel": "Налаштування", "img2img": "Зображення із зображення (img2img)", - "unifiedCanvas": "Універсальне полотно", "nodes": "Вузли", "upload": "Завантажити", "load": "Завантажити", @@ -23,208 +22,7 @@ "gallery": { "galleryImageSize": "Розмір зображень", "gallerySettings": "Налаштування галереї", - "autoSwitchNewImages": "Автоматично вибирати нові", - "loadMore": "Завантажити більше", - "noImagesInGallery": "Зображень немає" - }, - "hotkeys": { - "keyboardShortcuts": "Клавіатурні скорочення", - "appHotkeys": "Гарячі клавіші програми", - "generalHotkeys": "Загальні гарячі клавіші", - "galleryHotkeys": "Гарячі клавіші галереї", - "unifiedCanvasHotkeys": "Гарячі клавіші універсального полотна", - "invoke": { - "title": "Invoke", - "desc": "Згенерувати зображення" - }, - "cancel": { - "title": "Скасувати", - "desc": "Скасувати генерацію зображення" - }, - "focusPrompt": { - "title": "Переключитися на введення запиту", - "desc": "Перемикання на область введення запиту" - }, - "toggleOptions": { - "title": "Показати/приховати параметри", - "desc": "Відкривати і закривати панель параметрів" - }, - "pinOptions": { - "title": "Закріпити параметри", - "desc": "Закріпити панель параметрів" - }, - "toggleGallery": { - "title": "Показати галерею", - "desc": "Відкривати і закривати скриньку галереї" - }, - "maximizeWorkSpace": { - "title": "Максимізувати робочий простір", - "desc": "Приховати панелі і максимізувати робочу область" - }, - "changeTabs": { - "title": "Переключити вкладку", - "desc": "Переключитися на іншу робочу область" - }, - "consoleToggle": { - "title": "Показати консоль", - "desc": "Відкривати і закривати консоль" - }, - "setPrompt": { - "title": "Використовувати запит", - "desc": "Використати запит із поточного зображення" - }, - "setSeed": { - "title": "Використовувати сід", - "desc": "Використовувати сід поточного зображення" - }, - "setParameters": { - "title": "Використовувати всі параметри", - "desc": "Використовувати всі параметри поточного зображення" - }, - "restoreFaces": { - "title": "Відновити обличчя", - "desc": "Відновити обличчя на поточному зображенні" - }, - "upscale": { - "title": "Збільшення", - "desc": "Збільшити поточне зображення" - }, - "showInfo": { - "title": "Показати метадані", - "desc": "Показати метадані з поточного зображення" - }, - "sendToImageToImage": { - "title": "Відправити в img2img", - "desc": "Надіслати поточне зображення в Image To Image" - }, - "deleteImage": { - "title": "Видалити зображення", - "desc": "Видалити поточне зображення" - }, - "closePanels": { - "title": "Закрити панелі", - "desc": "Закриває відкриті панелі" - }, - "previousImage": { - "title": "Попереднє зображення", - "desc": "Відображати попереднє зображення в галереї" - }, - "nextImage": { - "title": "Наступне зображення", - "desc": "Відображення наступного зображення в галереї" - }, - "increaseGalleryThumbSize": { - "title": "Збільшити розмір мініатюр галереї", - "desc": "Збільшує розмір мініатюр галереї" - }, - "decreaseGalleryThumbSize": { - "title": "Зменшує розмір мініатюр галереї", - "desc": "Зменшує розмір мініатюр галереї" - }, - "selectBrush": { - "title": "Вибрати пензель", - "desc": "Вибирає пензель для полотна" - }, - "selectEraser": { - "title": "Вибрати ластик", - "desc": "Вибирає ластик для полотна" - }, - "decreaseBrushSize": { - "title": "Зменшити розмір пензля", - "desc": "Зменшує розмір пензля/ластика полотна" - }, - "increaseBrushSize": { - "title": "Збільшити розмір пензля", - "desc": "Збільшує розмір пензля/ластика полотна" - }, - "decreaseBrushOpacity": { - "title": "Зменшити непрозорість пензля", - "desc": "Зменшує непрозорість пензля полотна" - }, - "increaseBrushOpacity": { - "title": "Збільшити непрозорість пензля", - "desc": "Збільшує непрозорість пензля полотна" - }, - "moveTool": { - "title": "Інструмент переміщення", - "desc": "Дозволяє переміщатися по полотну" - }, - "fillBoundingBox": { - "title": "Заповнити обмежувальну рамку", - "desc": "Заповнює обмежувальну рамку кольором пензля" - }, - "eraseBoundingBox": { - "title": "Стерти обмежувальну рамку", - "desc": "Стирає область обмежувальної рамки" - }, - "colorPicker": { - "title": "Вибрати колір", - "desc": "Вибирає засіб вибору кольору полотна" - }, - "toggleSnap": { - "title": "Увімкнути прив'язку", - "desc": "Вмикає/вимикає прив'язку до сітки" - }, - "quickToggleMove": { - "title": "Швидке перемикання переміщення", - "desc": "Тимчасово перемикає режим переміщення" - }, - "toggleLayer": { - "title": "Переключити шар", - "desc": "Перемикання маски/базового шару" - }, - "clearMask": { - "title": "Очистити маску", - "desc": "Очистити всю маску" - }, - "hideMask": { - "title": "Приховати маску", - "desc": "Приховує/показує маску" - }, - "showHideBoundingBox": { - "title": "Показати/приховати обмежувальну рамку", - "desc": "Переключити видимість обмежувальної рамки" - }, - "mergeVisible": { - "title": "Об'єднати видимі", - "desc": "Об'єднати всі видимі шари полотна" - }, - "saveToGallery": { - "title": "Зберегти в галерею", - "desc": "Зберегти поточне полотно в галерею" - }, - "copyToClipboard": { - "title": "Копіювати в буфер обміну", - "desc": "Копіювати поточне полотно в буфер обміну" - }, - "downloadImage": { - "title": "Завантажити зображення", - "desc": "Завантажити вміст полотна" - }, - "undoStroke": { - "title": "Скасувати пензель", - "desc": "Скасувати мазок пензля" - }, - "redoStroke": { - "title": "Повторити мазок пензля", - "desc": "Повторити мазок пензля" - }, - "resetView": { - "title": "Вид за замовчуванням", - "desc": "Скинути вид полотна" - }, - "previousStagingImage": { - "title": "Попереднє зображення", - "desc": "Попереднє зображення" - }, - "nextStagingImage": { - "title": "Наступне зображення", - "desc": "Наступне зображення" - }, - "acceptStagingImage": { - "title": "Прийняти зображення", - "desc": "Прийняти поточне зображення" - } + "autoSwitchNewImages": "Автоматично вибирати нові" }, "modelManager": { "modelManager": "Менеджер моделей", @@ -253,8 +51,6 @@ "convertToDiffusersHelpText3": "Файл моделі на диску НЕ буде видалено або змінено. Ви можете знову додати його в Model Manager, якщо потрібно.", "alpha": "Альфа", "repo_id": "ID репозиторію", - "v2_base": "v2 (512px)", - "v2_768": "v2 (768px)", "convertToDiffusersHelpText5": "Переконайтеся, що у вас достатньо місця на диску. Моделі зазвичай займають від 4 до 7 Гб.", "convertToDiffusersHelpText6": "Ви хочете перетворити цю модель?", "modelConverted": "Модель перетворено", @@ -276,8 +72,6 @@ "type": "Тип", "strength": "Сила", "upscaling": "Збільшення", - "upscale": "Збільшити", - "upscaleImage": "Збільшити зображення", "scale": "Масштаб", "imageFit": "Вмістити зображення", "scaleBeforeProcessing": "Масштабувати", @@ -285,14 +79,10 @@ "scaledHeight": "Масштаб В", "infillMethod": "Засіб заповнення", "tileSize": "Розмір області", - "sendToImg2Img": "Надіслати у img2img", - "sendToUnifiedCanvas": "Надіслати на полотно", - "downloadImage": "Завантажити", "usePrompt": "Використати запит", "useSeed": "Використати сід", "useAll": "Використати все", "info": "Метадані", - "showOptionsPanel": "Показати панель налаштувань", "general": "Основне", "denoisingStrength": "Сила шумоподавлення", "copyImage": "Копіювати зображення", @@ -302,7 +92,6 @@ "models": "Моделі", "displayInProgress": "Показувати процес генерації", "confirmOnDelete": "Підтверджувати видалення", - "enableImageDebugging": "Увімкнути налагодження", "resetWebUI": "Повернути початкові", "resetWebUIDesc1": "Скидання настройок веб-інтерфейсу видаляє лише локальний кеш браузера з вашими зображеннями та налаштуваннями. Це не призводить до видалення зображень з диску.", "resetWebUIDesc2": "Якщо зображення не відображаються в галереї або не працює ще щось, спробуйте скинути налаштування, перш ніж повідомляти про проблему на GitHub.", @@ -311,83 +100,17 @@ "toast": { "uploadFailed": "Не вдалося завантажити", "imageCopied": "Зображення скопійоване", - "imageNotLoadedDesc": "Не знайдено зображення для надсилання до img2img", - "canvasMerged": "Полотно об'єднане", - "sentToImageToImage": "Надіслати до img2img", - "sentToUnifiedCanvas": "Надіслати на полотно", "parametersNotSet": "Параметри не задані", - "metadataLoadFailed": "Не вдалося завантажити метадані", "serverError": "Помилка сервера", "connected": "Підключено до сервера", "canceled": "Обробку скасовано" }, - "tooltip": { - "feature": { - "prompt": "Це поле для тексту запиту, включаючи об'єкти генерації та стилістичні терміни. У запит можна включити і коефіцієнти ваги (значущості токена), але консольні команди та параметри не працюватимуть.", - "gallery": "Тут відображаються генерації з папки outputs у міру їх появи.", - "other": "Ці опції включають альтернативні режими обробки для Invoke. 'Безшовний узор' створить на виході узори, що повторюються. 'Висока роздільна здатність' - це генерація у два етапи за допомогою img2img: використовуйте це налаштування, коли хочете отримати цільне зображення більшого розміру без артефактів.", - "seed": "Значення сіду впливає на початковий шум, з якого сформується зображення. Можна використовувати вже наявний сід із попередніх зображень. 'Поріг шуму' використовується для пом'якшення артефактів при високих значеннях CFG (спробуйте в діапазоні 0-10), а 'Перлін' - для додавання шуму Перліна в процесі генерації: обидва параметри служать для більшої варіативності результатів.", - "upscale": "Використовуйте ESRGAN, щоб збільшити зображення відразу після генерації.", - "boundingBox": "'Обмежуюча рамка' аналогічна налаштуванням 'Ширина' і 'Висота' для 'Зображення з тексту' або 'Зображення до зображення'. Буде оброблена тільки область у рамці." - } - }, - "unifiedCanvas": { - "layer": "Шар", - "base": "Базовий", - "mask": "Маска", - "maskingOptions": "Параметри маски", - "enableMask": "Увiмкнути маску", - "preserveMaskedArea": "Зберiгати замасковану область", - "clearMask": "Очистити маску", - "brush": "Пензель", - "eraser": "Гумка", - "fillBoundingBox": "Заповнити обмежуючу рамку", - "eraseBoundingBox": "Стерти обмежуючу рамку", - "colorPicker": "Пiпетка", - "brushOptions": "Параметри пензля", - "brushSize": "Розмiр", - "move": "Перемiстити", - "resetView": "Скинути вигляд", - "mergeVisible": "Об'єднати видимi", - "saveToGallery": "Зберегти до галереї", - "copyToClipboard": "Копiювати до буферу обмiну", - "downloadAsImage": "Завантажити як зображення", - "undo": "Вiдмiнити", - "redo": "Повторити", - "clearCanvas": "Очистити полотно", - "canvasSettings": "Налаштування полотна", - "showIntermediates": "Показувати процес", - "showGrid": "Показувати сiтку", - "snapToGrid": "Прив'язати до сітки", - "darkenOutsideSelection": "Затемнити полотно зовні", - "autoSaveToGallery": "Автозбереження до галереї", - "saveBoxRegionOnly": "Зберiгати тiльки видiлення", - "limitStrokesToBox": "Обмежити штрихи виділенням", - "showCanvasDebugInfo": "Показати дод. інформацію про полотно", - "clearCanvasHistory": "Очистити iсторiю полотна", - "clearHistory": "Очистити iсторiю", - "clearCanvasHistoryMessage": "Очищення історії полотна залишає поточне полотно незайманим, але видаляє історію скасування та повтору.", - "clearCanvasHistoryConfirm": "Ви впевнені, що хочете очистити історію полотна?", - "activeLayer": "Активний шар", - "canvasScale": "Масштаб полотна", - "boundingBox": "Обмежуюча рамка", - "scaledBoundingBox": "Масштабування рамки", - "boundingBoxPosition": "Позиція обмежуючої рамки", - "canvasDimensions": "Разміри полотна", - "canvasPosition": "Розташування полотна", - "cursorPosition": "Розташування курсора", - "previous": "Попереднє", - "next": "Наступне", - "accept": "Приняти", - "discardAll": "Відмінити все" - }, "accessibility": { "nextImage": "Наступне зображення", "invokeProgressBar": "Індикатор виконання", "reset": "Скинути", "uploadImage": "Завантажити зображення", "previousImage": "Попереднє зображення", - "showOptionsPanel": "Показати опції", "menu": "Меню" } } diff --git a/invokeai/frontend/web/public/locales/vi.json b/invokeai/frontend/web/public/locales/vi.json index 0967ef424bc..929cf2cbf79 100644 --- a/invokeai/frontend/web/public/locales/vi.json +++ b/invokeai/frontend/web/public/locales/vi.json @@ -1 +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/public/locales/zh_CN.json b/invokeai/frontend/web/public/locales/zh_CN.json deleted file mode 100644 index 45bab5c6daa..00000000000 --- a/invokeai/frontend/web/public/locales/zh_CN.json +++ /dev/null @@ -1,1242 +0,0 @@ -{ - "common": { - "hotkeysLabel": "快捷键", - "languagePickerLabel": "语言", - "reportBugLabel": "反馈错误", - "settingsLabel": "设置", - "img2img": "图生图", - "unifiedCanvas": "统一画布", - "nodes": "工作流编辑器", - "upload": "上传", - "load": "加载", - "statusDisconnected": "未连接", - "accept": "同意", - "cancel": "取消", - "dontAskMeAgain": "不要再次询问", - "areYouSure": "你确认吗?", - "random": "随机", - "openInNewTab": "在新的标签页打开", - "back": "返回", - "githubLabel": "GitHub", - "discordLabel": "Discord", - "txt2img": "文生图", - "postprocessing": "后期处理", - "loading": "加载中", - "linear": "线性的", - "batch": "批次管理器", - "communityLabel": "社区", - "modelManager": "模型管理器", - "nodeEditor": "节点编辑器", - "imageFailedToLoad": "无法加载图像", - "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": "输入", - "notInstalled": "非 $t(common.installed)", - "delete": "删除", - "updated": "已上传", - "save": "保存", - "created": "已创建", - "prevPage": "上一页", - "unknownError": "未知错误", - "direction": "指向", - "orderBy": "排序方式:", - "nextPage": "下一页", - "saveAs": "保存为", - "ai": "ai", - "or": "或", - "aboutDesc": "使用 Invoke 工作?来看看:", - "add": "添加", - "loglevel": "日志级别", - "copy": "复制", - "localSystem": "本地系统" - }, - "gallery": { - "galleryImageSize": "预览大小", - "gallerySettings": "预览设置", - "autoSwitchNewImages": "自动切换到新图像", - "loadMore": "加载更多", - "noImagesInGallery": "无图像可用于显示", - "deleteImage_other": "删除图片", - "deleteImageBin": "被删除的图片会发送到你操作系统的回收站。", - "deleteImagePermanent": "删除的图片无法被恢复。", - "assets": "素材", - "autoAssignBoardOnClick": "点击后自动分配面板", - "featuresWillReset": "如果您删除该图像,这些功能会立即被重置。", - "loading": "加载中", - "unableToLoad": "无法加载图库", - "currentlyInUse": "该图像目前在以下功能中使用:", - "copy": "复制", - "download": "下载", - "setCurrentImage": "设为当前图像", - "downloadSelection": "下载所选内容", - "noImageSelected": "无选中的图像", - "deleteSelection": "删除所选内容", - "image": "图像", - "drop": "弃用", - "dropOrUpload": "$t(gallery.drop) 或上传", - "dropToUpload": "$t(gallery.drop) 以上传", - "problemDeletingImagesDesc": "有一张或多张图像无法被删除", - "problemDeletingImages": "删除图像时出现问题", - "unstarImage": "取消收藏图像", - "starImage": "收藏图像" - }, - "hotkeys": { - "keyboardShortcuts": "快捷键", - "appHotkeys": "应用", - "generalHotkeys": "一般", - "galleryHotkeys": "图库", - "unifiedCanvasHotkeys": "统一画布", - "invoke": { - "title": "Invoke", - "desc": "生成图像" - }, - "cancel": { - "title": "取消", - "desc": "取消当前队列项目" - }, - "focusPrompt": { - "title": "打开提示词框", - "desc": "打开提示词文本框" - }, - "toggleOptions": { - "title": "切换选项卡", - "desc": "打开或关闭选项浮窗" - }, - "pinOptions": { - "title": "常开选项卡", - "desc": "保持选项浮窗常开" - }, - "toggleGallery": { - "title": "切换图库", - "desc": "打开或关闭图库" - }, - "maximizeWorkSpace": { - "title": "工作区最大化", - "desc": "关闭所有浮窗,将工作区域最大化" - }, - "changeTabs": { - "title": "切换选项卡", - "desc": "切换到另一个工作区" - }, - "consoleToggle": { - "title": "切换命令行", - "desc": "打开或关闭命令行" - }, - "setPrompt": { - "title": "使用当前提示词", - "desc": "使用当前图像的提示词" - }, - "setSeed": { - "title": "使用种子", - "desc": "使用当前图像的种子" - }, - "setParameters": { - "title": "使用当前参数", - "desc": "使用当前图像的所有参数" - }, - "restoreFaces": { - "title": "面部修复", - "desc": "对当前图像进行面部修复" - }, - "upscale": { - "title": "放大", - "desc": "对当前图像进行放大" - }, - "showInfo": { - "title": "显示信息", - "desc": "显示当前图像的元数据" - }, - "sendToImageToImage": { - "title": "发送到图生图", - "desc": "发送当前图像到图生图" - }, - "deleteImage": { - "title": "删除图像", - "desc": "删除当前图像" - }, - "closePanels": { - "title": "关闭浮窗", - "desc": "关闭目前打开的浮窗" - }, - "previousImage": { - "title": "上一张图像", - "desc": "显示图库中的上一张图像" - }, - "nextImage": { - "title": "下一张图像", - "desc": "显示图库中的下一张图像" - }, - "increaseGalleryThumbSize": { - "title": "增大预览尺寸", - "desc": "增大图库中预览的尺寸" - }, - "decreaseGalleryThumbSize": { - "title": "缩小预览尺寸", - "desc": "缩小图库中预览的尺寸" - }, - "selectBrush": { - "title": "选择刷子", - "desc": "选择统一画布上的刷子" - }, - "selectEraser": { - "title": "选择橡皮擦", - "desc": "选择统一画布上的橡皮擦" - }, - "decreaseBrushSize": { - "title": "减小刷子大小", - "desc": "减小统一画布上的刷子或橡皮擦的大小" - }, - "increaseBrushSize": { - "title": "增大刷子大小", - "desc": "增大统一画布上的刷子或橡皮擦的大小" - }, - "decreaseBrushOpacity": { - "title": "减小刷子不透明度", - "desc": "减小统一画布上的刷子的不透明度" - }, - "increaseBrushOpacity": { - "title": "增大刷子不透明度", - "desc": "增大统一画布上的刷子的不透明度" - }, - "moveTool": { - "title": "移动工具", - "desc": "画布允许导航" - }, - "fillBoundingBox": { - "title": "填充选择区域", - "desc": "在选择区域中填充刷子颜色" - }, - "eraseBoundingBox": { - "title": "擦除选择框", - "desc": "将选择区域擦除" - }, - "colorPicker": { - "title": "选择颜色拾取工具", - "desc": "选择画布颜色拾取工具" - }, - "toggleSnap": { - "title": "切换网格对齐", - "desc": "打开或关闭网格对齐" - }, - "quickToggleMove": { - "title": "快速切换移动模式", - "desc": "临时性地切换移动模式" - }, - "toggleLayer": { - "title": "切换图层", - "desc": "切换遮罩/基础层的选择" - }, - "clearMask": { - "title": "清除遮罩", - "desc": "清除整个遮罩" - }, - "hideMask": { - "title": "隐藏遮罩", - "desc": "隐藏或显示遮罩" - }, - "showHideBoundingBox": { - "title": "显示/隐藏框选区", - "desc": "切换框选区的的显示状态" - }, - "mergeVisible": { - "title": "合并可见层", - "desc": "将画板上可见层合并" - }, - "saveToGallery": { - "title": "保存至图库", - "desc": "将画布当前内容保存至图库" - }, - "copyToClipboard": { - "title": "复制到剪贴板", - "desc": "将画板当前内容复制到剪贴板" - }, - "downloadImage": { - "title": "下载图像", - "desc": "下载画板当前内容" - }, - "undoStroke": { - "title": "撤销画笔", - "desc": "撤销上一笔刷子的动作" - }, - "redoStroke": { - "title": "重做画笔", - "desc": "重做上一笔刷子的动作" - }, - "resetView": { - "title": "重置视图", - "desc": "重置画布视图" - }, - "previousStagingImage": { - "title": "上一张暂存图像", - "desc": "上一张暂存区中的图像" - }, - "nextStagingImage": { - "title": "下一张暂存图像", - "desc": "下一张暂存区中的图像" - }, - "acceptStagingImage": { - "title": "接受暂存图像", - "desc": "接受当前暂存区中的图像" - }, - "nodesHotkeys": "节点", - "addNodes": { - "title": "添加节点", - "desc": "打开添加节点菜单" - }, - "cancelAndClear": { - "desc": "取消当前队列项目并且清除所有待定项目", - "title": "取消和清除" - }, - "resetOptionsAndGallery": { - "title": "重置选项和图库", - "desc": "重置选项和图库面板" - }, - "searchHotkeys": "检索快捷键", - "noHotkeysFound": "未找到快捷键", - "toggleOptionsAndGallery": { - "desc": "打开和关闭选项和图库面板", - "title": "开关选项和图库" - }, - "clearSearch": "清除检索项" - }, - "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": "你希望转换这个模型吗?", - "v2_768": "v2 (768px)", - "allModels": "全部模型", - "convertToDiffusers": "转换为 Diffusers", - "repo_id": "项目 ID", - "modelConverted": "模型已转换", - "convertToDiffusersHelpText3": "磁盘中放置在 InvokeAI 根文件夹的 checkpoint 文件会被删除. 若位于自定义目录, 则不会受影响.", - "v2_base": "v2 (512px)", - "convertToDiffusersHelpText5": "请确认你有足够的磁盘空间,模型大小通常在 2 GB - 7 GB 之间。", - "convert": "转换", - "none": "无", - "modelsSynced": "模型已同步", - "modelSyncFailed": "模型同步失败", - "modelDeleteFailed": "模型删除失败", - "selectModel": "选择模型", - "settings": "设置", - "syncModels": "同步模型", - "modelDeleted": "模型已删除", - "modelUpdateFailed": "模型更新失败", - "modelConversionFailed": "模型转换失败", - "baseModel": "基底模型", - "convertingModelBegin": "模型转换中. 请稍候.", - "predictionType": "预测类型(适用于 Stable Diffusion 2.x 模型和部分 Stable Diffusion 1.x 模型)", - "advanced": "高级", - "modelType": "模型类别", - "variant": "变体", - "vae": "VAE", - "alpha": "Alpha", - "vaePrecision": "VAE 精度", - "noModelSelected": "无选中的模型" - }, - "parameters": { - "images": "图像", - "steps": "步数", - "cfgScale": "CFG 等级", - "width": "宽度", - "height": "高度", - "seed": "种子", - "shuffle": "随机生成种子", - "noiseThreshold": "噪声阈值", - "perlinNoise": "Perlin 噪声", - "type": "种类", - "strength": "强度", - "upscaling": "放大", - "upscale": "放大 (Shift + U)", - "upscaleImage": "放大图像", - "scale": "等级", - "imageFit": "使生成图像长宽适配初始图像", - "scaleBeforeProcessing": "处理前缩放", - "scaledWidth": "缩放宽度", - "scaledHeight": "缩放长度", - "infillMethod": "填充方法", - "tileSize": "方格尺寸", - "sendToImg2Img": "发送到图生图", - "sendToUnifiedCanvas": "发送到统一画布", - "downloadImage": "下载图像", - "usePrompt": "使用提示", - "useSeed": "使用种子", - "useAll": "使用所有参数", - "info": "信息", - "showOptionsPanel": "显示侧栏浮窗 (O 或 T)", - "seamlessYAxis": "无缝平铺 Y 轴", - "seamlessXAxis": "无缝平铺 X 轴", - "denoisingStrength": "去噪强度", - "cancel": { - "cancel": "取消" - }, - "copyImage": "复制图片", - "symmetry": "对称性", - "positivePromptPlaceholder": "正向提示词", - "negativePromptPlaceholder": "负向提示词", - "scheduler": "调度器", - "general": "通用", - "controlNetControlMode": "控制模式", - "maskBlur": "模糊", - "invoke": { - "noNodesInGraph": "节点图中无节点", - "noModelSelected": "无已选中的模型", - "invoke": "调用", - "noInitialImageSelected": "无选中的初始图像", - "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} 缺失输入", - "systemDisconnected": "系统已断开连接", - "missingNodeTemplate": "缺失节点模板", - "missingFieldTemplate": "缺失模板", - "addingImagesTo": "添加图像到", - "noPrompts": "没有已生成的提示词", - "noControlImageForControlAdapter": "有 #{{number}} 个 Control Adapter 缺失控制图像", - "noModelForControlAdapter": "有 #{{number}} 个 Control Adapter 没有选择模型。", - "incompatibleBaseModelForControlAdapter": "有 #{{number}} 个 Control Adapter 模型与主模型不兼容。" - }, - "patchmatchDownScaleSize": "缩小", - "clipSkip": "CLIP 跳过层", - "useCpuNoise": "使用 CPU 噪声", - "coherenceMode": "模式", - "imageActions": "图像操作", - "iterations": "迭代数", - "isAllowedToUpscale": { - "useX2Model": "图像太大,无法使用 x4 模型,使用 x2 模型作为替代", - "tooLarge": "图像太大无法进行放大,请选择更小的图像" - }, - "cfgRescaleMultiplier": "CFG 重缩放倍数", - "useSize": "使用尺寸", - "setToOptimalSize": "优化模型大小", - "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (可能过小)", - "lockAspectRatio": "锁定纵横比", - "swapDimensions": "交换尺寸", - "aspect": "纵横", - "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (可能过大)" - }, - "settings": { - "models": "模型", - "displayInProgress": "显示处理中的图像", - "confirmOnDelete": "删除时确认", - "enableImageDebugging": "开启图像调试", - "resetWebUI": "重置网页界面", - "resetWebUIDesc1": "重置网页只会重置浏览器中缓存的图像和设置,不会删除任何图像。", - "resetWebUIDesc2": "如果图像没有显示在图库中,或者其他东西不工作,请在GitHub上提交问题之前尝试重置。", - "resetComplete": "网页界面已重置。", - "showProgressInViewer": "在查看器中展示过程图片", - "antialiasProgressImages": "对过程图像应用抗锯齿", - "generation": "生成", - "ui": "用户界面", - "general": "通用", - "shouldLogToConsole": "终端日志", - "developer": "开发者", - "beta": "Beta", - "clearIntermediates": "清除中间产物", - "clearIntermediatesDesc3": "您图库中的图像不会被删除。", - "clearIntermediatesDesc2": "中间产物图像是生成过程中产生的副产品,与图库中的结果图像不同。清除中间产物可释放磁盘空间。", - "intermediatesCleared_other": "已清除 {{count}} 个中间产物", - "clearIntermediatesDesc1": "清除中间产物会重置您的画布和 ControlNet 状态。", - "intermediatesClearedFailed": "清除中间产物时出现问题", - "clearIntermediatesWithCount_other": "清除 {{count}} 个中间产物", - "clearIntermediatesDisabled": "队列为空才能清理中间产物", - "enableNSFWChecker": "启用成人内容检测器", - "enableInvisibleWatermark": "启用不可见水印", - "enableInformationalPopovers": "启用信息弹窗", - "reloadingIn": "重新加载中" - }, - "toast": { - "uploadFailed": "上传失败", - "imageCopied": "图像已复制", - "imageNotLoadedDesc": "找不到图片", - "canvasMerged": "画布已合并", - "sentToImageToImage": "已发送到图生图", - "sentToUnifiedCanvas": "已发送到统一画布", - "parametersNotSet": "参数未设定", - "metadataLoadFailed": "加载元数据失败", - "uploadFailedInvalidUploadDesc": "必须是单张的 PNG 或 JPEG 图片", - "connected": "服务器连接", - "parameterSet": "参数已设定", - "parameterNotSet": "参数未设定", - "serverError": "服务器错误", - "canceled": "处理取消", - "problemCopyingImage": "无法复制图像", - "modelAddedSimple": "已添加模型", - "imageSavingFailed": "图像保存失败", - "canvasSentControlnetAssets": "画布已发送到 ControlNet & 素材", - "problemCopyingCanvasDesc": "无法导出基础层", - "loadedWithWarnings": "已加载带有警告的工作流", - "setInitialImage": "设为初始图像", - "canvasCopiedClipboard": "画布已复制到剪贴板", - "setControlImage": "设为控制图像", - "setNodeField": "设为节点字段", - "problemSavingMask": "保存遮罩时出现问题", - "problemSavingCanvasDesc": "无法导出基础层", - "maskSavedAssets": "遮罩已保存到素材", - "problemDownloadingCanvas": "下载画布时出现问题", - "problemMergingCanvas": "合并画布时出现问题", - "setCanvasInitialImage": "设定画布初始图像", - "imageUploaded": "图像已上传", - "addedToBoard": "已添加到面板", - "workflowLoaded": "工作流已加载", - "problemImportingMaskDesc": "无法导出遮罩", - "problemCopyingCanvas": "复制画布时出现问题", - "problemSavingCanvas": "保存画布时出现问题", - "canvasDownloaded": "画布已下载", - "problemMergingCanvasDesc": "无法导出基础层", - "problemDownloadingCanvasDesc": "无法导出基础层", - "problemSavingMaskDesc": "无法导出遮罩", - "imageSaved": "图像已保存", - "maskSentControlnetAssets": "遮罩已发送到 ControlNet & 素材", - "canvasSavedGallery": "画布已保存到图库", - "imageUploadFailed": "图像上传失败", - "problemImportingMask": "导入遮罩时出现问题", - "baseModelChangedCleared_other": "基础模型已更改, 已清除或禁用 {{count}} 个不兼容的子模型", - "setAsCanvasInitialImage": "设为画布初始图像", - "invalidUpload": "无效的上传", - "problemDeletingWorkflow": "删除工作流时出现问题", - "workflowDeleted": "已删除工作流", - "problemRetrievingWorkflow": "检索工作流时发生问题" - }, - "unifiedCanvas": { - "layer": "图层", - "base": "基础层", - "mask": "遮罩", - "maskingOptions": "遮罩选项", - "enableMask": "启用遮罩", - "preserveMaskedArea": "保留遮罩区域", - "clearMask": "清除遮罩 (Shift+C)", - "brush": "刷子", - "eraser": "橡皮擦", - "fillBoundingBox": "填充选择区域", - "eraseBoundingBox": "取消选择区域", - "colorPicker": "颜色提取", - "brushOptions": "刷子选项", - "brushSize": "大小", - "move": "移动", - "resetView": "重置视图", - "mergeVisible": "合并可见层", - "saveToGallery": "保存至图库", - "copyToClipboard": "复制到剪贴板", - "downloadAsImage": "下载图像", - "undo": "撤销", - "redo": "重做", - "clearCanvas": "清除画布", - "canvasSettings": "画布设置", - "showIntermediates": "显示中间产物", - "showGrid": "显示网格", - "snapToGrid": "切换网格对齐", - "darkenOutsideSelection": "暗化外部区域", - "autoSaveToGallery": "自动保存至图库", - "saveBoxRegionOnly": "只保存框内区域", - "limitStrokesToBox": "限制画笔在框内", - "showCanvasDebugInfo": "显示附加画布信息", - "clearCanvasHistory": "清除画布历史", - "clearHistory": "清除历史", - "clearCanvasHistoryMessage": "清除画布历史不会影响当前画布,但会不可撤销地清除所有撤销/重做历史。", - "clearCanvasHistoryConfirm": "确认清除所有画布历史?", - "activeLayer": "活跃图层", - "canvasScale": "画布缩放", - "boundingBox": "选择区域", - "scaledBoundingBox": "缩放选择区域", - "boundingBoxPosition": "选择区域位置", - "canvasDimensions": "画布长宽", - "canvasPosition": "画布位置", - "cursorPosition": "光标位置", - "previous": "上一张", - "next": "下一张", - "accept": "接受", - "discardAll": "放弃所有", - "antialiasing": "抗锯齿", - "showResultsOn": "显示结果 (开)", - "showResultsOff": "显示结果 (关)", - "saveMask": "保存 $t(unifiedCanvas.mask)" - }, - "accessibility": { - "invokeProgressBar": "Invoke 进度条", - "reset": "重置", - "nextImage": "下一张图片", - "uploadImage": "上传图片", - "previousImage": "上一张图片", - "showOptionsPanel": "显示侧栏浮窗", - "menu": "菜单", - "showGalleryPanel": "显示图库浮窗", - "loadMore": "加载更多", - "mode": "模式", - "resetUI": "$t(accessibility.reset) UI", - "createIssue": "创建问题", - "about": "关于" - }, - "tooltip": { - "feature": { - "prompt": "这是提示词区域。提示词包括生成对象和风格术语。您也可以在提示词中添加权重(Token 的重要性),但命令行命令和参数不起作用。", - "upscale": "使用 ESRGAN 可以在图片生成后立即放大图片。", - "boundingBox": "边界框的高和宽的设定对文生图和图生图模式是一样的,只有边界框中的区域会被处理。", - "other": "这些选项将为 Invoke 启用替代处理模式。 \"无缝拼贴\" 将在输出中创建重复图案。\"高分辨率\" 是通过图生图进行两步生成:当您想要更大、更连贯且不带伪影的图像时,请使用此设置。这将比通常的文生图需要更长的时间。", - "gallery": "图片库展示输出文件夹中的图片,设置和文件一起储存,可以通过内容菜单访问。", - "seed": "种子值影响形成图像的初始噪声。您可以使用以前图像中已存在的种子。 “噪声阈值”用于减轻在高 CFG 等级(尝试 0 - 10 范围)下的伪像,并使用 Perlin 在生成过程中添加 Perlin 噪声:这两者都可以为您的输出添加变化。" - } - }, - "nodes": { - "zoomInNodes": "放大", - "loadWorkflow": "加载工作流", - "zoomOutNodes": "缩小", - "reloadNodeTemplates": "重载节点模板", - "fitViewportNodes": "自适应视图", - "showMinimapnodes": "显示缩略图", - "hideMinimapnodes": "隐藏缩略图", - "showLegendNodes": "显示字段类型图例", - "hideLegendNodes": "隐藏字段类型图例", - "downloadWorkflow": "下载工作流 JSON", - "workflowDescription": "简述", - "versionUnknown": " 未知版本", - "noNodeSelected": "无选中的节点", - "addNode": "添加节点", - "unableToValidateWorkflow": "无法验证工作流", - "noOutputRecorded": "无已记录输出", - "updateApp": "升级 App", - "colorCodeEdgesHelp": "根据连接区域对边缘编码颜色", - "workflowContact": "联系", - "animatedEdges": "边缘动效", - "nodeTemplate": "节点模板", - "unableToLoadWorkflow": "无法加载工作流", - "snapToGrid": "对齐网格", - "noFieldsLinearview": "线性视图中未添加任何字段", - "nodeSearch": "检索节点", - "version": "版本", - "validateConnections": "验证连接和节点图", - "inputMayOnlyHaveOneConnection": "输入仅能有一个连接", - "notes": "注释", - "nodeOutputs": "节点输出", - "currentImageDescription": "在节点编辑器中显示当前图像", - "validateConnectionsHelp": "防止建立无效连接和调用无效节点图", - "problemSettingTitle": "设定标题时出现问题", - "noConnectionInProgress": "没有正在进行的连接", - "workflowVersion": "版本", - "noConnectionData": "无连接数据", - "fieldTypesMustMatch": "类型必须匹配", - "workflow": "工作流", - "animatedEdgesHelp": "为选中边缘和其连接的选中节点的边缘添加动画", - "unknownTemplate": "未知模板", - "removeLinearView": "从线性视图中移除", - "workflowTags": "标签", - "fullyContainNodesHelp": "节点必须完全位于选择框中才能被选中", - "workflowValidation": "工作流验证错误", - "noMatchingNodes": "无相匹配的节点", - "executionStateInProgress": "处理中", - "noFieldType": "无字段类型", - "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": "字符串", - "mismatchedVersion": "无效的节点:类型为 {{type}} 的节点 {{node}} 版本不匹配(是否尝试更新?)", - "cannotDuplicateConnection": "无法创建重复的连接", - "enum": "Enum (枚举)", - "float": "浮点", - "integer": "整数", - "boolean": "布尔值", - "ipAdapter": "IP-Adapter", - "updateAllNodes": "更新节点", - "unableToUpdateNodes_other": "{{count}} 个节点无法完成更新", - "inputFieldTypeParseError": "无法解析 {{node}} 的输入类型 {{field}}。({{message}})", - "unsupportedArrayItemType": "不支持的数组类型 \"{{type}}\"", - "addLinearView": "添加到线性视图", - "targetNodeFieldDoesNotExist": "无效的边缘:{{node}} 的目标/输入区域 {{field}} 不存在", - "unsupportedMismatchedUnion": "合集或标量类型与基类 {{firstType}} 和 {{secondType}} 不匹配", - "allNodesUpdated": "已更新所有节点", - "sourceNodeDoesNotExist": "无效的边缘:{{node}} 的源/输出节点不存在", - "unableToExtractEnumOptions": "无法提取枚举选项", - "unableToParseFieldType": "无法解析类型", - "outputFieldTypeParseError": "无法解析 {{node}} 的输出类型 {{field}}。({{message}})", - "sourceNodeFieldDoesNotExist": "无效的边缘:{{node}} 的源/输出区域 {{field}} 不存在", - "unableToGetWorkflowVersion": "无法获取工作流架构版本", - "nodePack": "节点包", - "unableToExtractSchemaNameFromRef": "无法从参考中提取架构名", - "unknownOutput": "未知输出:{{name}}", - "unknownErrorValidatingWorkflow": "验证工作流时出现未知错误", - "collectionFieldType": "{{name}} 合集", - "unknownNodeType": "未知节点类型", - "targetNodeDoesNotExist": "无效的边缘:{{node}} 的目标/输入节点不存在", - "unknownFieldType": "$t(nodes.unknownField) 类型:{{type}}", - "collectionOrScalarFieldType": "{{name}} 合集 | 标量", - "nodeVersion": "节点版本", - "deletedInvalidEdge": "已删除无效的边缘 {{source}} -> {{target}}", - "unknownInput": "未知输入:{{name}}", - "prototypeDesc": "此调用是一个原型 (prototype)。它可能会在本项目更新期间发生破坏性更改,并且随时可能被删除。", - "betaDesc": "此调用尚处于测试阶段。在稳定之前,它可能会在项目更新期间发生破坏性更改。本项目计划长期支持这种调用。", - "newWorkflow": "新建工作流", - "newWorkflowDesc": "是否创建一个新的工作流?", - "newWorkflowDesc2": "当前工作流有未保存的更改。", - "unsupportedAnyOfLength": "联合(union)数据类型数目过多 ({{count}})" - }, - "controlnet": { - "resize": "直接缩放", - "showAdvanced": "显示高级", - "contentShuffleDescription": "随机打乱图像内容", - "importImageFromCanvas": "从画布导入图像", - "lineartDescription": "将图像转换为线稿", - "importMaskFromCanvas": "从画布导入遮罩", - "hideAdvanced": "隐藏高级", - "resetControlImage": "重置控制图像", - "beginEndStepPercent": "开始 / 结束步数百分比", - "mlsdDescription": "简洁的分割线段(直线)检测器", - "duplicate": "复制", - "balanced": "平衡", - "prompt": "Prompt (提示词控制)", - "depthMidasDescription": "使用 Midas 生成深度图", - "resizeMode": "缩放模式", - "weight": "权重", - "selectModel": "选择一个模型", - "crop": "裁剪", - "processor": "处理器", - "none": "无", - "detectResolution": "检测分辨率", - "pidiDescription": "像素差分 (PIDI) 图像处理", - "controlMode": "控制模式", - "fill": "填充", - "cannyDescription": "Canny 边缘检测", - "colorMapDescription": "从图像生成一张颜色图", - "imageResolution": "图像分辨率", - "autoConfigure": "自动配置处理器", - "normalBaeDescription": "法线 BAE 处理", - "noneDescription": "不应用任何处理", - "saveControlImage": "保存控制图像", - "toggleControlNet": "开关此 ControlNet", - "delete": "删除", - "colorMapTileSize": "分块大小", - "mediapipeFaceDescription": "使用 Mediapipe 检测面部", - "depthZoeDescription": "使用 Zoe 生成深度图", - "hedDescription": "整体嵌套边缘检测", - "setControlImageDimensions": "设定控制图像尺寸宽/高为", - "amult": "角度倍率 (a_mult)", - "bgth": "背景移除阈值 (bg_th)", - "lineartAnimeDescription": "动漫风格线稿处理", - "minConfidence": "最小置信度", - "lowThreshold": "弱判断阈值", - "highThreshold": "强判断阈值", - "addT2IAdapter": "添加 $t(common.t2iAdapter)", - "addControlNet": "添加 $t(common.controlNet)", - "addIPAdapter": "添加 $t(common.ipAdapter)", - "safe": "保守模式", - "scribble": "草绘 (scribble)", - "maxFaces": "最大面部数", - "pidi": "PIDI", - "normalBae": "Normal BAE", - "hed": "HED", - "contentShuffle": "Content Shuffle", - "f": "F", - "h": "H", - "controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))", - "control": "Control (普通控制)", - "coarse": "Coarse", - "depthMidas": "Depth (Midas)", - "w": "W", - "ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))", - "mediapipeFace": "Mediapipe Face", - "mlsd": "M-LSD", - "lineart": "Lineart", - "t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))", - "megaControl": "Mega Control (超级控制)", - "depthZoe": "Depth (Zoe)", - "colorMap": "Color", - "controlAdapter_other": "Control Adapters", - "lineartAnime": "Lineart Anime", - "canny": "Canny", - "resizeSimple": "缩放(简单)" - }, - "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": "节点图加入队列失败", - "batchFieldValues": "批处理值", - "time": "时间", - "openQueue": "打开队列" - }, - "sdxl": { - "refinerStart": "Refiner 开始作用时机", - "scheduler": "调度器", - "cfgScale": "CFG 等级", - "negStylePrompt": "负向样式提示词", - "noModelsAvailable": "无可用模型", - "negAestheticScore": "负向美学评分", - "denoisingStrength": "去噪强度", - "refinermodel": "Refiner 模型", - "posAestheticScore": "正向美学评分", - "concatPromptStyle": "链接提示词 & 样式", - "loading": "加载中...", - "steps": "步数", - "posStylePrompt": "正向样式提示词", - "refiner": "Refiner", - "freePromptStyle": "手动输入样式提示词" - }, - "metadata": { - "positivePrompt": "正向提示词", - "negativePrompt": "负向提示词", - "generationMode": "生成模式", - "Threshold": "噪声阈值", - "metadata": "元数据", - "strength": "图生图强度", - "seed": "种子", - "imageDetails": "图像详细信息", - "model": "模型", - "noImageDetails": "未找到图像详细信息", - "cfgScale": "CFG 等级", - "initImage": "初始图像", - "height": "高度", - "noMetaData": "未找到元数据", - "width": "宽度", - "createdBy": "创建者是", - "workflow": "工作流", - "steps": "步数", - "scheduler": "调度器", - "seamless": "无缝", - "fit": "图生图匹配", - "recallParameters": "召回参数", - "noRecallParameters": "未找到要召回的参数", - "vae": "VAE", - "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)" - }, - "models": { - "noMatchingModels": "无相匹配的模型", - "loading": "加载中", - "noMatchingLoRAs": "无相匹配的 LoRA", - "noModelsAvailable": "无可用模型", - "selectModel": "选择一个模型", - "noRefinerModelsInstalled": "无已安装的 SDXL Refiner 模型", - "noLoRAsInstalled": "无已安装的 LoRA", - "esrganModel": "ESRGAN 模型", - "addLora": "添加 LoRA", - "lora": "LoRA", - "defaultVAE": "默认 VAE" - }, - "boards": { - "autoAddBoard": "自动添加面板", - "topMessage": "该面板包含的图像正使用以下功能:", - "move": "移动", - "menuItemAutoAdd": "自动添加到该面板", - "myBoard": "我的面板", - "searchBoard": "检索面板...", - "noMatching": "没有相匹配的面板", - "selectBoard": "选择一个面板", - "cancel": "取消", - "addBoard": "添加面板", - "bottomMessage": "删除该面板并且将其对应的图像将重置当前使用该面板的所有功能。", - "uncategorized": "未分类", - "changeBoard": "更改面板", - "loading": "加载中...", - "clearSearch": "清除检索", - "downloadBoard": "下载面板", - "deleteBoardOnly": "仅删除面板", - "deleteBoard": "删除面板", - "deleteBoardAndImages": "删除面板和图像", - "deletedBoardsCannotbeRestored": "已删除的面板无法被恢复", - "movingImagesToBoard_other": "移动 {{count}} 张图像到面板:" - }, - "dynamicPrompts": { - "seedBehaviour": { - "perPromptDesc": "每次生成图像使用不同的种子", - "perIterationLabel": "每次迭代的种子", - "perIterationDesc": "每次迭代使用不同的种子", - "perPromptLabel": "每张图像的种子", - "label": "种子行为" - }, - "maxPrompts": "最大提示词数", - "dynamicPrompts": "动态提示词", - "promptsWithCount_other": "{{count}} 个提示词", - "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": [ - "ControlNet 输入图像适应输出图像大小的方法。" - ] - }, - "clipSkip": { - "paragraphs": [ - "选择要跳过 CLIP 模型多少层。", - "部分模型跳过特定数值的层时效果会更好。" - ], - "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": [ - "ControlNet 对生成图像的影响强度。" - ] - }, - "paramCFGScale": { - "heading": "CFG 等级", - "paragraphs": [ - "控制提示词对生成过程的影响程度。" - ] - }, - "paramSteps": { - "heading": "步数", - "paragraphs": [ - "每次生成迭代执行的步数。", - "通常情况下步数越多结果越好,但需要更多生成时间。" - ] - }, - "paramPositiveConditioning": { - "heading": "正向提示词", - "paragraphs": [ - "引导生成过程。您可以使用任何单词或短语。", - "Compel 语法、动态提示词语法和 embeddings。" - ] - }, - "lora": { - "heading": "LoRA 权重", - "paragraphs": [ - "更高的 LoRA 权重会对最终图像产生更大的影响。" - ] - }, - "infillMethod": { - "heading": "填充方法", - "paragraphs": [ - "填充选定区域的方式。" - ] - }, - "controlNetBeginEnd": { - "heading": "开始 / 结束步数百分比", - "paragraphs": [ - "去噪过程中在哪部分步数应用 ControlNet。", - "在组合处理开始阶段应用 ControlNet,且在引导细节生成的结束阶段应用 ControlNet。" - ] - }, - "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 引导的重缩放倍率,用于通过 zero-terminal SNR (ztsnr) 训练的模型。推荐设为 0.7。" - ] - } - }, - "invocationCache": { - "disable": "禁用", - "misses": "缓存未中", - "enableFailed": "启用调用缓存时出现问题", - "invocationCache": "调用缓存", - "clearSucceeded": "调用缓存已清除", - "enableSucceeded": "调用缓存已启用", - "clearFailed": "清除调用缓存时出现问题", - "hits": "缓存命中", - "disableSucceeded": "调用缓存已禁用", - "disableFailed": "禁用调用缓存时出现问题", - "enable": "启用", - "clear": "清除", - "maxCacheSize": "最大缓存大小", - "cacheSize": "缓存大小", - "useCache": "使用缓存" - }, - "hrf": { - "enableHrf": "启用高分辨率修复", - "upscaleMethod": "放大方法", - "metadata": { - "strength": "高分辨率修复强度", - "enabled": "高分辨率修复已启用", - "method": "高分辨率修复方法" - }, - "hrf": "高分辨率修复" - }, - "workflows": { - "saveWorkflowAs": "保存工作流为", - "workflowEditorMenu": "工作流编辑器菜单", - "workflowName": "工作流名称", - "saveWorkflow": "保存工作流", - "openWorkflow": "打开工作流", - "clearWorkflowSearchFilter": "清除工作流检索过滤器", - "workflowLibrary": "工作流库", - "downloadWorkflow": "保存到文件", - "workflowSaved": "已保存工作流", - "unnamedWorkflow": "未命名的工作流", - "savingWorkflow": "保存工作流中...", - "problemLoading": "加载工作流时出现问题", - "loading": "加载工作流中", - "searchWorkflows": "检索工作流", - "problemSavingWorkflow": "保存工作流时出现问题", - "deleteWorkflow": "删除工作流", - "workflows": "工作流", - "noDescription": "无描述", - "uploadWorkflow": "从文件中加载", - "newWorkflowCreated": "已创建新的工作流", - "name": "名称", - "defaultWorkflows": "默认工作流", - "created": "已创建", - "ascending": "升序", - "descending": "降序", - "updated": "已更新", - "userWorkflows": "我的工作流", - "projectWorkflows": "项目工作流", - "opened": "已打开" - }, - "app": { - "storeNotInitialized": "商店尚未初始化" - }, - "accordions": { - "compositing": { - "infillTab": "内补", - "coherenceTab": "一致性层", - "title": "合成" - }, - "control": { - "title": "Control" - }, - "generation": { - "title": "生成" - }, - "advanced": { - "title": "高级", - "options": "$t(accordions.advanced.title) 选项" - }, - "image": { - "title": "图像" - } - } -} diff --git a/invokeai/frontend/web/public/locales/zh_Hant.json b/invokeai/frontend/web/public/locales/zh_Hant.json deleted file mode 100644 index 77489474786..00000000000 --- a/invokeai/frontend/web/public/locales/zh_Hant.json +++ /dev/null @@ -1,249 +0,0 @@ -{ - "common": { - "nodes": "工作流程", - "img2img": "圖片轉圖片", - "statusDisconnected": "已中斷連線", - "back": "返回", - "load": "載入", - "settingsLabel": "設定", - "upload": "上傳", - "discordLabel": "Discord", - "reportBugLabel": "回報錯誤", - "githubLabel": "GitHub", - "hotkeysLabel": "快捷鍵", - "languagePickerLabel": "語言", - "unifiedCanvas": "統一畫布", - "cancel": "取消", - "txt2img": "文字轉圖片", - "controlNet": "ControlNet", - "advanced": "進階", - "folder": "資料夾", - "installed": "已安裝", - "accept": "接受", - "goTo": "前往", - "input": "輸入", - "random": "隨機", - "selected": "已選擇", - "communityLabel": "社群", - "loading": "載入中", - "delete": "刪除", - "copy": "複製", - "error": "錯誤", - "file": "檔案", - "format": "格式", - "imageFailedToLoad": "無法載入圖片" - }, - "accessibility": { - "invokeProgressBar": "Invoke 進度條", - "uploadImage": "上傳圖片", - "reset": "重置", - "nextImage": "下一張圖片", - "previousImage": "上一張圖片", - "menu": "選單", - "loadMore": "載入更多", - "about": "關於", - "createIssue": "建立問題", - "resetUI": "$t(accessibility.reset) 介面", - "submitSupportTicket": "提交支援工單", - "mode": "模式" - }, - "boards": { - "loading": "載入中…", - "movingImagesToBoard_other": "正在移動 {{count}} 張圖片至板上:", - "move": "移動", - "uncategorized": "未分類", - "cancel": "取消" - }, - "metadata": { - "workflow": "工作流程", - "steps": "步數", - "model": "模型", - "seed": "種子", - "vae": "VAE", - "seamless": "無縫", - "metadata": "元數據", - "width": "寬度", - "height": "高度" - }, - "accordions": { - "control": { - "title": "控制" - }, - "compositing": { - "title": "合成" - }, - "advanced": { - "title": "進階", - "options": "$t(accordions.advanced.title) 選項" - } - }, - "hotkeys": { - "nodesHotkeys": "節點", - "cancel": { - "title": "取消" - }, - "generalHotkeys": "一般", - "keyboardShortcuts": "快捷鍵", - "appHotkeys": "應用程式" - }, - "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": "設定" - }, - "controlnet": { - "mlsd": "M-LSD", - "canny": "Canny", - "duplicate": "重複", - "none": "無", - "pidi": "PIDI", - "h": "H", - "balanced": "平衡", - "crop": "裁切", - "processor": "處理器", - "control": "控制", - "f": "F", - "lineart": "線條藝術", - "w": "W", - "hed": "HED", - "delete": "刪除" - }, - "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": "步數" - } - }, - "unifiedCanvas": { - "undo": "復原", - "mask": "遮罩", - "eraser": "橡皮擦", - "antialiasing": "抗鋸齒", - "redo": "重做", - "layer": "圖層", - "accept": "接受", - "brush": "刷子", - "move": "移動", - "brushSize": "大小" - }, - "nodes": { - "workflowName": "名稱", - "notes": "註釋", - "workflowVersion": "版本", - "workflowNotes": "註釋", - "executionStateError": "錯誤", - "unableToUpdateNodes_other": "無法更新 {{count}} 個節點", - "integer": "整數", - "workflow": "工作流程", - "enum": "枚舉", - "edit": "編輯", - "string": "字串", - "workflowTags": "標籤", - "node": "節點", - "boolean": "布林值", - "workflowAuthor": "作者", - "version": "版本", - "executionStateCompleted": "已完成", - "edge": "邊緣", - "versionUnknown": " 版本未知" - }, - "sdxl": { - "steps": "步數", - "loading": "載入中…", - "refiner": "精煉器" - }, - "gallery": { - "copy": "複製", - "download": "下載", - "loading": "載入中" - }, - "ui": { - "tabs": { - "models": "模型", - "queueTab": "$t(ui.tabs.queue) $t(common.tab)", - "queue": "佇列" - } - }, - "models": { - "loading": "載入中" - }, - "workflows": { - "name": "名稱" - } -} diff --git a/invokeai/frontend/web/scripts/clean_translations.py b/invokeai/frontend/web/scripts/clean_translations.py index 473a036cb45..a422747ef5c 100644 --- a/invokeai/frontend/web/scripts/clean_translations.py +++ b/invokeai/frontend/web/scripts/clean_translations.py @@ -3,6 +3,7 @@ # 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 diff --git a/invokeai/frontend/web/scripts/package.json b/invokeai/frontend/web/scripts/package.json index 3dbc1ca591c..985bcf7d652 100644 --- a/invokeai/frontend/web/scripts/package.json +++ b/invokeai/frontend/web/scripts/package.json @@ -1,3 +1,4 @@ { - "type": "module" + "type": "module", + "packageManager": "pnpm@10.12.4" } diff --git a/invokeai/frontend/web/scripts/typegen.js b/invokeai/frontend/web/scripts/typegen.js index fa2d791350d..87c00a28833 100644 --- a/invokeai/frontend/web/scripts/typegen.js +++ b/invokeai/frontend/web/scripts/typegen.js @@ -1,30 +1,74 @@ /* eslint-disable no-console */ import fs from 'node:fs'; -import openapiTS from 'openapi-typescript'; +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 ? 'Blob | null' : 'Blob'; + 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'; + return RECORD_STRING_UNKNOWN; } }, + defaultNonNullable: false, }); - fs.writeFileSync(OUTPUT_FILE, types); + 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`); } -async function main() { +function main() { const encoding = 'utf-8'; if (process.stdin.isTTY) { diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 2d878d96e78..0f9fb5292b8 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -1,102 +1,123 @@ -import { Box, useGlobalModifiersInit } from '@invoke-ai/ui-library'; -import { useSocketIO } from 'app/hooks/useSocketIO'; -import { useSyncQueueStatus } from 'app/hooks/useSyncQueueStatus'; -import { useLogger } from 'app/logging/useLogger'; -import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import type { PartialAppConfig } from 'app/types/invokeai'; -import ImageUploadOverlay from 'common/components/ImageUploadOverlay'; -import { useClearStorage } from 'common/hooks/useClearStorage'; -import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone'; -import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; -import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal'; -import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal'; -import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal'; -import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast'; -import { configChanged } from 'features/system/store/configSlice'; -import { languageSelector } from 'features/system/store/systemSelectors'; -import InvokeTabs from 'features/ui/components/InvokeTabs'; -import { AnimatePresence } from 'framer-motion'; -import i18n from 'i18n'; -import { size } from 'lodash-es'; -import { memo, useCallback, useEffect } from 'react'; +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 { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; +import { Route, Routes, useNavigate } from 'react-router-dom'; +import { useGetSetupStatusQuery } from 'services/api/endpoints/auth'; import AppErrorBoundaryFallback from './AppErrorBoundaryFallback'; -import PreselectedImage from './PreselectedImage'; +import ThemeLocaleProvider from './ThemeLocaleProvider'; -const DEFAULT_CONFIG = {}; - -interface Props { - config?: PartialAppConfig; - selectedImage?: { - imageName: string; - action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters'; - }; -} - -const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => { - const language = useAppSelector(languageSelector); - const logger = useLogger('system'); - const dispatch = useAppDispatch(); - const clearStorage = useClearStorage(); - - // singleton! - useSocketIO(); - useGlobalModifiersInit(); - useGlobalHotkeys(); - useGetOpenAPISchemaQuery(); +const errorBoundaryOnReset = () => { + clearStorage(); + location.reload(); + return false; +}; - const { dropzone, isHandlingUpload, setIsHandlingUpload } = useFullscreenDropzone(); +const MainApp = () => { + const isNavigationAPIConnected = useStore(navigationApi.$isConnected); + return ( + + {isNavigationAPIConnected ? : } + + ); +}; - const handleReset = useCallback(() => { - clearStorage(); - location.reload(); - return false; - }, [clearStorage]); +const SetupChecker = () => { + const { data, isLoading } = useGetSetupStatusQuery(); + const navigate = useNavigate(); - useEffect(() => { - i18n.changeLanguage(language); - }, [language]); + // Check if user is already authenticated + const token = localStorage.getItem('auth_token'); + const isAuthenticated = !!token; useEffect(() => { - if (size(config)) { - logger.info({ config }, 'Received config'); - dispatch(configChanged(config)); + 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 }); + } } - }, [dispatch, config, logger]); + }, [data, isLoading, navigate, isAuthenticated]); - useEffect(() => { - dispatch(appStarted()); - }, [dispatch]); + if (isLoading) { + return ( +
+ +
+ ); + } + + return null; +}; - useStarterModelsToast(); - useSyncQueueStatus(); +/** Full-page wrapper for user management / profile pages rendered inside the protected area */ +const FullPageWrapper = ({ children }: { children: ReactNode }) => ( + + {children} + +); +const App = () => { return ( - - - - - - {dropzone.isDragActive && isHandlingUpload && ( - - )} - - - - - - - + + + + } /> + } /> + } /> + + + + + + } + /> + + + + + + } + /> + + + + } + /> + + + + + ); }; diff --git a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx index ced3037a405..f22a94c33fc 100644 --- a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx +++ b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx @@ -1,5 +1,5 @@ import { Button, Flex, Heading, Image, Link, Text } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +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'; @@ -15,32 +15,29 @@ type Props = { const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => { const { t } = useTranslation(); - const isLocal = useAppSelector((s) => s.config.isLocal); + const clipboard = useClipboard(); const handleCopy = useCallback(() => { const text = JSON.stringify(serializeError(error), null, 2); - navigator.clipboard.writeText(`\`\`\`\n${text}\n\`\`\``); - toast({ - id: 'ERROR_COPIED', - title: t('toast.errorCopied'), + clipboard.writeText(`\`\`\`\n${text}\n\`\`\``, () => { + toast({ + id: 'ERROR_COPIED', + title: t('toast.errorCopied'), + }); }); - }, [error, t]); + }, [clipboard, error, t]); const url = useMemo(() => { - if (isLocal) { - return newGithubIssueUrl({ - user: 'invoke-ai', - repo: 'InvokeAI', - template: 'BUG_REPORT.yml', - title: `[bug]: ${error.name}: ${error.message}`, - }); - } else { - return 'https://support.invoke.ai/support/tickets/new'; - } - }, [error.message, error.name, isLocal]); + return newGithubIssueUrl({ + user: 'invoke-ai', + repo: 'InvokeAI', + template: 'BUG_REPORT.yml', + title: `[bug]: ${error.name}: ${error.message}`, + }); + }, [error.message, error.name]); return ( - + invoke-logo @@ -68,9 +65,7 @@ const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => { {t('common.copyError')} - + 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 index 12611943bcc..685e1ee3a91 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -1,203 +1,39 @@ import 'i18n'; -import type { Middleware } from '@reduxjs/toolkit'; -import { $socketOptions } from 'app/hooks/useSocketIO'; -import { $authToken } from 'app/store/nanostores/authToken'; -import { $baseUrl } from 'app/store/nanostores/baseUrl'; -import { $customNavComponent } from 'app/store/nanostores/customNavComponent'; -import type { CustomStarUi } from 'app/store/nanostores/customStarUI'; -import { $customStarUI } from 'app/store/nanostores/customStarUI'; -import { $galleryHeader } from 'app/store/nanostores/galleryHeader'; -import { $isDebugging } from 'app/store/nanostores/isDebugging'; -import { $logo } from 'app/store/nanostores/logo'; -import { $openAPISchemaUrl } from 'app/store/nanostores/openAPISchemaUrl'; -import { $projectId } from 'app/store/nanostores/projectId'; -import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId'; +import { configureLogging } from 'app/logging/logger'; +import { addStorageListeners } from 'app/store/enhancers/reduxRemember/driver'; import { $store } from 'app/store/nanostores/store'; -import { $workflowCategories } from 'app/store/nanostores/workflowCategories'; import { createStore } from 'app/store/store'; -import type { PartialAppConfig } from 'app/types/invokeai'; import Loading from 'common/components/Loading/Loading'; -import AppDndContext from 'features/dnd/components/AppDndContext'; -import type { WorkflowCategory } from 'features/nodes/types/workflow'; -import type { PropsWithChildren, ReactNode } from 'react'; -import React, { lazy, memo, useEffect, useMemo } from 'react'; +import React, { lazy, useEffect, useState } from 'react'; import { Provider } from 'react-redux'; -import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares'; -import type { ManagerOptions, SocketOptions } from 'socket.io-client'; +import { BrowserRouter } from 'react-router-dom'; -const App = lazy(() => import('./App')); -const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); - -interface Props extends PropsWithChildren { - apiUrl?: string; - openAPISchemaUrl?: string; - token?: string; - config?: PartialAppConfig; - customNavComponent?: ReactNode; - middleware?: Middleware[]; - projectId?: string; - galleryHeader?: ReactNode; - queueId?: string; - selectedImage?: { - imageName: string; - action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters'; - }; - customStarUi?: CustomStarUi; - socketOptions?: Partial; - isDebugging?: boolean; - logo?: ReactNode; - workflowCategories?: WorkflowCategory[]; -} - -const InvokeAIUI = ({ - apiUrl, - openAPISchemaUrl, - token, - config, - customNavComponent, - middleware, - projectId, - galleryHeader, - queueId, - selectedImage, - customStarUi, - socketOptions, - isDebugging = false, - logo, - workflowCategories, -}: Props) => { - useEffect(() => { - // configure API client token - if (token) { - $authToken.set(token); - } - - // configure API client base url - if (apiUrl) { - $baseUrl.set(apiUrl); - } - - // configure API client project header - if (projectId) { - $projectId.set(projectId); - } - - // configure API client project header - if (queueId) { - $queueId.set(queueId); - } - - // reset dynamically added middlewares - resetMiddlewares(); - - // TODO: at this point, after resetting the middleware, we really ought to clean up the socket - // stuff by calling `dispatch(socketReset())`. but we cannot dispatch from here as we are - // outside the provider. it's not needed until there is the possibility that we will change - // the `apiUrl`/`token` dynamically. - - // rebuild socket middleware with token and apiUrl - if (middleware && middleware.length > 0) { - addMiddleware(...middleware); - } - - return () => { - // Reset the API client token and base url on unmount - $baseUrl.set(undefined); - $authToken.set(undefined); - $projectId.set(undefined); - $queueId.set(DEFAULT_QUEUE_ID); - }; - }, [apiUrl, token, middleware, projectId, queueId]); +/* + * 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', '*'); - useEffect(() => { - if (customStarUi) { - $customStarUI.set(customStarUi); - } - - return () => { - $customStarUI.set(undefined); - }; - }, [customStarUi]); - - useEffect(() => { - if (customNavComponent) { - $customNavComponent.set(customNavComponent); - } - - return () => { - $customNavComponent.set(undefined); - }; - }, [customNavComponent]); - - useEffect(() => { - if (openAPISchemaUrl) { - $openAPISchemaUrl.set(openAPISchemaUrl); - } - - return () => { - $openAPISchemaUrl.set(undefined); - }; - }, [openAPISchemaUrl]); - - useEffect(() => { - if (galleryHeader) { - $galleryHeader.set(galleryHeader); - } - - return () => { - $galleryHeader.set(undefined); - }; - }, [galleryHeader]); - - useEffect(() => { - if (logo) { - $logo.set(logo); - } - - return () => { - $logo.set(undefined); - }; - }, [logo]); - - useEffect(() => { - if (workflowCategories) { - $workflowCategories.set(workflowCategories); - } - - return () => { - $workflowCategories.set([]); - }; - }, [workflowCategories]); - - useEffect(() => { - if (socketOptions) { - $socketOptions.set(socketOptions); - } - return () => { - $socketOptions.set({}); - }; - }, [socketOptions]); - - useEffect(() => { - if (isDebugging) { - $isDebugging.set(isDebugging); - } - return () => { - $isDebugging.set(false); - }; - }, [isDebugging]); +const App = lazy(() => import('./App')); - const store = useMemo(() => { - return createStore(projectId); - }, [projectId]); +const InvokeAIUI = () => { + const [didRehydrate, setDidRehydrate] = useState(false); + const [store] = useState(() => + createStore({ persist: true, persistDebounce: 300, onRehydrated: () => setDidRehydrate(true) }) + ); useEffect(() => { $store.set(store); if (import.meta.env.MODE === 'development') { window.$store = $store; } - () => { + const removeStorageListeners = addStorageListeners(); + return () => { + removeStorageListeners(); $store.set(undefined); if (import.meta.env.MODE === 'development') { window.$store = undefined; @@ -205,19 +41,21 @@ const InvokeAIUI = ({ }; }, [store]); + if (!didRehydrate) { + return ; + } + return ( - }> - - - - - - + + }> + + + ); }; -export default memo(InvokeAIUI); +export default InvokeAIUI; diff --git a/invokeai/frontend/web/src/app/components/PreselectedImage.tsx b/invokeai/frontend/web/src/app/components/PreselectedImage.tsx deleted file mode 100644 index 8fa4fd2ffd8..00000000000 --- a/invokeai/frontend/web/src/app/components/PreselectedImage.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { usePreselectedImage } from 'features/parameters/hooks/usePreselectedImage'; -import { memo } from 'react'; - -type Props = { - selectedImage?: { - imageName: string; - action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters'; - }; -}; - -const PreselectedImage = (props: Props) => { - usePreselectedImage(props.selectedImage); - return null; -}; - -export default memo(PreselectedImage); diff --git a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx index aa3a24209c3..62b7114288d 100644 --- a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx +++ b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx @@ -1,37 +1,42 @@ 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 _theme, TOAST_OPTIONS } from '@invoke-ai/ui-library'; +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, useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo, useMemo } from 'react'; type ThemeLocaleProviderProps = { children: ReactNode; }; -function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) { - const { i18n } = useTranslation(); - - const direction = i18n.dir(); - - const theme = useMemo(() => { - return extendTheme({ - ..._theme, - direction, - shadows: { - ..._theme.shadows, - selectedForCompare: - '0px 0px 0px 1px var(--invoke-colors-base-900), 0px 0px 0px 4px var(--invoke-colors-green-400)', - hoverSelectedForCompare: - '0px 0px 0px 1px var(--invoke-colors-base-900), 0px 0px 0px 4px var(--invoke-colors-green-300)', - }, - }); - }, [direction]); +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)', + }, + }); +}; - useEffect(() => { - document.body.dir = direction; - }, [direction]); +function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) { + const direction = useStore($direction); + const theme = useMemo(() => buildTheme(direction), [direction]); return ( 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/eslintConfig.test.ts b/invokeai/frontend/web/src/app/eslintConfig.test.ts new file mode 100644 index 00000000000..05fb4bc23db --- /dev/null +++ b/invokeai/frontend/web/src/app/eslintConfig.test.ts @@ -0,0 +1,11 @@ +import { readFileSync } from 'node:fs'; + +import { describe, expect, it } from 'vitest'; + +const eslintConfig = readFileSync(new URL('../../eslint.config.mjs', import.meta.url), 'utf8'); + +describe('eslint config', () => { + it('includes React Compiler diagnostics from eslint-plugin-react-hooks', () => { + expect(eslintConfig).toContain("pluginReactHooks.configs['recommended-latest'].rules"); + }); +}); diff --git a/invokeai/frontend/web/src/app/hooks/useSocketIO.ts b/invokeai/frontend/web/src/app/hooks/useSocketIO.ts deleted file mode 100644 index d3baf5f4524..00000000000 --- a/invokeai/frontend/web/src/app/hooks/useSocketIO.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useStore } from '@nanostores/react'; -import { $authToken } from 'app/store/nanostores/authToken'; -import { $baseUrl } from 'app/store/nanostores/baseUrl'; -import { $isDebugging } from 'app/store/nanostores/isDebugging'; -import { useAppDispatch } from 'app/store/storeHooks'; -import type { MapStore } from 'nanostores'; -import { atom, map } from 'nanostores'; -import { useEffect, useMemo } from 'react'; -import { setEventListeners } from 'services/events/setEventListeners'; -import type { ClientToServerEvents, ServerToClientEvents } from 'services/events/types'; -import type { ManagerOptions, Socket, SocketOptions } from 'socket.io-client'; -import { io } from 'socket.io-client'; - -// Inject socket options and url into window for debugging -declare global { - interface Window { - $socketOptions?: MapStore>; - } -} - -export const $socketOptions = map>({}); -const $isSocketInitialized = atom(false); - -/** - * Initializes the socket.io connection and sets up event listeners. - */ -export const useSocketIO = () => { - const dispatch = useAppDispatch(); - const baseUrl = useStore($baseUrl); - const authToken = useStore($authToken); - const addlSocketOptions = useStore($socketOptions); - - const socketUrl = useMemo(() => { - const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; - if (baseUrl) { - return baseUrl.replace(/^https?:\/\//i, ''); - } - - return `${wsProtocol}://${window.location.host}`; - }, [baseUrl]); - - const socketOptions = useMemo(() => { - const options: Partial = { - timeout: 60000, - path: baseUrl ? '/ws/socket.io' : `${window.location.pathname}ws/socket.io`, - autoConnect: false, // achtung! removing this breaks the dynamic middleware - forceNew: true, - }; - - if (authToken) { - options.auth = { token: authToken }; - options.transports = ['websocket', 'polling']; - } - - return { ...options, ...addlSocketOptions }; - }, [authToken, addlSocketOptions, baseUrl]); - - useEffect(() => { - if ($isSocketInitialized.get()) { - // Singleton! - return; - } - - const socket: Socket = io(socketUrl, socketOptions); - setEventListeners({ dispatch, socket }); - socket.connect(); - - if ($isDebugging.get() || import.meta.env.MODE === 'development') { - window.$socketOptions = $socketOptions; - // This is only enabled manually for debugging, console is allowed. - /* eslint-disable-next-line no-console */ - console.log('Socket initialized', socket); - } - - $isSocketInitialized.set(true); - - return () => { - if ($isDebugging.get() || import.meta.env.MODE === 'development') { - window.$socketOptions = undefined; - // This is only enabled manually for debugging, console is allowed. - /* eslint-disable-next-line no-console */ - console.log('Socket teardown', socket); - } - socket.disconnect(); - $isSocketInitialized.set(false); - }; - }, [dispatch, socketOptions, socketUrl]); -}; 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/hooks/useSyncQueueStatus.ts b/invokeai/frontend/web/src/app/hooks/useSyncQueueStatus.ts deleted file mode 100644 index d6874c3bb5e..00000000000 --- a/invokeai/frontend/web/src/app/hooks/useSyncQueueStatus.ts +++ /dev/null @@ -1,25 +0,0 @@ -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'; - -/** - * 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 useSyncQueueStatus = () => { - const { queueSize } = useGetQueueStatusQuery(undefined, { - selectFromResult: (res) => ({ - queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0, - }), - }); - useEffect(() => { - document.title = queueSize > 0 ? `(${queueSize}) ${baseTitle}` : baseTitle; - const faviconEl = document.getElementById('invoke-favicon'); - if (faviconEl instanceof HTMLLinkElement) { - faviconEl.href = queueSize > 0 ? invokeAlertLogoSVG : invokeLogoSVG; - } - }, [queueSize]); -}; diff --git a/invokeai/frontend/web/src/app/logging/logger.ts b/invokeai/frontend/web/src/app/logging/logger.ts index c0de4e3685c..d20ef77090f 100644 --- a/invokeai/frontend/web/src/app/logging/logger.ts +++ b/invokeai/frontend/web/src/app/logging/logger.ts @@ -9,34 +9,35 @@ const serializeMessage: MessageSerializer = (message) => { }; ROARR.serializeMessage = serializeMessage; -ROARR.write = createLogWriter(); -export const BASE_CONTEXT = {}; +const BASE_CONTEXT = {}; -export const $logger = atom(Roarr.child(BASE_CONTEXT)); +const $logger = atom(Roarr.child(BASE_CONTEXT)); -export type LoggerNamespace = - | 'images' - | 'models' - | 'config' - | 'canvas' - | 'generation' - | 'nodes' - | 'system' - | 'socketio' - | 'session' - | 'queue' - | 'dnd' - | 'controlLayers'; +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: LoggerNamespace) => $logger.get().child({ namespace }); +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 -export const LOG_LEVEL_MAP: Record = { +const LOG_LEVEL_MAP: Record = { trace: 10, debug: 20, info: 30, @@ -44,3 +45,42 @@ export const LOG_LEVEL_MAP: Record = { 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/useLogger.ts b/invokeai/frontend/web/src/app/logging/useLogger.ts deleted file mode 100644 index 6e170ca3764..00000000000 --- a/invokeai/frontend/web/src/app/logging/useLogger.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createLogWriter } from '@roarr/browser-log-writer'; -import { useAppSelector } from 'app/store/storeHooks'; -import { useEffect, useMemo } from 'react'; -import { ROARR, Roarr } from 'roarr'; - -import type { LoggerNamespace } from './logger'; -import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP, logger } from './logger'; - -export const useLogger = (namespace: LoggerNamespace) => { - const consoleLogLevel = useAppSelector((s) => s.system.consoleLogLevel); - const shouldLogToConsole = useAppSelector((s) => s.system.shouldLogToConsole); - - // The provided Roarr browser log writer uses localStorage to config logging to console - useEffect(() => { - if (shouldLogToConsole) { - // Enable console log output - localStorage.setItem('ROARR_LOG', 'true'); - - // Use a filter to show only logs of the given level - localStorage.setItem('ROARR_FILTER', `context.logLevel:>=${LOG_LEVEL_MAP[consoleLogLevel]}`); - } else { - // Disable console log output - localStorage.setItem('ROARR_LOG', 'false'); - } - ROARR.write = createLogWriter(); - }, [consoleLogLevel, shouldLogToConsole]); - - // Update the module-scoped logger context as needed - useEffect(() => { - // TODO: type this properly - //eslint-disable-next-line @typescript-eslint/no-explicit-any - const newContext: Record = { - ...BASE_CONTEXT, - }; - - $logger.set(Roarr.child(newContext)); - }, []); - - const log = useMemo(() => logger(namespace), [namespace]); - - return log; -}; 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/actions.ts b/invokeai/frontend/web/src/app/store/actions.ts deleted file mode 100644 index 85debfc6077..00000000000 --- a/invokeai/frontend/web/src/app/store/actions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import type { InvokeTabName } from 'features/ui/store/tabMap'; - -export const enqueueRequested = createAction<{ - tabName: InvokeTabName; - prepend: boolean; -}>('app/enqueueRequested'); diff --git a/invokeai/frontend/web/src/app/store/constants.ts b/invokeai/frontend/web/src/app/store/constants.ts index 14a2c0b77f9..381f7f85d26 100644 --- a/invokeai/frontend/web/src/app/store/constants.ts +++ b/invokeai/frontend/web/src/app/store/constants.ts @@ -1,2 +1,2 @@ -export const STORAGE_PREFIX = '@@invokeai-'; 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 index 8e2559927ad..7e32afbd3c6 100644 --- a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts +++ b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts @@ -1,19 +1,18 @@ +import { objectEquals } from '@observ33r/object-equals'; import { createDraftSafeSelectorCreator, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'; -import type { GetSelectorsOptions } from '@reduxjs/toolkit/dist/entities/state_selectors'; -import { isEqual } from 'lodash-es'; /** - * A memoized selector creator that uses LRU cache and lodash's isEqual for equality check. + * 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: isEqual, + resultEqualityCheck: objectEquals, }, argsMemoize: lruMemoize, }); -export const getSelectorsOptions: GetSelectorsOptions = { +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 index 7196e1fceac..fdb25b37d2c 100644 --- a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts @@ -1,40 +1,211 @@ +import { logger } from 'app/logging/logger'; import { StorageError } from 'app/store/enhancers/reduxRemember/errors'; -import { $projectId } from 'app/store/nanostores/projectId'; import type { UseStore } from 'idb-keyval'; -import { clear, createStore as createIDBKeyValStore, get, set } from 'idb-keyval'; -import { atom } from 'nanostores'; +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'; -// Create a custom idb-keyval store (just needed to customize the name) -const $idbKeyValStore = atom(createIDBKeyValStore('invoke', 'invoke-store')); - -export const clearIdbKeyValStore = () => { - clear($idbKeyValStore.get()); -}; - -// Create redux-remember driver, wrapping idb-keyval -export const idbKeyValDriver: Driver = { - getItem: (key) => { - try { - return get(key, $idbKeyValStore.get()); - } catch (originalError) { - throw new StorageError({ - key, - projectId: $projectId.get(), - originalError, - }); - } - }, - setItem: (key, value) => { - try { - return set(key, value, $idbKeyValStore.get()); - } catch (originalError) { - throw new StorageError({ - key, - value, - projectId: $projectId.get(), - originalError, - }); - } - }, +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 index 9704c49cf2d..87c89b27f51 100644 --- a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/errors.ts +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/errors.ts @@ -1,5 +1,4 @@ import { logger } from 'app/logging/logger'; -import { parseify } from 'common/util/serialize'; import { PersistError, RehydrateError } from 'redux-remember'; import { serializeError } from 'serialize-error'; @@ -8,7 +7,6 @@ type StorageErrorArgs = { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ // any is correct value?: any; originalError?: unknown; - projectId?: string; }; export class StorageError extends Error { @@ -16,31 +14,28 @@ export class StorageError extends Error { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ // any is correct value?: any; originalError?: Error; - projectId?: string; - constructor({ key, value, originalError, projectId }: StorageErrorArgs) { + constructor({ key, value, originalError }: StorageErrorArgs) { super(`Error setting ${key}`); this.name = 'StorageSetError'; this.key = key; if (value !== undefined) { this.value = value; } - if (projectId !== undefined) { - this.projectId = projectId; - } if (originalError instanceof Error) { this.originalError = originalError; } } } +const log = logger('system'); + export const errorHandler = (err: PersistError | RehydrateError) => { - const log = logger('system'); 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: parseify(err) }, 'Problem in persistence layer'); + 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 index 89010275d19..04680b54e10 100644 --- a/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts +++ b/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts @@ -7,12 +7,23 @@ import { diff } from 'jsondiffpatch'; /** * Super simple logger middleware. Useful for debugging when the redux devtools are awkward. */ -export const debugLoggerMiddleware: Middleware = (api: MiddlewareAPI) => (next) => (action) => { - const originalState = api.getState(); - console.log('REDUX: dispatching', action); - const result = next(action); - const nextState = api.getState(); - console.log('REDUX: next state', nextState); - console.log('REDUX: diff', diff(originalState, nextState)); - return result; -}; +export const 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 index f0ea175aec7..3fba4fba0d0 100644 --- a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts @@ -1,22 +1,7 @@ import type { UnknownAction } from '@reduxjs/toolkit'; -import { deepClone } from 'common/util/deepClone'; -import { isAnyGraphBuilt } from 'features/nodes/store/actions'; import { appInfoApi } from 'services/api/endpoints/appInfo'; -import type { Graph } from 'services/api/types'; -import { socketGeneratorProgress } from 'services/events/actions'; export const actionSanitizer = (action: A): A => { - if (isAnyGraphBuilt(action)) { - if (action.payload.nodes) { - const sanitizedNodes: Graph['nodes'] = {}; - - return { - ...action, - payload: { ...action.payload, nodes: sanitizedNodes }, - }; - } - } - if (appInfoApi.endpoints.getOpenAPISchema.matchFulfilled(action)) { return { ...action, @@ -24,13 +9,5 @@ export const actionSanitizer = (action: A): A => { }; } - if (socketGeneratorProgress.match(action)) { - const sanitized = deepClone(action); - if (sanitized.payload.data.progress_image) { - sanitized.payload.data.progress_image.dataURL = ''; - } - return sanitized; - } - return action; }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts deleted file mode 100644 index 0fd2f1b79c9..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { TypedStartListening } from '@reduxjs/toolkit'; -import { createListenerMiddleware } from '@reduxjs/toolkit'; -import { addCommitStagingAreaImageListener } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener'; -import { addFirstListImagesListener } from 'app/store/middleware/listenerMiddleware/listeners/addFirstListImagesListener.ts'; -import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued'; -import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived'; -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 { addCanvasCopiedToClipboardListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard'; -import { addCanvasDownloadedAsImageListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage'; -import { addCanvasImageToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet'; -import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery'; -import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet'; -import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged'; -import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery'; -import { addControlAdapterPreprocessor } from 'app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor'; -import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess'; -import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed'; -import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas'; -import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear'; -import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes'; -import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked'; -import { addGetOpenAPISchemaListener } from 'app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema'; -import { addImageAddedToBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard'; -import { addRequestedSingleImageDeletionListener } from 'app/store/middleware/listenerMiddleware/listeners/imageDeleted'; -import { addImageDroppedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageDropped'; -import { addImageRemovedFromBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard'; -import { addImagesStarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesStarred'; -import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred'; -import { addImageToDeleteSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected'; -import { addImageUploadedFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageUploaded'; -import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected'; -import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded'; -import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged'; -import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMiddleware/listeners/setDefaultSettings'; -import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected'; -import { addSocketDisconnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected'; -import { addGeneratorProgressEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress'; -import { addInvocationCompleteEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete'; -import { addInvocationErrorEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError'; -import { addInvocationStartedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted'; -import { addModelInstallEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall'; -import { addModelLoadEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad'; -import { addSocketQueueItemStatusChangedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged'; -import { addStagingAreaImageSavedListener } from 'app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved'; -import { addUpdateAllNodesRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested'; -import { addUpscaleRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested'; -import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested'; -import type { AppDispatch, RootState } from 'app/store/store'; - -export const listenerMiddleware = createListenerMiddleware(); - -export type AppStartListening = TypedStartListening; - -const startAppListening = listenerMiddleware.startListening as AppStartListening; - -/** - * The RTK listener middleware is a lightweight alternative sagas/observables. - * - * Most side effect logic should live in a listener. - */ - -// Image uploaded -addImageUploadedFulfilledListener(startAppListening); - -// Image deleted -addRequestedSingleImageDeletionListener(startAppListening); -addDeleteBoardAndImagesFulfilledListener(startAppListening); -addImageToDeleteSelectedListener(startAppListening); - -// Image starred -addImagesStarredListener(startAppListening); -addImagesUnstarredListener(startAppListening); - -// Gallery -addGalleryImageClickedListener(startAppListening); - -// User Invoked -addEnqueueRequestedCanvasListener(startAppListening); -addEnqueueRequestedNodes(startAppListening); -addEnqueueRequestedLinear(startAppListening); -addAnyEnqueuedListener(startAppListening); -addBatchEnqueuedListener(startAppListening); - -// Canvas actions -addCanvasSavedToGalleryListener(startAppListening); -addCanvasMaskSavedToGalleryListener(startAppListening); -addCanvasImageToControlNetListener(startAppListening); -addCanvasMaskToControlNetListener(startAppListening); -addCanvasDownloadedAsImageListener(startAppListening); -addCanvasCopiedToClipboardListener(startAppListening); -addCanvasMergedListener(startAppListening); -addStagingAreaImageSavedListener(startAppListening); -addCommitStagingAreaImageListener(startAppListening); - -// Socket.IO -addGeneratorProgressEventListener(startAppListening); -addInvocationCompleteEventListener(startAppListening); -addInvocationErrorEventListener(startAppListening); -addInvocationStartedEventListener(startAppListening); -addSocketConnectedEventListener(startAppListening); -addSocketDisconnectedEventListener(startAppListening); -addModelLoadEventListener(startAppListening); -addModelInstallEventListener(startAppListening); -addSocketQueueItemStatusChangedEventListener(startAppListening); -addBulkDownloadListeners(startAppListening); - -// ControlNet -addControlNetImageProcessedListener(startAppListening); -addControlNetAutoProcessListener(startAppListening); - -// Boards -addImageAddedToBoardFulfilledListener(startAppListening); -addImageRemovedFromBoardFulfilledListener(startAppListening); -addBoardIdSelectedListener(startAppListening); - -// Node schemas -addGetOpenAPISchemaListener(startAppListening); - -// Workflows -addWorkflowLoadRequestedListener(startAppListening); -addUpdateAllNodesRequestedListener(startAppListening); - -// DND -addImageDroppedListener(startAppListening); - -// Models -addModelSelectedListener(startAppListening); - -// app startup -addAppStartedListener(startAppListening); -addModelsLoadedListener(startAppListening); -addAppConfigReceivedListener(startAppListening); -addFirstListImagesListener(startAppListening); - -// Ad-hoc upscale workflwo -addUpscaleRequestedListener(startAppListening); - -// Prompts -addDynamicPromptsListener(startAppListening); - -addSetDefaultSettingsListener(startAppListening); -addControlAdapterPreprocessor(startAppListening); 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/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts deleted file mode 100644 index 9095a08431e..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { isAnyOf } from '@reduxjs/toolkit'; -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { - canvasBatchIdsReset, - commitStagingAreaImage, - discardStagedImages, - resetCanvas, - setInitialCanvasImage, -} from 'features/canvas/store/canvasSlice'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { queueApi } from 'services/api/endpoints/queue'; - -const matcher = isAnyOf(commitStagingAreaImage, discardStagedImages, resetCanvas, setInitialCanvasImage); - -export const addCommitStagingAreaImageListener = (startAppListening: AppStartListening) => { - startAppListening({ - matcher, - effect: async (_, { dispatch, getState }) => { - const log = logger('canvas'); - const state = getState(); - const { batchIds } = state.canvas; - - try { - const req = dispatch( - queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: batchIds }, { fixedCacheKey: 'cancelByBatchIds' }) - ); - const { canceled } = await req.unwrap(); - req.reset(); - if (canceled > 0) { - log.debug(`Canceled ${canceled} canvas batches`); - toast({ - id: 'CANCEL_BATCH_SUCCEEDED', - title: t('queue.cancelBatchSucceeded'), - status: 'success', - }); - } - dispatch(canvasBatchIdsReset()); - } catch { - log.error('Failed to cancel canvas batches'); - toast({ - id: 'CANCEL_BATCH_FAILED', - title: t('queue.cancelBatchFailed'), - status: 'error', - }); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addFirstListImagesListener.ts.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addFirstListImagesListener.ts.ts deleted file mode 100644 index 3f831de5c6d..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addFirstListImagesListener.ts.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { imageSelected } from 'features/gallery/store/gallerySlice'; -import { IMAGE_CATEGORIES } from 'features/gallery/store/types'; -import { imagesApi } from 'services/api/endpoints/images'; -import type { ImageCache } from 'services/api/types'; -import { getListImagesUrl, imagesSelectors } from 'services/api/util'; - -export const addFirstListImagesListener = (startAppListening: AppStartListening) => { - startAppListening({ - matcher: imagesApi.endpoints.listImages.matchFulfilled, - effect: async (action, { dispatch, unsubscribe, cancelActiveListeners }) => { - // Only run this listener on the first listImages request for no-board images - if (action.meta.arg.queryCacheKey !== getListImagesUrl({ board_id: 'none', categories: IMAGE_CATEGORIES })) { - return; - } - - // this should only run once - cancelActiveListeners(); - unsubscribe(); - - // TODO: figure out how to type the predicate - const data = action.payload as ImageCache; - - if (data.ids.length > 0) { - // Select the first image - const firstImage = imagesSelectors.selectAll(data)[0]; - dispatch(imageSelected(firstImage ?? null)); - } - }, - }); -}; 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 index 373fa3dd28c..1d744f8581f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/anyEnqueued.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/anyEnqueued.ts @@ -1,10 +1,10 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +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: async (_, { dispatch, getState }) => { + effect: (_, { dispatch, getState }) => { const { data } = selectQueueStatus(getState()); if (!data || data.processor.is_started) { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts deleted file mode 100644 index 4ee73af6423..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appConfigReceived.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { setInfillMethod } from 'features/parameters/store/generationSlice'; -import { shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged } from 'features/system/store/systemSlice'; -import { appInfoApi } from 'services/api/endpoints/appInfo'; - -export const addAppConfigReceivedListener = (startAppListening: AppStartListening) => { - startAppListening({ - matcher: appInfoApi.endpoints.getAppConfig.matchFulfilled, - effect: async (action, { getState, dispatch }) => { - const { infill_methods = [], nsfw_methods = [], watermarking_methods = [] } = action.payload; - const infillMethod = getState().generation.infillMethod; - - if (!infill_methods.includes(infillMethod)) { - // if there is no infill method, set it to the first one - // if there is no first one... god help us - dispatch(setInfillMethod(infill_methods[0] as string)); - } - - if (!nsfw_methods.includes('nsfw_checker')) { - dispatch(shouldUseNSFWCheckerChanged(false)); - } - - if (!watermarking_methods.includes('invisible_watermark')) { - dispatch(shouldUseWatermarkerChanged(false)); - } - }, - }); -}; 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 index 729067ee825..b1d60edc2dc 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/appStarted.ts @@ -1,15 +1,51 @@ import { createAction } from '@reduxjs/toolkit'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +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 }) => { + 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 index 3f74bf9b612..fae7436d2e1 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts @@ -1,27 +1,30 @@ import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { parseify } from 'common/util/serialize'; +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 { truncate, upperFirst } from 'lodash-es'; +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: async (action) => { - const response = action.payload; + effect: (action) => { + const enqueueResult = action.payload; const arg = action.meta.arg.originalArgs; - logger('queue').debug({ enqueueResult: parseify(response) }, 'Batch enqueued'); + log.debug({ enqueueResult } as JsonObject, 'Batch enqueued'); toast({ id: 'QUEUE_BATCH_SUCCEEDED', title: t('queue.batchQueued'), status: 'success', description: t('queue.batchQueuedDesc', { - count: response.enqueued, + count: enqueueResult.enqueued, direction: arg.prepend ? t('queue.front') : t('queue.back'), }), }); @@ -31,9 +34,9 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) = // error startAppListening({ matcher: queueApi.endpoints.enqueueBatch.matchRejected, - effect: async (action) => { + effect: (action) => { const response = action.payload; - const arg = action.meta.arg.originalArgs; + const batchConfig = action.meta.arg.originalArgs; if (!response) { toast({ @@ -42,22 +45,19 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) = status: 'error', description: t('common.unknownError'), }); - logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue')); + 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: truncate(upperFirst(e.msg), { length: 128 }), + title: t('queue.batchFailedToQueue'), status: 'error', - description: truncate( - `Path: - ${e.loc.join('.')}`, - { length: 128 } - ), + description, }); }); } else if (response.status !== 403) { @@ -68,7 +68,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) = description: t('common.unknownError'), }); } - logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue')); + 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 index 244e0cdf8a0..d185a03f220 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -1,47 +1,35 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { resetCanvas } from 'features/canvas/store/canvasSlice'; -import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice'; -import { getImageUsage } from 'features/deleteImageModal/store/selectors'; +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: async (action, { dispatch, getState }) => { + effect: (action, { dispatch, getState }) => { const { deleted_images } = action.payload; // Remove all deleted images from the UI - let wasCanvasReset = false; let wasNodeEditorReset = false; - let wereControlAdaptersReset = false; - let wereControlLayersReset = false; - const { canvas, nodes, controlAdapters, controlLayers } = getState(); - deleted_images.forEach((image_name) => { - const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, controlLayers.present, image_name); + const state = getState(); + const nodes = selectNodesSlice(state); + const canvas = selectCanvasSlice(state); + const upscale = selectUpscaleSlice(state); + const refImages = selectRefImagesSlice(state); - if (imageUsage.isCanvasImage && !wasCanvasReset) { - dispatch(resetCanvas()); - wasCanvasReset = true; - } + deleted_images.forEach((image_name) => { + const imageUsage = getImageUsage(nodes, canvas, upscale, refImages, image_name); if (imageUsage.isNodesImage && !wasNodeEditorReset) { dispatch(nodeEditorReset()); wasNodeEditorReset = true; } - - if (imageUsage.isControlImage && !wereControlAdaptersReset) { - dispatch(controlAdaptersReset()); - wereControlAdaptersReset = true; - } - - if (imageUsage.isControlLayerImage && !wereControlLayersReset) { - dispatch(allLayersDeleted()); - wereControlLayersReset = 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 index 2c1aa6ec8b5..9fd777fb29b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts @@ -1,9 +1,8 @@ import { isAnyOf } from '@reduxjs/toolkit'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +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 { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types'; import { imagesApi } from 'services/api/endpoints/images'; -import { imagesSelectors } from 'services/api/util'; export const addBoardIdSelectedListener = (startAppListening: AppStartListening) => { startAppListening({ @@ -12,42 +11,34 @@ export const addBoardIdSelectedListener = (startAppListening: AppStartListening) // Cancel any in-progress instances of this listener, we don't want to select an image from a previous board cancelActiveListeners(); - const state = getState(); - - const board_id = boardIdSelected.match(action) ? action.payload.boardId : state.gallery.selectedBoardId; - - const galleryView = galleryViewChanged.match(action) ? action.payload : state.gallery.galleryView; + if (boardIdSelected.match(action) && action.payload.select) { + // This action already has a resource selection - skip the below auto-selection logic + return; + } - // when a board is selected, we need to wait until the board has loaded *some* images, then select the first one - const categories = galleryView === 'images' ? IMAGE_CATEGORIES : ASSETS_CATEGORIES; + const state = getState(); - const queryArgs = { board_id: board_id ?? 'none', categories }; + 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.listImages.select(queryArgs)(getState()).isSuccess, + () => imagesApi.endpoints.getImageNames.select(queryArgs)(getState()).isSuccess, 5000 ); - if (isSuccess) { - // the board was just changed - we can select the first image - const { data: boardImagesData } = imagesApi.endpoints.listImages.select(queryArgs)(getState()); - - if (boardImagesData && boardIdSelected.match(action) && action.payload.selectedImageName) { - const selectedImage = imagesSelectors.selectById(boardImagesData, action.payload.selectedImageName); - dispatch(imageSelected(selectedImage || null)); - } else if (boardImagesData) { - const firstImage = imagesSelectors.selectAll(boardImagesData)[0]; - dispatch(imageSelected(firstImage || null)); - } else { - // board has no images - deselect - dispatch(imageSelected(null)); - } - } else { - // fallback - deselect + 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 index 489f218370c..fa4c29b8f42 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx @@ -1,27 +1,25 @@ -import { ExternalLink } from '@invoke-ai/ui-library'; import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import type { AppStartListening } from 'app/store/store'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; -import { - socketBulkDownloadComplete, - socketBulkDownloadError, - socketBulkDownloadStarted, -} from 'services/events/actions'; -const log = logger('images'); +const log = logger('gallery'); export const addBulkDownloadListeners = (startAppListening: AppStartListening) => { startAppListening({ matcher: imagesApi.endpoints.bulkDownloadImages.matchFulfilled, - effect: async (action) => { + effect: (action) => { log.debug(action.payload, 'Bulk download requested'); - // If we have an item name, we are processing the bulk download locally and should use it as the toast id to - // prevent multiple toasts for the same item. + // 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: action.payload.bulk_download_item_name ?? undefined, + id: itemName ? `preparing:${itemName}` : undefined, title: t('gallery.bulkDownloadRequested'), status: 'success', // Show the response message if it exists, otherwise show the default message @@ -33,7 +31,7 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) = startAppListening({ matcher: imagesApi.endpoints.bulkDownloadImages.matchRejected, - effect: async () => { + effect: () => { log.debug('Bulk download request failed'); // There isn't any toast to update if we get this event. @@ -44,55 +42,4 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) = }); }, }); - - startAppListening({ - actionCreator: socketBulkDownloadStarted, - effect: async (action) => { - // This should always happen immediately after the bulk download request, so we don't need to show a toast here. - log.debug(action.payload.data, 'Bulk download preparation started'); - }, - }); - - startAppListening({ - actionCreator: socketBulkDownloadComplete, - effect: async (action) => { - log.debug(action.payload.data, 'Bulk download preparation completed'); - - const { bulk_download_item_name } = action.payload.data; - - // TODO(psyche): This URL may break in in some environments (e.g. Nvidia workbench) but we need to test it first - const url = `/api/v1/images/download/${bulk_download_item_name}`; - - toast({ - id: bulk_download_item_name, - title: t('gallery.bulkDownloadReady', 'Download ready'), - status: 'success', - description: ( - - ), - duration: null, - }); - }, - }); - - startAppListening({ - actionCreator: socketBulkDownloadError, - effect: async (action) => { - log.debug(action.payload.data, 'Bulk download preparation failed'); - - const { bulk_download_item_name } = action.payload.data; - - toast({ - id: bulk_download_item_name, - title: t('gallery.bulkDownloadFailed'), - status: 'error', - description: action.payload.data.error, - duration: null, - }); - }, - }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts deleted file mode 100644 index 311dda3e2e3..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { $logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { canvasCopiedToClipboard } from 'features/canvas/store/actions'; -import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; -import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; - -export const addCanvasCopiedToClipboardListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: canvasCopiedToClipboard, - effect: async (action, { getState }) => { - const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' }); - const state = getState(); - - try { - const blob = getBaseLayerBlob(state); - - copyBlobToClipboard(blob); - } catch (err) { - moduleLog.error(String(err)); - toast({ - id: 'CANVAS_COPY_FAILED', - title: t('toast.problemCopyingCanvas'), - description: t('toast.problemCopyingCanvasDesc'), - status: 'error', - }); - return; - } - - toast({ - id: 'CANVAS_COPY_SUCCEEDED', - title: t('toast.canvasCopiedClipboard'), - status: 'success', - }); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts deleted file mode 100644 index 71e616b9eaa..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { $logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { canvasDownloadedAsImage } from 'features/canvas/store/actions'; -import { downloadBlob } from 'features/canvas/util/downloadBlob'; -import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; - -export const addCanvasDownloadedAsImageListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: canvasDownloadedAsImage, - effect: async (action, { getState }) => { - const moduleLog = $logger.get().child({ namespace: 'canvasSavedToGalleryListener' }); - const state = getState(); - - let blob; - try { - blob = await getBaseLayerBlob(state); - } catch (err) { - moduleLog.error(String(err)); - toast({ - id: 'CANVAS_DOWNLOAD_FAILED', - title: t('toast.problemDownloadingCanvas'), - description: t('toast.problemDownloadingCanvasDesc'), - status: 'error', - }); - return; - } - - downloadBlob(blob, 'canvas.png'); - toast({ id: 'CANVAS_DOWNLOAD_SUCCEEDED', title: t('toast.canvasDownloaded'), status: 'success' }); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts deleted file mode 100644 index 2aa1f52d6cb..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { canvasImageToControlAdapter } from 'features/canvas/store/actions'; -import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; -import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { imagesApi } from 'services/api/endpoints/images'; - -export const addCanvasImageToControlNetListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: canvasImageToControlAdapter, - effect: async (action, { dispatch, getState }) => { - const log = logger('canvas'); - const state = getState(); - const { id } = action.payload; - - let blob: Blob; - try { - blob = await getBaseLayerBlob(state, true); - } catch (err) { - log.error(String(err)); - toast({ - id: 'PROBLEM_SAVING_CANVAS', - title: t('toast.problemSavingCanvas'), - description: t('toast.problemSavingCanvasDesc'), - status: 'error', - }); - return; - } - - const { autoAddBoardId } = state.gallery; - - const imageDTO = await dispatch( - imagesApi.endpoints.uploadImage.initiate({ - file: new File([blob], 'savedCanvas.png', { - type: 'image/png', - }), - image_category: 'control', - is_intermediate: true, - board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId, - crop_visible: false, - postUploadAction: { - type: 'TOAST', - title: t('toast.canvasSentControlnetAssets'), - }, - }) - ).unwrap(); - - const { image_name } = imageDTO; - - dispatch( - controlAdapterImageChanged({ - id, - controlImage: image_name, - }) - ); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts deleted file mode 100644 index 454342b997b..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { canvasMaskSavedToGallery } from 'features/canvas/store/actions'; -import { getCanvasData } from 'features/canvas/util/getCanvasData'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { imagesApi } from 'services/api/endpoints/images'; - -export const addCanvasMaskSavedToGalleryListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: canvasMaskSavedToGallery, - effect: async (action, { dispatch, getState }) => { - const log = logger('canvas'); - const state = getState(); - - const canvasBlobsAndImageData = await getCanvasData( - state.canvas.layerState, - state.canvas.boundingBoxCoordinates, - state.canvas.boundingBoxDimensions, - state.canvas.isMaskEnabled, - state.canvas.shouldPreserveMaskedArea - ); - - if (!canvasBlobsAndImageData) { - return; - } - - const { maskBlob } = canvasBlobsAndImageData; - - if (!maskBlob) { - log.error('Problem getting mask layer blob'); - toast({ - id: 'PROBLEM_SAVING_MASK', - title: t('toast.problemSavingMask'), - description: t('toast.problemSavingMaskDesc'), - status: 'error', - }); - return; - } - - const { autoAddBoardId } = state.gallery; - - dispatch( - imagesApi.endpoints.uploadImage.initiate({ - file: new File([maskBlob], 'canvasMaskImage.png', { - type: 'image/png', - }), - image_category: 'mask', - is_intermediate: false, - board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId, - crop_visible: true, - postUploadAction: { - type: 'TOAST', - title: t('toast.maskSavedAssets'), - }, - }) - ); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts deleted file mode 100644 index 2e6ca61d8ae..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { canvasMaskToControlAdapter } from 'features/canvas/store/actions'; -import { getCanvasData } from 'features/canvas/util/getCanvasData'; -import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { imagesApi } from 'services/api/endpoints/images'; - -export const addCanvasMaskToControlNetListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: canvasMaskToControlAdapter, - effect: async (action, { dispatch, getState }) => { - const log = logger('canvas'); - const state = getState(); - const { id } = action.payload; - const canvasBlobsAndImageData = await getCanvasData( - state.canvas.layerState, - state.canvas.boundingBoxCoordinates, - state.canvas.boundingBoxDimensions, - state.canvas.isMaskEnabled, - state.canvas.shouldPreserveMaskedArea - ); - - if (!canvasBlobsAndImageData) { - return; - } - - const { maskBlob } = canvasBlobsAndImageData; - - if (!maskBlob) { - log.error('Problem getting mask layer blob'); - toast({ - id: 'PROBLEM_IMPORTING_MASK', - title: t('toast.problemImportingMask'), - description: t('toast.problemImportingMaskDesc'), - status: 'error', - }); - return; - } - - const { autoAddBoardId } = state.gallery; - - const imageDTO = await dispatch( - imagesApi.endpoints.uploadImage.initiate({ - file: new File([maskBlob], 'canvasMaskImage.png', { - type: 'image/png', - }), - image_category: 'mask', - is_intermediate: true, - board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId, - crop_visible: false, - postUploadAction: { - type: 'TOAST', - title: t('toast.maskSentControlnetAssets'), - }, - }) - ).unwrap(); - - const { image_name } = imageDTO; - - dispatch( - controlAdapterImageChanged({ - id, - controlImage: image_name, - }) - ); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts deleted file mode 100644 index 9ae6de2e760..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { $logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { canvasMerged } from 'features/canvas/store/actions'; -import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore'; -import { setMergedCanvas } from 'features/canvas/store/canvasSlice'; -import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { imagesApi } from 'services/api/endpoints/images'; - -export const addCanvasMergedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: canvasMerged, - effect: async (action, { dispatch }) => { - const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' }); - const blob = await getFullBaseLayerBlob(); - - if (!blob) { - moduleLog.error('Problem getting base layer blob'); - toast({ - id: 'PROBLEM_MERGING_CANVAS', - title: t('toast.problemMergingCanvas'), - description: t('toast.problemMergingCanvasDesc'), - status: 'error', - }); - return; - } - - const canvasBaseLayer = $canvasBaseLayer.get(); - - if (!canvasBaseLayer) { - moduleLog.error('Problem getting canvas base layer'); - toast({ - id: 'PROBLEM_MERGING_CANVAS', - title: t('toast.problemMergingCanvas'), - description: t('toast.problemMergingCanvasDesc'), - status: 'error', - }); - return; - } - - const baseLayerRect = canvasBaseLayer.getClientRect({ - relativeTo: canvasBaseLayer.getParent() ?? undefined, - }); - - const imageDTO = await dispatch( - imagesApi.endpoints.uploadImage.initiate({ - file: new File([blob], 'mergedCanvas.png', { - type: 'image/png', - }), - image_category: 'general', - is_intermediate: true, - postUploadAction: { - type: 'TOAST', - title: t('toast.canvasMerged'), - }, - }) - ).unwrap(); - - // TODO: I can't figure out how to do the type narrowing in the `take()` so just brute forcing it here - const { image_name } = imageDTO; - - dispatch( - setMergedCanvas({ - kind: 'image', - layer: 'base', - imageName: image_name, - ...baseLayerRect, - }) - ); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts deleted file mode 100644 index 71586b5f6ea..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { parseify } from 'common/util/serialize'; -import { canvasSavedToGallery } from 'features/canvas/store/actions'; -import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { imagesApi } from 'services/api/endpoints/images'; - -export const addCanvasSavedToGalleryListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: canvasSavedToGallery, - effect: async (action, { dispatch, getState }) => { - const log = logger('canvas'); - const state = getState(); - - let blob; - try { - blob = await getBaseLayerBlob(state); - } catch (err) { - log.error(String(err)); - toast({ - id: 'CANVAS_SAVE_FAILED', - title: t('toast.problemSavingCanvas'), - description: t('toast.problemSavingCanvasDesc'), - status: 'error', - }); - return; - } - - const { autoAddBoardId } = state.gallery; - - dispatch( - imagesApi.endpoints.uploadImage.initiate({ - file: new File([blob], 'savedCanvas.png', { - type: 'image/png', - }), - image_category: 'general', - is_intermediate: false, - board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId, - crop_visible: true, - postUploadAction: { - type: 'TOAST', - title: t('toast.canvasSavedGallery'), - }, - metadata: { - _canvas_objects: parseify(state.canvas.layerState.objects), - }, - }) - ); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts deleted file mode 100644 index a1eb917ebb3..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { isAnyOf } from '@reduxjs/toolkit'; -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import type { AppDispatch } from 'app/store/store'; -import { parseify } from 'common/util/serialize'; -import { - caLayerImageChanged, - caLayerModelChanged, - caLayerProcessedImageChanged, - caLayerProcessorConfigChanged, - caLayerProcessorPendingBatchIdChanged, - caLayerRecalled, - isControlAdapterLayer, -} from 'features/controlLayers/store/controlLayersSlice'; -import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { isEqual } from 'lodash-es'; -import { getImageDTO } from 'services/api/endpoints/images'; -import { queueApi } from 'services/api/endpoints/queue'; -import type { BatchConfig } from 'services/api/types'; -import { socketInvocationComplete } from 'services/events/actions'; -import { assert } from 'tsafe'; - -const matcher = isAnyOf( - caLayerImageChanged, - caLayerProcessedImageChanged, - caLayerProcessorConfigChanged, - caLayerModelChanged, - caLayerRecalled -); - -const DEBOUNCE_MS = 300; -const log = logger('session'); - -/** - * Simple helper to cancel a batch and reset the pending batch ID - */ -const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batchId: string) => { - const req = dispatch(queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: [batchId] })); - log.trace({ batchId }, 'Cancelling existing preprocessor batch'); - try { - await req.unwrap(); - } catch { - // no-op - } finally { - req.reset(); - // Always reset the pending batch ID - the cancel req could fail if the batch doesn't exist - dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null })); - } -}; - -export const addControlAdapterPreprocessor = (startAppListening: AppStartListening) => { - startAppListening({ - matcher, - effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take, signal }) => { - const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId; - const state = getState(); - const originalState = getOriginalState(); - - // Cancel any in-progress instances of this listener - cancelActiveListeners(); - log.trace('Control Layer CA auto-process triggered'); - - // Delay before starting actual work - await delay(DEBOUNCE_MS); - - const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); - - if (!layer) { - return; - } - - // We should only process if the processor settings or image have changed - const originalLayer = originalState.controlLayers.present.layers - .filter(isControlAdapterLayer) - .find((l) => l.id === layerId); - const originalImage = originalLayer?.controlAdapter.image; - const originalConfig = originalLayer?.controlAdapter.processorConfig; - - const image = layer.controlAdapter.image; - const processedImage = layer.controlAdapter.processedImage; - const config = layer.controlAdapter.processorConfig; - - if (isEqual(config, originalConfig) && isEqual(image, originalImage) && processedImage) { - // Neither config nor image have changed, we can bail - return; - } - - if (!image || !config) { - // - If we have no image, we have nothing to process - // - If we have no processor config, we have nothing to process - // Clear the processed image and bail - dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null })); - return; - } - - // At this point, the user has stopped fiddling with the processor settings and there is a processor selected. - - // If there is a pending processor batch, cancel it. - if (layer.controlAdapter.processorPendingBatchId) { - cancelProcessorBatch(dispatch, layerId, layer.controlAdapter.processorPendingBatchId); - } - - // TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now - const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config as never); - const enqueueBatchArg: BatchConfig = { - prepend: true, - batch: { - graph: { - nodes: { - [processorNode.id]: { - ...processorNode, - // Control images are always intermediate - do not save to gallery - is_intermediate: true, - }, - }, - edges: [], - }, - runs: 1, - }, - }; - - // Kick off the processor batch - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, { - fixedCacheKey: 'enqueueBatch', - }) - ); - - try { - const enqueueResult = await req.unwrap(); - // TODO(psyche): Update the pydantic models, pretty sure we will _always_ have a batch_id here, but the model says it's optional - assert(enqueueResult.batch.batch_id, 'Batch ID not returned from queue'); - dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id })); - log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued')); - - // Wait for the processor node to complete - const [invocationCompleteAction] = await take( - (action): action is ReturnType => - socketInvocationComplete.match(action) && - action.payload.data.batch_id === enqueueResult.batch.batch_id && - action.payload.data.invocation_source_id === processorNode.id - ); - - // We still have to check the output type - assert( - invocationCompleteAction.payload.data.result.type === 'image_output', - `Processor did not return an image output, got: ${invocationCompleteAction.payload.data.result}` - ); - const { image_name } = invocationCompleteAction.payload.data.result.image; - - const imageDTO = await getImageDTO(image_name); - assert(imageDTO, "Failed to fetch processor output's image DTO"); - - // Whew! We made it. Update the layer with the processed image - log.debug({ layerId, imageDTO }, 'ControlNet image processed'); - dispatch(caLayerProcessedImageChanged({ layerId, imageDTO })); - dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null })); - } catch (error) { - if (signal.aborted) { - // The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now). - const pendingBatchId = getState() - .controlLayers.present.layers.filter(isControlAdapterLayer) - .find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId; - if (pendingBatchId) { - cancelProcessorBatch(dispatch, layerId, pendingBatchId); - } - log.trace('Control Adapter preprocessor cancelled'); - } else { - // Some other error condition... - log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue')); - - if (error instanceof Object) { - if ('data' in error && 'status' in error) { - if (error.status === 403) { - dispatch(caLayerImageChanged({ layerId, imageDTO: null })); - return; - } - } - } - - toast({ - id: 'GRAPH_QUEUE_FAILED', - title: t('queue.graphFailedToQueue'), - status: 'error', - }); - } - } finally { - req.reset(); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts deleted file mode 100644 index e52df30681c..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { AnyListenerPredicate } from '@reduxjs/toolkit'; -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import type { RootState } from 'app/store/store'; -import { controlAdapterImageProcessed } from 'features/controlAdapters/store/actions'; -import { - controlAdapterAutoConfigToggled, - controlAdapterImageChanged, - controlAdapterModelChanged, - controlAdapterProcessorParamsChanged, - controlAdapterProcessortTypeChanged, - selectControlAdapterById, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; - -type AnyControlAdapterParamChangeAction = - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType; - -const predicate: AnyListenerPredicate = (action, state, prevState) => { - const isActionMatched = - controlAdapterProcessorParamsChanged.match(action) || - controlAdapterModelChanged.match(action) || - controlAdapterImageChanged.match(action) || - controlAdapterProcessortTypeChanged.match(action) || - controlAdapterAutoConfigToggled.match(action); - - if (!isActionMatched) { - return false; - } - - const { id } = action.payload; - const prevCA = selectControlAdapterById(prevState.controlAdapters, id); - const ca = selectControlAdapterById(state.controlAdapters, id); - if (!prevCA || !isControlNetOrT2IAdapter(prevCA) || !ca || !isControlNetOrT2IAdapter(ca)) { - return false; - } - - if (controlAdapterAutoConfigToggled.match(action)) { - // do not process if the user just disabled auto-config - if (prevCA.shouldAutoConfig === true) { - return false; - } - } - - const { controlImage, processorType, shouldAutoConfig } = ca; - if (controlAdapterModelChanged.match(action) && !shouldAutoConfig) { - // do not process if the action is a model change but the processor settings are dirty - return false; - } - - const isProcessorSelected = processorType !== 'none'; - - const hasControlImage = Boolean(controlImage); - - return isProcessorSelected && hasControlImage; -}; - -const DEBOUNCE_MS = 300; - -/** - * Listener that automatically processes a ControlNet image when its processor parameters are changed. - * - * The network request is debounced. - */ -export const addControlNetAutoProcessListener = (startAppListening: AppStartListening) => { - startAppListening({ - predicate, - effect: async (action, { dispatch, cancelActiveListeners, delay }) => { - const log = logger('session'); - const { id } = (action as AnyControlAdapterParamChangeAction).payload; - - // Cancel any in-progress instances of this listener - cancelActiveListeners(); - log.trace('ControlNet auto-process triggered'); - // Delay before starting actual work - await delay(DEBOUNCE_MS); - - dispatch(controlAdapterImageProcessed({ id })); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts deleted file mode 100644 index 574dad00eb6..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { parseify } from 'common/util/serialize'; -import { controlAdapterImageProcessed } from 'features/controlAdapters/store/actions'; -import { - controlAdapterImageChanged, - controlAdapterProcessedImageChanged, - pendingControlImagesCleared, - selectControlAdapterById, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { imagesApi } from 'services/api/endpoints/images'; -import { queueApi } from 'services/api/endpoints/queue'; -import type { BatchConfig, ImageDTO } from 'services/api/types'; -import { socketInvocationComplete } from 'services/events/actions'; - -export const addControlNetImageProcessedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: controlAdapterImageProcessed, - effect: async (action, { dispatch, getState, take }) => { - const log = logger('session'); - const { id } = action.payload; - const ca = selectControlAdapterById(getState().controlAdapters, id); - - if (!ca?.controlImage || !isControlNetOrT2IAdapter(ca)) { - log.error('Unable to process ControlNet image'); - return; - } - - if (ca.processorType === 'none' || ca.processorNode.type === 'none') { - return; - } - - // ControlNet one-off procressing graph is just the processor node, no edges. - // Also we need to grab the image. - - const nodeId = ca.processorNode.id; - const enqueueBatchArg: BatchConfig = { - prepend: true, - batch: { - graph: { - nodes: { - [ca.processorNode.id]: { - ...ca.processorNode, - is_intermediate: true, - use_cache: false, - image: { image_name: ca.controlImage }, - }, - }, - edges: [], - }, - runs: 1, - }, - }; - - try { - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, { - fixedCacheKey: 'enqueueBatch', - }) - ); - const enqueueResult = await req.unwrap(); - req.reset(); - log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued')); - - const [invocationCompleteAction] = await take( - (action): action is ReturnType => - socketInvocationComplete.match(action) && - action.payload.data.batch_id === enqueueResult.batch.batch_id && - action.payload.data.invocation_source_id === nodeId - ); - - // We still have to check the output type - if (invocationCompleteAction.payload.data.result.type === 'image_output') { - const { image_name } = invocationCompleteAction.payload.data.result.image; - - // Wait for the ImageDTO to be received - const [{ payload }] = await take( - (action) => - imagesApi.endpoints.getImageDTO.matchFulfilled(action) && action.payload.image_name === image_name - ); - - const processedControlImage = payload as ImageDTO; - - log.debug({ controlNetId: action.payload, processedControlImage }, 'ControlNet image processed'); - - // Update the processed image in the store - dispatch( - controlAdapterProcessedImageChanged({ - id, - processedControlImage: processedControlImage.image_name, - }) - ); - } - } catch (error) { - log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue')); - - if (error instanceof Object) { - if ('data' in error && 'status' in error) { - if (error.status === 403) { - dispatch(pendingControlImagesCleared()); - dispatch(controlAdapterImageChanged({ id, controlImage: null })); - return; - } - } - } - - toast({ - id: 'GRAPH_QUEUE_FAILED', - title: t('queue.graphFailedToQueue'), - status: 'error', - }); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts deleted file mode 100644 index a7491ab01b8..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { logger } from 'app/logging/logger'; -import { enqueueRequested } from 'app/store/actions'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; -import { parseify } from 'common/util/serialize'; -import { canvasBatchIdAdded, stagingAreaInitialized } from 'features/canvas/store/canvasSlice'; -import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; -import { getCanvasData } from 'features/canvas/util/getCanvasData'; -import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode'; -import { canvasGraphBuilt } from 'features/nodes/store/actions'; -import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; -import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph'; -import { imagesApi } from 'services/api/endpoints/images'; -import { queueApi } from 'services/api/endpoints/queue'; -import type { ImageDTO } from 'services/api/types'; - -/** - * This listener is responsible invoking the canvas. This involves a number of steps: - * - * 1. Generate image blobs from the canvas layers - * 2. Determine the generation mode from the layers (txt2img, img2img, inpaint) - * 3. Build the canvas graph - * 4. Create the session with the graph - * 5. Upload the init image if necessary - * 6. Upload the mask image if necessary - * 7. Update the init and mask images with the session ID - * 8. Initialize the staging area if not yet initialized - * 9. Dispatch the sessionReadyToInvoke action to invoke the session - */ -export const addEnqueueRequestedCanvasListener = (startAppListening: AppStartListening) => { - startAppListening({ - predicate: (action): action is ReturnType => - enqueueRequested.match(action) && action.payload.tabName === 'canvas', - effect: async (action, { getState, dispatch }) => { - const log = logger('queue'); - const { prepend } = action.payload; - const state = getState(); - - const { layerState, boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, shouldPreserveMaskedArea } = - state.canvas; - - // Build canvas blobs - const canvasBlobsAndImageData = await getCanvasData( - layerState, - boundingBoxCoordinates, - boundingBoxDimensions, - isMaskEnabled, - shouldPreserveMaskedArea - ); - - if (!canvasBlobsAndImageData) { - log.error('Unable to create canvas data'); - return; - } - - const { baseBlob, baseImageData, maskBlob, maskImageData } = canvasBlobsAndImageData; - - // Determine the generation mode - const generationMode = getCanvasGenerationMode(baseImageData, maskImageData); - - if (state.system.enableImageDebugging) { - const baseDataURL = await blobToDataURL(baseBlob); - const maskDataURL = await blobToDataURL(maskBlob); - openBase64ImageInTab([ - { base64: maskDataURL, caption: 'mask b64' }, - { base64: baseDataURL, caption: 'image b64' }, - ]); - } - - log.debug(`Generation mode: ${generationMode}`); - - // Temp placeholders for the init and mask images - let canvasInitImage: ImageDTO | undefined; - let canvasMaskImage: ImageDTO | undefined; - - // For img2img and inpaint/outpaint, we need to upload the init images - if (['img2img', 'inpaint', 'outpaint'].includes(generationMode)) { - // upload the image, saving the request id - canvasInitImage = await dispatch( - imagesApi.endpoints.uploadImage.initiate({ - file: new File([baseBlob], 'canvasInitImage.png', { - type: 'image/png', - }), - image_category: 'general', - is_intermediate: true, - }) - ).unwrap(); - } - - // For inpaint/outpaint, we also need to upload the mask layer - if (['inpaint', 'outpaint'].includes(generationMode)) { - // upload the image, saving the request id - canvasMaskImage = await dispatch( - imagesApi.endpoints.uploadImage.initiate({ - file: new File([maskBlob], 'canvasMaskImage.png', { - type: 'image/png', - }), - image_category: 'mask', - is_intermediate: true, - }) - ).unwrap(); - } - - const graph = await buildCanvasGraph(state, generationMode, canvasInitImage, canvasMaskImage); - - log.debug({ graph: parseify(graph) }, `Canvas graph built`); - - // currently this action is just listened to for logging - dispatch(canvasGraphBuilt(graph)); - - const batchConfig = prepareLinearUIBatch(state, graph, prepend); - - try { - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(batchConfig, { - fixedCacheKey: 'enqueueBatch', - }) - ); - - const enqueueResult = await req.unwrap(); - req.reset(); - - const batchId = enqueueResult.batch.batch_id as string; // we know the is a string, backend provides it - - // Prep the canvas staging area if it is not yet initialized - if (!state.canvas.layerState.stagingArea.boundingBox) { - dispatch( - stagingAreaInitialized({ - boundingBox: { - ...state.canvas.boundingBoxCoordinates, - ...state.canvas.boundingBoxDimensions, - }, - }) - ); - } - - // Associate the session with the canvas session ID - dispatch(canvasBatchIdAdded(batchId)); - } catch { - // no-op - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts deleted file mode 100644 index 6ca7ee7ffa9..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { enqueueRequested } from 'app/store/actions'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; -import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; -import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph'; -import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph'; -import { queueApi } from 'services/api/endpoints/queue'; - -export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { - startAppListening({ - predicate: (action): action is ReturnType => - enqueueRequested.match(action) && action.payload.tabName === 'generation', - effect: async (action, { getState, dispatch }) => { - const state = getState(); - const { shouldShowProgressInViewer } = state.ui; - const model = state.generation.model; - const { prepend } = action.payload; - - let graph; - - if (model?.base === 'sdxl') { - graph = await buildGenerationTabSDXLGraph(state); - } else { - graph = await buildGenerationTabGraph(state); - } - - const batchConfig = prepareLinearUIBatch(state, graph, prepend); - - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(batchConfig, { - fixedCacheKey: 'enqueueBatch', - }) - ); - try { - await req.unwrap(); - if (shouldShowProgressInViewer) { - dispatch(isImageViewerOpenChanged(true)); - } - } finally { - req.reset(); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts deleted file mode 100644 index c4087aacded..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { enqueueRequested } from 'app/store/actions'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph'; -import { buildWorkflowWithValidation } from 'features/nodes/util/workflow/buildWorkflow'; -import { queueApi } from 'services/api/endpoints/queue'; -import type { BatchConfig } from 'services/api/types'; - -export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) => { - startAppListening({ - predicate: (action): action is ReturnType => - enqueueRequested.match(action) && action.payload.tabName === 'workflows', - effect: async (action, { getState, dispatch }) => { - const state = getState(); - const { nodes, edges } = state.nodes.present; - const workflow = state.workflow; - const graph = buildNodesGraph(state.nodes.present); - const builtWorkflow = buildWorkflowWithValidation({ - nodes, - edges, - workflow, - }); - - if (builtWorkflow) { - // embedded workflows don't have an id - delete builtWorkflow.id; - } - - const batchConfig: BatchConfig = { - batch: { - graph, - workflow: builtWorkflow, - runs: state.generation.iterations, - }, - prepend: action.payload.prepend, - }; - - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(batchConfig, { - fixedCacheKey: 'enqueueBatch', - }) - ); - try { - await req.unwrap(); - } finally { - req.reset(); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts deleted file mode 100644 index 43f93551255..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; -import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice'; -import { imagesApi } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; -import { imagesSelectors } from 'services/api/util'; - -export const galleryImageClicked = createAction<{ - imageDTO: ImageDTO; - shiftKey: boolean; - ctrlKey: boolean; - metaKey: boolean; - altKey: boolean; -}>('gallery/imageClicked'); - -/** - * This listener handles the logic for selecting images in the gallery. - * - * Previously, this logic was in a `useCallback` with the whole gallery selection as a dependency. Every time - * the selection changed, the callback got recreated and all images rerendered. This could easily block for - * hundreds of ms, more for lower end devices. - * - * Moving this logic into a listener means we don't need to recalculate anything dynamically and the gallery - * is much more responsive. - */ - -export const addGalleryImageClickedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: galleryImageClicked, - effect: async (action, { dispatch, getState }) => { - const { imageDTO, shiftKey, ctrlKey, metaKey, altKey } = action.payload; - const state = getState(); - const queryArgs = selectListImagesQueryArgs(state); - const { data: listImagesData } = imagesApi.endpoints.listImages.select(queryArgs)(state); - - if (!listImagesData) { - // Should never happen if we have clicked a gallery image - return; - } - - const imageDTOs = imagesSelectors.selectAll(listImagesData); - const selection = state.gallery.selection; - - if (altKey) { - if (state.gallery.imageToCompare?.image_name === imageDTO.image_name) { - dispatch(imageToCompareChanged(null)); - } else { - dispatch(imageToCompareChanged(imageDTO)); - } - } else if (shiftKey) { - const rangeEndImageName = imageDTO.image_name; - const lastSelectedImage = selection[selection.length - 1]?.image_name; - const lastClickedIndex = imageDTOs.findIndex((n) => n.image_name === lastSelectedImage); - const currentClickedIndex = imageDTOs.findIndex((n) => n.image_name === rangeEndImageName); - if (lastClickedIndex > -1 && currentClickedIndex > -1) { - // We have a valid range! - const start = Math.min(lastClickedIndex, currentClickedIndex); - const end = Math.max(lastClickedIndex, currentClickedIndex); - const imagesToSelect = imageDTOs.slice(start, end + 1); - dispatch(selectionChanged(selection.concat(imagesToSelect))); - } - } else if (ctrlKey || metaKey) { - if (selection.some((i) => i.image_name === imageDTO.image_name) && selection.length > 1) { - dispatch(selectionChanged(selection.filter((n) => n.image_name !== imageDTO.image_name))); - } else { - dispatch(selectionChanged(selection.concat(imageDTO))); - } - } else { - dispatch(selectionChanged([imageDTO])); - } - }, - }); -}; 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 index 923b2c01977..416c77b9dd7 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts @@ -1,24 +1,26 @@ import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +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 { size } from 'lodash-es'; +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, { getState }) => { - const log = logger('system'); + effect: (action) => { const schemaJSON = action.payload; - log.debug({ schemaJSON: parseify(schemaJSON) }, 'Received OpenAPI schema'); - const { nodesAllowlist, nodesDenylist } = getState().config; + log.debug({ schemaJSON: parseify(schemaJSON) } as JsonObject, 'Received OpenAPI schema'); - const nodeTemplates = parseSchema(schemaJSON, nodesAllowlist, nodesDenylist); + const nodeTemplates = parseSchema(schemaJSON); - log.debug({ nodeTemplates: parseify(nodeTemplates) }, `Built ${size(nodeTemplates)} node templates`); + log.debug({ nodeTemplates } as JsonObject, `Built ${size(nodeTemplates)} node templates`); $templates.set(nodeTemplates); }, @@ -30,8 +32,7 @@ export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening // 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) { - const log = logger('system'); - log.error({ error: parseify(action.error) }, 'Problem retrieving OpenAPI Schema'); + 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 index 5412e0f2367..beb963a198f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts @@ -1,27 +1,23 @@ import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +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 log = logger('images'); - const { board_id, imageDTO } = action.meta.arg.originalArgs; - - // TODO: update listImages cache for this board - - log.debug({ board_id, imageDTO }, 'Image added to board'); + 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 log = logger('images'); - const { board_id, imageDTO } = action.meta.arg.originalArgs; - - log.debug({ board_id, imageDTO }, 'Problem adding image to board'); + 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/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts deleted file mode 100644 index 8c24badc769..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import type { AppDispatch, RootState } from 'app/store/store'; -import { resetCanvas } from 'features/canvas/store/canvasSlice'; -import { - controlAdapterImageChanged, - controlAdapterProcessedImageChanged, - selectControlAdapterAll, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { - isControlAdapterLayer, - isInitialImageLayer, - isIPAdapterLayer, - isRegionalGuidanceLayer, - layerDeleted, -} from 'features/controlLayers/store/controlLayersSlice'; -import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; -import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; -import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; -import { imageSelected } from 'features/gallery/store/gallerySlice'; -import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; -import { isImageFieldInputInstance } from 'features/nodes/types/field'; -import { isInvocationNode } from 'features/nodes/types/invocation'; -import { clamp, forEach } from 'lodash-es'; -import { api } from 'services/api'; -import { imagesApi } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; -import { imagesSelectors } from 'services/api/util'; - -const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.nodes.present.nodes.forEach((node) => { - if (!isInvocationNode(node)) { - return; - } - - forEach(node.data.inputs, (input) => { - if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) { - dispatch( - fieldImageValueChanged({ - nodeId: node.data.id, - fieldName: input.name, - value: undefined, - }) - ); - } - }); - }); -}; - -const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - forEach(selectControlAdapterAll(state.controlAdapters), (ca) => { - if ( - ca.controlImage === imageDTO.image_name || - (isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name) - ) { - dispatch( - controlAdapterImageChanged({ - id: ca.id, - controlImage: null, - }) - ); - dispatch( - controlAdapterProcessedImageChanged({ - id: ca.id, - processedControlImage: null, - }) - ); - } - }); -}; - -const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.controlLayers.present.layers.forEach((l) => { - if (isRegionalGuidanceLayer(l)) { - if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) { - dispatch(layerDeleted(l.id)); - } - } - if (isControlAdapterLayer(l)) { - if ( - l.controlAdapter.image?.name === imageDTO.image_name || - l.controlAdapter.processedImage?.name === imageDTO.image_name - ) { - dispatch(layerDeleted(l.id)); - } - } - if (isIPAdapterLayer(l)) { - if (l.ipAdapter.image?.name === imageDTO.image_name) { - dispatch(layerDeleted(l.id)); - } - } - if (isInitialImageLayer(l)) { - if (l.image?.name === imageDTO.image_name) { - dispatch(layerDeleted(l.id)); - } - } - }); -}; - -export const addRequestedSingleImageDeletionListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: imageDeletionConfirmed, - effect: async (action, { dispatch, getState, condition }) => { - const { imageDTOs, imagesUsage } = action.payload; - - if (imageDTOs.length !== 1 || imagesUsage.length !== 1) { - // handle multiples in separate listener - return; - } - - const imageDTO = imageDTOs[0]; - const imageUsage = imagesUsage[0]; - - if (!imageDTO || !imageUsage) { - // satisfy noUncheckedIndexedAccess - return; - } - - dispatch(isModalOpenChanged(false)); - - const state = getState(); - const lastSelectedImage = state.gallery.selection[state.gallery.selection.length - 1]?.image_name; - - if (imageDTO && imageDTO?.image_name === lastSelectedImage) { - const { image_name } = imageDTO; - - const baseQueryArgs = selectListImagesQueryArgs(state); - const { data } = imagesApi.endpoints.listImages.select(baseQueryArgs)(state); - - const cachedImageDTOs = data ? imagesSelectors.selectAll(data) : []; - - const deletedImageIndex = cachedImageDTOs.findIndex((i) => i.image_name === image_name); - - const filteredImageDTOs = cachedImageDTOs.filter((i) => i.image_name !== image_name); - - const newSelectedImageIndex = clamp(deletedImageIndex, 0, filteredImageDTOs.length - 1); - - const newSelectedImageDTO = filteredImageDTOs[newSelectedImageIndex]; - - if (newSelectedImageDTO) { - dispatch(imageSelected(newSelectedImageDTO)); - } else { - dispatch(imageSelected(null)); - } - } - - // We need to reset the features where the image is in use - none of these work if their image(s) don't exist - if (imageUsage.isCanvasImage) { - dispatch(resetCanvas()); - } - - imageDTOs.forEach((imageDTO) => { - deleteControlAdapterImages(state, dispatch, imageDTO); - deleteNodesImages(state, dispatch, imageDTO); - deleteControlLayerImages(state, dispatch, imageDTO); - }); - - // Delete from server - const { requestId } = dispatch(imagesApi.endpoints.deleteImage.initiate(imageDTO)); - - // Wait for successful deletion, then trigger boards to re-fetch - const wasImageDeleted = await condition( - (action) => imagesApi.endpoints.deleteImage.matchFulfilled(action) && action.meta.requestId === requestId, - 30000 - ); - - if (wasImageDeleted) { - dispatch(api.util.invalidateTags([{ type: 'Board', id: imageDTO.board_id ?? 'none' }])); - } - }, - }); - - startAppListening({ - actionCreator: imageDeletionConfirmed, - effect: async (action, { dispatch, getState }) => { - const { imageDTOs, imagesUsage } = action.payload; - - if (imageDTOs.length <= 1 || imagesUsage.length <= 1) { - // handle singles in separate listener - return; - } - - try { - // Delete from server - await dispatch(imagesApi.endpoints.deleteImages.initiate({ imageDTOs })).unwrap(); - const state = getState(); - const queryArgs = selectListImagesQueryArgs(state); - const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state); - - const newSelectedImageDTO = data ? imagesSelectors.selectAll(data)[0] : undefined; - - if (newSelectedImageDTO) { - dispatch(imageSelected(newSelectedImageDTO)); - } else { - dispatch(imageSelected(null)); - } - - dispatch(isModalOpenChanged(false)); - - // We need to reset the features where the image is in use - none of these work if their image(s) don't exist - - if (imagesUsage.some((i) => i.isCanvasImage)) { - dispatch(resetCanvas()); - } - - imageDTOs.forEach((imageDTO) => { - deleteControlAdapterImages(state, dispatch, imageDTO); - deleteNodesImages(state, dispatch, imageDTO); - deleteControlLayerImages(state, dispatch, imageDTO); - }); - } catch { - // no-op - } - }, - }); - - startAppListening({ - matcher: imagesApi.endpoints.deleteImage.matchPending, - effect: () => { - // - }, - }); - - startAppListening({ - matcher: imagesApi.endpoints.deleteImage.matchFulfilled, - effect: (action) => { - const log = logger('images'); - log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Image deleted'); - }, - }); - - startAppListening({ - matcher: imagesApi.endpoints.deleteImage.matchRejected, - effect: (action) => { - const log = logger('images'); - log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Unable to delete image'); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts deleted file mode 100644 index 7cb0703af8f..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { parseify } from 'common/util/serialize'; -import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; -import { - controlAdapterImageChanged, - controlAdapterIsEnabledChanged, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { - caLayerImageChanged, - iiLayerImageChanged, - ipaLayerImageChanged, - rgLayerIPAdapterImageChanged, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { isValidDrop } from 'features/dnd/util/isValidDrop'; -import { imageSelected, imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; -import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import { imagesApi } from 'services/api/endpoints/images'; - -export const dndDropped = createAction<{ - overData: TypesafeDroppableData; - activeData: TypesafeDraggableData; -}>('dnd/dndDropped'); - -export const addImageDroppedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: dndDropped, - effect: async (action, { dispatch, getState }) => { - const log = logger('dnd'); - const { activeData, overData } = action.payload; - if (!isValidDrop(overData, activeData)) { - return; - } - - if (activeData.payloadType === 'IMAGE_DTO') { - log.debug({ activeData, overData }, 'Image dropped'); - } else if (activeData.payloadType === 'GALLERY_SELECTION') { - log.debug({ activeData, overData }, `Images (${getState().gallery.selection.length}) dropped`); - } else if (activeData.payloadType === 'NODE_FIELD') { - log.debug({ activeData: parseify(activeData), overData: parseify(overData) }, 'Node field dropped'); - } else { - log.debug({ activeData, overData }, `Unknown payload dropped`); - } - - /** - * Image dropped on current image - */ - if ( - overData.actionType === 'SET_CURRENT_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - dispatch(imageSelected(activeData.payload.imageDTO)); - dispatch(isImageViewerOpenChanged(true)); - return; - } - - /** - * Image dropped on ControlNet - */ - if ( - overData.actionType === 'SET_CONTROL_ADAPTER_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { id } = overData.context; - dispatch( - controlAdapterImageChanged({ - id, - controlImage: activeData.payload.imageDTO.image_name, - }) - ); - dispatch( - controlAdapterIsEnabledChanged({ - id, - isEnabled: true, - }) - ); - return; - } - - /** - * Image dropped on Control Adapter Layer - */ - if ( - overData.actionType === 'SET_CA_LAYER_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { layerId } = overData.context; - dispatch( - caLayerImageChanged({ - layerId, - imageDTO: activeData.payload.imageDTO, - }) - ); - return; - } - - /** - * Image dropped on IP Adapter Layer - */ - if ( - overData.actionType === 'SET_IPA_LAYER_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { layerId } = overData.context; - dispatch( - ipaLayerImageChanged({ - layerId, - imageDTO: activeData.payload.imageDTO, - }) - ); - return; - } - - /** - * Image dropped on RG Layer IP Adapter - */ - if ( - overData.actionType === 'SET_RG_LAYER_IP_ADAPTER_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { layerId, ipAdapterId } = overData.context; - dispatch( - rgLayerIPAdapterImageChanged({ - layerId, - ipAdapterId, - imageDTO: activeData.payload.imageDTO, - }) - ); - return; - } - - /** - * Image dropped on II Layer Image - */ - if ( - overData.actionType === 'SET_II_LAYER_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { layerId } = overData.context; - dispatch( - iiLayerImageChanged({ - layerId, - imageDTO: activeData.payload.imageDTO, - }) - ); - return; - } - - /** - * Image dropped on Canvas - */ - if ( - overData.actionType === 'SET_CANVAS_INITIAL_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - dispatch(setInitialCanvasImage(activeData.payload.imageDTO, selectOptimalDimension(getState()))); - return; - } - - /** - * Image dropped on node image field - */ - if ( - overData.actionType === 'SET_NODES_IMAGE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { fieldName, nodeId } = overData.context; - dispatch( - fieldImageValueChanged({ - nodeId, - fieldName, - value: activeData.payload.imageDTO, - }) - ); - return; - } - - /** - * Image selected for compare - */ - if ( - overData.actionType === 'SELECT_FOR_COMPARE' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { imageDTO } = activeData.payload; - dispatch(imageToCompareChanged(imageDTO)); - dispatch(isImageViewerOpenChanged(true)); - return; - } - - /** - * Image dropped on user board - */ - if ( - overData.actionType === 'ADD_TO_BOARD' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { imageDTO } = activeData.payload; - const { boardId } = overData.context; - dispatch( - imagesApi.endpoints.addImageToBoard.initiate({ - imageDTO, - board_id: boardId, - }) - ); - return; - } - - /** - * Image dropped on 'none' board - */ - if ( - overData.actionType === 'REMOVE_FROM_BOARD' && - activeData.payloadType === 'IMAGE_DTO' && - activeData.payload.imageDTO - ) { - const { imageDTO } = activeData.payload; - dispatch( - imagesApi.endpoints.removeImageFromBoard.initiate({ - imageDTO, - }) - ); - return; - } - - /** - * Multiple images dropped on user board - */ - if (overData.actionType === 'ADD_TO_BOARD' && activeData.payloadType === 'GALLERY_SELECTION') { - const imageDTOs = getState().gallery.selection; - const { boardId } = overData.context; - dispatch( - imagesApi.endpoints.addImagesToBoard.initiate({ - imageDTOs, - board_id: boardId, - }) - ); - return; - } - - /** - * Multiple images dropped on 'none' board - */ - if (overData.actionType === 'REMOVE_FROM_BOARD' && activeData.payloadType === 'GALLERY_SELECTION') { - const imageDTOs = getState().gallery.selection; - dispatch( - imagesApi.endpoints.removeImagesFromBoard.initiate({ - imageDTOs, - }) - ); - return; - } - }, - }); -}; 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 index 274e4c51c28..2ee25dea329 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts @@ -1,14 +1,14 @@ import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +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 log = logger('images'); const imageDTO = action.meta.arg.originalArgs; - log.debug({ imageDTO }, 'Image removed from board'); }, }); @@ -16,9 +16,7 @@ export const addImageRemovedFromBoardFulfilledListener = (startAppListening: App startAppListening({ matcher: imagesApi.endpoints.removeImageFromBoard.matchRejected, effect: (action) => { - const log = logger('images'); 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/imageToDeleteSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts deleted file mode 100644 index 845c9a21f2b..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; -import { selectImageUsage } from 'features/deleteImageModal/store/selectors'; -import { imagesToDeleteSelected, isModalOpenChanged } from 'features/deleteImageModal/store/slice'; - -export const addImageToDeleteSelectedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: imagesToDeleteSelected, - effect: async (action, { dispatch, getState }) => { - const imageDTOs = action.payload; - const state = getState(); - const { shouldConfirmOnDelete } = state.system; - const imagesUsage = selectImageUsage(getState()); - - const isImageInUse = - imagesUsage.some((i) => i.isCanvasImage) || - imagesUsage.some((i) => i.isControlImage) || - imagesUsage.some((i) => i.isNodesImage); - - if (shouldConfirmOnDelete || isImageInUse) { - dispatch(isModalOpenChanged(true)); - return; - } - - dispatch(imageDeletionConfirmed({ imageDTOs, imagesUsage })); - }, - }); -}; 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 index cd5304c32bb..f421009030f 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -1,45 +1,57 @@ import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; -import { - controlAdapterImageChanged, - controlAdapterIsEnabledChanged, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { - caLayerImageChanged, - iiLayerImageChanged, - ipaLayerImageChanged, - rgLayerIPAdapterImageChanged, -} from 'features/controlLayers/store/controlLayersSlice'; -import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; +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 { omit } from 'lodash-es'; 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 }) => { - const log = logger('images'); - const imageDTO = action.payload; + 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(); - const { autoAddBoardId } = state.gallery; log.debug({ imageDTO }, 'Image uploaded'); - const { postUploadAction } = action.meta.arg.originalArgs; - - if ( - // No further actions needed for intermediate images, - action.payload.is_intermediate && - // unless they have an explicit post-upload action - !postUploadAction - ) { - return; - } + const boardId = imageDTO.board_id ?? 'none'; const DEFAULT_UPLOADED_TOAST = { id: 'IMAGE_UPLOADED', @@ -48,110 +60,34 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis } as const; // default action - just upload and alert user - if (postUploadAction?.type === 'TOAST') { - if (!autoAddBoardId || autoAddBoardId === 'none') { - const title = postUploadAction.title || DEFAULT_UPLOADED_TOAST.title; - toast({ ...DEFAULT_UPLOADED_TOAST, title }); - } else { - // Add this image to the board - dispatch( - imagesApi.endpoints.addImageToBoard.initiate({ - board_id: autoAddBoardId, - imageDTO, - }) - ); - - // Attempt to get the board's name for the toast - const { data } = boardsApi.endpoints.listAllBoards.select()(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 === autoAddBoardId); - const description = board - ? `${t('toast.addedToBoard')} ${board.board_name}` - : `${t('toast.addedToBoard')} ${autoAddBoardId}`; - - toast({ - ...DEFAULT_UPLOADED_TOAST, - description, - }); - } - return; - } - - if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') { - dispatch(setInitialCanvasImage(imageDTO, selectOptimalDimension(state))); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setAsCanvasInitialImage'), - }); - return; + if (lastUploadedToastTimeout !== null) { + window.clearTimeout(lastUploadedToastTimeout); } - - if (postUploadAction?.type === 'SET_CONTROL_ADAPTER_IMAGE') { - const { id } = postUploadAction; - dispatch( - controlAdapterIsEnabledChanged({ - id, - isEnabled: true, - }) - ); - dispatch( - controlAdapterImageChanged({ - id, - controlImage: imageDTO.image_name, - }) - ); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }); - return; - } - - if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') { - const { layerId } = postUploadAction; - dispatch(caLayerImageChanged({ layerId, imageDTO })); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }); - } - - if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') { - const { layerId } = postUploadAction; - dispatch(ipaLayerImageChanged({ layerId, imageDTO })); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }); - } - - if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') { - const { layerId, ipAdapterId } = postUploadAction; - dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }); - } - - if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') { - const { layerId } = postUploadAction; - dispatch(iiLayerImageChanged({ layerId, imageDTO })); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }); - } - - if (postUploadAction?.type === 'SET_NODES_IMAGE') { - const { nodeId, fieldName } = postUploadAction; - dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO })); - toast({ - ...DEFAULT_UPLOADED_TOAST, - description: `${t('toast.setNodeField')} ${fieldName}`, - }); - return; + 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')); } }, }); @@ -159,7 +95,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis startAppListening({ matcher: imagesApi.endpoints.uploadImage.matchRejected, effect: (action) => { - const log = logger('images'); const sanitizedData = { arg: { ...omit(action.meta.arg.originalArgs, ['file', 'postUploadAction']), diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesStarred.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesStarred.ts deleted file mode 100644 index 74b36e3297b..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesStarred.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { selectionChanged } from 'features/gallery/store/gallerySlice'; -import { imagesApi } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; - -export const addImagesStarredListener = (startAppListening: AppStartListening) => { - startAppListening({ - matcher: imagesApi.endpoints.starImages.matchFulfilled, - effect: async (action, { dispatch, getState }) => { - const { updated_image_names: starredImages } = action.payload; - - const state = getState(); - - const { selection } = state.gallery; - const updatedSelection: ImageDTO[] = []; - - selection.forEach((selectedImageDTO) => { - if (starredImages.includes(selectedImageDTO.image_name)) { - updatedSelection.push({ - ...selectedImageDTO, - starred: true, - }); - } else { - updatedSelection.push(selectedImageDTO); - } - }); - dispatch(selectionChanged(updatedSelection)); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesUnstarred.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesUnstarred.ts deleted file mode 100644 index ebae7885c18..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imagesUnstarred.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { selectionChanged } from 'features/gallery/store/gallerySlice'; -import { imagesApi } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; - -export const addImagesUnstarredListener = (startAppListening: AppStartListening) => { - startAppListening({ - matcher: imagesApi.endpoints.unstarImages.matchFulfilled, - effect: async (action, { dispatch, getState }) => { - const { updated_image_names: unstarredImages } = action.payload; - - const state = getState(); - - const { selection } = state.gallery; - const updatedSelection: ImageDTO[] = []; - - selection.forEach((selectedImageDTO) => { - if (unstarredImages.includes(selectedImageDTO.image_name)) { - updatedSelection.push({ - ...selectedImageDTO, - starred: false, - }); - } else { - updatedSelection.push(selectedImageDTO); - } - }); - dispatch(selectionChanged(updatedSelection)); - }, - }); -}; 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 index 239a5b863d8..9e67e013946 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,23 +1,77 @@ import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +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 { - controlAdapterIsEnabledChanged, - selectControlAdapterAll, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { loraRemoved } from 'features/lora/store/loraSlice'; + 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 { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; -import { forEach } from 'lodash-es'; +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 log = logger('models'); - const state = getState(); const result = zParameterModel.safeParse(action.payload); @@ -27,50 +81,503 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = } const newModel = result.data; - - const newBaseModel = newModel.base; - const didBaseModelChange = state.generation.model?.base !== newBaseModel; + const newBase = newModel.base; + const didBaseModelChange = state.params.model?.base !== newBase; if (didBaseModelChange) { // we may need to reset some incompatible submodels - let modelsCleared = 0; + let modelsUpdatedDisabledOrCleared = 0; // handle incompatible loras - forEach(state.lora.loras, (lora, id) => { - if (lora.model.base !== newBaseModel) { - dispatch(loraRemoved(id)); - modelsCleared += 1; + 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.generation; - if (vae && vae.base !== newBaseModel) { + const { vae } = state.params; + if (vae && vae.base !== newBase) { dispatch(vaeSelected(null)); - modelsCleared += 1; + modelsUpdatedDisabledOrCleared += 1; } - // handle incompatible controlnets - selectControlAdapterAll(state.controlAdapters).forEach((ca) => { - if (ca.model?.base !== newBaseModel) { - dispatch(controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false })); - modelsCleared += 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); - if (modelsCleared > 0) { + 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: modelsCleared, + count: modelsUpdatedDisabledOrCleared, }), status: 'warning', }); } } - dispatch(modelChanged(newModel, state.generation.model)); + // 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 index eb86f54c84f..8cbbc72343b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -1,30 +1,73 @@ import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import type { AppDispatch, RootState } from 'app/store/store'; -import type { JSONObject } from 'common/types'; +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 { - controlAdapterModelCleared, - selectControlAdapterAll, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import { loraRemoved } from 'features/lora/store/loraSlice'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice'; -import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas'; -import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; -import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice'; -import { forEach } from 'lodash-es'; + 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 { isNonRefinerMainModelConfig, isRefinerMainModelModelConfig, isVAEModelConfig } 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: async (action, { getState, dispatch }) => { + effect: (action, { getState, dispatch }) => { // models loaded, we need to ensure the selected model is available and if not, select the first one - const log = logger('models'); log.info({ models: action.payload.entities }, `Models loaded (${action.payload.ids.length})`); const state = getState(); @@ -36,6 +79,14 @@ export const addModelsLoadedListener = (startAppListening: AppStartListening) => 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); }, }); }; @@ -44,136 +95,374 @@ type ModelHandler = ( models: AnyModelConfig[], state: RootState, dispatch: AppDispatch, - log: Logger + log: Logger ) => undefined; const handleMainModels: ModelHandler = (models, state, dispatch, log) => { - const currentModel = state.generation.model; - const mainModels = models.filter(isNonRefinerMainModelConfig); - if (mainModels.length === 0) { - // No models loaded at all - dispatch(modelChanged(null)); + 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; } - const isCurrentMainModelAvailable = currentModel ? mainModels.some((m) => m.key === currentModel.key) : false; - if (isCurrentMainModelAvailable) { + 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; } - const defaultModel = state.config.sd.defaultModel; - const defaultModelInList = defaultModel ? mainModels.find((m) => m.key === defaultModel) : false; + // We have a refiner model selected, need to check if it is available - if (defaultModelInList) { - const result = zParameterModel.safeParse(defaultModelInList); - if (result.success) { - dispatch(modelChanged(defaultModelInList, currentModel)); + // Grab just the refiner models + const allRefinerModels = models.filter(isRefinerMainModelModelConfig); - const optimalDimension = getOptimalDimension(defaultModelInList); - if ( - getIsSizeOptimal( - state.controlLayers.present.size.width, - state.controlLayers.present.size.height, - optimalDimension - ) - ) { + // 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; } - const { width, height } = calculateNewSize( - state.controlLayers.present.size.aspectRatio.value, - optimalDimension * optimalDimension + log.debug({ selectedIPAdapterModel }, 'Selected IP adapter model is not available, clearing'); + dispatch( + rgRefImageModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null }) ); + }); + }); +}; - dispatch(widthChanged({ width })); - dispatch(heightChanged({ height })); +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 })); + }); - const result = zParameterModel.safeParse(mainModels[0]); + selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => { + entity.referenceImages.forEach(({ id: referenceImageId, config }) => { + if (!isRegionalGuidanceFLUXReduxConfig(config)) { + return; + } - if (!result.success) { - log.error({ error: result.error.format() }, 'Failed to parse main model'); + 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; } - dispatch(modelChanged(result.data, currentModel)); + // 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 handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => { - const currentRefinerModel = state.sdxl.refinerModel; - const refinerModels = models.filter(isRefinerMainModelModelConfig); - if (models.length === 0) { - // No models loaded at all - dispatch(refinerModelChanged(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; } - const isCurrentRefinerModelAvailable = currentRefinerModel - ? refinerModels.some((m) => m.key === currentRefinerModel.key) - : false; - - if (!isCurrentRefinerModelAvailable) { - dispatch(refinerModelChanged(null)); + // 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 handleVAEModels: ModelHandler = (models, state, dispatch, log) => { - const currentVae = state.generation.vae; +const handleTileControlNetModel: ModelHandler = (models, state, dispatch, log) => { + const selectedTileControlNetModel = state.upscale.tileControlnetModel; + const controlNetModels = models.filter(isControlNetModelConfig); - if (currentVae === null) { - // null is a valid VAE! it means "use the default with the main model" + // If the currently selected model is available, we don't need to do anything + if (selectedTileControlNetModel && controlNetModels.some((m) => m.key === selectedTileControlNetModel.key)) { return; } - const vaeModels = models.filter(isVAEModelConfig); - const isCurrentVAEAvailable = vaeModels.some((m) => m.key === currentVae.key); + // 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 (isCurrentVAEAvailable) { + // 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; } - const firstModel = vaeModels[0]; - - if (!firstModel) { - // No custom VAEs loaded at all; use the default - dispatch(vaeSelected(null)); + // 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; } - const result = zParameterVAEModel.safeParse(firstModel); + // 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 (!result.success) { - log.error({ error: result.error.format() }, 'Failed to parse VAE model'); + // If the currently selected model is available, we don't need to do anything + if (selectedT5EncoderModel && t5EncoderModels.some((m) => m.key === selectedT5EncoderModel.key)) { return; } - dispatch(vaeSelected(result.data)); + // 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 handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => { - const loras = state.lora.loras; +const handleCLIPEmbedModels: ModelHandler = (models, state, dispatch, log) => { + const selectedCLIPEmbedModel = state.params.clipEmbedModel; + const CLIPEmbedModels = models.filter((m) => isCLIPEmbedModelConfigOrSubmodel(m)); - forEach(loras, (lora, id) => { - const isLoRAAvailable = models.some((m) => m.key === lora.model.key); + // If the currently selected model is available, we don't need to do anything + if (selectedCLIPEmbedModel && CLIPEmbedModels.some((m) => m.key === selectedCLIPEmbedModel.key)) { + return; + } - if (isLoRAAvailable) { - 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; + } - dispatch(loraRemoved(id)); - }); + // 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 handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => { - selectControlAdapterAll(state.controlAdapters).forEach((ca) => { - const isModelAvailable = models.some((m) => m.key === ca.model?.key); +const handleFLUXVAEModels: ModelHandler = (models, state, dispatch, log) => { + const selectedFLUXVAEModel = state.params.fluxVAE; + const fluxVAEModels = models.filter((m) => isFluxVAEModelConfig(m)); - if (isModelAvailable) { - return; - } + // If the currently selected model is available, we don't need to do anything + if (selectedFLUXVAEModel && fluxVAEModels.some((m) => m.key === selectedFLUXVAEModel.key)) { + return; + } - dispatch(controlAdapterModelCleared({ id: ca.id })); - }); + // 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/promptChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts deleted file mode 100644 index 4633eb45a57..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { isAnyOf } from '@reduxjs/toolkit'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; -import { - combinatorialToggled, - isErrorChanged, - isLoadingChanged, - maxPromptsChanged, - maxPromptsReset, - parsingErrorChanged, - promptsChanged, -} from 'features/dynamicPrompts/store/dynamicPromptsSlice'; -import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; -import { utilitiesApi } from 'services/api/endpoints/utilities'; -import { socketConnected } from 'services/events/actions'; - -const matcher = isAnyOf( - positivePromptChanged, - combinatorialToggled, - maxPromptsChanged, - maxPromptsReset, - socketConnected -); - -export const addDynamicPromptsListener = (startAppListening: AppStartListening) => { - startAppListening({ - matcher, - effect: async (action, { dispatch, getState, cancelActiveListeners, delay }) => { - cancelActiveListeners(); - const state = getState(); - const { positivePrompt } = state.controlLayers.present; - const { maxPrompts } = state.dynamicPrompts; - - if (state.config.disabledFeatures.includes('dynamicPrompting')) { - return; - } - - const cachedPrompts = utilitiesApi.endpoints.dynamicPrompts.select({ - prompt: positivePrompt, - max_prompts: maxPrompts, - })(state).data; - - if (cachedPrompts) { - dispatch(promptsChanged(cachedPrompts.prompts)); - dispatch(parsingErrorChanged(cachedPrompts.error)); - return; - } - - if (!getShouldProcessPrompt(positivePrompt)) { - dispatch(promptsChanged([positivePrompt])); - dispatch(parsingErrorChanged(undefined)); - dispatch(isErrorChanged(false)); - return; - } - - if (!state.dynamicPrompts.isLoading) { - dispatch(isLoadingChanged(true)); - } - - // debounce request - await delay(1000); - - try { - const req = dispatch( - utilitiesApi.endpoints.dynamicPrompts.initiate({ - prompt: positivePrompt, - max_prompts: maxPrompts, - }) - ); - - const res = await req.unwrap(); - req.unsubscribe(); - - dispatch(promptsChanged(res.prompts)); - dispatch(parsingErrorChanged(res.error)); - dispatch(isErrorChanged(false)); - } catch { - dispatch(isErrorChanged(true)); - dispatch(isLoadingChanged(false)); - } - }, - }); -}; 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 index 415c359d70c..1ebad3a0694 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -1,17 +1,23 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import { setDefaultSettings } from 'features/parameters/store/actions'; +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, -} from 'features/parameters/store/generationSlice'; + widthChanged, +} from 'features/controlLayers/store/paramsSlice'; +import { setDefaultSettings } from 'features/parameters/store/actions'; import { isParameterCFGRescaleMultiplier, isParameterCFGScale, + isParameterGuidance, isParameterHeight, isParameterPrecision, isParameterScheduler, @@ -20,6 +26,7 @@ import { 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'; @@ -30,15 +37,14 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni effect: async (action, { dispatch, getState }) => { const state = getState(); - const currentModel = state.generation.model; + const currentModel = state.params.model; if (!currentModel) { return; } - const request = dispatch(modelsApi.endpoints.getModelConfigs.initiate()); + const request = dispatch(modelsApi.endpoints.getModelConfigs.initiate(undefined, { subscribe: false })); const data = await request.unwrap(); - request.unsubscribe(); const models = modelConfigsAdapterSelectors.selectAll(data); const modelConfig = models.find((model) => model.key === currentModel.key); @@ -48,7 +54,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni } if (isNonRefinerMainModelConfig(modelConfig) && modelConfig.default_settings) { - const { vae, vae_precision, cfg_scale, cfg_rescale_multiplier, steps, scheduler, width, height } = + const { vae, vae_precision, cfg_scale, cfg_rescale_multiplier, steps, scheduler, width, height, guidance } = modelConfig.default_settings; if (vae) { @@ -72,16 +78,28 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni } } - if (cfg_scale) { + if (guidance) { + if (isParameterGuidance(guidance)) { + dispatch(setGuidance(guidance)); + } + } + + if (!isNil(cfg_scale)) { if (isParameterCFGScale(cfg_scale)) { dispatch(setCfgScale(cfg_scale)); } } - if (cfg_rescale_multiplier) { + 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) { @@ -96,18 +114,30 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni } } const setSizeOptions = { updateAspectRatio: true, clamp: true }; - if (width) { + + const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state); + + const activeTab = selectActiveTab(getState()); + if (activeTab === 'generate') { if (isParameterWidth(width)) { dispatch(widthChanged({ width, ...setSizeOptions })); } - } - - if (height) { 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/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts deleted file mode 100644 index 0b2644f1243..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { $baseUrl } from 'app/store/nanostores/baseUrl'; -import { isEqual } from 'lodash-es'; -import { atom } from 'nanostores'; -import { api } from 'services/api'; -import { modelsApi } from 'services/api/endpoints/models'; -import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue'; -import { socketConnected } from 'services/events/actions'; - -const log = logger('socketio'); - -const $isFirstConnection = atom(true); - -export const addSocketConnectedEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketConnected, - effect: async (action, { dispatch, getState, cancelActiveListeners, delay }) => { - log.debug('Connected'); - - /** - * 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. In practice, the only - * thing that could change while disconnected is a queue item finishes processing. - * - * 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. This query cache has a 24hr timeout, so we can immediately - // unsubscribe. - const request = dispatch(modelsApi.endpoints.getModelConfigs.initiate()); - request.unsubscribe(); - - $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()); - } - - // Else, we need to compare the last-known queue status with the current queue status, re-fetching - // everything if it has changed. - - if ($baseUrl.get()) { - // If we have a baseUrl (e.g. not localhost), we need to debounce the re-fetch to not hammer server - cancelActiveListeners(); - // Add artificial jitter to the debounce - await delay(1000 + Math.random() * 1000); - } - - const prevQueueStatusData = selectQueueStatus(getState()).data; - - try { - // Fetch the queue status again - const queueStatusRequest = dispatch( - await queueApi.endpoints.getQueueStatus.initiate(undefined, { - forceRefetch: true, - }) - ); - const nextQueueStatusData = await queueStatusRequest.unwrap(); - queueStatusRequest.unsubscribe(); - - // If the queue hasn't changed, we don't need to do anything. - if (isEqual(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/middleware/listenerMiddleware/listeners/socketio/socketDisconnected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected.ts deleted file mode 100644 index be1a7663b38..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { socketDisconnected } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addSocketDisconnectedEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketDisconnected, - effect: () => { - log.debug('Disconnected'); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts deleted file mode 100644 index 08ad830ba48..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { deepClone } from 'common/util/deepClone'; -import { parseify } from 'common/util/serialize'; -import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; -import { zNodeStatus } from 'features/nodes/types/invocation'; -import { socketGeneratorProgress } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addGeneratorProgressEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketGeneratorProgress, - effect: (action) => { - log.trace(parseify(action.payload), `Generator progress`); - const { invocation_source_id, step, total_steps, progress_image } = action.payload.data; - const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); - if (nes) { - nes.status = zNodeStatus.enum.IN_PROGRESS; - nes.progress = (step + 1) / total_steps; - nes.progressImage = progress_image ?? null; - upsertExecutionState(nes.nodeId, nes); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts deleted file mode 100644 index 2841493ca6d..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { deepClone } from 'common/util/deepClone'; -import { parseify } from 'common/util/serialize'; -import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; -import { - boardIdSelected, - galleryViewChanged, - imageSelected, - isImageViewerOpenChanged, -} from 'features/gallery/store/gallerySlice'; -import { IMAGE_CATEGORIES } from 'features/gallery/store/types'; -import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; -import { zNodeStatus } from 'features/nodes/types/invocation'; -import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants'; -import { boardsApi } from 'services/api/endpoints/boards'; -import { imagesApi } from 'services/api/endpoints/images'; -import { imagesAdapter } from 'services/api/util'; -import { socketInvocationComplete } from 'services/events/actions'; - -// These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them -const nodeTypeDenylist = ['load_image', 'image']; - -const log = logger('socketio'); - -export const addInvocationCompleteEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketInvocationComplete, - effect: async (action, { dispatch, getState }) => { - const { data } = action.payload; - log.debug({ data: parseify(data) }, `Invocation complete (${data.invocation.type})`); - - const { result, invocation_source_id } = data; - // This complete event has an associated image output - if (data.result.type === 'image_output' && !nodeTypeDenylist.includes(data.invocation.type)) { - const { image_name } = data.result.image; - const { canvas, gallery } = getState(); - - // This populates the `getImageDTO` cache - const imageDTORequest = dispatch( - imagesApi.endpoints.getImageDTO.initiate(image_name, { - forceRefetch: true, - }) - ); - - const imageDTO = await imageDTORequest.unwrap(); - imageDTORequest.unsubscribe(); - - // Add canvas images to the staging area - if (canvas.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) { - dispatch(addImageToStagingArea(imageDTO)); - } - - if (!imageDTO.is_intermediate) { - /** - * Cache updates for when an image result is received - * - add it to the no_board/images - */ - - dispatch( - imagesApi.util.updateQueryData( - 'listImages', - { - board_id: imageDTO.board_id ?? 'none', - categories: IMAGE_CATEGORIES, - }, - (draft) => { - imagesAdapter.addOne(draft, imageDTO); - } - ) - ); - - // update the total images for the board - dispatch( - boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - draft.total += 1; - }) - ); - - dispatch(imagesApi.util.invalidateTags([{ type: 'Board', id: imageDTO.board_id ?? 'none' }])); - - const { shouldAutoSwitch } = gallery; - - // If auto-switch is enabled, select the new image - if (shouldAutoSwitch) { - // if auto-add is enabled, switch the gallery view and board if needed as the image comes in - if (gallery.galleryView !== 'images') { - dispatch(galleryViewChanged('images')); - } - - if (imageDTO.board_id && imageDTO.board_id !== gallery.selectedBoardId) { - dispatch( - boardIdSelected({ - boardId: imageDTO.board_id, - selectedImageName: imageDTO.image_name, - }) - ); - } - - if (!imageDTO.board_id && gallery.selectedBoardId !== 'none') { - dispatch( - boardIdSelected({ - boardId: 'none', - selectedImageName: imageDTO.image_name, - }) - ); - } - - dispatch(imageSelected(imageDTO)); - dispatch(isImageViewerOpenChanged(true)); - } - } - } - - const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); - if (nes) { - nes.status = zNodeStatus.enum.COMPLETED; - if (nes.progress !== null) { - nes.progress = 1; - } - nes.outputs.push(result); - upsertExecutionState(nes.nodeId, nes); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts deleted file mode 100644 index b34f34a079e..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { deepClone } from 'common/util/deepClone'; -import { parseify } from 'common/util/serialize'; -import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; -import { zNodeStatus } from 'features/nodes/types/invocation'; -import { socketInvocationError } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addInvocationErrorEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketInvocationError, - effect: (action) => { - const { invocation_source_id, invocation, error_type, error_message, error_traceback } = action.payload.data; - log.error(parseify(action.payload), `Invocation error (${invocation.type})`); - const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); - if (nes) { - nes.status = zNodeStatus.enum.FAILED; - nes.progress = null; - nes.progressImage = null; - nes.error = { - error_type, - error_message, - error_traceback, - }; - upsertExecutionState(nes.nodeId, nes); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts deleted file mode 100644 index 7dae869ce21..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { deepClone } from 'common/util/deepClone'; -import { parseify } from 'common/util/serialize'; -import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; -import { zNodeStatus } from 'features/nodes/types/invocation'; -import { socketInvocationStarted } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addInvocationStartedEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketInvocationStarted, - effect: (action) => { - log.debug(parseify(action.payload), `Invocation started (${action.payload.data.invocation.type})`); - const { invocation_source_id } = action.payload.data; - const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); - if (nes) { - nes.status = zNodeStatus.enum.IN_PROGRESS; - upsertExecutionState(nes.nodeId, nes); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall.ts deleted file mode 100644 index 22ad87fbe94..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall.ts +++ /dev/null @@ -1,196 +0,0 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { api, LIST_TAG } from 'services/api'; -import { modelsApi } from 'services/api/endpoints/models'; -import { - socketModelInstallCancelled, - socketModelInstallComplete, - socketModelInstallDownloadProgress, - socketModelInstallDownloadsComplete, - socketModelInstallDownloadStarted, - socketModelInstallError, - socketModelInstallStarted, -} from 'services/events/actions'; - -/** - * A model install has two main stages - downloading and installing. All these events are namespaced under `model_install_` - * which is a bit misleading. For example, a `model_install_started` event is actually fired _after_ the model has fully - * downloaded and is being "physically" installed. - * - * Note: the download events are only fired for remote model installs, not local. - * - * Here's the expected flow: - * - API receives install request, model manager preps the install - * - `model_install_download_started` fired when the download starts - * - `model_install_download_progress` fired continually until the download is complete - * - `model_install_download_complete` fired when the download is complete - * - `model_install_started` fired when the "physical" installation starts - * - `model_install_complete` fired when the installation is complete - * - `model_install_cancelled` fired if the installation is cancelled - * - `model_install_error` fired if the installation has an error - */ - -const selectModelInstalls = modelsApi.endpoints.listModelInstalls.select(); - -export const addModelInstallEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketModelInstallDownloadStarted, - effect: async (action, { dispatch, getState }) => { - const { id } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'downloading'; - } - return draft; - }) - ); - } - }, - }); - - startAppListening({ - actionCreator: socketModelInstallStarted, - effect: async (action, { dispatch, getState }) => { - const { id } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'running'; - } - return draft; - }) - ); - } - }, - }); - - startAppListening({ - actionCreator: socketModelInstallDownloadProgress, - effect: async (action, { dispatch, getState }) => { - const { bytes, total_bytes, id } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.bytes = bytes; - modelImport.total_bytes = total_bytes; - modelImport.status = 'downloading'; - } - return draft; - }) - ); - } - }, - }); - - startAppListening({ - actionCreator: socketModelInstallComplete, - effect: (action, { dispatch, getState }) => { - const { id } = action.payload.data; - - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'completed'; - } - return draft; - }) - ); - } - - dispatch(api.util.invalidateTags([{ type: 'ModelConfig', id: LIST_TAG }])); - dispatch(api.util.invalidateTags([{ type: 'ModelScanFolderResults', id: LIST_TAG }])); - }, - }); - - startAppListening({ - actionCreator: socketModelInstallError, - effect: (action, { dispatch, getState }) => { - const { id, error, error_type } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'error'; - modelImport.error_reason = error_type; - modelImport.error = error; - } - return draft; - }) - ); - } - }, - }); - - startAppListening({ - actionCreator: socketModelInstallCancelled, - effect: (action, { dispatch, getState }) => { - const { id } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'cancelled'; - } - return draft; - }) - ); - } - }, - }); - - startAppListening({ - actionCreator: socketModelInstallDownloadsComplete, - effect: (action, { dispatch, getState }) => { - const { id } = action.payload.data; - const { data } = selectModelInstalls(getState()); - - if (!data || !data.find((m) => m.id === id)) { - dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }])); - } else { - dispatch( - modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => { - const modelImport = draft.find((m) => m.id === id); - if (modelImport) { - modelImport.status = 'downloads_done'; - } - return draft; - }) - ); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad.ts deleted file mode 100644 index 0240fe219a1..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { socketModelLoadComplete, socketModelLoadStarted } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addModelLoadEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketModelLoadStarted, - effect: (action) => { - const { config, submodel_type } = action.payload.data; - const { name, base, type } = config; - - const extras: string[] = [base, type]; - - if (submodel_type) { - extras.push(submodel_type); - } - - const message = `Model load started: ${name} (${extras.join(', ')})`; - - log.debug(action.payload, message); - }, - }); - - startAppListening({ - actionCreator: socketModelLoadComplete, - effect: (action) => { - const { config, submodel_type } = action.payload.data; - const { name, base, type } = config; - - const extras: string[] = [base, type]; - if (submodel_type) { - extras.push(submodel_type); - } - - const message = `Model load complete: ${name} (${extras.join(', ')})`; - - log.debug(action.payload, message); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.tsx b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.tsx deleted file mode 100644 index 8a83609b3c8..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { deepClone } from 'common/util/deepClone'; -import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState'; -import { zNodeStatus } from 'features/nodes/types/invocation'; -import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription'; -import { toast } from 'features/toast/toast'; -import { forEach } from 'lodash-es'; -import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue'; -import { socketQueueItemStatusChanged } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addSocketQueueItemStatusChangedEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketQueueItemStatusChanged, - effect: async (action, { dispatch, getState }) => { - // we've got new status for the queue item, batch and queue - const { - item_id, - session_id, - status, - started_at, - updated_at, - completed_at, - batch_status, - queue_status, - error_type, - error_message, - error_traceback, - } = action.payload.data; - - log.debug(action.payload, `Queue item ${item_id} status updated: ${status}`); - - // Update this specific queue item in the list of queue items (this is the queue item DTO, without the session) - dispatch( - queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => { - queueItemsAdapter.updateOne(draft, { - id: String(item_id), - changes: { - status, - started_at, - updated_at: updated_at ?? undefined, - completed_at: completed_at ?? undefined, - error_type, - error_message, - error_traceback, - }, - }); - }) - ); - - // Update the queue status (we do not get the processor status here) - dispatch( - queueApi.util.updateQueryData('getQueueStatus', undefined, (draft) => { - if (!draft) { - return; - } - Object.assign(draft.queue, queue_status); - }) - ); - - // Update the batch status - dispatch( - queueApi.util.updateQueryData('getBatchStatus', { batch_id: batch_status.batch_id }, () => batch_status) - ); - - // Invalidate caches for things we cannot update - // TODO: technically, we could possibly update the current session queue item, but feels safer to just request it again - dispatch( - queueApi.util.invalidateTags([ - 'CurrentSessionQueueItem', - 'NextSessionQueueItem', - 'InvocationCacheStatus', - { type: 'SessionQueueItem', id: item_id }, - ]) - ); - - if (status === 'in_progress') { - forEach($nodeExecutionStates.get(), (nes) => { - if (!nes) { - return; - } - const clone = deepClone(nes); - clone.status = zNodeStatus.enum.PENDING; - clone.error = null; - clone.progress = null; - clone.progressImage = null; - clone.outputs = []; - $nodeExecutionStates.setKey(clone.nodeId, clone); - }); - } else if (status === 'failed' && error_type) { - const isLocal = getState().config.isLocal ?? true; - const sessionId = session_id; - - toast({ - id: `INVOCATION_ERROR_${error_type}`, - title: getTitleFromErrorType(error_type), - status: 'error', - duration: null, - updateDescription: isLocal, - description: ( - - ), - }); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts deleted file mode 100644 index 6c4c2a9df19..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { stagingAreaImageSaved } from 'features/canvas/store/actions'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { imagesApi } from 'services/api/endpoints/images'; - -export const addStagingAreaImageSavedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: stagingAreaImageSaved, - effect: async (action, { dispatch, getState }) => { - const { imageDTO } = action.payload; - - try { - const newImageDTO = await dispatch( - imagesApi.endpoints.changeImageIsIntermediate.initiate({ - imageDTO, - is_intermediate: false, - }) - ).unwrap(); - - // we may need to add it to the autoadd board - const { autoAddBoardId } = getState().gallery; - - if (autoAddBoardId && autoAddBoardId !== 'none') { - await dispatch( - imagesApi.endpoints.addImageToBoard.initiate({ - imageDTO: newImageDTO, - board_id: autoAddBoardId, - }) - ); - } - toast({ id: 'IMAGE_SAVED', title: t('toast.imageSaved'), status: 'success' }); - } catch (error) { - toast({ - id: 'IMAGE_SAVE_FAILED', - title: t('toast.imageSavingFailed'), - description: (error as Error)?.message, - status: 'error', - }); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts deleted file mode 100644 index 07df2a4f424..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { updateAllNodesRequested } from 'features/nodes/store/actions'; -import { $templates, nodesChanged } from 'features/nodes/store/nodesSlice'; -import { NodeUpdateError } from 'features/nodes/types/error'; -import { isInvocationNode } from 'features/nodes/types/invocation'; -import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; - -export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: updateAllNodesRequested, - effect: (action, { dispatch, getState }) => { - const log = logger('nodes'); - const { nodes } = getState().nodes.present; - const templates = $templates.get(); - - let unableToUpdateCount = 0; - - nodes.filter(isInvocationNode).forEach((node) => { - const template = templates[node.data.type]; - if (!template) { - unableToUpdateCount++; - return; - } - if (!getNeedsUpdate(node.data, template)) { - // No need to increment the count here, since we're not actually updating - return; - } - try { - const updatedNode = updateNode(node, template); - dispatch( - nodesChanged([ - { type: 'remove', id: updatedNode.id }, - { type: 'add', item: updatedNode }, - ]) - ); - } catch (e) { - if (e instanceof NodeUpdateError) { - unableToUpdateCount++; - } - } - }); - - if (unableToUpdateCount) { - log.warn( - t('nodes.unableToUpdateNodes', { - count: unableToUpdateCount, - }) - ); - toast({ - id: 'UNABLE_TO_UPDATE_NODES', - title: t('nodes.unableToUpdateNodes', { - count: unableToUpdateCount, - }), - }); - } else { - toast({ - id: 'ALL_NODES_UPDATED', - title: t('nodes.allNodesUpdated'), - status: 'success', - }); - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts deleted file mode 100644 index ce480a35733..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { parseify } from 'common/util/serialize'; -import { buildAdHocUpscaleGraph } from 'features/nodes/util/graph/buildAdHocUpscaleGraph'; -import { createIsAllowedToUpscaleSelector } from 'features/parameters/hooks/useIsAllowedToUpscale'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { queueApi } from 'services/api/endpoints/queue'; -import type { BatchConfig, ImageDTO } from 'services/api/types'; - -export const upscaleRequested = createAction<{ imageDTO: ImageDTO }>(`upscale/upscaleRequested`); - -export const addUpscaleRequestedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: upscaleRequested, - effect: async (action, { dispatch, getState }) => { - const log = logger('session'); - - const { imageDTO } = action.payload; - const { image_name } = imageDTO; - const state = getState(); - - const { isAllowedToUpscale, detailTKey } = createIsAllowedToUpscaleSelector(imageDTO)(state); - - // if we can't upscale, show a toast and return - if (!isAllowedToUpscale) { - log.error( - { imageDTO }, - t(detailTKey ?? 'parameters.isAllowedToUpscale.tooLarge') // should never coalesce - ); - toast({ - id: 'NOT_ALLOWED_TO_UPSCALE', - title: t(detailTKey ?? 'parameters.isAllowedToUpscale.tooLarge'), // should never coalesce - status: 'error', - }); - return; - } - - const enqueueBatchArg: BatchConfig = { - prepend: true, - batch: { - graph: buildAdHocUpscaleGraph({ - image_name, - state, - }), - runs: 1, - }, - }; - - try { - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, { - fixedCacheKey: 'enqueueBatch', - }) - ); - - const enqueueResult = await req.unwrap(); - req.reset(); - log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued')); - } catch (error) { - log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, 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/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts deleted file mode 100644 index 2c0caa0ec93..00000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { parseify } from 'common/util/serialize'; -import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { $needsFit } from 'features/nodes/store/reactFlowInstance'; -import type { Templates } from 'features/nodes/store/types'; -import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error'; -import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow'; -import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow'; -import { toast } from 'features/toast/toast'; -import { t } from 'i18next'; -import { checkBoardAccess, checkImageAccess, checkModelAccess } from 'services/api/hooks/accessChecks'; -import type { GraphAndWorkflowResponse, NonNullableGraph } from 'services/api/types'; -import { z } from 'zod'; -import { fromZodError } from 'zod-validation-error'; - -const getWorkflow = async (data: GraphAndWorkflowResponse, templates: Templates) => { - if (data.workflow) { - // Prefer to load the workflow if it's available - it has more information - const parsed = JSON.parse(data.workflow); - return await validateWorkflow(parsed, templates, checkImageAccess, checkBoardAccess, checkModelAccess); - } else if (data.graph) { - // Else we fall back on the graph, using the graphToWorkflow function to convert and do layout - const parsed = JSON.parse(data.graph); - const workflow = graphToWorkflow(parsed as NonNullableGraph, true); - return await validateWorkflow(workflow, templates, checkImageAccess, checkBoardAccess, checkModelAccess); - } else { - throw new Error('No workflow or graph provided'); - } -}; - -export const addWorkflowLoadRequestedListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: workflowLoadRequested, - effect: async (action, { dispatch }) => { - const log = logger('nodes'); - const { data, asCopy } = action.payload; - const nodeTemplates = $templates.get(); - - try { - const { workflow, warnings } = await getWorkflow(data, nodeTemplates); - - if (asCopy) { - // If we're loading a copy, we need to remove the ID so that the backend will create a new workflow - delete workflow.id; - } - - dispatch(workflowLoaded(workflow)); - if (!warnings.length) { - toast({ - id: 'WORKFLOW_LOADED', - title: t('toast.workflowLoaded'), - status: 'success', - }); - } else { - toast({ - id: 'WORKFLOW_LOADED', - title: t('toast.loadedWithWarnings'), - status: 'warning', - }); - - warnings.forEach(({ message, ...rest }) => { - log.warn(rest, message); - }); - } - - $needsFit.set(true); - } catch (e) { - if (e instanceof WorkflowVersionError) { - // The workflow version was not recognized in the valid list of versions - log.error({ error: parseify(e) }, e.message); - toast({ - id: 'UNABLE_TO_VALIDATE_WORKFLOW', - title: t('nodes.unableToValidateWorkflow'), - status: 'error', - description: e.message, - }); - } else if (e instanceof WorkflowMigrationError) { - // There was a problem migrating the workflow to the latest version - log.error({ error: parseify(e) }, e.message); - toast({ - id: 'UNABLE_TO_VALIDATE_WORKFLOW', - title: t('nodes.unableToValidateWorkflow'), - status: 'error', - description: e.message, - }); - } else if (e instanceof z.ZodError) { - // There was a problem validating the workflow itself - const { message } = fromZodError(e, { - prefix: t('nodes.workflowValidation'), - }); - log.error({ error: parseify(e) }, message); - toast({ - id: 'UNABLE_TO_VALIDATE_WORKFLOW', - title: t('nodes.unableToValidateWorkflow'), - status: 'error', - description: message, - }); - } else { - // Some other error occurred - log.error({ error: parseify(e) }, t('nodes.unknownErrorValidatingWorkflow')); - toast({ - id: 'UNABLE_TO_VALIDATE_WORKFLOW', - title: t('nodes.unableToValidateWorkflow'), - status: 'error', - description: t('nodes.unknownErrorValidatingWorkflow'), - }); - } - } - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/nanostores/authToken.ts b/invokeai/frontend/web/src/app/store/nanostores/authToken.ts deleted file mode 100644 index 9f07e3535e8..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/authToken.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { atom } from 'nanostores'; - -/** - * The user's auth token. - */ -export const $authToken = atom(); diff --git a/invokeai/frontend/web/src/app/store/nanostores/baseUrl.ts b/invokeai/frontend/web/src/app/store/nanostores/baseUrl.ts deleted file mode 100644 index 19bebab0ef8..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/baseUrl.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { atom } from 'nanostores'; - -/** - * The OpenAPI base url. - */ -export const $baseUrl = atom(); diff --git a/invokeai/frontend/web/src/app/store/nanostores/bulkDownloadId.ts b/invokeai/frontend/web/src/app/store/nanostores/bulkDownloadId.ts deleted file mode 100644 index 4f7118e2ebc..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/bulkDownloadId.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { atom } from 'nanostores'; - -const DEFAULT_BULK_DOWNLOAD_ID = 'default'; - -/** - * The download id for a bulk download. Used for socket subscriptions. - */ - -export const $bulkDownloadId = atom(DEFAULT_BULK_DOWNLOAD_ID); diff --git a/invokeai/frontend/web/src/app/store/nanostores/customNavComponent.ts b/invokeai/frontend/web/src/app/store/nanostores/customNavComponent.ts deleted file mode 100644 index 1a6a5571a03..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/customNavComponent.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { atom } from 'nanostores'; -import type { ReactNode } from 'react'; - -export const $customNavComponent = atom(undefined); diff --git a/invokeai/frontend/web/src/app/store/nanostores/customStarUI.ts b/invokeai/frontend/web/src/app/store/nanostores/customStarUI.ts deleted file mode 100644 index 9f6628ac9cd..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/customStarUI.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { MenuItemProps } from '@invoke-ai/ui-library'; -import { atom } from 'nanostores'; - -export type CustomStarUi = { - on: { - icon: MenuItemProps['icon']; - text: string; - }; - off: { - icon: MenuItemProps['icon']; - text: string; - }; -}; -export const $customStarUI = atom(undefined); diff --git a/invokeai/frontend/web/src/app/store/nanostores/galleryHeader.ts b/invokeai/frontend/web/src/app/store/nanostores/galleryHeader.ts deleted file mode 100644 index 5de7b1dd40a..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/galleryHeader.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { atom } from 'nanostores'; -import type { ReactNode } from 'react'; - -export const $galleryHeader = atom(undefined); diff --git a/invokeai/frontend/web/src/app/store/nanostores/isDebugging.ts b/invokeai/frontend/web/src/app/store/nanostores/isDebugging.ts deleted file mode 100644 index b71cab53088..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/isDebugging.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { atom } from 'nanostores'; - -export const $isDebugging = atom(false); diff --git a/invokeai/frontend/web/src/app/store/nanostores/logo.ts b/invokeai/frontend/web/src/app/store/nanostores/logo.ts deleted file mode 100644 index 5fd94ebd901..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/logo.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { atom } from 'nanostores'; -import type { ReactNode } from 'react'; - -export const $logo = atom(undefined); diff --git a/invokeai/frontend/web/src/app/store/nanostores/openAPISchemaUrl.ts b/invokeai/frontend/web/src/app/store/nanostores/openAPISchemaUrl.ts deleted file mode 100644 index 124815f7ead..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/openAPISchemaUrl.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { atom } from 'nanostores'; - -export const $openAPISchemaUrl = atom(undefined); diff --git a/invokeai/frontend/web/src/app/store/nanostores/projectId.ts b/invokeai/frontend/web/src/app/store/nanostores/projectId.ts deleted file mode 100644 index 2268ccdff13..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/projectId.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { atom } from 'nanostores'; - -/** - * The optional project-id header. - */ -export const $projectId = atom(); diff --git a/invokeai/frontend/web/src/app/store/nanostores/queueId.ts b/invokeai/frontend/web/src/app/store/nanostores/queueId.ts deleted file mode 100644 index 462cf69d0a6..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/queueId.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { atom } from 'nanostores'; - -export const DEFAULT_QUEUE_ID = 'default'; - -export const $queueId = atom(DEFAULT_QUEUE_ID); diff --git a/invokeai/frontend/web/src/app/store/nanostores/store.ts b/invokeai/frontend/web/src/app/store/nanostores/store.ts index 65c59dad5d3..d8248b79a0a 100644 --- a/invokeai/frontend/web/src/app/store/nanostores/store.ts +++ b/invokeai/frontend/web/src/app/store/nanostores/store.ts @@ -1,4 +1,4 @@ -import type { createStore } from 'app/store/store'; +import type { AppStore } from 'app/store/store'; import { atom } from 'nanostores'; // Inject socket options and url into window for debugging @@ -22,7 +22,7 @@ class ReduxStoreNotInitialized extends Error { } } -export const $store = atom> | undefined>(); +export const $store = atom>(); export const getStore = () => { const store = $store.get(); 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/nanostores/workflowCategories.ts b/invokeai/frontend/web/src/app/store/nanostores/workflowCategories.ts deleted file mode 100644 index e0d61071294..00000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/workflowCategories.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { WorkflowCategory } from 'features/nodes/types/workflow'; -import { atom } from 'nanostores'; - -export const $workflowCategories = atom(['user', 'default']); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 062cdc1cbf4..f24d2d0105c 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -1,190 +1,223 @@ -import type { ThunkDispatch, UnknownAction } from '@reduxjs/toolkit'; -import { autoBatchEnhancer, combineReducers, configureStore } from '@reduxjs/toolkit'; +import type { ThunkDispatch, TypedStartListening, UnknownAction } from '@reduxjs/toolkit'; +import { addListener, combineReducers, configureStore, createAction, createListenerMiddleware } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; -import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver'; import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; -import type { JSONObject } from 'common/types'; -import { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice'; -import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; -import { - controlAdaptersPersistConfig, - controlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { - controlLayersPersistConfig, - controlLayersSlice, - controlLayersUndoableConfig, -} from 'features/controlLayers/store/controlLayersSlice'; -import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice'; -import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; -import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice'; -import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice'; -import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice'; -import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice'; -import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice'; -import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; -import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice'; -import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice'; -import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice'; -import { queueSlice } from 'features/queue/store/queueSlice'; -import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice'; -import { configSlice } from 'features/system/store/configSlice'; -import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice'; -import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice'; +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 { defaultsDeep, keys, omit, pick } from 'lodash-es'; -import dynamicMiddlewares from 'redux-dynamic-middlewares'; import type { SerializeFunction, UnserializeFunction } from 'redux-remember'; -import { rememberEnhancer, rememberReducer } from 'redux-remember'; -import undoable from 'redux-undo'; +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 { authToastMiddleware } from 'services/api/authToastMiddleware'; +import type { JsonObject } from 'type-fest'; -import { STORAGE_PREFIX } from './constants'; +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 { listenerMiddleware } from './middleware/listenerMiddleware'; - -const allReducers = { - [canvasSlice.name]: canvasSlice.reducer, - [gallerySlice.name]: gallerySlice.reducer, - [generationSlice.name]: generationSlice.reducer, - [nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig), - [postprocessingSlice.name]: postprocessingSlice.reducer, - [systemSlice.name]: systemSlice.reducer, - [configSlice.name]: configSlice.reducer, - [uiSlice.name]: uiSlice.reducer, - [controlAdaptersSlice.name]: controlAdaptersSlice.reducer, - [dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer, - [deleteImageModalSlice.name]: deleteImageModalSlice.reducer, - [changeBoardModalSlice.name]: changeBoardModalSlice.reducer, - [loraSlice.name]: loraSlice.reducer, - [modelManagerV2Slice.name]: modelManagerV2Slice.reducer, - [sdxlSlice.name]: sdxlSlice.reducer, - [queueSlice.name]: queueSlice.reducer, - [workflowSlice.name]: workflowSlice.reducer, - [hrfSlice.name]: hrfSlice.reducer, - [controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig), - [workflowSettingsSlice.name]: workflowSettingsSlice.reducer, - [api.reducerPath]: api.reducer, -}; +import { addArchivedOrDeletedBoardListener } from './middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener'; +import { addPBRFilterListener } from './middleware/listenerMiddleware/listeners/addPBRFilterListener'; +import { addImageUploadedFulfilledListener } from './middleware/listenerMiddleware/listeners/imageUploaded'; -const rootReducer = combineReducers(allReducers); +const listenerMiddleware = createListenerMiddleware(); -const rememberedRootReducer = rememberReducer(rootReducer); +const log = logger('system'); -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -export type PersistConfig = { - /** - * The name of the slice. - */ - name: keyof typeof allReducers; - /** - * The initial state of the slice. - */ - initialState: T; - /** - * Migrate the state to the current version during rehydration. - * @param state The rehydrated state. - * @returns A correctly-shaped state. - */ - migrate: (state: unknown) => T; - /** - * Keys to omit from the persisted state. - */ - persistDenylist: (keyof T)[]; +// 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, }; -const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { - [canvasPersistConfig.name]: canvasPersistConfig, - [galleryPersistConfig.name]: galleryPersistConfig, - [generationPersistConfig.name]: generationPersistConfig, - [nodesPersistConfig.name]: nodesPersistConfig, - [postprocessingPersistConfig.name]: postprocessingPersistConfig, - [systemPersistConfig.name]: systemPersistConfig, - [workflowPersistConfig.name]: workflowPersistConfig, - [uiPersistConfig.name]: uiPersistConfig, - [controlAdaptersPersistConfig.name]: controlAdaptersPersistConfig, - [dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig, - [sdxlPersistConfig.name]: sdxlPersistConfig, - [loraPersistConfig.name]: loraPersistConfig, - [modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig, - [hrfPersistConfig.name]: hrfPersistConfig, - [controlLayersPersistConfig.name]: controlLayersPersistConfig, - [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, +// 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 log = logger('system'); - const persistConfig = persistConfigs[key as keyof typeof persistConfigs]; - if (!persistConfig) { + 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, migrate } = persistConfig; + const initialState = getInitialState(); const parsed = JSON.parse(data); - // strip out old keys - const stripped = pick(parsed, keys(initialState)); - // run (additive) migrations - const migrated = migrate(stripped); - // merge in initial state as default values, covering any missing keys - const transformed = defaultsDeep(migrated, initialState); + // 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, - rehydratedData: transformed, - diff: diff(parsed, transformed) as JSONObject, // this is always serializable + persistedData: parsed as JsonObject, + rehydratedData: migrated as JsonObject, + diff: diff(data, migrated) as JsonObject, }, `Rehydrated slice "${key}"` ); - return transformed; + state = migrated; } catch (err) { - log.warn({ error: serializeError(err) }, `Error rehydrating slice "${key}", falling back to default initial state`); - return persistConfig.initialState; + 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 persistConfig = persistConfigs[key as keyof typeof persistConfigs]; - if (!persistConfig) { + const sliceConfig = SLICE_CONFIGS[key as keyof typeof SLICE_CONFIGS]; + if (!sliceConfig?.persistConfig) { throw new Error(`No persist config for slice "${key}"`); } - // Heuristic to determine if the slice is undoable - could just hardcode it in the persistConfig - const isUndoable = 'present' in data && 'past' in data && 'future' in data && '_latestUnfiltered' in data; - const result = omit(isUndoable ? data.present : data, persistConfig.persistDenylist); + + const result = omit( + sliceConfig.undoableConfig ? data.present : data, + sliceConfig.persistConfig.persistDenylist ?? [] + ); + return JSON.stringify(result); }; -export const createStore = (uniqueStoreKey?: string, persist = true) => - configureStore({ +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: false, + // immutableCheck: false, + serializableCheck: import.meta.env.MODE === 'development', + immutableCheck: import.meta.env.MODE === 'development', }) .concat(api.middleware) - .concat(dynamicMiddlewares) - .concat(authToastMiddleware) + // .concat(getDebugLoggerMiddleware({ withDiff: true, withNextState: true })) .prepend(listenerMiddleware.middleware), enhancers: (getDefaultEnhancers) => { - const _enhancers = getDefaultEnhancers().concat(autoBatchEnhancer()); - if (persist) { - _enhancers.push( - rememberEnhancer(idbKeyValDriver, keys(persistConfigs), { - persistDebounce: 300, + const enhancers = getDefaultEnhancers(); + if (options?.persist) { + return enhancers.prepend( + rememberEnhancer(reduxRememberDriver, PERSISTED_KEYS, { + persistDebounce: options?.persistDebounce ?? 2000, serialize, unserialize, - prefix: uniqueStoreKey ? `${STORAGE_PREFIX}${uniqueStoreKey}-` : STORAGE_PREFIX, + prefix: '', errorHandler, }) ); + } else { + return enhancers; } - return _enhancers; }, devTools: { actionSanitizer, @@ -199,7 +232,65 @@ export const createStore = (uniqueStoreKey?: string, persist = true) => }, }); -export type RootState = ReturnType['getState']>; -// eslint-disable-next-line @typescript-eslint/no-explicit-any + // 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 index 6bc904acb31..cd0e41e55d1 100644 --- a/invokeai/frontend/web/src/app/store/storeHooks.ts +++ b/invokeai/frontend/web/src/app/store/storeHooks.ts @@ -1,8 +1,8 @@ -import type { AppThunkDispatch, RootState } from 'app/store/store'; +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(); +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/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts deleted file mode 100644 index 21636ada492..00000000000 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import type { ParameterPrecision, ParameterScheduler } from 'features/parameters/types/parameterSchemas'; -import type { InvokeTabName } from 'features/ui/store/tabMap'; -import type { O } from 'ts-toolbelt'; - -/** - * A disable-able application feature - */ -export type AppFeature = - | 'faceRestore' - | 'upscaling' - | 'lightbox' - | 'modelManager' - | 'githubLink' - | 'discordLink' - | 'bugLink' - | 'localization' - | 'consoleLogging' - | 'dynamicPrompting' - | 'batches' - | 'syncModels' - | 'multiselect' - | 'pauseQueue' - | 'resumeQueue' - | 'prependQueue' - | 'invocationCache' - | 'bulkDownload' - | 'starterModels' - | 'hfToken'; - -/** - * A disable-able Stable Diffusion feature - */ -export type SDFeature = - | 'controlNet' - | 'noise' - | 'perlinNoise' - | 'noiseThreshold' - | 'variation' - | 'symmetry' - | 'seamless' - | 'hires' - | 'lora' - | 'embedding' - | 'vae' - | 'hrf'; - -export type NumericalParameterConfig = { - initial: number; - sliderMin: number; - sliderMax: number; - numberInputMin: number; - numberInputMax: number; - fineStep: number; - coarseStep: number; -}; - -/** - * Configuration options for the InvokeAI UI. - * Distinct from system settings which may be changed inside the app. - */ -export type AppConfig = { - /** - * Whether or not we should update image urls when image loading errors - */ - shouldUpdateImagesOnConnect: boolean; - shouldFetchMetadataFromApi: boolean; - disabledTabs: InvokeTabName[]; - disabledFeatures: AppFeature[]; - disabledSDFeatures: SDFeature[]; - canRestoreDeletedImagesFromBin: boolean; - nodesAllowlist: string[] | undefined; - nodesDenylist: string[] | undefined; - maxUpscalePixels?: number; - metadataFetchDebounce?: number; - workflowFetchDebounce?: number; - isLocal?: boolean; - sd: { - defaultModel?: string; - disabledControlNetModels: string[]; - disabledControlNetProcessors: (keyof typeof CONTROLNET_PROCESSORS)[]; - // Core parameters - iterations: NumericalParameterConfig; - width: NumericalParameterConfig; // initial value comes from model - height: NumericalParameterConfig; // initial value comes from model - steps: NumericalParameterConfig; - guidance: NumericalParameterConfig; - cfgRescaleMultiplier: NumericalParameterConfig; - img2imgStrength: NumericalParameterConfig; - scheduler?: ParameterScheduler; - vaePrecision?: ParameterPrecision; - // Canvas - boundingBoxHeight: NumericalParameterConfig; // initial value comes from model - boundingBoxWidth: NumericalParameterConfig; // initial value comes from model - scaledBoundingBoxHeight: NumericalParameterConfig; // initial value comes from model - scaledBoundingBoxWidth: NumericalParameterConfig; // initial value comes from model - canvasCoherenceStrength: NumericalParameterConfig; - canvasCoherenceEdgeSize: NumericalParameterConfig; - infillTileSize: NumericalParameterConfig; - infillPatchmatchDownscaleSize: NumericalParameterConfig; - // Misc advanced - clipSkip: NumericalParameterConfig; // slider and input max are ignored for this, because the values depend on the model - maskBlur: NumericalParameterConfig; - hrfStrength: NumericalParameterConfig; - dynamicPrompts: { - maxPrompts: NumericalParameterConfig; - }; - ca: { - weight: NumericalParameterConfig; - }; - }; -}; - -export type PartialAppConfig = O.Partial; 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..ceba1310d81 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/ColorPicker/RgbColorPicker.tsx @@ -0,0 +1,104 @@ +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 { type RgbColor, RgbColorPicker as ColorfulRgbColorPicker } from 'react-colorful'; +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..8319625dc35 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/ColorPicker/RgbaColorPicker.tsx @@ -0,0 +1,191 @@ +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, useRef, useState } from 'react'; +import { type RgbaColor, RgbaColorPicker as ColorfulRgbaColorPicker } from 'react-colorful'; +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 [hexValue, setHexValue] = useState(rgbaToHex(color, true)); + const didUpdateFromHex = useRef(false); + + useEffect(() => { + if (didUpdateFromHex.current) { + didUpdateFromHex.current = false; + return; + } + setHexValue(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); + setHexValue(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); + didUpdateFromHex.current = true; + onChange(next); + } + }, + [color.a, onChange] + ); + const onChangeAlpha = useCallback( + (a: number) => { + const next = { ...color, a: Math.max(0, Math.min(1, a)) }; + onChange(next); + }, + [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/IAIColorPicker.tsx b/invokeai/frontend/web/src/common/components/IAIColorPicker.tsx deleted file mode 100644 index 25b129f6782..00000000000 --- a/invokeai/frontend/web/src/common/components/IAIColorPicker.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import type { ChakraProps } from '@invoke-ai/ui-library'; -import { CompositeNumberInput, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { CSSProperties } from 'react'; -import { memo, useCallback } from 'react'; -import { RgbaColorPicker } from 'react-colorful'; -import type { ColorPickerBaseProps, RgbaColor } from 'react-colorful/dist/types'; -import { useTranslation } from 'react-i18next'; - -type IAIColorPickerProps = ColorPickerBaseProps & { - withNumberInput?: 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: 5, - flexDir: 'column', -}; - -const colorPickerStyles: CSSProperties = { width: '100%' }; - -const numberInputWidth: ChakraProps['w'] = '3.5rem'; - -const IAIColorPicker = (props: IAIColorPickerProps) => { - const { color, onChange, withNumberInput, ...rest } = props; - const { t } = useTranslation(); - const handleChangeR = useCallback((r: number) => onChange({ ...color, r }), [color, onChange]); - const handleChangeG = useCallback((g: number) => onChange({ ...color, g }), [color, onChange]); - const handleChangeB = useCallback((b: number) => onChange({ ...color, b }), [color, onChange]); - const handleChangeA = useCallback((a: number) => onChange({ ...color, a }), [color, onChange]); - return ( - - - {withNumberInput && ( - - - {t('common.red')[0]} - - - - {t('common.green')[0]} - - - - {t('common.blue')[0]} - - - - {t('common.alpha')[0]} - - - - )} - - ); -}; - -export default memo(IAIColorPicker); diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx deleted file mode 100644 index f16aa3d4b4b..00000000000 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import type { ChakraProps, FlexProps, SystemStyleObject } from '@invoke-ai/ui-library'; -import { Flex, Icon, Image } from '@invoke-ai/ui-library'; -import { IAILoadingImageFallback, IAINoContentFallback } from 'common/components/IAIImageFallback'; -import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; -import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; -import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; -import type { MouseEvent, ReactElement, ReactNode, SyntheticEvent } from 'react'; -import { memo, useCallback, useMemo, useState } from 'react'; -import { PiImageBold, PiUploadSimpleBold } from 'react-icons/pi'; -import type { ImageDTO, PostUploadAction } from 'services/api/types'; - -import IAIDraggable from './IAIDraggable'; -import IAIDroppable from './IAIDroppable'; -import SelectionOverlay from './SelectionOverlay'; - -const defaultUploadElement = ; - -const defaultNoContentFallback = ; - -type IAIDndImageProps = FlexProps & { - imageDTO: ImageDTO | undefined; - onError?: (event: SyntheticEvent) => void; - onLoad?: (event: SyntheticEvent) => void; - onClick?: (event: MouseEvent) => void; - withMetadataOverlay?: boolean; - isDragDisabled?: boolean; - isDropDisabled?: boolean; - isUploadDisabled?: boolean; - minSize?: number; - postUploadAction?: PostUploadAction; - imageSx?: ChakraProps['sx']; - fitContainer?: boolean; - droppableData?: TypesafeDroppableData; - draggableData?: TypesafeDraggableData; - dropLabel?: ReactNode; - isSelected?: boolean; - isSelectedForCompare?: boolean; - thumbnail?: boolean; - noContentFallback?: ReactElement; - useThumbailFallback?: boolean; - withHoverOverlay?: boolean; - children?: JSX.Element; - uploadElement?: ReactNode; - dataTestId?: string; -}; - -const IAIDndImage = (props: IAIDndImageProps) => { - const { - imageDTO, - onError, - onClick, - withMetadataOverlay = false, - isDropDisabled = false, - isDragDisabled = false, - isUploadDisabled = false, - minSize = 24, - postUploadAction, - imageSx, - fitContainer = false, - droppableData, - draggableData, - dropLabel, - isSelected = false, - isSelectedForCompare = false, - thumbnail = false, - noContentFallback = defaultNoContentFallback, - uploadElement = defaultUploadElement, - useThumbailFallback, - withHoverOverlay = false, - children, - onMouseOver, - onMouseOut, - dataTestId, - ...rest - } = props; - - const [isHovered, setIsHovered] = useState(false); - const handleMouseOver = useCallback( - (e: MouseEvent) => { - if (onMouseOver) { - onMouseOver(e); - } - setIsHovered(true); - }, - [onMouseOver] - ); - const handleMouseOut = useCallback( - (e: MouseEvent) => { - if (onMouseOut) { - onMouseOut(e); - } - setIsHovered(false); - }, - [onMouseOut] - ); - - const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({ - postUploadAction, - isDisabled: isUploadDisabled, - }); - - const uploadButtonStyles = useMemo(() => { - const styles: SystemStyleObject = { - minH: minSize, - w: 'full', - h: 'full', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 'base', - transitionProperty: 'common', - transitionDuration: '0.1s', - color: 'base.500', - }; - if (!isUploadDisabled) { - Object.assign(styles, { - cursor: 'pointer', - bg: 'base.700', - _hover: { - bg: 'base.650', - color: 'base.300', - }, - }); - } - return styles; - }, [isUploadDisabled, minSize]); - - return ( - - {(ref) => ( - - {imageDTO && ( - - } - onError={onError} - draggable={false} - w={imageDTO.width} - objectFit="contain" - maxW="full" - maxH="full" - borderRadius="base" - sx={imageSx} - data-testid={dataTestId} - /> - {withMetadataOverlay && } - - - )} - {!imageDTO && !isUploadDisabled && ( - <> - - - {uploadElement} - - - )} - {!imageDTO && isUploadDisabled && noContentFallback} - {imageDTO && !isDragDisabled && ( - - )} - {children} - {!isDropDisabled && } - - )} - - ); -}; - -export default memo(IAIDndImage); diff --git a/invokeai/frontend/web/src/common/components/IAIDndImageIcon.tsx b/invokeai/frontend/web/src/common/components/IAIDndImageIcon.tsx deleted file mode 100644 index 650a0b6a14e..00000000000 --- a/invokeai/frontend/web/src/common/components/IAIDndImageIcon.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { IconButton } from '@invoke-ai/ui-library'; -import type { MouseEvent, ReactElement } from 'react'; -import { memo, useMemo } from 'react'; - -type Props = { - onClick: (event: MouseEvent) => void; - tooltip: string; - icon?: ReactElement; - styleOverrides?: SystemStyleObject; -}; - -const IAIDndImageIcon = (props: Props) => { - const { onClick, tooltip, icon, styleOverrides } = props; - - const sx = useMemo( - () => ({ - position: 'absolute', - top: 1, - insetInlineEnd: 1, - p: 0, - minW: 0, - svg: { - transitionProperty: 'common', - transitionDuration: 'normal', - fill: 'base.100', - _hover: { fill: 'base.50' }, - filter: 'drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))', - }, - ...styleOverrides, - }), - [styleOverrides] - ); - - return ( - - ); -}; - -export default memo(IAIDndImageIcon); diff --git a/invokeai/frontend/web/src/common/components/IAIDraggable.tsx b/invokeai/frontend/web/src/common/components/IAIDraggable.tsx deleted file mode 100644 index 9e0b5206bc8..00000000000 --- a/invokeai/frontend/web/src/common/components/IAIDraggable.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { BoxProps } from '@invoke-ai/ui-library'; -import { Box } from '@invoke-ai/ui-library'; -import { useDraggableTypesafe } from 'features/dnd/hooks/typesafeHooks'; -import type { TypesafeDraggableData } from 'features/dnd/types'; -import { memo, useRef } from 'react'; -import { v4 as uuidv4 } from 'uuid'; - -type IAIDraggableProps = BoxProps & { - disabled?: boolean; - data?: TypesafeDraggableData; -}; - -const IAIDraggable = (props: IAIDraggableProps) => { - const { data, disabled, ...rest } = props; - const dndId = useRef(uuidv4()); - - const { attributes, listeners, setNodeRef } = useDraggableTypesafe({ - id: dndId.current, - disabled, - data, - }); - - return ( - - ); -}; - -export default memo(IAIDraggable); diff --git a/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx b/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx deleted file mode 100644 index cd3e0cbee11..00000000000 --- a/invokeai/frontend/web/src/common/components/IAIDropOverlay.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Box, Flex } from '@invoke-ai/ui-library'; -import type { AnimationProps } from 'framer-motion'; -import { motion } from 'framer-motion'; -import type { ReactNode } from 'react'; -import { memo, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { v4 as uuidv4 } from 'uuid'; -type Props = { - isOver: boolean; - label?: ReactNode; -}; - -const initial: AnimationProps['initial'] = { - opacity: 0, -}; -const animate: AnimationProps['animate'] = { - opacity: 1, - transition: { duration: 0.1 }, -}; -const exit: AnimationProps['exit'] = { - opacity: 0, - transition: { duration: 0.1 }, -}; - -const IAIDropOverlay = (props: Props) => { - const { t } = useTranslation(); - const { isOver, label = t('gallery.drop') } = props; - const motionId = useRef(uuidv4()); - return ( - - - - - - - {label} - - - - - ); -}; - -export default memo(IAIDropOverlay); diff --git a/invokeai/frontend/web/src/common/components/IAIDroppable.tsx b/invokeai/frontend/web/src/common/components/IAIDroppable.tsx deleted file mode 100644 index ef331c43777..00000000000 --- a/invokeai/frontend/web/src/common/components/IAIDroppable.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Box } from '@invoke-ai/ui-library'; -import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks'; -import type { TypesafeDroppableData } from 'features/dnd/types'; -import { isValidDrop } from 'features/dnd/util/isValidDrop'; -import { AnimatePresence } from 'framer-motion'; -import type { ReactNode } from 'react'; -import { memo, useRef } from 'react'; -import { v4 as uuidv4 } from 'uuid'; - -import IAIDropOverlay from './IAIDropOverlay'; - -type IAIDroppableProps = { - dropLabel?: ReactNode; - disabled?: boolean; - data?: TypesafeDroppableData; -}; - -const IAIDroppable = (props: IAIDroppableProps) => { - const { dropLabel, data, disabled } = props; - const dndId = useRef(uuidv4()); - - const { isOver, setNodeRef, active } = useDroppableTypesafe({ - id: dndId.current, - disabled, - data, - }); - - return ( - - - {isValidDrop(data, active?.data.current) && } - - - ); -}; - -export default memo(IAIDroppable); diff --git a/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx b/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx deleted file mode 100644 index 20e9fa2c68c..00000000000 --- a/invokeai/frontend/web/src/common/components/IAIFillSkeleton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { Box, Skeleton } from '@invoke-ai/ui-library'; -import { memo } from 'react'; - -const skeletonStyles: SystemStyleObject = { - position: 'relative', - height: 'full', - width: 'full', - '::before': { - content: "''", - display: 'block', - pt: '100%', - }, -}; - -const IAIFillSkeleton = () => { - return ( - - - - ); -}; - -export default memo(IAIFillSkeleton); diff --git a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx index 1a23c458cf5..756ffec0d91 100644 --- a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx +++ b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx @@ -1,12 +1,13 @@ -import type { As, ChakraProps, FlexProps } from '@invoke-ai/ui-library'; +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 }; -export const IAILoadingImageFallback = memo((props: Props) => { +const IAILoadingImageFallback = memo((props: Props) => { if (props.image) { return ( { - const { icon = PiImageBold, boxSize = 16, 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] - ); + const { icon = PiImageBold, boxSize = 16, ...rest } = props; return ( - + {icon && } - {props.label && ( - - {props.label} - - )} + {props.label && {props.label}} ); }); 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/ImageMetadataOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx deleted file mode 100644 index 531b2b42983..00000000000 --- a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Badge, Flex } from '@invoke-ai/ui-library'; -import { memo } from 'react'; -import type { ImageDTO } from 'services/api/types'; - -type ImageMetadataOverlayProps = { - imageDTO: ImageDTO; -}; - -const ImageMetadataOverlay = ({ imageDTO }: ImageMetadataOverlayProps) => { - return ( - - - {imageDTO.width} × {imageDTO.height} - - - ); -}; - -export default memo(ImageMetadataOverlay); diff --git a/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx deleted file mode 100644 index abd68e72cbf..00000000000 --- a/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { Box, Flex, Heading } from '@invoke-ai/ui-library'; -import type { AnimationProps } from 'framer-motion'; -import { motion } from 'framer-motion'; -import { memo } from 'react'; -import type { DropzoneState } from 'react-dropzone'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; - -const initial: AnimationProps['initial'] = { - opacity: 0, -}; -const animate: AnimationProps['animate'] = { - opacity: 1, - transition: { duration: 0.1 }, -}; -const exit: AnimationProps['exit'] = { - opacity: 0, - transition: { duration: 0.1 }, -}; - -type ImageUploadOverlayProps = { - dropzone: DropzoneState; - setIsHandlingUpload: (isHandlingUpload: boolean) => void; -}; - -const ImageUploadOverlay = (props: ImageUploadOverlayProps) => { - const { t } = useTranslation(); - const { dropzone, setIsHandlingUpload } = props; - - useHotkeys( - 'esc', - () => { - setIsHandlingUpload(false); - }, - [setIsHandlingUpload] - ); - - return ( - - - - - {dropzone.isDragAccept ? ( - {t('gallery.dropToUpload')} - ) : ( - <> - {t('toast.invalidUpload')} - {t('toast.uploadFailedInvalidUploadDesc')} - - )} - - - - ); -}; -export default memo(ImageUploadOverlay); diff --git a/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx b/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx index 935212ee58d..d967650541b 100644 --- a/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx +++ b/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx @@ -10,10 +10,14 @@ import { PopoverContent, PopoverTrigger, Portal, + Spacer, Text, } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { merge, omit } from 'lodash-es'; +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'; @@ -25,73 +29,93 @@ import { OPEN_DELAY, POPOVER_DATA, POPPER_MODIFIERS } from './constants'; type Props = { feature: Feature; inPortal?: boolean; + hideDisable?: boolean; children: ReactElement; }; -export const InformationalPopover = memo(({ feature, children, inPortal = true, ...rest }: Props) => { - const shouldEnableInformationalPopovers = useAppSelector((s) => s.system.shouldEnableInformationalPopovers); +const selectShouldEnableInformationalPopovers = createSelector( + selectSystemSlice, + (system) => system.shouldEnableInformationalPopovers +); - const data = useMemo(() => POPOVER_DATA[feature], [feature]); +export const InformationalPopover = memo( + ({ feature, children, inPortal = true, hideDisable = false, ...rest }: Props) => { + const shouldEnableInformationalPopovers = useAppSelector(selectShouldEnableInformationalPopovers); - const popoverProps = useMemo(() => merge(omit(data, ['image', 'href', 'buttonLabel']), rest), [data, rest]); + const data = useMemo(() => POPOVER_DATA[feature], [feature]); - if (!shouldEnableInformationalPopovers) { - return children; - } + const popoverProps = useMemo(() => merge(omit(data, ['image', 'href', 'buttonLabel']), rest), [data, rest]); - return ( - - {children} - {inPortal ? ( - - - - ) : ( - - )} - - ); -}); + if (!hideDisable && !shouldEnableInformationalPopovers) { + return children; + } + + return ( + + {children} + {inPortal ? ( + + + + ) : ( + + )} + + ); + } +); InformationalPopover.displayName = 'InformationalPopover'; type ContentProps = { data?: PopoverData; feature: Feature; + hideDisable: boolean; }; -const Content = ({ data, feature }: ContentProps) => { +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`, { + t(`popovers.${feature}.paragraphs`, { returnObjects: true, }) ?? [], [feature, t] ); - const handleClick = useCallback(() => { - if (!data?.href) { + const href = data?.href; + + const onClickLearnMore = useCallback(() => { + if (!href) { return; } - window.open(data.href); - }, [data?.href]); + window.open(href); + }, [href]); + + const onClickDontShowMeThese = useCallback(() => { + dispatch(setShouldEnableInformationalPopovers(false)); + toast({ + title: t('settings.informationalPopoversDisabled'), + description: t('settings.informationalPopoversDisabledDesc'), + status: 'info', + }); + }, [dispatch, t]); return ( - - + + {heading && ( @@ -102,34 +126,28 @@ const Content = ({ data, feature }: ContentProps) => { )} {data?.image && ( <> - Optional Image + Optional Image )} {paragraphs.map((p) => ( {p} ))} - {data?.href && ( - <> - - + )} + + {data?.href && ( + - - )} + )} + diff --git a/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts b/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts index f973d98f1a5..e9d855648ad 100644 --- a/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts +++ b/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts @@ -1,11 +1,18 @@ 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' @@ -22,14 +29,17 @@ export type Feature = | 'dynamicPrompts' | 'dynamicPromptsMaxPrompts' | 'dynamicPromptsSeedBehaviour' + | 'globalReferenceImage' | 'imageFit' | 'infillMethod' + | 'inpainting' | 'ipAdapterMethod' | 'lora' | 'loraWeight' | 'noiseUseCPU' | 'paramAspect' | 'paramCFGScale' + | 'paramGuidance' | 'paramCFGRescaleMultiplier' | 'paramDenoisingStrength' | 'paramHeight' @@ -44,6 +54,7 @@ export type Feature = | 'paramVAEPrecision' | 'paramWidth' | 'patchmatchDownScaleSize' + | 'rasterLayer' | 'refinerModel' | 'refinerNegativeAestheticScore' | 'refinerPositiveAestheticScore' @@ -51,9 +62,23 @@ export type Feature = | 'refinerStart' | 'refinerSteps' | 'refinerCfgScale' + | 'regionalGuidance' + | 'regionalGuidanceAndReferenceImage' + | 'regionalReferenceImage' | 'scaleBeforeProcessing' | 'seamlessTilingXAxis' - | 'seamlessTilingYAxis'; + | 'seamlessTilingYAxis' + | 'colorCompensation' + | 'upscaleModel' + | 'scale' + | 'creativity' + | 'structure' + | 'tileSize' + | 'tileOverlap' + | 'optimizedDenoising' + | 'fluxDevLicense' + | 'cpuOnly' + | 'fp8Storage'; export type PopoverData = PopoverProps & { image?: string; @@ -68,6 +93,33 @@ export const POPOVER_DATA: { [key in Feature]?: PopoverData } = { 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', }, @@ -93,10 +145,10 @@ export const POPOVER_DATA: { [key in Feature]?: PopoverData } = { href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings', }, infillMethod: { - href: 'https://support.invoke.ai/support/solutions/articles/151000158841-infill-and-scaling', + href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings', }, scaleBeforeProcessing: { - href: 'https://support.invoke.ai/support/solutions/articles/151000158841', + href: 'https://support.invoke.ai/support/solutions/articles/151000179777-scale-before-processing', }, paramCFGScale: { href: 'https://www.youtube.com/watch?v=1OeHEJrsTpI', @@ -106,6 +158,7 @@ export const POPOVER_DATA: { [key in Feature]?: PopoverData } = { }, 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-', 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 index dae5b40e8ce..63798f46fa9 100644 --- a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx +++ b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx @@ -6,11 +6,18 @@ import { memo } from 'react'; const Loading = () => { return ( - + { - const shadow = useMemo(() => { - if (isSelected && isHovered) { - return 'nodeHoveredSelected'; - } - if (isSelected) { - return 'nodeSelected'; - } - if (isHovered) { - return 'nodeHovered'; - } - return undefined; - }, [isHovered, isSelected]); - return ( - - ); -}; - -export default memo(SelectionOverlay); diff --git a/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx b/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx index c42fb485202..5da75b10c69 100644 --- a/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx +++ b/invokeai/frontend/web/src/common/components/OverlayScrollbars/ScrollableContent.tsx @@ -1,27 +1,53 @@ +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, useMemo } 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 = { height: '100%', width: '100%' }; +const styles: CSSProperties = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }; -const ScrollableContent = ({ children, maxHeight, overflowX = 'hidden', overflowY = 'scroll' }: Props) => { +const ScrollableContent = ({ children, maxHeight, maxWidth, overflowX = 'hidden', overflowY = 'scroll' }: Props) => { const overlayscrollbarsOptions = useMemo( - () => getOverlayScrollbarsParams(overflowX, overflowY).options, + () => 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} diff --git a/invokeai/frontend/web/src/common/components/OverlayScrollbars/constants.ts b/invokeai/frontend/web/src/common/components/OverlayScrollbars/constants.ts index d72d20e8465..ea3633900dc 100644 --- a/invokeai/frontend/web/src/common/components/OverlayScrollbars/constants.ts +++ b/invokeai/frontend/web/src/common/components/OverlayScrollbars/constants.ts @@ -1,7 +1,8 @@ import { deepClone } from 'common/util/deepClone'; -import { merge } from 'lodash-es'; +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); @@ -19,11 +20,26 @@ export const overlayScrollbarsParams: UseOverlayScrollbarsParams = { }, }; -export const getOverlayScrollbarsParams = ( - overflowX: 'hidden' | 'scroll' = 'hidden', - overflowY: 'hidden' | 'scroll' = 'scroll' -) => { +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 } } }); + 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 index 88279874014..96f8a67ccc9 100644 --- a/invokeai/frontend/web/src/common/components/OverlayScrollbars/overlayscrollbars.css +++ b/invokeai/frontend/web/src/common/components/OverlayScrollbars/overlayscrollbars.css @@ -1,6 +1,6 @@ .os-scrollbar { /* The size of the scrollbar */ - --os-size: 9px; + --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) */ @@ -8,11 +8,11 @@ /* 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.3); */ + --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.3); */ + --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.3); */ + --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 */ @@ -22,11 +22,11 @@ /* The border radius of the scrollbar handle */ /* --os-handle-border-radius: 2px; */ /* The background of the scrollbar handle */ - /* --os-handle-bg: var(--invokeai-colors-accentAlpha-500); */ + --os-handle-bg: var(--invoke-colors-base-400); /* The :hover background of the scrollbar handle */ - /* --os-handle-bg-hover: var(--invokeai-colors-accentAlpha-700); */ + --os-handle-bg-hover: var(--invoke-colors-base-300); /* The :active background of the scrollbar handle */ - /* --os-handle-bg-active: var(--invokeai-colors-accentAlpha-800); */ + --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 */ @@ -34,23 +34,23 @@ /* The :active border of the scrollbar handle */ /* --os-handle-border-active: none; */ /* The min size of the scrollbar handle */ - --os-handle-min-size: 50px; + --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%; */ + --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: 0; */ + --os-handle-interactive-area-offset: -1px; } .os-scrollbar-handle { - cursor: grab; + /* cursor: grab; */ } .os-scrollbar-handle:active { - cursor: grabbing; + /* 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..ce8c2530e4d --- /dev/null +++ b/invokeai/frontend/web/src/common/components/Picker/Picker.tsx @@ -0,0 +1,1129 @@ +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, StoreValue, Task, WritableAtom } from 'nanostores'; +import { atom, computed } from 'nanostores'; +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'); + +type StoreValues = { + [Index in keyof Stores]: StoreValue; +}; + +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/RgbColorPicker.tsx b/invokeai/frontend/web/src/common/components/RgbColorPicker.tsx deleted file mode 100644 index ecb9405a3d6..00000000000 --- a/invokeai/frontend/web/src/common/components/RgbColorPicker.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import type { ChakraProps } from '@invoke-ai/ui-library'; -import { CompositeNumberInput, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import type { CSSProperties } from 'react'; -import { memo, useCallback } from 'react'; -import { RgbColorPicker as ColorfulRgbColorPicker } from 'react-colorful'; -import type { ColorPickerBaseProps, RgbColor } from 'react-colorful/dist/types'; -import { useTranslation } from 'react-i18next'; - -type RgbColorPickerProps = ColorPickerBaseProps & { - withNumberInput?: 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: 5, - flexDir: 'column', -}; - -const colorPickerStyles: CSSProperties = { width: '100%' }; - -const numberInputWidth: ChakraProps['w'] = '3.5rem'; - -const RgbColorPicker = (props: RgbColorPickerProps) => { - const { color, onChange, withNumberInput, ...rest } = props; - const { t } = useTranslation(); - const handleChangeR = useCallback((r: number) => onChange({ ...color, r }), [color, onChange]); - const handleChangeG = useCallback((g: number) => onChange({ ...color, g }), [color, onChange]); - const handleChangeB = useCallback((b: number) => onChange({ ...color, b }), [color, onChange]); - return ( - - - {withNumberInput && ( - - - {t('common.red')[0]} - - - - {t('common.green')[0]} - - - - {t('common.blue')[0]} - - - - )} - - ); -}; - -export default memo(RgbColorPicker); diff --git a/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx b/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx deleted file mode 100644 index 3e2ecca4aef..00000000000 --- a/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Box } from '@invoke-ai/ui-library'; -import { memo, useMemo } from 'react'; - -type Props = { - isSelected: boolean; - isSelectedForCompare: boolean; - isHovered: boolean; -}; -const SelectionOverlay = ({ isSelected, isSelectedForCompare, isHovered }: Props) => { - const shadow = useMemo(() => { - if (isSelectedForCompare && isHovered) { - return 'hoverSelectedForCompare'; - } - if (isSelectedForCompare && !isHovered) { - return 'selectedForCompare'; - } - if (isSelected && isHovered) { - return 'hoverSelected'; - } - if (isSelected && !isHovered) { - return 'selected'; - } - if (!isSelected && isHovered) { - return 'hoverUnselected'; - } - return undefined; - }, [isHovered, isSelected, isSelectedForCompare]); - return ( - - ); -}; - -export default memo(SelectionOverlay); 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..b9a59594d14 --- /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 index 123e48cd755..ec68457ecdd 100644 --- a/invokeai/frontend/web/src/common/hooks/useBoolean.ts +++ b/invokeai/frontend/web/src/common/hooks/useBoolean.ts @@ -1,21 +1,151 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useStore } from '@nanostores/react'; +import type { WritableAtom } from 'nanostores'; +import { atom } from 'nanostores'; +import { useCallback, useState } from 'react'; -export const useBoolean = (initialValue: boolean) => { - const [isTrue, set] = useState(initialValue); - const setTrue = useCallback(() => set(true), []); - const setFalse = useCallback(() => set(false), []); - const toggle = useCallback(() => set((v) => !v), []); +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 api = useMemo( - () => ({ + const useBoolean = () => { + const isTrue = useStore($boolean); + + return { isTrue, - set, 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, - }), - [isTrue, set, setTrue, setFalse, 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 api; + 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..c11189db870 --- /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/useChakraThemeTokens.ts b/invokeai/frontend/web/src/common/hooks/useChakraThemeTokens.ts deleted file mode 100644 index 93345f6a4cb..00000000000 --- a/invokeai/frontend/web/src/common/hooks/useChakraThemeTokens.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { useToken } from '@invoke-ai/ui-library'; - -export const useChakraThemeTokens = () => { - const [ - base50, - base100, - base150, - base200, - base250, - base300, - base350, - base400, - base450, - base500, - base550, - base600, - base650, - base700, - base750, - base800, - base850, - base900, - base950, - accent50, - accent100, - accent150, - accent200, - accent250, - accent300, - accent350, - accent400, - accent450, - accent500, - accent550, - accent600, - accent650, - accent700, - accent750, - accent800, - accent850, - accent900, - accent950, - baseAlpha50, - baseAlpha100, - baseAlpha150, - baseAlpha200, - baseAlpha250, - baseAlpha300, - baseAlpha350, - baseAlpha400, - baseAlpha450, - baseAlpha500, - baseAlpha550, - baseAlpha600, - baseAlpha650, - baseAlpha700, - baseAlpha750, - baseAlpha800, - baseAlpha850, - baseAlpha900, - baseAlpha950, - accentAlpha50, - accentAlpha100, - accentAlpha150, - accentAlpha200, - accentAlpha250, - accentAlpha300, - accentAlpha350, - accentAlpha400, - accentAlpha450, - accentAlpha500, - accentAlpha550, - accentAlpha600, - accentAlpha650, - accentAlpha700, - accentAlpha750, - accentAlpha800, - accentAlpha850, - accentAlpha900, - accentAlpha950, - ] = useToken('colors', [ - 'base.50', - 'base.100', - 'base.150', - 'base.200', - 'base.250', - 'base.300', - 'base.350', - 'base.400', - 'base.450', - 'base.500', - 'base.550', - 'base.600', - 'base.650', - 'base.700', - 'base.750', - 'base.800', - 'base.850', - 'base.900', - 'base.950', - 'accent.50', - 'accent.100', - 'accent.150', - 'accent.200', - 'accent.250', - 'accent.300', - 'accent.350', - 'accent.400', - 'accent.450', - 'accent.500', - 'accent.550', - 'accent.600', - 'accent.650', - 'accent.700', - 'accent.750', - 'accent.800', - 'accent.850', - 'accent.900', - 'accent.950', - 'baseAlpha.50', - 'baseAlpha.100', - 'baseAlpha.150', - 'baseAlpha.200', - 'baseAlpha.250', - 'baseAlpha.300', - 'baseAlpha.350', - 'baseAlpha.400', - 'baseAlpha.450', - 'baseAlpha.500', - 'baseAlpha.550', - 'baseAlpha.600', - 'baseAlpha.650', - 'baseAlpha.700', - 'baseAlpha.750', - 'baseAlpha.800', - 'baseAlpha.850', - 'baseAlpha.900', - 'baseAlpha.950', - 'accentAlpha.50', - 'accentAlpha.100', - 'accentAlpha.150', - 'accentAlpha.200', - 'accentAlpha.250', - 'accentAlpha.300', - 'accentAlpha.350', - 'accentAlpha.400', - 'accentAlpha.450', - 'accentAlpha.500', - 'accentAlpha.550', - 'accentAlpha.600', - 'accentAlpha.650', - 'accentAlpha.700', - 'accentAlpha.750', - 'accentAlpha.800', - 'accentAlpha.850', - 'accentAlpha.900', - 'accentAlpha.950', - ]); - - return { - base50, - base100, - base150, - base200, - base250, - base300, - base350, - base400, - base450, - base500, - base550, - base600, - base650, - base700, - base750, - base800, - base850, - base900, - base950, - accent50, - accent100, - accent150, - accent200, - accent250, - accent300, - accent350, - accent400, - accent450, - accent500, - accent550, - accent600, - accent650, - accent700, - accent750, - accent800, - accent850, - accent900, - accent950, - baseAlpha50, - baseAlpha100, - baseAlpha150, - baseAlpha200, - baseAlpha250, - baseAlpha300, - baseAlpha350, - baseAlpha400, - baseAlpha450, - baseAlpha500, - baseAlpha550, - baseAlpha600, - baseAlpha650, - baseAlpha700, - baseAlpha750, - baseAlpha800, - baseAlpha850, - baseAlpha900, - baseAlpha950, - accentAlpha50, - accentAlpha100, - accentAlpha150, - accentAlpha200, - accentAlpha250, - accentAlpha300, - accentAlpha350, - accentAlpha400, - accentAlpha450, - accentAlpha500, - accentAlpha550, - accentAlpha600, - accentAlpha650, - accentAlpha700, - accentAlpha750, - accentAlpha800, - accentAlpha850, - accentAlpha900, - accentAlpha950, - }; -}; diff --git a/invokeai/frontend/web/src/common/hooks/useClearStorage.ts b/invokeai/frontend/web/src/common/hooks/useClearStorage.ts deleted file mode 100644 index b8338829021..00000000000 --- a/invokeai/frontend/web/src/common/hooks/useClearStorage.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { clearIdbKeyValStore } from 'app/store/enhancers/reduxRemember/driver'; -import { useCallback } from 'react'; - -export const useClearStorage = () => { - const clearStorage = useCallback(() => { - clearIdbKeyValStore(); - localStorage.clear(); - }, []); - - return clearStorage; -}; 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 index 233b8410341..e46227b2f5d 100644 --- a/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts +++ b/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts @@ -1,40 +1,28 @@ -import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob'; -import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard'; +import { useClipboard } from 'common/hooks/useClipboard'; +import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob'; import { toast } from 'features/toast/toast'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const useCopyImageToClipboard = () => { const { t } = useTranslation(); - const imageUrlToBlob = useImageUrlToBlob(); - - const isClipboardAPIAvailable = useMemo(() => { - return Boolean(navigator.clipboard) && Boolean(window.ClipboardItem); - }, []); + const clipboard = useClipboard(); const copyImageToClipboard = useCallback( async (image_url: string) => { - if (!isClipboardAPIAvailable) { - toast({ - id: 'PROBLEM_COPYING_IMAGE', - title: t('toast.problemCopyingImage'), - description: "Your browser doesn't support the Clipboard API.", - status: 'error', - }); - } try { - const blob = await imageUrlToBlob(image_url); + const blob = await convertImageUrlToBlob(image_url); if (!blob) { throw new Error('Unable to create Blob'); } - copyBlobToClipboard(blob); - - toast({ - id: 'IMAGE_COPIED', - title: t('toast.imageCopied'), - status: 'success', + clipboard.writeImage(blob, () => { + toast({ + id: 'IMAGE_COPIED', + title: t('toast.imageCopied'), + status: 'success', + }); }); } catch (err) { toast({ @@ -45,8 +33,8 @@ export const useCopyImageToClipboard = () => { }); } }, - [imageUrlToBlob, isClipboardAPIAvailable, t] + [clipboard, t] ); - return { isClipboardAPIAvailable, copyImageToClipboard }; + return copyImageToClipboard; }; diff --git a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts index ede247b9fbe..33b90e1d7fe 100644 --- a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts +++ b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts @@ -1,27 +1,14 @@ -import { useStore } from '@nanostores/react'; -import { $authToken } from 'app/store/nanostores/authToken'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { imageDownloaded } from 'features/gallery/store/actions'; import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -export const useDownloadImage = () => { +export const useDownloadItem = () => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const authToken = useStore($authToken); - const downloadImage = useCallback( - async (image_url: string, image_name: string) => { + const downloadItem = useCallback( + async (item_url: string, item_id: string) => { try { - const requestOpts = authToken - ? { - headers: { - Authorization: `Bearer ${authToken}`, - }, - } - : {}; - const blob = await fetch(image_url, requestOpts).then((resp) => resp.blob()); + const blob = await fetch(item_url).then((resp) => resp.blob()); if (!blob) { throw new Error('Unable to create Blob'); } @@ -30,11 +17,10 @@ export const useDownloadImage = () => { const a = document.createElement('a'); a.style.display = 'none'; a.href = url; - a.download = image_name; + a.download = item_id; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); - dispatch(imageDownloaded()); } catch (err) { toast({ id: 'PROBLEM_DOWNLOADING_IMAGE', @@ -44,8 +30,8 @@ export const useDownloadImage = () => { }); } }, - [t, dispatch, authToken] + [t] ); - return { downloadImage }; + 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..566b1212ec0 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useEditable.ts @@ -0,0 +1,73 @@ +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 [prevValue, setPrevValue] = useState(value); + const [localValue, setLocalValue] = useState(value); + + if (value !== prevValue) { + setPrevValue(value); + setLocalValue(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(() => { + 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/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts deleted file mode 100644 index 5b1bf1f5b39..00000000000 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { toast } from 'features/toast/toast'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { useCallback, useEffect, useState } from 'react'; -import type { Accept, FileRejection } from 'react-dropzone'; -import { useDropzone } from 'react-dropzone'; -import { useTranslation } from 'react-i18next'; -import { useUploadImageMutation } from 'services/api/endpoints/images'; -import type { PostUploadAction } from 'services/api/types'; - -const accept: Accept = { - 'image/png': ['.png'], - 'image/jpeg': ['.jpg', '.jpeg', '.png'], -}; - -const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (activeTabName) => { - let postUploadAction: PostUploadAction = { type: 'TOAST' }; - - if (activeTabName === 'canvas') { - postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' }; - } - - return postUploadAction; -}); - -export const useFullscreenDropzone = () => { - const { t } = useTranslation(); - const postUploadAction = useAppSelector(selectPostUploadAction); - const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); - const [isHandlingUpload, setIsHandlingUpload] = useState(false); - - const [uploadImage] = useUploadImageMutation(); - - const fileRejectionCallback = useCallback( - (rejection: FileRejection) => { - setIsHandlingUpload(true); - - toast({ - id: 'UPLOAD_FAILED', - title: t('toast.uploadFailed'), - description: rejection.errors.map((error) => error.message).join('\n'), - status: 'error', - }); - }, - [t] - ); - - const fileAcceptedCallback = useCallback( - async (file: File) => { - uploadImage({ - file, - image_category: 'user', - is_intermediate: false, - postUploadAction, - board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId, - }); - }, - [autoAddBoardId, postUploadAction, uploadImage] - ); - - const onDrop = useCallback( - (acceptedFiles: Array, fileRejections: Array) => { - if (fileRejections.length > 1) { - toast({ - id: 'UPLOAD_FAILED', - title: t('toast.uploadFailed'), - description: t('toast.uploadFailedInvalidUploadDesc'), - status: 'error', - }); - return; - } - - fileRejections.forEach((rejection: FileRejection) => { - fileRejectionCallback(rejection); - }); - - acceptedFiles.forEach((file: File) => { - fileAcceptedCallback(file); - }); - }, - [t, fileAcceptedCallback, fileRejectionCallback] - ); - - const onDragOver = useCallback(() => { - setIsHandlingUpload(true); - }, []); - - const dropzone = useDropzone({ - accept, - noClick: true, - onDrop, - onDragOver, - multiple: false, - noKeyboard: true, - }); - - useEffect(() => { - // This is a hack to allow pasting images into the uploader - const handlePaste = async (e: ClipboardEvent) => { - if (!dropzone.inputRef.current) { - return; - } - - if (e.clipboardData?.files) { - // Set the files on the dropzone.inputRef - dropzone.inputRef.current.files = e.clipboardData.files; - // Dispatch the change event, dropzone catches this and we get to use its own validation - dropzone.inputRef.current?.dispatchEvent(new Event('change', { bubbles: true })); - } - }; - - // Add the paste event listener - document.addEventListener('paste', handlePaste); - - return () => { - document.removeEventListener('paste', handlePaste); - }; - }, [dropzone.inputRef]); - - return { dropzone, isHandlingUpload, setIsHandlingUpload }; -}; diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts index 9ba044199f9..dd43c0b0947 100644 --- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -1,108 +1,147 @@ -import { useAppDispatch } from 'app/store/storeHooks'; -import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; +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 { useQueueBack } from 'features/queue/hooks/useQueueBack'; -import { useQueueFront } from 'features/queue/hooks/useQueueFront'; -import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { setActiveTab } from 'features/ui/store/uiSlice'; -import { useHotkeys } from 'react-hotkeys-hook'; +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 = useAppDispatch(); - const isModelManagerEnabled = useFeatureStatus('modelManager'); - const { queueBack, isDisabled: isDisabledQueueBack, isLoading: isLoadingQueueBack } = useQueueBack(); - - useHotkeys( - ['ctrl+enter', 'meta+enter'], - queueBack, - { - enabled: () => !isDisabledQueueBack && !isLoadingQueueBack, + 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'], }, - [queueBack, isDisabledQueueBack, isLoadingQueueBack] - ); + dependencies: [queue], + }); - const { queueFront, isDisabled: isDisabledQueueFront, isLoading: isLoadingQueueFront } = useQueueFront(); - - useHotkeys( - ['ctrl+shift+enter', 'meta+shift+enter'], - queueFront, - { - enabled: () => !isDisabledQueueFront && !isLoadingQueueFront, + useRegisteredHotkeys({ + id: 'invokeFront', + category: 'app', + callback: queue.enqueueFront, + options: { + enabled: !queue.isDisabled && !queue.isLoading, preventDefault: true, enableOnFormTags: ['input', 'textarea', 'select'], }, - [queueFront, isDisabledQueueFront, isLoadingQueueFront] - ); - - const { - cancelQueueItem, - isDisabled: isDisabledCancelQueueItem, - isLoading: isLoadingCancelQueueItem, - } = useCancelCurrentQueueItem(); - - useHotkeys( - ['shift+x'], - cancelQueueItem, - { - enabled: () => !isDisabledCancelQueueItem && !isLoadingCancelQueueItem, + dependencies: [queue], + }); + + const deleteCurrentQueueItem = useDeleteCurrentQueueItem(); + + useRegisteredHotkeys({ + id: 'cancelQueueItem', + category: 'app', + callback: deleteCurrentQueueItem.trigger, + options: { + enabled: !deleteCurrentQueueItem.isDisabled && !deleteCurrentQueueItem.isLoading, preventDefault: true, }, - [cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem] - ); + dependencies: [deleteCurrentQueueItem], + }); - const { clearQueue, isDisabled: isDisabledClearQueue, isLoading: isLoadingClearQueue } = useClearQueue(); + const clearQueue = useClearQueue(); - useHotkeys( - ['ctrl+shift+x', 'meta+shift+x'], - clearQueue, - { - enabled: () => !isDisabledClearQueue && !isLoadingClearQueue, + useRegisteredHotkeys({ + id: 'clearQueue', + category: 'app', + callback: clearQueue.trigger, + options: { + enabled: !clearQueue.isDisabled && !clearQueue.isLoading, preventDefault: true, }, - [clearQueue, isDisabledClearQueue, isLoadingClearQueue] - ); + dependencies: [clearQueue], + }); + + useRegisteredHotkeys({ + id: 'selectGenerateTab', + category: 'app', + callback: () => { + navigationApi.switchToTab('generate'); + }, + dependencies: [dispatch], + }); - useHotkeys( - '1', - () => { - dispatch(setActiveTab('generation')); + useRegisteredHotkeys({ + id: 'selectCanvasTab', + category: 'app', + callback: () => { + navigationApi.switchToTab('canvas'); }, - [dispatch] - ); + dependencies: [dispatch], + }); - useHotkeys( - '2', - () => { - dispatch(setActiveTab('canvas')); + useRegisteredHotkeys({ + id: 'selectUpscalingTab', + category: 'app', + callback: () => { + navigationApi.switchToTab('upscaling'); }, - [dispatch] - ); + dependencies: [dispatch], + }); - useHotkeys( - '3', - () => { - dispatch(setActiveTab('workflows')); + useRegisteredHotkeys({ + id: 'selectWorkflowsTab', + category: 'app', + callback: () => { + navigationApi.switchToTab('workflows'); }, - [dispatch] - ); - - useHotkeys( - '4', - () => { - if (isModelManagerEnabled) { - dispatch(setActiveTab('models')); + 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); }, - [dispatch, isModelManagerEnabled] - ); + dependencies: [getState, deleteImageModalApi], + }); - useHotkeys( - isModelManagerEnabled ? '5' : '4', - () => { - dispatch(setActiveTab('queue')); + useRegisteredHotkeys({ + id: 'toggleViewer', + category: 'viewer', + callback: () => { + navigationApi.toggleViewerPanel(); }, - [dispatch, isModelManagerEnabled] - ); + dependencies: [], + }); }; diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index 5b57fcd2bbd..a7f8c812af2 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -1,8 +1,11 @@ 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 { groupBy, reduce } from 'lodash-es'; +import { selectSystemShouldEnableModelDescriptions } from 'features/system/store/systemSlice'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import type { AnyModelConfig } from 'services/api/types'; @@ -28,11 +31,14 @@ const groupByBaseFunc = (model: T) => model.base.toUpp 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_model = useAppSelector((s) => s.generation.model?.base ?? 'sdxl'); + const base = useAppSelector(selectBaseWithSDXLFallback); + const shouldShowModelDescriptions = useAppSelector(selectSystemShouldEnableModelDescriptions); const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg; const options = useMemo[]>(() => { if (!modelConfigs) { @@ -47,6 +53,7 @@ export const useGroupedModelCombobox = ( options: val.map((model) => ({ label: model.name, value: model.key, + description: (shouldShowModelDescriptions && model.description) || undefined, isDisabled: getIsDisabled ? getIsDisabled(model) : false, })), }); @@ -54,9 +61,9 @@ export const useGroupedModelCombobox = ( }, [] as GroupBase[] ); - _options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base_model) ? -1 : 1)); + _options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base) ? -1 : 1)); return _options; - }, [modelConfigs, groupByType, getIsDisabled, base_model]); + }, [modelConfigs, groupByType, getIsDisabled, base, shouldShowModelDescriptions]); const value = useMemo( () => diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx index 011f49ec269..fc173de979f 100644 --- a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx +++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx @@ -1,14 +1,49 @@ +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 { useCallback } from 'react'; +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 { useUploadImageMutation } from 'services/api/endpoints/images'; -import type { PostUploadAction } from 'services/api/types'; +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'; -type UseImageUploadButtonArgs = { - postUploadAction?: PostUploadAction; - isDisabled?: boolean; +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. * @@ -28,26 +63,94 @@ type UseImageUploadButtonArgs = { * + + + ); +}); +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/useImageUrlToBlob.ts b/invokeai/frontend/web/src/common/hooks/useImageUrlToBlob.ts deleted file mode 100644 index 31faf5f22f1..00000000000 --- a/invokeai/frontend/web/src/common/hooks/useImageUrlToBlob.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { $authToken } from 'app/store/nanostores/authToken'; -import { useCallback } from 'react'; - -/** - * 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 useImageUrlToBlob = () => { - const imageUrlToBlob = useCallback( - async (url: string) => - new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - - const context = canvas.getContext('2d'); - if (!context) { - return; - } - context.drawImage(img, 0, 0); - resolve( - new Promise((resolve) => { - canvas.toBlob(function (blob) { - resolve(blob); - }, 'image/png'); - }) - ); - }; - img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous'; - img.src = url; - }), - [] - ); - - return imageUrlToBlob; -}; diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts deleted file mode 100644 index dbf3c414807..00000000000 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { useStore } from '@nanostores/react'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterAll, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer } from 'features/controlLayers/store/types'; -import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; -import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; -import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import type { Templates } from 'features/nodes/store/types'; -import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; -import { selectSystemSlice } from 'features/system/store/systemSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import i18n from 'i18next'; -import { forEach, upperFirst } from 'lodash-es'; -import { useMemo } from 'react'; -import { getConnectedEdges } from 'reactflow'; - -const LAYER_TYPE_TO_TKEY: Record = { - initial_image_layer: 'controlLayers.globalInitialImage', - control_adapter_layer: 'controlLayers.globalControlAdapter', - ip_adapter_layer: 'controlLayers.globalIPAdapter', - regional_guidance_layer: 'controlLayers.regionalGuidance', -}; - -const createSelector = (templates: Templates) => - createMemoizedSelector( - [ - selectControlAdaptersSlice, - selectGenerationSlice, - selectSystemSlice, - selectNodesSlice, - selectWorkflowSettingsSlice, - selectDynamicPromptsSlice, - selectControlLayersSlice, - activeTabNameSelector, - ], - (controlAdapters, generation, system, nodes, workflowSettings, dynamicPrompts, controlLayers, activeTabName) => { - const { model } = generation; - const { size } = controlLayers.present; - const { positivePrompt } = controlLayers.present; - - const { isConnected } = system; - - const reasons: { prefix?: string; content: string }[] = []; - - // Cannot generate if not connected - if (!isConnected) { - reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') }); - } - - if (activeTabName === 'workflows') { - if (workflowSettings.shouldValidateGraph) { - if (!nodes.nodes.length) { - reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') }); - } - - nodes.nodes.forEach((node) => { - if (!isInvocationNode(node)) { - return; - } - - const nodeTemplate = templates[node.data.type]; - - if (!nodeTemplate) { - // Node type not found - reasons.push({ content: i18n.t('parameters.invoke.missingNodeTemplate') }); - return; - } - - const connectedEdges = getConnectedEdges([node], nodes.edges); - - forEach(node.data.inputs, (field) => { - const fieldTemplate = nodeTemplate.inputs[field.name]; - const hasConnection = connectedEdges.some( - (edge) => edge.target === node.id && edge.targetHandle === field.name - ); - - if (!fieldTemplate) { - reasons.push({ content: i18n.t('parameters.invoke.missingFieldTemplate') }); - return; - } - - if (fieldTemplate.required && field.value === undefined && !hasConnection) { - reasons.push({ - content: i18n.t('parameters.invoke.missingInputForField', { - nodeLabel: node.data.label || nodeTemplate.title, - fieldLabel: field.label || fieldTemplate.title, - }), - }); - return; - } - }); - }); - } - } else { - if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) { - reasons.push({ content: i18n.t('parameters.invoke.noPrompts') }); - } - - if (!model) { - reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); - } - - if (activeTabName === 'generation') { - // Handling for generation tab - controlLayers.present.layers - .filter((l) => l.isEnabled) - .forEach((l, i) => { - const layerLiteral = i18n.t('controlLayers.layers_one'); - const layerNumber = i + 1; - const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]); - const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; - const problems: string[] = []; - if (l.type === 'control_adapter_layer') { - // Must have model - if (!l.controlAdapter.model) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); - } - // Model base must match - if (l.controlAdapter.model?.base !== model?.base) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); - } - // Must have a control image OR, if it has a processor, it must have a processed image - if (!l.controlAdapter.image) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); - } else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) { - problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); - } - // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) - if (l.controlAdapter.type === 't2i_adapter') { - const multiple = model?.base === 'sdxl' ? 32 : 64; - if (size.width % multiple !== 0 || size.height % multiple !== 0) { - problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); - } - } - } - - if (l.type === 'ip_adapter_layer') { - // Must have model - if (!l.ipAdapter.model) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); - } - // Model base must match - if (l.ipAdapter.model?.base !== model?.base) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); - } - // Must have an image - if (!l.ipAdapter.image) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); - } - } - - if (l.type === 'initial_image_layer') { - // Must have an image - if (!l.image) { - problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected')); - } - } - - if (l.type === 'regional_guidance_layer') { - // Must have a region - if (l.maskObjects.length === 0) { - problems.push(i18n.t('parameters.invoke.layer.rgNoRegion')); - } - // Must have at least 1 prompt or IP Adapter - if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) { - problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters')); - } - l.ipAdapters.forEach((ipAdapter) => { - // Must have model - if (!ipAdapter.model) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); - } - // Model base must match - if (ipAdapter.model?.base !== model?.base) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); - } - // Must have an image - if (!ipAdapter.image) { - problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); - } - }); - } - - if (problems.length) { - const content = upperFirst(problems.join(', ')); - reasons.push({ prefix, content }); - } - }); - } else { - // Handling for all other tabs - selectControlAdapterAll(controlAdapters) - .filter((ca) => ca.isEnabled) - .forEach((ca, i) => { - if (!ca.isEnabled) { - return; - } - - if (!ca.model) { - reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) }); - } else if (ca.model.base !== model?.base) { - // This should never happen, just a sanity check - reasons.push({ - content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }), - }); - } - - if ( - !ca.controlImage || - (isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none') - ) { - reasons.push({ - content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }), - }); - } - }); - } - } - - return { isReady: !reasons.length, reasons }; - } - ); - -export const useIsReadyToEnqueue = () => { - const templates = useStore($templates); - const selector = useMemo(() => createSelector(templates), [templates]); - const value = useAppSelector(selector); - return value; -}; 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..200c0d1470d --- /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 index d57ef483373..b79ca940e0b 100644 --- a/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts @@ -1,5 +1,7 @@ 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'; @@ -24,16 +26,19 @@ type UseModelComboboxReturn = { 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]); + }, [optionsFilter, getIsDisabled, modelConfigs, shouldShowModelDescriptions]); const value = useMemo( - () => options.find((m) => (selectedModel ? m.value === selectedModel.key : false)), + () => options.find((m) => (selectedModel ? m.value === selectedModel.key : false)) ?? null, [options, selectedModel] ); 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..76bbc6d2df2 --- /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/useProgressDeviceLabel.ts b/invokeai/frontend/web/src/common/hooks/useProgressDeviceLabel.ts new file mode 100644 index 00000000000..701f7ce1ae9 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useProgressDeviceLabel.ts @@ -0,0 +1,39 @@ +import { getCudaDeviceIndex } from 'common/util/getCudaDeviceIndex'; +import { getDeviceNameLabels } from 'common/util/getDeviceNameLabels'; +import { useMemo } from 'react'; +import { useGetGenerationDeviceOptionsQuery } from 'services/api/endpoints/appInfo'; + +type ProgressDeviceLabel = { + /** The CUDA device index, shown in the center of the progress circle (e.g. `0`). */ + index: number; + /** The human-readable device name and number, shown on hover (e.g. `"AMD Radeon PRO W7900 #1"`). */ + name: string; +}; + +/** + * Resolve a device string (e.g. `"cuda:0"`) to the GPU index + name used to annotate progress + * previews. + * + * Returns `null` when there is nothing to show: the device is not a CUDA GPU, or only a single GPU + * is available (single-GPU setups show neither the index nor the hover tooltip). + */ +export const useProgressDeviceLabel = (device: string | null | undefined): ProgressDeviceLabel | null => { + const { data: deviceOptions } = useGetGenerationDeviceOptionsQuery(); + + return useMemo(() => { + const index = getCudaDeviceIndex(device); + if (index === null) { + return null; + } + const options = deviceOptions ?? []; + // With a single GPU there is no ambiguity to resolve, so we show nothing. + if (options.length <= 1) { + return null; + } + const name = device ? getDeviceNameLabels(options)[device] : undefined; + if (!name) { + return null; + } + return { index, name }; + }, [device, deviceOptions]); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useSingleAndDoubleClick.ts b/invokeai/frontend/web/src/common/hooks/useSingleAndDoubleClick.ts deleted file mode 100644 index 7a02ae54ecc..00000000000 --- a/invokeai/frontend/web/src/common/hooks/useSingleAndDoubleClick.ts +++ /dev/null @@ -1,35 +0,0 @@ -// https://stackoverflow.com/a/73731908 -import { useCallback, useEffect, useState } from 'react'; - -type UseSingleAndDoubleClickOptions = { - onSingleClick: () => void; - onDoubleClick: () => void; - latency?: number; -}; - -export function useSingleAndDoubleClick({ - onSingleClick, - onDoubleClick, - latency = 250, -}: UseSingleAndDoubleClickOptions): () => void { - const [click, setClick] = useState(0); - - useEffect(() => { - const timer = setTimeout(() => { - if (click === 1) { - onSingleClick(); - } - setClick(0); - }, latency); - - if (click === 2) { - onDoubleClick(); - } - - return () => clearTimeout(timer); - }, [click, onDoubleClick, latency, onSingleClick]); - - const onClick = useCallback(() => setClick((prev) => prev + 1), []); - - return onClick; -} 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..1e04ab4a6bb --- /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/types.ts b/invokeai/frontend/web/src/common/types.ts deleted file mode 100644 index f3037dcc2be..00000000000 --- a/invokeai/frontend/web/src/common/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue }; - -export interface JSONObject { - [k: string]: JSONValue; -} 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/arrayBuffer.ts b/invokeai/frontend/web/src/common/util/arrayBuffer.ts deleted file mode 100644 index a21c9d8a479..00000000000 --- a/invokeai/frontend/web/src/common/util/arrayBuffer.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const getImageDataTransparency = (pixels: Uint8ClampedArray) => { - let isFullyTransparent = true; - let isPartiallyTransparent = false; - const len = pixels.length; - let i = 3; - for (i; i < len; i += 4) { - if (pixels[i] === 255) { - isFullyTransparent = false; - } else { - isPartiallyTransparent = true; - } - if (!isFullyTransparent && isPartiallyTransparent) { - return { isFullyTransparent, isPartiallyTransparent }; - } - } - return { isFullyTransparent, isPartiallyTransparent }; -}; - -export const areAnyPixelsBlack = (pixels: Uint8ClampedArray) => { - const len = pixels.length; - let i = 0; - for (i; i < len; ) { - if (pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 255) { - return true; - } - } - return false; -}; diff --git a/invokeai/frontend/web/src/common/util/arrayUtils.test.ts b/invokeai/frontend/web/src/common/util/arrayUtils.test.ts index 5d0fd090f75..e1922fdbbeb 100644 --- a/invokeai/frontend/web/src/common/util/arrayUtils.test.ts +++ b/invokeai/frontend/web/src/common/util/arrayUtils.test.ts @@ -1,85 +1,170 @@ -import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; +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('moveForwardOne', () => { - it('should move an item forward by one position', () => { - const array = [...originalArray]; - const result = moveForward(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 = moveForward(array, (item) => item === 'd'); - expect(result).toEqual(['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 = moveForward(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + 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('moveToFront', () => { - it('should move an item to the front', () => { - const array = [...originalArray]; - const result = moveToFront(array, (item) => item === 'c'); - expect(result).toEqual(['c', 'a', 'b', 'd']); - }); + 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 do nothing if the item is already at the front', () => { - const array = [...originalArray]; - const result = moveToFront(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 = moveToFront(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + 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('moveBackwardsOne', () => { - it('should move an item backward by one position', () => { - const array = [...originalArray]; - const result = moveBackward(array, (item) => item === 'c'); - expect(result).toEqual(['a', 'c', 'b', 'd']); - }); + 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 do nothing if the item is at the beginning', () => { - const array = [...originalArray]; - const result = moveBackward(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 = moveBackward(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + 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('moveToBack', () => { - it('should move an item to the back', () => { - const array = [...originalArray]; - const result = moveToBack(array, (item) => item === 'b'); - expect(result).toEqual(['a', 'c', 'd', 'b']); - }); + 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 do nothing if the item is already at the back', () => { - const array = [...originalArray]; - const result = moveToBack(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 = moveToBack(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + 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 index 38c99b63ec4..9f0d4cfbf6c 100644 --- a/invokeai/frontend/web/src/common/util/arrayUtils.ts +++ b/invokeai/frontend/web/src/common/util/arrayUtils.ts @@ -1,37 +1,45 @@ -export const moveForward = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); - 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; -}; - -export const moveToFront = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); +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 const moveBackward = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); +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 const moveToBack = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); +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 index 835b2a3e35b..85635f93b55 100644 --- a/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts +++ b/invokeai/frontend/web/src/common/util/colorCodeTransformers.ts @@ -1,4 +1,4 @@ -import type { RgbaColor } from 'react-colorful'; +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); @@ -15,3 +15,13 @@ export function hexToRGBA(hex: string, alpha: number) { 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/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/dateComparator.ts b/invokeai/frontend/web/src/common/util/dateComparator.ts deleted file mode 100644 index 27af542261a..00000000000 --- a/invokeai/frontend/web/src/common/util/dateComparator.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Comparator function for sorting dates in ascending order - */ -export const dateComparator = (a: string, b: string) => { - const dateA = new Date(a); - const dateB = new Date(b); - - // sort in ascending order - if (dateA > dateB) { - return 1; - } - if (dateA < dateB) { - return -1; - } - return 0; -}; 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 index c79685fedad..06faf2cf26e 100644 --- a/invokeai/frontend/web/src/common/util/generateSeeds.ts +++ b/invokeai/frontend/web/src/common/util/generateSeeds.ts @@ -1,5 +1,5 @@ import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; -import { random } from 'lodash-es'; +import { random } from 'es-toolkit/compat'; type GenerateSeedsArg = { count: number; diff --git a/invokeai/frontend/web/src/common/util/getCudaDeviceIndex.test.ts b/invokeai/frontend/web/src/common/util/getCudaDeviceIndex.test.ts new file mode 100644 index 00000000000..3348ae14a2f --- /dev/null +++ b/invokeai/frontend/web/src/common/util/getCudaDeviceIndex.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; + +import { getCudaDeviceIndex } from './getCudaDeviceIndex'; + +describe('getCudaDeviceIndex', () => { + it('parses the index from a cuda device string', () => { + expect(getCudaDeviceIndex('cuda:0')).toBe(0); + expect(getCudaDeviceIndex('cuda:1')).toBe(1); + expect(getCudaDeviceIndex('cuda:11')).toBe(11); + }); + + it('returns null for non-cuda devices', () => { + expect(getCudaDeviceIndex('cpu')).toBeNull(); + expect(getCudaDeviceIndex('mps')).toBeNull(); + }); + + it('returns null for null/undefined/empty', () => { + expect(getCudaDeviceIndex(null)).toBeNull(); + expect(getCudaDeviceIndex(undefined)).toBeNull(); + expect(getCudaDeviceIndex('')).toBeNull(); + }); + + it('returns null for malformed cuda strings', () => { + expect(getCudaDeviceIndex('cuda')).toBeNull(); + expect(getCudaDeviceIndex('cuda:')).toBeNull(); + expect(getCudaDeviceIndex('cuda:x')).toBeNull(); + expect(getCudaDeviceIndex('cuda:0:0')).toBeNull(); + }); +}); diff --git a/invokeai/frontend/web/src/common/util/getCudaDeviceIndex.ts b/invokeai/frontend/web/src/common/util/getCudaDeviceIndex.ts new file mode 100644 index 00000000000..d4a394b48fc --- /dev/null +++ b/invokeai/frontend/web/src/common/util/getCudaDeviceIndex.ts @@ -0,0 +1,13 @@ +/** + * Parse the CUDA device index from a device string (e.g. `"cuda:1"` → `1`). + * + * Returns `null` when the device is null/undefined or is not a CUDA device (e.g. `"cpu"`, `"mps"`). + * Used to label progress previews and queue items with their GPU number in multi-GPU setups. + */ +export const getCudaDeviceIndex = (device: string | null | undefined): number | null => { + if (!device) { + return null; + } + const match = /^cuda:(\d+)$/.exec(device); + return match ? Number(match[1]) : null; +}; diff --git a/invokeai/frontend/web/src/common/util/getDeviceNameLabels.test.ts b/invokeai/frontend/web/src/common/util/getDeviceNameLabels.test.ts new file mode 100644 index 00000000000..7a35d57d4f8 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/getDeviceNameLabels.test.ts @@ -0,0 +1,38 @@ +import type { S } from 'services/api/types'; +import { describe, expect, it } from 'vitest'; + +import { getDeviceNameLabels } from './getDeviceNameLabels'; + +const opt = (device: string, name: string): S['GenerationDeviceOption'] => ({ device, name }); + +describe('getDeviceNameLabels', () => { + it('adds a 1-based #N suffix to identically-named devices', () => { + const labels = getDeviceNameLabels([opt('cuda:0', 'AMD Radeon PRO W7900'), opt('cuda:1', 'AMD Radeon PRO W7900')]); + expect(labels).toEqual({ + 'cuda:0': 'AMD Radeon PRO W7900 #1', + 'cuda:1': 'AMD Radeon PRO W7900 #2', + }); + }); + + it('does not add a suffix to a uniquely-named device', () => { + const labels = getDeviceNameLabels([opt('cuda:0', 'AMD Radeon PRO W7900')]); + expect(labels).toEqual({ 'cuda:0': 'AMD Radeon PRO W7900' }); + }); + + it('only suffixes the names that are duplicated', () => { + const labels = getDeviceNameLabels([ + opt('cuda:0', 'RTX 4090'), + opt('cuda:1', 'RTX 3090'), + opt('cuda:2', 'RTX 3090'), + ]); + expect(labels).toEqual({ + 'cuda:0': 'RTX 4090', + 'cuda:1': 'RTX 3090 #1', + 'cuda:2': 'RTX 3090 #2', + }); + }); + + it('returns an empty map for no options', () => { + expect(getDeviceNameLabels([])).toEqual({}); + }); +}); diff --git a/invokeai/frontend/web/src/common/util/getDeviceNameLabels.ts b/invokeai/frontend/web/src/common/util/getDeviceNameLabels.ts new file mode 100644 index 00000000000..210e7b88c67 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/getDeviceNameLabels.ts @@ -0,0 +1,25 @@ +import type { S } from 'services/api/types'; + +/** + * Build a map of device id (e.g. `"cuda:0"`) → human-readable label (e.g. `"AMD Radeon PRO W7900 #1"`). + * + * Devices that share a name get a 1-based `#N` suffix so identical GPUs can be told apart; a + * uniquely-named device gets no suffix. The ordinal is assigned in the order the options are + * provided (which the backend returns in CUDA-index order). Used to label progress previews with + * the GPU they are rendering on in multi-GPU setups. + */ +export const getDeviceNameLabels = (options: S['GenerationDeviceOption'][]): Record => { + const nameCounts = new Map(); + for (const option of options) { + nameCounts.set(option.name, (nameCounts.get(option.name) ?? 0) + 1); + } + + const ordinals = new Map(); + const labels: Record = {}; + for (const option of options) { + const ordinal = (ordinals.get(option.name) ?? 0) + 1; + ordinals.set(option.name, ordinal); + labels[option.device] = (nameCounts.get(option.name) ?? 0) > 1 ? `${option.name} #${ordinal}` : option.name; + } + return labels; +}; diff --git a/invokeai/frontend/web/src/common/util/isInputElement.ts b/invokeai/frontend/web/src/common/util/isInputElement.ts deleted file mode 100644 index abb8fba7b8d..00000000000 --- a/invokeai/frontend/web/src/common/util/isInputElement.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const isInputElement = (el: HTMLElement) => { - return ( - el.tagName.toLowerCase() === 'input' || - el.tagName.toLowerCase() === 'textarea' || - el.tagName.toLowerCase() === 'select' - ); -}; diff --git a/invokeai/frontend/web/src/common/util/objectKeys.ts b/invokeai/frontend/web/src/common/util/objectKeys.ts deleted file mode 100644 index bea0905c7ff..00000000000 --- a/invokeai/frontend/web/src/common/util/objectKeys.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Get the keys of an object. This is a wrapper around `Object.keys` that types the result as an array of the keys of the object. - * @param obj The object to get the keys of. - * @returns The keys of the object. - */ -export const objectKeys = >(obj: T) => Object.keys(obj) as Array; diff --git a/invokeai/frontend/web/src/common/util/openBase64ImageInTab.ts b/invokeai/frontend/web/src/common/util/openBase64ImageInTab.ts deleted file mode 100644 index 71d3bcd661a..00000000000 --- a/invokeai/frontend/web/src/common/util/openBase64ImageInTab.ts +++ /dev/null @@ -1,23 +0,0 @@ -type Base64AndCaption = { - base64: string; - caption: string; -}; - -const openBase64ImageInTab = (images: Base64AndCaption[]) => { - const w = window.open(''); - if (!w) { - return; - } - - images.forEach((i) => { - const image = new Image(); - image.src = i.base64; - - w.document.write(i.caption); - w.document.write('
'); - w.document.write(image.outerHTML); - w.document.write('

'); - }); -}; - -export default openBase64ImageInTab; 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/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 index 792b6d38e49..ce47effe406 100644 --- a/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts +++ b/invokeai/frontend/web/src/common/util/roundDownToMultiple.ts @@ -1,10 +1,14 @@ export const roundDownToMultiple = (num: number, multiple: number): number => { return Math.floor(num / multiple) * multiple; }; -export const roundDownToMultipleMin = (num: number, multiple: number): number => { - return Math.max(multiple, 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/stopPropagation.ts b/invokeai/frontend/web/src/common/util/stopPropagation.ts index 0c6a1fc5078..a4bafe68cf9 100644 --- a/invokeai/frontend/web/src/common/util/stopPropagation.ts +++ b/invokeai/frontend/web/src/common/util/stopPropagation.ts @@ -1,7 +1,5 @@ -export const stopPropagation = (e: React.MouseEvent) => { - e.stopPropagation(); -}; +import type { MouseEvent } from 'react'; -export const preventDefault = (e: React.MouseEvent) => { +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 index 321833cdba2..adf3473de04 100644 --- a/invokeai/frontend/web/src/common/util/typedMemo.ts +++ b/invokeai/frontend/web/src/common/util/typedMemo.ts @@ -1,6 +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: (c: T) => T = memo; +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/canvas/components/ClearCanvasHistoryButtonModal.tsx b/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx deleted file mode 100644 index e49976e532e..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/ClearCanvasHistoryButtonModal.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Button, ConfirmationAlertDialog, useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { clearCanvasHistory } from 'features/canvas/store/canvasSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiTrashSimpleFill } from 'react-icons/pi'; - -const ClearCanvasHistoryButtonModal = () => { - const isStaging = useAppSelector(isStagingSelector); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const { isOpen, onOpen, onClose } = useDisclosure(); - const acceptCallback = useCallback(() => dispatch(clearCanvasHistory()), [dispatch]); - - return ( - <> - - -

{t('unifiedCanvas.clearCanvasHistoryMessage')}

-
-

{t('unifiedCanvas.clearCanvasHistoryConfirm')}

-
- - ); -}; -export default memo(ClearCanvasHistoryButtonModal); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx deleted file mode 100644 index c08b70a7839..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { Box, chakra, Flex } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import useCanvasDragMove from 'features/canvas/hooks/useCanvasDragMove'; -import useCanvasHotkeys from 'features/canvas/hooks/useCanvasHotkeys'; -import useCanvasMouseDown from 'features/canvas/hooks/useCanvasMouseDown'; -import useCanvasMouseMove from 'features/canvas/hooks/useCanvasMouseMove'; -import useCanvasMouseOut from 'features/canvas/hooks/useCanvasMouseOut'; -import useCanvasMouseUp from 'features/canvas/hooks/useCanvasMouseUp'; -import useCanvasWheel from 'features/canvas/hooks/useCanvasZoom'; -import { - $canvasBaseLayer, - $canvasStage, - $isModifyingBoundingBox, - $isMouseOverBoundingBox, - $isMovingStage, - $isTransformingBoundingBox, - $tool, -} from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { canvasResized, selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import type Konva from 'konva'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import type { Vector2d } from 'konva/lib/types'; -import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; -import { Layer, Stage } from 'react-konva'; - -import IAICanvasBoundingBoxOverlay from './IAICanvasBoundingBoxOverlay'; -import IAICanvasGrid from './IAICanvasGrid'; -import IAICanvasIntermediateImage from './IAICanvasIntermediateImage'; -import IAICanvasMaskCompositor from './IAICanvasMaskCompositor'; -import IAICanvasMaskLines from './IAICanvasMaskLines'; -import IAICanvasObjectRenderer from './IAICanvasObjectRenderer'; -import IAICanvasStagingArea from './IAICanvasStagingArea'; -import IAICanvasStagingAreaToolbar from './IAICanvasStagingAreaToolbar'; -import IAICanvasStatusText from './IAICanvasStatusText'; -import IAICanvasBoundingBox from './IAICanvasToolbar/IAICanvasBoundingBox'; -import IAICanvasToolPreview from './IAICanvasToolPreview'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - return { - stageCoordinates: canvas.stageCoordinates, - stageDimensions: canvas.stageDimensions, - }; -}); - -const ChakraStage = chakra(Stage, { - shouldForwardProp: (prop) => !['sx'].includes(prop), -}); - -const IAICanvas = () => { - const isStaging = useAppSelector(isStagingSelector); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const shouldShowBoundingBox = useAppSelector((s) => s.canvas.shouldShowBoundingBox); - const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const shouldShowIntermediates = useAppSelector((s) => s.canvas.shouldShowIntermediates); - const shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias); - const shouldRestrictStrokesToBox = useAppSelector((s) => s.canvas.shouldRestrictStrokesToBox); - const { stageCoordinates, stageDimensions } = useAppSelector(selector); - const dispatch = useAppDispatch(); - const containerRef = useRef(null); - const stageRef = useRef(null); - const canvasBaseLayerRef = useRef(null); - const isModifyingBoundingBox = useStore($isModifyingBoundingBox); - const isMovingStage = useStore($isMovingStage); - const isTransformingBoundingBox = useStore($isTransformingBoundingBox); - const isMouseOverBoundingBox = useStore($isMouseOverBoundingBox); - const tool = useStore($tool); - useCanvasHotkeys(); - const canvasStageRefCallback = useCallback((stageElement: Konva.Stage) => { - $canvasStage.set(stageElement); - stageRef.current = stageElement; - }, []); - const stageCursor = useMemo(() => { - if (tool === 'move' || isStaging) { - if (isMovingStage) { - return 'grabbing'; - } else { - return 'grab'; - } - } else if (isTransformingBoundingBox) { - return undefined; - } else if (shouldRestrictStrokesToBox && !isMouseOverBoundingBox) { - return 'default'; - } - return 'none'; - }, [isMouseOverBoundingBox, isMovingStage, isStaging, isTransformingBoundingBox, shouldRestrictStrokesToBox, tool]); - - const canvasBaseLayerRefCallback = useCallback((layerElement: Konva.Layer) => { - $canvasBaseLayer.set(layerElement); - canvasBaseLayerRef.current = layerElement; - }, []); - - const lastCursorPositionRef = useRef({ x: 0, y: 0 }); - - // Use refs for values that do not affect rendering, other values in redux - const didMouseMoveRef = useRef(false); - - const handleWheel = useCanvasWheel(stageRef); - const handleMouseDown = useCanvasMouseDown(stageRef); - const handleMouseUp = useCanvasMouseUp(stageRef, didMouseMoveRef); - const handleMouseMove = useCanvasMouseMove(stageRef, didMouseMoveRef, lastCursorPositionRef); - const { handleDragStart, handleDragMove, handleDragEnd } = useCanvasDragMove(); - const handleMouseOut = useCanvasMouseOut(); - const handleContextMenu = useCallback((e: KonvaEventObject) => e.evt.preventDefault(), []); - - useEffect(() => { - if (!containerRef.current) { - return; - } - const resizeObserver = new ResizeObserver(() => { - if (!containerRef.current) { - return; - } - const { width, height } = containerRef.current.getBoundingClientRect(); - dispatch(canvasResized({ width, height })); - }); - - resizeObserver.observe(containerRef.current); - const { width, height } = containerRef.current.getBoundingClientRect(); - dispatch(canvasResized({ width, height })); - - return () => { - resizeObserver.disconnect(); - }; - }, [dispatch]); - - const stageStyles = useMemo( - () => ({ - outline: 'none', - overflow: 'hidden', - cursor: stageCursor ? stageCursor : undefined, - canvas: { - outline: 'none', - }, - }), - [stageCursor] - ); - - const scale = useMemo(() => ({ x: stageScale, y: stageScale }), [stageScale]); - - return ( - - - - - - - - - - - - - - - - - - - {!isStaging && } - - {shouldShowIntermediates && } - - - - - - - - ); -}; - -export default memo(IAICanvas); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx deleted file mode 100644 index 3cb75c09c6e..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasBoundingBoxOverlay.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { memo } from 'react'; -import { Group, Rect } from 'react-konva'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { boundingBoxCoordinates, boundingBoxDimensions, stageDimensions, stageCoordinates } = canvas; - - return { - boundingBoxCoordinates, - boundingBoxDimensions, - stageCoordinates, - stageDimensions, - }; -}); - -const IAICanvasBoundingBoxOverlay = () => { - const { boundingBoxCoordinates, boundingBoxDimensions, stageCoordinates, stageDimensions } = useAppSelector(selector); - const shouldDarkenOutsideBoundingBox = useAppSelector((s) => s.canvas.shouldDarkenOutsideBoundingBox); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - - return ( - - - - - ); -}; - -export default memo(IAICanvasBoundingBoxOverlay); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx deleted file mode 100644 index b9105ce9dd7..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasGrid.tsx +++ /dev/null @@ -1,126 +0,0 @@ -// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/ -import { getArbitraryBaseColor } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import type { ReactElement } from 'react'; -import { memo, useCallback, useMemo } from 'react'; -import { Group, Line as KonvaLine } from 'react-konva'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - return { - stageCoordinates: canvas.stageCoordinates, - stageDimensions: canvas.stageDimensions, - }; -}); - -const baseGridLineColor = getArbitraryBaseColor(27); -const fineGridLineColor = getArbitraryBaseColor(18); - -const IAICanvasGrid = () => { - const { stageCoordinates, stageDimensions } = useAppSelector(selector); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - - const gridSpacing = useMemo(() => { - if (stageScale >= 2) { - return 8; - } - if (stageScale >= 1 && stageScale < 2) { - return 16; - } - if (stageScale >= 0.5 && stageScale < 1) { - return 32; - } - return 64; - }, [stageScale]); - - const unscale = useCallback( - (value: number) => { - return value / stageScale; - }, - [stageScale] - ); - - const gridLines = useMemo(() => { - const { width, height } = stageDimensions; - const { x, y } = stageCoordinates; - - const stageRect = { - x1: 0, - y1: 0, - x2: width, - y2: height, - offset: { - x: unscale(x), - y: unscale(y), - }, - }; - - const gridOffset = { - x: Math.ceil(unscale(x) / gridSpacing) * gridSpacing, - y: Math.ceil(unscale(y) / gridSpacing) * gridSpacing, - }; - - const gridRect = { - x1: -gridOffset.x, - y1: -gridOffset.y, - x2: unscale(width) - gridOffset.x + gridSpacing, - y2: unscale(height) - gridOffset.y + gridSpacing, - }; - - const gridFullRect = { - x1: Math.min(stageRect.x1, gridRect.x1), - y1: Math.min(stageRect.y1, gridRect.y1), - x2: Math.max(stageRect.x2, gridRect.x2), - y2: Math.max(stageRect.y2, gridRect.y2), - }; - - const // find the x & y size of the grid - xSize = gridFullRect.x2 - gridFullRect.x1; - const ySize = gridFullRect.y2 - gridFullRect.y1; - // compute the number of steps required on each axis. - const xSteps = Math.round(xSize / gridSpacing) + 1; - const ySteps = Math.round(ySize / gridSpacing) + 1; - - const strokeWidth = unscale(1); - - const gridLines: ReactElement[] = new Array(xSteps + ySteps); - let _x = 0; - let _y = 0; - for (let i = 0; i < xSteps; i++) { - _x = gridFullRect.x1 + i * gridSpacing; - gridLines.push( - - ); - } - - for (let i = 0; i < ySteps; i++) { - _y = gridFullRect.y1 + i * gridSpacing; - gridLines.push( - - ); - } - - return gridLines; - }, [stageDimensions, stageCoordinates, unscale, gridSpacing]); - - return {gridLines}; -}; - -export default memo(IAICanvasGrid); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx deleted file mode 100644 index 75ae983f23d..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImage.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/query'; -import { $authToken } from 'app/store/nanostores/authToken'; -import type { CanvasImage } from 'features/canvas/store/canvasTypes'; -import { memo } from 'react'; -import { Image } from 'react-konva'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import useImage from 'use-image'; - -import IAICanvasImageErrorFallback from './IAICanvasImageErrorFallback'; - -type IAICanvasImageProps = { - canvasImage: CanvasImage; -}; -const IAICanvasImage = (props: IAICanvasImageProps) => { - const { x, y, imageName } = props.canvasImage; - const { currentData: imageDTO, isError } = useGetImageDTOQuery(imageName ?? skipToken); - const [image, status] = useImage(imageDTO?.image_url ?? '', $authToken.get() ? 'use-credentials' : 'anonymous'); - - if (isError || status === 'failed') { - return ; - } - - return ; -}; - -export default memo(IAICanvasImage); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx deleted file mode 100644 index 1606dfa8446..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasImageErrorFallback.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useToken } from '@invoke-ai/ui-library'; -import type { CanvasImage } from 'features/canvas/store/canvasTypes'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Group, Rect, Text } from 'react-konva'; - -type IAICanvasImageErrorFallbackProps = { - canvasImage: CanvasImage; -}; -const IAICanvasImageErrorFallback = ({ canvasImage }: IAICanvasImageErrorFallbackProps) => { - const [rectFill, textFill] = useToken('colors', ['base.500', 'base.900']); - const { t } = useTranslation(); - return ( - - - - - ); -}; - -export default memo(IAICanvasImageErrorFallback); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx deleted file mode 100644 index bd9b93997ec..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { selectSystemSlice } from 'features/system/store/systemSlice'; -import { memo, useEffect, useState } from 'react'; -import { Image as KonvaImage } from 'react-konva'; - -const progressImageSelector = createMemoizedSelector([selectSystemSlice, selectCanvasSlice], (system, canvas) => { - const { denoiseProgress } = system; - const { batchIds } = canvas; - - return { - progressImage: - denoiseProgress && batchIds.includes(denoiseProgress.batch_id) ? denoiseProgress.progress_image : undefined, - boundingBox: canvas.layerState.stagingArea.boundingBox, - }; -}); - -const IAICanvasIntermediateImage = () => { - const { progressImage, boundingBox } = useAppSelector(progressImageSelector); - const [loadedImageElement, setLoadedImageElement] = useState(null); - - useEffect(() => { - if (!progressImage) { - return; - } - - const tempImage = new Image(); - - tempImage.onload = () => { - setLoadedImageElement(tempImage); - }; - - tempImage.src = progressImage.dataURL; - }, [progressImage]); - - if (!(progressImage && boundingBox) || !loadedImageElement) { - return null; - } - - return ( - - ); -}; - -export default memo(IAICanvasIntermediateImage); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositor.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositor.tsx deleted file mode 100644 index a339cf5352e..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskCompositor.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { getColoredMaskSVG } from 'features/canvas/util/getColoredMaskSVG'; -import type Konva from 'konva'; -import type { RectConfig } from 'konva/lib/shapes/Rect'; -import { isNumber } from 'lodash-es'; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Rect } from 'react-konva'; - -const canvasMaskCompositerSelector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - return { - stageCoordinates: canvas.stageCoordinates, - stageDimensions: canvas.stageDimensions, - }; -}); - -type IAICanvasMaskCompositorProps = RectConfig; - -const IAICanvasMaskCompositor = (props: IAICanvasMaskCompositorProps) => { - const { ...rest } = props; - - const { stageCoordinates, stageDimensions } = useAppSelector(canvasMaskCompositerSelector); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const maskColorString = useAppSelector((s) => rgbaColorToString(s.canvas.maskColor)); - const [fillPatternImage, setFillPatternImage] = useState(null); - - const [offset, setOffset] = useState(0); - - const rectRef = useRef(null); - const incrementOffset = useCallback(() => { - setOffset(offset + 1); - setTimeout(incrementOffset, 500); - }, [offset]); - - useEffect(() => { - if (fillPatternImage) { - return; - } - const image = new Image(); - - image.onload = () => { - setFillPatternImage(image); - }; - image.src = getColoredMaskSVG(maskColorString); - }, [fillPatternImage, maskColorString]); - - useEffect(() => { - if (!fillPatternImage) { - return; - } - fillPatternImage.src = getColoredMaskSVG(maskColorString); - }, [fillPatternImage, maskColorString]); - - useEffect(() => { - const timer = setInterval(() => setOffset((i) => (i + 1) % 5), 50); - return () => clearInterval(timer); - }, []); - - const fillPatternScale = useMemo(() => ({ x: 1 / stageScale, y: 1 / stageScale }), [stageScale]); - - if ( - !fillPatternImage || - !isNumber(stageCoordinates.x) || - !isNumber(stageCoordinates.y) || - !isNumber(stageScale) || - !isNumber(stageDimensions.width) || - !isNumber(stageDimensions.height) - ) { - return null; - } - - return ( - - ); -}; - -export default memo(IAICanvasMaskCompositor); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx deleted file mode 100644 index 27a733cb5e5..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasMaskLines.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { isCanvasMaskLine } from 'features/canvas/store/canvasTypes'; -import type { GroupConfig } from 'konva/lib/Group'; -import { memo } from 'react'; -import { Group, Line } from 'react-konva'; - -type InpaintingCanvasLinesProps = GroupConfig; - -/** - * Draws the lines which comprise the mask. - * - * Uses globalCompositeOperation to handle the brush and eraser tools. - */ -const IAICanvasLines = (props: InpaintingCanvasLinesProps) => { - const objects = useAppSelector((s) => s.canvas.layerState.objects); - - return ( - - {objects.filter(isCanvasMaskLine).map((line, i) => ( - 0 - strokeWidth={line.strokeWidth * 2} - tension={0} - lineCap="round" - lineJoin="round" - shadowForStrokeEnabled={false} - listening={false} - globalCompositeOperation={line.tool === 'brush' ? 'source-over' : 'destination-out'} - /> - ))} - - ); -}; - -export default memo(IAICanvasLines); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx deleted file mode 100644 index 23005a9d246..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { - isCanvasBaseImage, - isCanvasBaseLine, - isCanvasEraseRect, - isCanvasFillRect, -} from 'features/canvas/store/canvasTypes'; -import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { memo } from 'react'; -import { Group, Line, Rect } from 'react-konva'; - -import IAICanvasImage from './IAICanvasImage'; - -const IAICanvasObjectRenderer = () => { - const objects = useAppSelector((s) => s.canvas.layerState.objects); - - return ( - - {objects.map((obj, i) => { - if (isCanvasBaseImage(obj)) { - return ; - } else if (isCanvasBaseLine(obj)) { - const line = ( - 0 - strokeWidth={obj.strokeWidth * 2} - tension={0} - lineCap="round" - lineJoin="round" - shadowForStrokeEnabled={false} - listening={false} - globalCompositeOperation={obj.tool === 'brush' ? 'source-over' : 'destination-out'} - /> - ); - if (obj.clip) { - return ( - - {line} - - ); - } else { - return line; - } - } else if (isCanvasFillRect(obj)) { - return ( - - ); - } else if (isCanvasEraseRect(obj)) { - return ( - - ); - } - })} - - ); -}; - -export default memo(IAICanvasObjectRenderer); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx deleted file mode 100644 index 8b9b580e63c..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import type { GroupConfig } from 'konva/lib/Group'; -import { memo } from 'react'; -import { Group, Rect } from 'react-konva'; - -import IAICanvasImage from './IAICanvasImage'; - -const dash = [4, 4]; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { - layerState, - shouldShowStagingImage, - shouldShowStagingOutline, - boundingBoxCoordinates: stageBoundingBoxCoordinates, - boundingBoxDimensions: stageBoundingBoxDimensions, - } = canvas; - - const { selectedImageIndex, images, boundingBox } = layerState.stagingArea; - - return { - currentStagingAreaImage: - images.length > 0 && selectedImageIndex !== undefined ? images[selectedImageIndex] : undefined, - isOnFirstImage: selectedImageIndex === 0, - isOnLastImage: selectedImageIndex === images.length - 1, - shouldShowStagingImage, - shouldShowStagingOutline, - x: boundingBox?.x ?? stageBoundingBoxCoordinates.x, - y: boundingBox?.y ?? stageBoundingBoxCoordinates.y, - width: boundingBox?.width ?? stageBoundingBoxDimensions.width, - height: boundingBox?.height ?? stageBoundingBoxDimensions.height, - }; -}); - -type Props = GroupConfig; - -const IAICanvasStagingArea = (props: Props) => { - const { currentStagingAreaImage, shouldShowStagingImage, shouldShowStagingOutline, x, y, width, height } = - useAppSelector(selector); - - return ( - - {shouldShowStagingImage && currentStagingAreaImage && } - {shouldShowStagingOutline && ( - - - - - )} - - ); -}; - -export default memo(IAICanvasStagingArea); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx deleted file mode 100644 index ed394599761..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { Button, ButtonGroup, Flex, IconButton } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { stagingAreaImageSaved } from 'features/canvas/store/actions'; -import { - commitStagingAreaImage, - discardStagedImage, - discardStagedImages, - nextStagingAreaImage, - prevStagingAreaImage, - selectCanvasSlice, - setShouldShowStagingImage, - setShouldShowStagingOutline, -} from 'features/canvas/store/canvasSlice'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { - PiArrowLeftBold, - PiArrowRightBold, - PiCheckBold, - PiEyeBold, - PiEyeSlashBold, - PiFloppyDiskBold, - PiTrashSimpleBold, - PiXBold, -} from 'react-icons/pi'; -import { useGetImageDTOQuery } from 'services/api/endpoints/images'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { - layerState: { - stagingArea: { images, selectedImageIndex }, - }, - shouldShowStagingOutline, - shouldShowStagingImage, - } = canvas; - - return { - currentIndex: selectedImageIndex, - total: images.length, - currentStagingAreaImage: images.length > 0 ? images[selectedImageIndex] : undefined, - shouldShowStagingImage, - shouldShowStagingOutline, - }; -}); - -const ClearStagingIntermediatesIconButton = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const totalStagedImages = useAppSelector((s) => s.canvas.layerState.stagingArea.images.length); - - const handleDiscardStagingArea = useCallback(() => { - dispatch(discardStagedImages()); - }, [dispatch]); - - const handleDiscardStagingImage = useCallback(() => { - // Discarding all staged images triggers cancelation of all canvas batches. It's too easy to accidentally - // click the discard button, so to prevent accidental cancelation of all batches, we only discard the current - // image if there are more than one staged images. - if (totalStagedImages > 1) { - dispatch(discardStagedImage()); - } - }, [dispatch, totalStagedImages]); - - return ( - <> - } - onClick={handleDiscardStagingImage} - colorScheme="invokeBlue" - fontSize={16} - isDisabled={totalStagedImages <= 1} - /> - } - onClick={handleDiscardStagingArea} - colorScheme="error" - fontSize={16} - /> - - ); -}; - -const IAICanvasStagingAreaToolbar = () => { - const dispatch = useAppDispatch(); - const { currentStagingAreaImage, shouldShowStagingImage, currentIndex, total } = useAppSelector(selector); - - const { t } = useTranslation(); - - const handleMouseOver = useCallback(() => { - dispatch(setShouldShowStagingOutline(true)); - }, [dispatch]); - - const handleMouseOut = useCallback(() => { - dispatch(setShouldShowStagingOutline(false)); - }, [dispatch]); - - const handlePrevImage = useCallback(() => dispatch(prevStagingAreaImage()), [dispatch]); - - const handleNextImage = useCallback(() => dispatch(nextStagingAreaImage()), [dispatch]); - - const handleAccept = useCallback(() => dispatch(commitStagingAreaImage()), [dispatch]); - - useHotkeys(['left'], handlePrevImage, { - enabled: () => true, - preventDefault: true, - }); - - useHotkeys(['right'], handleNextImage, { - enabled: () => true, - preventDefault: true, - }); - - useHotkeys(['enter'], handleAccept, { - enabled: () => true, - preventDefault: true, - }); - - useHotkeys( - ['esc'], - () => { - handleDiscardStagingArea(); - }, - { - preventDefault: true, - } - ); - - const { data: imageDTO } = useGetImageDTOQuery(currentStagingAreaImage?.imageName ?? skipToken); - - const handleToggleShouldShowStagingImage = useCallback(() => { - dispatch(setShouldShowStagingImage(!shouldShowStagingImage)); - }, [dispatch, shouldShowStagingImage]); - - const handleSaveToGallery = useCallback(() => { - if (!imageDTO) { - return; - } - - dispatch( - stagingAreaImageSaved({ - imageDTO, - }) - ); - }, [dispatch, imageDTO]); - - useHotkeys( - ['shift+s'], - () => { - shouldShowStagingImage && handleSaveToGallery(); - }, - { - preventDefault: true, - }, - [shouldShowStagingImage, handleSaveToGallery] - ); - - const handleDiscardStagingArea = useCallback(() => { - dispatch(discardStagedImages()); - }, [dispatch]); - - if (!currentStagingAreaImage) { - return null; - } - - return ( - - - } - onClick={handlePrevImage} - colorScheme="invokeBlue" - isDisabled={!shouldShowStagingImage} - /> - - } - onClick={handleNextImage} - colorScheme="invokeBlue" - isDisabled={!shouldShowStagingImage} - /> - - - } - onClick={handleAccept} - colorScheme="invokeBlue" - /> - : } - onClick={handleToggleShouldShowStagingImage} - colorScheme="invokeBlue" - /> - } - onClick={handleSaveToGallery} - colorScheme="invokeBlue" - /> - - - - ); -}; - -export default memo(IAICanvasStagingAreaToolbar); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx deleted file mode 100644 index 4c8153b83fe..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Box, Flex } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import roundToHundreth from 'features/canvas/util/roundToHundreth'; -import GenerationModeStatusText from 'features/parameters/components/Canvas/GenerationModeStatusText'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import IAICanvasStatusTextCursorPos from './IAICanvasStatusText/IAICanvasStatusTextCursorPos'; - -const warningColor = 'var(--invoke-colors-warning-500)'; - -const selector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { - stageDimensions: { width: stageWidth, height: stageHeight }, - stageCoordinates: { x: stageX, y: stageY }, - boundingBoxDimensions: { width: boxWidth, height: boxHeight }, - scaledBoundingBoxDimensions: { width: scaledBoxWidth, height: scaledBoxHeight }, - boundingBoxCoordinates: { x: boxX, y: boxY }, - stageScale, - shouldShowCanvasDebugInfo, - layer, - boundingBoxScaleMethod, - shouldPreserveMaskedArea, - } = canvas; - - let boundingBoxColor = 'inherit'; - - if ( - (boundingBoxScaleMethod === 'none' && (boxWidth < 512 || boxHeight < 512)) || - (boundingBoxScaleMethod === 'manual' && scaledBoxWidth * scaledBoxHeight < 512 * 512) - ) { - boundingBoxColor = warningColor; - } - - const activeLayerColor = layer === 'mask' ? warningColor : 'inherit'; - - return { - activeLayerColor, - layer, - boundingBoxColor, - boundingBoxCoordinatesString: `(${roundToHundreth(boxX)}, ${roundToHundreth(boxY)})`, - boundingBoxDimensionsString: `${boxWidth}×${boxHeight}`, - scaledBoundingBoxDimensionsString: `${scaledBoxWidth}×${scaledBoxHeight}`, - canvasCoordinatesString: `${roundToHundreth(stageX)}×${roundToHundreth(stageY)}`, - canvasDimensionsString: `${stageWidth}×${stageHeight}`, - canvasScaleString: Math.round(stageScale * 100), - shouldShowCanvasDebugInfo, - shouldShowBoundingBox: boundingBoxScaleMethod !== 'auto', - shouldShowScaledBoundingBox: boundingBoxScaleMethod !== 'none', - shouldPreserveMaskedArea, - }; -}); - -const IAICanvasStatusText = () => { - const { - activeLayerColor, - layer, - boundingBoxColor, - boundingBoxCoordinatesString, - boundingBoxDimensionsString, - scaledBoundingBoxDimensionsString, - shouldShowScaledBoundingBox, - canvasCoordinatesString, - canvasDimensionsString, - canvasScaleString, - shouldShowCanvasDebugInfo, - shouldShowBoundingBox, - shouldPreserveMaskedArea, - } = useAppSelector(selector); - - const { t } = useTranslation(); - - return ( - - - {`${t('unifiedCanvas.activeLayer')}: ${t(`unifiedCanvas.${layer}`)}`} - {`${t('unifiedCanvas.canvasScale')}: ${canvasScaleString}%`} - {shouldPreserveMaskedArea && ( - - {t('unifiedCanvas.preserveMaskedArea')}: {t('common.on')} - - )} - {shouldShowBoundingBox && ( - {`${t('unifiedCanvas.boundingBox')}: ${boundingBoxDimensionsString}`} - )} - {shouldShowScaledBoundingBox && ( - {`${t( - 'unifiedCanvas.scaledBoundingBox' - )}: ${scaledBoundingBoxDimensionsString}`} - )} - {shouldShowCanvasDebugInfo && ( - <> - {`${t('unifiedCanvas.boundingBoxPosition')}: ${boundingBoxCoordinatesString}`} - {`${t('unifiedCanvas.canvasDimensions')}: ${canvasDimensionsString}`} - {`${t('unifiedCanvas.canvasPosition')}: ${canvasCoordinatesString}`} - - - )} - - ); -}; - -export default memo(IAICanvasStatusText); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx deleted file mode 100644 index a7e9ecb1575..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStatusText/IAICanvasStatusTextCursorPos.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Box } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { $cursorPosition } from 'features/canvas/store/canvasNanostore'; -import roundToHundreth from 'features/canvas/util/roundToHundreth'; -import { memo, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -const IAICanvasStatusTextCursorPos = () => { - const { t } = useTranslation(); - const cursorPosition = useStore($cursorPosition); - const cursorCoordinatesString = useMemo(() => { - const x = cursorPosition?.x ?? -1; - const y = cursorPosition?.y ?? -1; - return `(${roundToHundreth(x)}, ${roundToHundreth(y)})`; - }, [cursorPosition?.x, cursorPosition?.y]); - - return {`${t('unifiedCanvas.cursorPosition')}: ${cursorCoordinatesString}`}; -}; - -export default memo(IAICanvasStatusTextCursorPos); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx deleted file mode 100644 index be1a4c2d4cc..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolPreview.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useStore } from '@nanostores/react'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - $cursorPosition, - $isMovingBoundingBox, - $isTransformingBoundingBox, - $tool, -} from 'features/canvas/store/canvasNanostore'; -import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { rgbaColorToString } from 'features/canvas/util/colorToString'; -import { COLOR_PICKER_SIZE, COLOR_PICKER_STROKE_RADIUS } from 'features/canvas/util/constants'; -import type { GroupConfig } from 'konva/lib/Group'; -import { memo, useMemo } from 'react'; -import { Circle, Group } from 'react-konva'; - -const canvasBrushPreviewSelector = createMemoizedSelector(selectCanvasSlice, (canvas) => { - const { stageDimensions, boundingBoxCoordinates, boundingBoxDimensions, shouldRestrictStrokesToBox } = canvas; - - const clip = shouldRestrictStrokesToBox - ? { - clipX: boundingBoxCoordinates.x, - clipY: boundingBoxCoordinates.y, - clipWidth: boundingBoxDimensions.width, - clipHeight: boundingBoxDimensions.height, - } - : {}; - - // // big brain time; this is the *inverse* of the clip that is needed for shouldRestrictStrokesToBox - // // it took some fiddling to work out, so I am leaving it here in case it is needed for something else... - // const clipFunc = shouldRestrictStrokesToBox - // ? (ctx: SceneContext) => { - // console.log( - // stageCoordinates.x / stageScale, - // stageCoordinates.y / stageScale, - // stageDimensions.height / stageScale, - // stageDimensions.width / stageScale - // ); - // ctx.fillStyle = 'red'; - // ctx.rect( - // -stageCoordinates.x / stageScale, - // -stageCoordinates.y / stageScale, - // stageDimensions.width / stageScale, - // stageCoordinates.y / stageScale + boundingBoxCoordinates.y - // ); - // ctx.rect( - // -stageCoordinates.x / stageScale, - // boundingBoxCoordinates.y + boundingBoxDimensions.height, - // stageDimensions.width / stageScale, - // stageDimensions.height / stageScale - // ); - // ctx.rect( - // -stageCoordinates.x / stageScale, - // -stageCoordinates.y / stageScale, - // stageCoordinates.x / stageScale + boundingBoxCoordinates.x, - // stageDimensions.height / stageScale - // ); - // ctx.rect( - // boundingBoxCoordinates.x + boundingBoxDimensions.width, - // -stageCoordinates.y / stageScale, - // stageDimensions.width / stageScale - - // (boundingBoxCoordinates.x + boundingBoxDimensions.width), - // stageDimensions.height / stageScale - // ); - // } - // : undefined; - - return { - clip, - stageDimensions, - }; -}); - -/** - * Draws a black circle around the canvas brush preview. - */ -const IAICanvasToolPreview = (props: GroupConfig) => { - const radius = useAppSelector((s) => s.canvas.brushSize / 2); - const maskColorString = useAppSelector((s) => rgbaColorToString({ ...s.canvas.maskColor, a: 0.5 })); - const tool = useStore($tool); - const layer = useAppSelector((s) => s.canvas.layer); - const dotRadius = useAppSelector((s) => 1.5 / s.canvas.stageScale); - const strokeWidth = useAppSelector((s) => 1.5 / s.canvas.stageScale); - const brushColorString = useAppSelector((s) => rgbaColorToString(s.canvas.brushColor)); - const colorPickerColorString = useAppSelector((s) => rgbaColorToString(s.canvas.colorPickerColor)); - const colorPickerInnerRadius = useAppSelector( - (s) => (COLOR_PICKER_SIZE - COLOR_PICKER_STROKE_RADIUS + 1) / s.canvas.stageScale - ); - const colorPickerOuterRadius = useAppSelector((s) => COLOR_PICKER_SIZE / s.canvas.stageScale); - const { clip, stageDimensions } = useAppSelector(canvasBrushPreviewSelector); - - const cursorPosition = useStore($cursorPosition); - const isMovingBoundingBox = useStore($isMovingBoundingBox); - const isTransformingBoundingBox = useStore($isTransformingBoundingBox); - - const brushX = useMemo( - () => (cursorPosition ? cursorPosition.x : stageDimensions.width / 2), - [cursorPosition, stageDimensions] - ); - const brushY = useMemo( - () => (cursorPosition ? cursorPosition.y : stageDimensions.height / 2), - [cursorPosition, stageDimensions] - ); - - const shouldDrawBrushPreview = useMemo( - () => !(isMovingBoundingBox || isTransformingBoundingBox || !cursorPosition), - [cursorPosition, isMovingBoundingBox, isTransformingBoundingBox] - ); - - if (!shouldDrawBrushPreview) { - return null; - } - - return ( - - {tool === 'colorPicker' ? ( - <> - - - - ) : ( - <> - - - - - )} - - - - ); -}; - -export default memo(IAICanvasToolPreview); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx deleted file mode 100644 index 98ca55351cc..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasBoundingBox.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import { useShiftModifier } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { roundDownToMultiple, roundDownToMultipleMin, roundToMultiple } from 'common/util/roundDownToMultiple'; -import { - $isDrawing, - $isMouseOverBoundingBox, - $isMouseOverBoundingBoxOutline, - $isMovingBoundingBox, - $isTransformingBoundingBox, - $tool, -} from 'features/canvas/store/canvasNanostore'; -import { - aspectRatioChanged, - setBoundingBoxCoordinates, - setBoundingBoxDimensions, - setShouldSnapToGrid, -} from 'features/canvas/store/canvasSlice'; -import { CANVAS_GRID_SIZE_COARSE, CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/constants'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import type Konva from 'konva'; -import type { GroupConfig } from 'konva/lib/Group'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import type { Vector2d } from 'konva/lib/types'; -import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { Group, Rect, Transformer } from 'react-konva'; - -const borderDash = [4, 4]; - -type IAICanvasBoundingBoxPreviewProps = GroupConfig; - -const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => { - const { ...rest } = props; - const dispatch = useAppDispatch(); - const boundingBoxCoordinates = useAppSelector((s) => s.canvas.boundingBoxCoordinates); - const boundingBoxDimensions = useAppSelector((s) => s.canvas.boundingBoxDimensions); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid); - const hitStrokeWidth = useAppSelector((s) => 20 / s.canvas.stageScale); - const aspectRatio = useAppSelector((s) => s.canvas.aspectRatio); - const optimalDimension = useAppSelector(selectOptimalDimension); - const transformerRef = useRef(null); - const shapeRef = useRef(null); - const shift = useShiftModifier(); - const tool = useStore($tool); - const isDrawing = useStore($isDrawing); - const isMovingBoundingBox = useStore($isMovingBoundingBox); - const isTransformingBoundingBox = useStore($isTransformingBoundingBox); - const isMouseOverBoundingBoxOutline = useStore($isMouseOverBoundingBoxOutline); - - useEffect(() => { - if (!transformerRef.current || !shapeRef.current) { - return; - } - transformerRef.current.nodes([shapeRef.current]); - transformerRef.current.getLayer()?.batchDraw(); - }, []); - - const gridSize = useMemo(() => (shift ? CANVAS_GRID_SIZE_FINE : CANVAS_GRID_SIZE_COARSE), [shift]); - const scaledStep = useMemo(() => gridSize * stageScale, [gridSize, stageScale]); - - useHotkeys( - 'N', - () => { - dispatch(setShouldSnapToGrid(!shouldSnapToGrid)); - }, - [shouldSnapToGrid] - ); - - const handleOnDragMove = useCallback( - (e: KonvaEventObject) => { - if (!shouldSnapToGrid) { - dispatch( - setBoundingBoxCoordinates({ - x: Math.floor(e.target.x()), - y: Math.floor(e.target.y()), - }) - ); - return; - } - - const dragX = e.target.x(); - const dragY = e.target.y(); - - const newX = roundToMultiple(dragX, gridSize); - const newY = roundToMultiple(dragY, gridSize); - - e.target.x(newX); - e.target.y(newY); - - dispatch( - setBoundingBoxCoordinates({ - x: newX, - y: newY, - }) - ); - }, - [dispatch, gridSize, shouldSnapToGrid] - ); - - const handleOnTransform = useCallback( - (_e: KonvaEventObject) => { - /** - * The Konva Transformer changes the object's anchor point and scale factor, - * not its width and height. We need to un-scale the width and height before - * setting the values. - */ - if (!shapeRef.current) { - return; - } - - const rect = shapeRef.current; - - const scaleX = rect.scaleX(); - const scaleY = rect.scaleY(); - - // undo the scaling - const width = Math.round(rect.width() * scaleX); - const height = Math.round(rect.height() * scaleY); - - const x = Math.round(rect.x()); - const y = Math.round(rect.y()); - - if (aspectRatio.isLocked) { - const newDimensions = calculateNewSize(aspectRatio.value, width * height); - dispatch( - setBoundingBoxDimensions( - { - width: roundDownToMultipleMin(newDimensions.width, gridSize), - height: roundDownToMultipleMin(newDimensions.height, gridSize), - }, - optimalDimension - ) - ); - } else { - dispatch( - setBoundingBoxDimensions( - { - width: roundDownToMultipleMin(width, gridSize), - height: roundDownToMultipleMin(height, gridSize), - }, - optimalDimension - ) - ); - dispatch( - aspectRatioChanged({ - isLocked: false, - id: 'Free', - value: width / height, - }) - ); - } - - dispatch( - setBoundingBoxCoordinates({ - x: shouldSnapToGrid ? roundDownToMultiple(x, gridSize) : x, - y: shouldSnapToGrid ? roundDownToMultiple(y, gridSize) : y, - }) - ); - - // Reset the scale now that the coords/dimensions have been un-scaled - rect.scaleX(1); - rect.scaleY(1); - }, - [aspectRatio.isLocked, aspectRatio.value, dispatch, shouldSnapToGrid, gridSize, optimalDimension] - ); - - const anchorDragBoundFunc = useCallback( - ( - oldPos: Vector2d, // old absolute position of anchor point - newPos: Vector2d, // new absolute position (potentially) of anchor point - _e: MouseEvent - ) => { - /** - * Konva does not transform with width or height. It transforms the anchor point - * and scale factor. This is then sent to the shape's onTransform listeners. - * - * We need to snap the new dimensions to steps of 8 (or 64). But because the whole - * stage is scaled, our actual desired step is actually 8 (or 64) * the stage scale. - * - * Additionally, we need to ensure we offset the position so that we snap to a - * multiple of 8 (or 64) that is aligned with the grid, and not from the absolute zero - * coordinate. - */ - - // Calculate the offset of the grid. - const offsetX = oldPos.x % scaledStep; - const offsetY = oldPos.y % scaledStep; - - const newCoordinates = { - x: roundDownToMultiple(newPos.x, scaledStep) + offsetX, - y: roundDownToMultiple(newPos.y, scaledStep) + offsetY, - }; - - return newCoordinates; - }, - [scaledStep] - ); - - const handleStartedTransforming = useCallback(() => { - $isTransformingBoundingBox.set(true); - }, []); - - const handleEndedTransforming = useCallback(() => { - $isTransformingBoundingBox.set(false); - $isMovingBoundingBox.set(false); - $isMouseOverBoundingBox.set(false); - $isMouseOverBoundingBoxOutline.set(false); - }, []); - - const handleStartedMoving = useCallback(() => { - $isMovingBoundingBox.set(true); - }, []); - - const handleEndedModifying = useCallback(() => { - $isTransformingBoundingBox.set(false); - $isMovingBoundingBox.set(false); - $isMouseOverBoundingBox.set(false); - $isMouseOverBoundingBoxOutline.set(false); - }, []); - - const handleMouseOver = useCallback(() => { - $isMouseOverBoundingBoxOutline.set(true); - }, []); - - const handleMouseOut = useCallback(() => { - !isTransformingBoundingBox && !isMovingBoundingBox && $isMouseOverBoundingBoxOutline.set(false); - }, [isMovingBoundingBox, isTransformingBoundingBox]); - - const handleMouseEnterBoundingBox = useCallback(() => { - $isMouseOverBoundingBox.set(true); - }, []); - - const handleMouseLeaveBoundingBox = useCallback(() => { - $isMouseOverBoundingBox.set(false); - }, []); - - const stroke = useMemo(() => { - if (isMouseOverBoundingBoxOutline || isMovingBoundingBox || isTransformingBoundingBox) { - return 'rgba(255,255,255,0.5)'; - } - return 'white'; - }, [isMouseOverBoundingBoxOutline, isMovingBoundingBox, isTransformingBoundingBox]); - - const strokeWidth = useMemo(() => { - if (isMouseOverBoundingBoxOutline || isMovingBoundingBox || isTransformingBoundingBox) { - return 6 / stageScale; - } - return 1 / stageScale; - }, [isMouseOverBoundingBoxOutline, isMovingBoundingBox, isTransformingBoundingBox, stageScale]); - - const enabledAnchors = useMemo(() => { - if (tool !== 'move') { - return emptyArray; - } - if (aspectRatio.isLocked) { - // TODO: The math to resize the bbox when locked and using other handles is confusing. - // Workaround for now is to only allow resizing from the bottom-right handle. - return ['bottom-right']; - } - return undefined; - }, [aspectRatio.isLocked, tool]); - - return ( - - - - - - ); -}; - -export default memo(IAICanvasBoundingBox); - -const emptyArray: string[] = []; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx deleted file mode 100644 index 6dacb7c59df..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import type { FormLabelProps } from '@invoke-ai/ui-library'; -import { - Box, - Button, - ButtonGroup, - Checkbox, - Flex, - FormControl, - FormControlGroup, - FormLabel, - IconButton, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIColorPicker from 'common/components/IAIColorPicker'; -import { canvasMaskSavedToGallery } from 'features/canvas/store/actions'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { - clearMask, - setIsMaskEnabled, - setLayer, - setMaskColor, - setShouldPreserveMaskedArea, -} from 'features/canvas/store/canvasSlice'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import type { RgbaColor } from 'react-colorful'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiExcludeBold, PiFloppyDiskBackFill, PiTrashSimpleFill } from 'react-icons/pi'; - -const formLabelProps: FormLabelProps = { - flexGrow: 1, -}; - -const IAICanvasMaskOptions = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const layer = useAppSelector((s) => s.canvas.layer); - const maskColor = useAppSelector((s) => s.canvas.maskColor); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const shouldPreserveMaskedArea = useAppSelector((s) => s.canvas.shouldPreserveMaskedArea); - const isStaging = useAppSelector(isStagingSelector); - - useHotkeys( - ['q'], - () => { - handleToggleMaskLayer(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [layer] - ); - - useHotkeys( - ['shift+c'], - () => { - handleClearMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['h'], - () => { - handleToggleEnableMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [isMaskEnabled] - ); - - const handleToggleMaskLayer = useCallback(() => { - dispatch(setLayer(layer === 'mask' ? 'base' : 'mask')); - }, [dispatch, layer]); - - const handleClearMask = useCallback(() => { - dispatch(clearMask()); - }, [dispatch]); - - const handleToggleEnableMask = useCallback(() => { - dispatch(setIsMaskEnabled(!isMaskEnabled)); - }, [dispatch, isMaskEnabled]); - - const handleSaveMask = useCallback(async () => { - dispatch(canvasMaskSavedToGallery()); - }, [dispatch]); - - const handleChangePreserveMaskedArea = useCallback( - (e: ChangeEvent) => { - dispatch(setShouldPreserveMaskedArea(e.target.checked)); - }, - [dispatch] - ); - - const handleChangeMaskColor = useCallback( - (newColor: RgbaColor) => { - dispatch(setMaskColor(newColor)); - }, - [dispatch] - ); - - return ( - - - } - isChecked={layer === 'mask'} - isDisabled={isStaging} - /> - - - - - - - {`${t('unifiedCanvas.enableMask')} (H)`} - - - - {t('unifiedCanvas.preserveMaskedArea')} - - - - - - - - - - - - - - - ); -}; - -export default memo(IAICanvasMaskOptions); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton.tsx deleted file mode 100644 index d156944a60b..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasRedoButton.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { redo } from 'features/canvas/store/canvasSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiArrowClockwiseBold } from 'react-icons/pi'; - -const IAICanvasRedoButton = () => { - const dispatch = useAppDispatch(); - const canRedo = useAppSelector((s) => s.canvas.futureLayerStates.length > 0); - const activeTabName = useAppSelector(activeTabNameSelector); - - const { t } = useTranslation(); - - const handleRedo = useCallback(() => { - dispatch(redo()); - }, [dispatch]); - - useHotkeys( - ['meta+shift+z', 'ctrl+shift+z', 'control+y', 'meta+y'], - () => { - handleRedo(); - }, - { - enabled: () => canRedo, - preventDefault: true, - }, - [activeTabName, canRedo] - ); - - return ( - } - onClick={handleRedo} - isDisabled={!canRedo} - /> - ); -}; - -export default memo(IAICanvasRedoButton); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx deleted file mode 100644 index 83ee900a43d..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import type { FormLabelProps } from '@invoke-ai/ui-library'; -import { - Checkbox, - Flex, - FormControl, - FormControlGroup, - FormLabel, - IconButton, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import ClearCanvasHistoryButtonModal from 'features/canvas/components/ClearCanvasHistoryButtonModal'; -import { - setShouldAntialias, - setShouldAutoSave, - setShouldCropToBoundingBoxOnSave, - setShouldDarkenOutsideBoundingBox, - setShouldFitImageSize, - setShouldInvertBrushSizeScrollDirection, - setShouldRestrictStrokesToBox, - setShouldShowCanvasDebugInfo, - setShouldShowGrid, - setShouldShowIntermediates, - setShouldSnapToGrid, -} from 'features/canvas/store/canvasSlice'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiGearSixBold } from 'react-icons/pi'; - -const formLabelProps: FormLabelProps = { - flexGrow: 1, -}; - -const IAICanvasSettingsButtonPopover = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const shouldAutoSave = useAppSelector((s) => s.canvas.shouldAutoSave); - const shouldCropToBoundingBoxOnSave = useAppSelector((s) => s.canvas.shouldCropToBoundingBoxOnSave); - const shouldDarkenOutsideBoundingBox = useAppSelector((s) => s.canvas.shouldDarkenOutsideBoundingBox); - const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); - const shouldShowCanvasDebugInfo = useAppSelector((s) => s.canvas.shouldShowCanvasDebugInfo); - const shouldShowGrid = useAppSelector((s) => s.canvas.shouldShowGrid); - const shouldShowIntermediates = useAppSelector((s) => s.canvas.shouldShowIntermediates); - const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid); - const shouldRestrictStrokesToBox = useAppSelector((s) => s.canvas.shouldRestrictStrokesToBox); - const shouldAntialias = useAppSelector((s) => s.canvas.shouldAntialias); - const shouldFitImageSize = useAppSelector((s) => s.canvas.shouldFitImageSize); - - useHotkeys( - ['n'], - () => { - dispatch(setShouldSnapToGrid(!shouldSnapToGrid)); - }, - { - enabled: true, - preventDefault: true, - }, - [shouldSnapToGrid] - ); - - const handleChangeShouldSnapToGrid = useCallback( - (e: ChangeEvent) => dispatch(setShouldSnapToGrid(e.target.checked)), - [dispatch] - ); - - const handleChangeShouldShowIntermediates = useCallback( - (e: ChangeEvent) => dispatch(setShouldShowIntermediates(e.target.checked)), - [dispatch] - ); - const handleChangeShouldShowGrid = useCallback( - (e: ChangeEvent) => dispatch(setShouldShowGrid(e.target.checked)), - [dispatch] - ); - const handleChangeShouldDarkenOutsideBoundingBox = useCallback( - (e: ChangeEvent) => dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked)), - [dispatch] - ); - const handleChangeShouldInvertBrushSizeScrollDirection = useCallback( - (e: ChangeEvent) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)), - [dispatch] - ); - const handleChangeShouldAutoSave = useCallback( - (e: ChangeEvent) => dispatch(setShouldAutoSave(e.target.checked)), - [dispatch] - ); - const handleChangeShouldCropToBoundingBoxOnSave = useCallback( - (e: ChangeEvent) => dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked)), - [dispatch] - ); - const handleChangeShouldRestrictStrokesToBox = useCallback( - (e: ChangeEvent) => dispatch(setShouldRestrictStrokesToBox(e.target.checked)), - [dispatch] - ); - const handleChangeShouldShowCanvasDebugInfo = useCallback( - (e: ChangeEvent) => dispatch(setShouldShowCanvasDebugInfo(e.target.checked)), - [dispatch] - ); - const handleChangeShouldAntialias = useCallback( - (e: ChangeEvent) => dispatch(setShouldAntialias(e.target.checked)), - [dispatch] - ); - const handleChangeShouldFitImageSize = useCallback( - (e: ChangeEvent) => dispatch(setShouldFitImageSize(e.target.checked)), - [dispatch] - ); - - return ( - - - } - /> - - - - - - - {t('unifiedCanvas.showIntermediates')} - - - - {t('unifiedCanvas.showGrid')} - - - - {t('unifiedCanvas.snapToGrid')} - - - - {t('unifiedCanvas.darkenOutsideSelection')} - - - - {t('unifiedCanvas.autoSaveToGallery')} - - - - {t('unifiedCanvas.saveBoxRegionOnly')} - - - - {t('unifiedCanvas.limitStrokesToBox')} - - - - {t('unifiedCanvas.invertBrushSizeScrollDirection')} - - - - {t('unifiedCanvas.showCanvasDebugInfo')} - - - - {t('unifiedCanvas.antialiasing')} - - - - {t('unifiedCanvas.initialFitImageSize')} - - - - - - - - - ); -}; - -export default memo(IAICanvasSettingsButtonPopover); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx deleted file mode 100644 index 697434739ea..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolChooserOptions.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { - Box, - ButtonGroup, - CompositeNumberInput, - CompositeSlider, - Flex, - FormControl, - FormLabel, - IconButton, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIColorPicker from 'common/components/IAIColorPicker'; -import { $tool, resetToolInteractionState } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { addEraseRect, addFillRect, setBrushColor, setBrushSize } from 'features/canvas/store/canvasSlice'; -import { clamp } from 'lodash-es'; -import { memo, useCallback } from 'react'; -import type { RgbaColor } from 'react-colorful'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { - PiEraserBold, - PiEyedropperBold, - PiPaintBrushBold, - PiPaintBucketBold, - PiSlidersHorizontalBold, - PiXBold, -} from 'react-icons/pi'; - -const marks = [1, 25, 50, 75, 100]; - -const IAICanvasToolChooserOptions = () => { - const dispatch = useAppDispatch(); - const tool = useStore($tool); - const brushColor = useAppSelector((s) => s.canvas.brushColor); - const brushSize = useAppSelector((s) => s.canvas.brushSize); - const isStaging = useAppSelector(isStagingSelector); - const { t } = useTranslation(); - - useHotkeys( - ['b'], - () => { - handleSelectBrushTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['e'], - () => { - handleSelectEraserTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [tool] - ); - - useHotkeys( - ['c'], - () => { - handleSelectColorPickerTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [tool] - ); - - useHotkeys( - ['shift+f'], - () => { - handleFillRect(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - } - ); - - useHotkeys( - ['delete', 'backspace'], - () => { - handleEraseBoundingBox(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - } - ); - - useHotkeys( - ['BracketLeft'], - () => { - if (brushSize - 5 <= 5) { - dispatch(setBrushSize(Math.max(brushSize - 1, 1))); - } else { - dispatch(setBrushSize(Math.max(brushSize - 5, 1))); - } - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushSize] - ); - - useHotkeys( - ['BracketRight'], - () => { - dispatch(setBrushSize(Math.min(brushSize + 5, 500))); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushSize] - ); - - useHotkeys( - ['Shift+BracketLeft'], - () => { - dispatch( - setBrushColor({ - ...brushColor, - a: clamp(brushColor.a - 0.05, 0.05, 1), - }) - ); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushColor] - ); - - useHotkeys( - ['Shift+BracketRight'], - () => { - dispatch( - setBrushColor({ - ...brushColor, - a: clamp(brushColor.a + 0.05, 0.05, 1), - }) - ); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [brushColor] - ); - - const handleSelectBrushTool = useCallback(() => { - $tool.set('brush'); - resetToolInteractionState(); - }, []); - const handleSelectEraserTool = useCallback(() => { - $tool.set('eraser'); - resetToolInteractionState(); - }, []); - const handleSelectColorPickerTool = useCallback(() => { - $tool.set('colorPicker'); - resetToolInteractionState(); - }, []); - const handleFillRect = useCallback(() => { - dispatch(addFillRect()); - }, [dispatch]); - const handleEraseBoundingBox = useCallback(() => { - dispatch(addEraseRect()); - }, [dispatch]); - const handleChangeBrushSize = useCallback( - (newSize: number) => { - dispatch(setBrushSize(newSize)); - }, - [dispatch] - ); - const handleChangeBrushColor = useCallback( - (newColor: RgbaColor) => { - dispatch(setBrushColor(newColor)); - }, - [dispatch] - ); - - return ( - - } - isChecked={tool === 'brush' && !isStaging} - onClick={handleSelectBrushTool} - isDisabled={isStaging} - /> - } - isChecked={tool === 'eraser' && !isStaging} - isDisabled={isStaging} - onClick={handleSelectEraserTool} - /> - } - isDisabled={isStaging} - onClick={handleFillRect} - /> - } - isDisabled={isStaging} - onClick={handleEraseBoundingBox} - /> - } - isChecked={tool === 'colorPicker' && !isStaging} - isDisabled={isStaging} - onClick={handleSelectColorPickerTool} - /> - - - } - /> - - - - - - - {t('unifiedCanvas.brushSize')} - - - - - - - - - - - - - ); -}; - -export default memo(IAICanvasToolChooserOptions); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx deleted file mode 100644 index 5ed5ffe5730..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { ButtonGroup, Combobox, Flex, FormControl, IconButton, Tooltip } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard'; -import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; -import { useSingleAndDoubleClick } from 'common/hooks/useSingleAndDoubleClick'; -import { - canvasCopiedToClipboard, - canvasDownloadedAsImage, - canvasMerged, - canvasSavedToGallery, -} from 'features/canvas/store/actions'; -import { $canvasBaseLayer, $tool } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { - resetCanvas, - resetCanvasView, - setIsMaskEnabled, - setLayer, - setShouldShowBoundingBox, -} from 'features/canvas/store/canvasSlice'; -import type { CanvasLayer } from 'features/canvas/store/canvasTypes'; -import { memo, useCallback, useMemo } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { - PiCopyBold, - PiCrosshairSimpleBold, - PiDownloadSimpleBold, - PiEyeBold, - PiEyeSlashBold, - PiFloppyDiskBold, - PiHandGrabbingBold, - PiStackBold, - PiTrashSimpleBold, - PiUploadSimpleBold, -} from 'react-icons/pi'; - -import IAICanvasMaskOptions from './IAICanvasMaskOptions'; -import IAICanvasRedoButton from './IAICanvasRedoButton'; -import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover'; -import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions'; -import IAICanvasUndoButton from './IAICanvasUndoButton'; - -const IAICanvasToolbar = () => { - const dispatch = useAppDispatch(); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const layer = useAppSelector((s) => s.canvas.layer); - const tool = useStore($tool); - const isStaging = useAppSelector(isStagingSelector); - const { t } = useTranslation(); - const { isClipboardAPIAvailable } = useCopyImageToClipboard(); - const shouldShowBoundingBox = useAppSelector((s) => s.canvas.shouldShowBoundingBox); - - const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({ - postUploadAction: { type: 'SET_CANVAS_INITIAL_IMAGE' }, - }); - - useHotkeys( - ['v'], - () => { - handleSelectMoveTool(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - 'shift+h', - () => { - dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox)); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [shouldShowBoundingBox] - ); - - useHotkeys( - ['r'], - () => { - handleResetCanvasView(); - }, - { - enabled: () => true, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['shift+m'], - () => { - handleMergeVisible(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - useHotkeys( - ['shift+s'], - () => { - !isStaging && handleSaveToGallery(); - }, - { - enabled: true, - preventDefault: true, - }, - [isStaging] - ); - - useHotkeys( - ['meta+c', 'ctrl+c'], - () => { - handleCopyImageToClipboard(); - }, - { - enabled: () => !isStaging && isClipboardAPIAvailable, - preventDefault: true, - }, - [isClipboardAPIAvailable] - ); - - useHotkeys( - ['shift+d'], - () => { - handleDownloadAsImage(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - const handleSelectMoveTool = useCallback(() => { - $tool.set('move'); - }, []); - - const handleSetShouldShowBoundingBox = useCallback(() => { - dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox)); - }, [dispatch, shouldShowBoundingBox]); - - const handleResetCanvasView = useCallback( - (shouldScaleTo1 = false) => { - const canvasBaseLayer = $canvasBaseLayer.get(); - if (!canvasBaseLayer) { - return; - } - const clientRect = canvasBaseLayer.getClientRect({ - skipTransform: true, - }); - dispatch( - resetCanvasView({ - contentRect: clientRect, - shouldScaleTo1, - }) - ); - }, - [dispatch] - ); - const onSingleClick = useCallback(() => { - handleResetCanvasView(false); - }, [handleResetCanvasView]); - const onDoubleClick = useCallback(() => { - handleResetCanvasView(true); - }, [handleResetCanvasView]); - - const handleClickResetCanvasView = useSingleAndDoubleClick({ - onSingleClick, - onDoubleClick, - }); - - const handleResetCanvas = useCallback(() => { - dispatch(resetCanvas()); - }, [dispatch]); - - const handleMergeVisible = useCallback(() => { - dispatch(canvasMerged()); - }, [dispatch]); - - const handleSaveToGallery = useCallback(() => { - dispatch(canvasSavedToGallery()); - }, [dispatch]); - - const handleCopyImageToClipboard = useCallback(() => { - if (!isClipboardAPIAvailable) { - return; - } - dispatch(canvasCopiedToClipboard()); - }, [dispatch, isClipboardAPIAvailable]); - - const handleDownloadAsImage = useCallback(() => { - dispatch(canvasDownloadedAsImage()); - }, [dispatch]); - - const handleChangeLayer = useCallback( - (v) => { - if (!v) { - return; - } - dispatch(setLayer(v.value as CanvasLayer)); - if (v.value === 'mask' && !isMaskEnabled) { - dispatch(setIsMaskEnabled(true)); - } - }, - [dispatch, isMaskEnabled] - ); - - const layerOptions = useMemo<{ label: string; value: CanvasLayer }[]>( - () => [ - { label: t('unifiedCanvas.base'), value: 'base' }, - { label: t('unifiedCanvas.mask'), value: 'mask' }, - ], - [t] - ); - const layerValue = useMemo(() => layerOptions.filter((o) => o.value === layer)[0] ?? null, [layer, layerOptions]); - - return ( - - - - - - - - - - - - } - isChecked={tool === 'move' || isStaging} - onClick={handleSelectMoveTool} - /> - : } - onClick={handleSetShouldShowBoundingBox} - isDisabled={isStaging} - /> - } - onClick={handleClickResetCanvasView} - /> - - - - } - onClick={handleMergeVisible} - isDisabled={isStaging} - /> - } - onClick={handleSaveToGallery} - isDisabled={isStaging} - /> - {isClipboardAPIAvailable && ( - } - onClick={handleCopyImageToClipboard} - isDisabled={isStaging} - /> - )} - } - onClick={handleDownloadAsImage} - isDisabled={isStaging} - /> - - - - - - - - } - isDisabled={isStaging} - {...getUploadButtonProps()} - /> - - } - onClick={handleResetCanvas} - colorScheme="error" - isDisabled={isStaging} - /> - - - - - - ); -}; - -export default memo(IAICanvasToolbar); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton.tsx deleted file mode 100644 index f1fcdf96e57..00000000000 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasUndoButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { undo } from 'features/canvas/store/canvasSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; - -const IAICanvasUndoButton = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const activeTabName = useAppSelector(activeTabNameSelector); - const canUndo = useAppSelector((s) => s.canvas.pastLayerStates.length > 0); - - const handleUndo = useCallback(() => { - dispatch(undo()); - }, [dispatch]); - - useHotkeys( - ['meta+z', 'ctrl+z'], - () => { - handleUndo(); - }, - { - enabled: () => canUndo, - preventDefault: true, - }, - [activeTabName, canUndo] - ); - - return ( - } - onClick={handleUndo} - isDisabled={!canUndo} - /> - ); -}; - -export default memo(IAICanvasUndoButton); diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts deleted file mode 100644 index d47b1c6ccdf..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasDragMove.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isMovingBoundingBox, $isMovingStage, $tool } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { setStageCoordinates } from 'features/canvas/store/canvasSlice'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import { useCallback } from 'react'; - -const useCanvasDrag = () => { - const dispatch = useAppDispatch(); - const isStaging = useAppSelector(isStagingSelector); - const handleDragStart = useCallback(() => { - if (!(($tool.get() === 'move' || isStaging) && !$isMovingBoundingBox.get())) { - return; - } - $isMovingStage.set(true); - }, [isStaging]); - - const handleDragMove = useCallback( - (e: KonvaEventObject) => { - const tool = $tool.get(); - if (!((tool === 'move' || isStaging) && !$isMovingBoundingBox.get())) { - return; - } - - const newCoordinates = { x: e.target.x(), y: e.target.y() }; - - dispatch(setStageCoordinates(newCoordinates)); - }, - [dispatch, isStaging] - ); - - const handleDragEnd = useCallback(() => { - if (!(($tool.get() === 'move' || isStaging) && !$isMovingBoundingBox.get())) { - return; - } - $isMovingStage.set(false); - }, [isStaging]); - - return { - handleDragStart, - handleDragMove, - handleDragEnd, - }; -}; - -export default useCanvasDrag; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasGenerationMode.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasGenerationMode.ts deleted file mode 100644 index 45852cd1bc1..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasGenerationMode.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import type { GenerationMode } from 'features/canvas/store/canvasTypes'; -import { getCanvasData } from 'features/canvas/util/getCanvasData'; -import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode'; -import { useEffect, useState } from 'react'; -import { useDebounce } from 'react-use'; - -export const useCanvasGenerationMode = () => { - const layerState = useAppSelector((s) => s.canvas.layerState); - - const boundingBoxCoordinates = useAppSelector((s) => s.canvas.boundingBoxCoordinates); - const boundingBoxDimensions = useAppSelector((s) => s.canvas.boundingBoxDimensions); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - - const shouldPreserveMaskedArea = useAppSelector((s) => s.canvas.shouldPreserveMaskedArea); - const [generationMode, setGenerationMode] = useState(); - - useEffect(() => { - setGenerationMode(undefined); - }, [layerState, boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, shouldPreserveMaskedArea]); - - useDebounce( - async () => { - // Build canvas blobs - const canvasBlobsAndImageData = await getCanvasData( - layerState, - boundingBoxCoordinates, - boundingBoxDimensions, - isMaskEnabled, - shouldPreserveMaskedArea - ); - - if (!canvasBlobsAndImageData) { - return; - } - - const { baseImageData, maskImageData } = canvasBlobsAndImageData; - - // Determine the generation mode - const generationMode = getCanvasGenerationMode(baseImageData, maskImageData); - - setGenerationMode(generationMode); - }, - 1000, - [layerState, boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, shouldPreserveMaskedArea] - ); - - return generationMode; -}; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts deleted file mode 100644 index ec833c5f3d5..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasHotkeys.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - $canvasStage, - $tool, - $toolStash, - resetCanvasInteractionState, - resetToolInteractionState, -} from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { clearMask, setIsMaskEnabled, setShouldSnapToGrid } from 'features/canvas/store/canvasSlice'; -import { isInteractiveTarget } from 'features/canvas/util/isInteractiveTarget'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { useCallback, useEffect } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; - -const useInpaintingCanvasHotkeys = () => { - const dispatch = useAppDispatch(); - const activeTabName = useAppSelector(activeTabNameSelector); - const isStaging = useAppSelector(isStagingSelector); - const isMaskEnabled = useAppSelector((s) => s.canvas.isMaskEnabled); - const shouldSnapToGrid = useAppSelector((s) => s.canvas.shouldSnapToGrid); - - // Beta Keys - const handleClearMask = useCallback(() => dispatch(clearMask()), [dispatch]); - - useHotkeys( - ['shift+c'], - () => { - handleClearMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [] - ); - - const handleToggleEnableMask = () => dispatch(setIsMaskEnabled(!isMaskEnabled)); - - useHotkeys( - ['h'], - () => { - handleToggleEnableMask(); - }, - { - enabled: () => !isStaging, - preventDefault: true, - }, - [isMaskEnabled] - ); - - useHotkeys( - ['n'], - () => { - dispatch(setShouldSnapToGrid(!shouldSnapToGrid)); - }, - { - enabled: true, - preventDefault: true, - }, - [shouldSnapToGrid] - ); - // - - useHotkeys( - 'esc', - () => { - resetCanvasInteractionState(); - }, - { - enabled: () => true, - preventDefault: true, - } - ); - - const onKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') { - return; - } - if ($toolStash.get() || $tool.get() === 'move') { - return; - } - $canvasStage.get()?.container().focus(); - $toolStash.set($tool.get()); - $tool.set('move'); - resetToolInteractionState(); - }, - [activeTabName] - ); - const onKeyUp = useCallback( - (e: KeyboardEvent) => { - if (e.repeat || e.key !== ' ' || isInteractiveTarget(e.target) || activeTabName !== 'canvas') { - return; - } - if (!$toolStash.get() || $tool.get() !== 'move') { - return; - } - $canvasStage.get()?.container().focus(); - $tool.set($toolStash.get() ?? 'move'); - $toolStash.set(null); - }, - [activeTabName] - ); - - useEffect(() => { - window.addEventListener('keydown', onKeyDown); - window.addEventListener('keyup', onKeyUp); - - return () => { - window.removeEventListener('keydown', onKeyDown); - window.removeEventListener('keyup', onKeyUp); - }; - }, [onKeyDown, onKeyUp]); -}; - -export default useInpaintingCanvasHotkeys; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts deleted file mode 100644 index b392b72e3fb..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseDown.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isDrawing, $isMovingStage, $tool } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { addLine } from 'features/canvas/store/canvasSlice'; -import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; -import type Konva from 'konva'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -import useColorPicker from './useColorUnderCursor'; - -const useCanvasMouseDown = (stageRef: MutableRefObject) => { - const dispatch = useAppDispatch(); - const isStaging = useAppSelector(isStagingSelector); - const { commitColorUnderCursor } = useColorPicker(); - - return useCallback( - (e: KonvaEventObject) => { - if (!stageRef.current) { - return; - } - - stageRef.current.container().focus(); - const tool = $tool.get(); - - if (tool === 'move' || isStaging) { - $isMovingStage.set(true); - return; - } - - if (tool === 'colorPicker') { - commitColorUnderCursor(); - return; - } - - const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - - if (!scaledCursorPosition) { - return; - } - - e.evt.preventDefault(); - - $isDrawing.set(true); - - // Add a new line starting from the current cursor position. - dispatch( - addLine({ - points: [scaledCursorPosition.x, scaledCursorPosition.y], - tool, - }) - ); - }, - [stageRef, isStaging, dispatch, commitColorUnderCursor] - ); -}; - -export default useCanvasMouseDown; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts deleted file mode 100644 index 6d0a97031b9..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseMove.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $cursorPosition, $isDrawing, $tool } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { addPointToCurrentLine } from 'features/canvas/store/canvasSlice'; -import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; -import type Konva from 'konva'; -import type { Vector2d } from 'konva/lib/types'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -import useColorPicker from './useColorUnderCursor'; - -const useCanvasMouseMove = ( - stageRef: MutableRefObject, - didMouseMoveRef: MutableRefObject, - lastCursorPositionRef: MutableRefObject -) => { - const dispatch = useAppDispatch(); - const isStaging = useAppSelector(isStagingSelector); - const { updateColorUnderCursor } = useColorPicker(); - - return useCallback(() => { - if (!stageRef.current) { - return; - } - - const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - - if (!scaledCursorPosition) { - return; - } - - $cursorPosition.set(scaledCursorPosition); - - lastCursorPositionRef.current = scaledCursorPosition; - const tool = $tool.get(); - - if (tool === 'colorPicker') { - updateColorUnderCursor(); - return; - } - - if (!$isDrawing.get() || tool === 'move' || isStaging) { - return; - } - - didMouseMoveRef.current = true; - dispatch(addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])); - }, [didMouseMoveRef, dispatch, isStaging, lastCursorPositionRef, stageRef, updateColorUnderCursor]); -}; - -export default useCanvasMouseMove; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts deleted file mode 100644 index 0b7220eb0b4..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseOut.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { setCanvasInteractionStateMouseOut } from 'features/canvas/store/canvasNanostore'; -import { useCallback } from 'react'; - -const useCanvasMouseOut = () => { - const onMouseOut = useCallback(() => { - setCanvasInteractionStateMouseOut(); - }, []); - - return onMouseOut; -}; - -export default useCanvasMouseOut; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts deleted file mode 100644 index e3c291f1e14..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasMouseUp.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isDrawing, $isMovingStage, $tool } from 'features/canvas/store/canvasNanostore'; -import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; -import { addPointToCurrentLine } from 'features/canvas/store/canvasSlice'; -import getScaledCursorPosition from 'features/canvas/util/getScaledCursorPosition'; -import type Konva from 'konva'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -const useCanvasMouseUp = ( - stageRef: MutableRefObject, - didMouseMoveRef: MutableRefObject -) => { - const dispatch = useAppDispatch(); - const isDrawing = useStore($isDrawing); - const isStaging = useAppSelector(isStagingSelector); - - return useCallback(() => { - if ($tool.get() === 'move' || isStaging) { - $isMovingStage.set(false); - return; - } - - if (!didMouseMoveRef.current && isDrawing && stageRef.current) { - const scaledCursorPosition = getScaledCursorPosition(stageRef.current); - - if (!scaledCursorPosition) { - return; - } - - /** - * Extend the current line. - * In this case, the mouse didn't move, so we append the same point to - * the line's existing points. This allows the line to render as a circle - * centered on that point. - */ - dispatch(addPointToCurrentLine([scaledCursorPosition.x, scaledCursorPosition.y])); - } else { - didMouseMoveRef.current = false; - } - $isDrawing.set(false); - }, [didMouseMoveRef, dispatch, isDrawing, isStaging, stageRef]); -}; - -export default useCanvasMouseUp; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts deleted file mode 100644 index 1434bc9afc5..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { $ctrl, $meta } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { $isMoveStageKeyHeld } from 'features/canvas/store/canvasNanostore'; -import { setBrushSize, setStageCoordinates, setStageScale } from 'features/canvas/store/canvasSlice'; -import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; -import type Konva from 'konva'; -import type { KonvaEventObject } from 'konva/lib/Node'; -import { clamp } from 'lodash-es'; -import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; - -export const calculateNewBrushSize = (brushSize: number, delta: number) => { - // This equation was derived by fitting a curve to the desired brush sizes and deltas - // see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565 - const targetDelta = Math.sign(delta) * 0.7363 * Math.pow(1.0394, brushSize); - // This needs to be clamped to prevent the delta from getting too large - const finalDelta = clamp(targetDelta, -20, 20); - // The new brush size is also clamped to prevent it from getting too large or small - const newBrushSize = clamp(brushSize + finalDelta, 1, 500); - - return newBrushSize; -}; - -const useCanvasWheel = (stageRef: MutableRefObject) => { - const dispatch = useAppDispatch(); - const stageScale = useAppSelector((s) => s.canvas.stageScale); - const isMoveStageKeyHeld = useStore($isMoveStageKeyHeld); - const brushSize = useAppSelector((s) => s.canvas.brushSize); - const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); - - return useCallback( - (e: KonvaEventObject) => { - // stop default scrolling - if (!stageRef.current || isMoveStageKeyHeld) { - return; - } - - e.evt.preventDefault(); - - // checking for ctrl key is pressed or not, - // so that brush size can be controlled using ctrl + scroll up/down - - // Invert the delta if the property is set to true - let delta = e.evt.deltaY; - if (shouldInvertBrushSizeScrollDirection) { - delta = -delta; - } - - if ($ctrl.get() || $meta.get()) { - dispatch(setBrushSize(calculateNewBrushSize(brushSize, delta))); - } else { - const cursorPos = stageRef.current.getPointerPosition(); - let delta = e.evt.deltaY; - - if (!cursorPos) { - return; - } - - const mousePointTo = { - x: (cursorPos.x - stageRef.current.x()) / stageScale, - y: (cursorPos.y - stageRef.current.y()) / stageScale, - }; - // when we zoom on trackpad, e.evt.ctrlKey is true - // in that case lets revert direction - if (e.evt.ctrlKey) { - delta = -delta; - } - - const newScale = clamp(stageScale * CANVAS_SCALE_BY ** delta, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE); - - const newCoordinates = { - x: cursorPos.x - mousePointTo.x * newScale, - y: cursorPos.y - mousePointTo.y * newScale, - }; - - dispatch(setStageScale(newScale)); - dispatch(setStageCoordinates(newCoordinates)); - } - }, - [stageRef, isMoveStageKeyHeld, brushSize, dispatch, stageScale, shouldInvertBrushSizeScrollDirection] - ); -}; - -export default useCanvasWheel; diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts b/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts deleted file mode 100644 index f07433a3deb..00000000000 --- a/invokeai/frontend/web/src/features/canvas/hooks/useColorUnderCursor.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useAppDispatch } from 'app/store/storeHooks'; -import { $canvasBaseLayer, $canvasStage, $tool } from 'features/canvas/store/canvasNanostore'; -import { commitColorPickerColor, setColorPickerColor } from 'features/canvas/store/canvasSlice'; -import Konva from 'konva'; -import { useCallback } from 'react'; - -const useColorPicker = () => { - const dispatch = useAppDispatch(); - - const updateColorUnderCursor = useCallback(() => { - const stage = $canvasStage.get(); - const canvasBaseLayer = $canvasBaseLayer.get(); - if (!stage || !canvasBaseLayer) { - return; - } - - const position = stage.getPointerPosition(); - - if (!position) { - return; - } - - const pixelRatio = Konva.pixelRatio; - - const [r, g, b, a] = canvasBaseLayer - .getContext() - .getImageData(position.x * pixelRatio, position.y * pixelRatio, 1, 1).data; - - if (r === undefined || g === undefined || b === undefined || a === undefined) { - return; - } - - dispatch(setColorPickerColor({ r, g, b, a })); - }, [dispatch]); - - const commitColorUnderCursor = useCallback(() => { - dispatch(commitColorPickerColor()); - $tool.set('brush'); - }, [dispatch]); - - return { updateColorUnderCursor, commitColorUnderCursor }; -}; - -export default useColorPicker; diff --git a/invokeai/frontend/web/src/features/canvas/store/actions.ts b/invokeai/frontend/web/src/features/canvas/store/actions.ts deleted file mode 100644 index b6483b7f3aa..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/actions.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import type { ImageDTO } from 'services/api/types'; - -export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery'); - -export const canvasMaskSavedToGallery = createAction('canvas/canvasMaskSavedToGallery'); - -export const canvasCopiedToClipboard = createAction('canvas/canvasCopiedToClipboard'); - -export const canvasDownloadedAsImage = createAction('canvas/canvasDownloadedAsImage'); - -export const canvasMerged = createAction('canvas/canvasMerged'); - -export const stagingAreaImageSaved = createAction<{ imageDTO: ImageDTO }>('canvas/stagingAreaImageSaved'); - -export const canvasMaskToControlAdapter = createAction<{ id: string }>('canvas/canvasMaskToControlAdapter'); - -export const canvasImageToControlAdapter = createAction<{ id: string }>('canvas/canvasImageToControlAdapter'); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts b/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts deleted file mode 100644 index b225f666773..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { CanvasTool } from 'features/canvas/store/canvasTypes'; -import type Konva from 'konva'; -import type { Vector2d } from 'konva/lib/types'; -import { atom, computed } from 'nanostores'; - -export const $cursorPosition = atom(null); -export const $tool = atom('move'); -export const $toolStash = atom(null); -export const $isDrawing = atom(false); -export const $isMouseOverBoundingBox = atom(false); -const $isMoveBoundingBoxKeyHeld = atom(false); -export const $isMoveStageKeyHeld = atom(false); -export const $isMovingBoundingBox = atom(false); -export const $isMovingStage = atom(false); -export const $isTransformingBoundingBox = atom(false); -export const $isMouseOverBoundingBoxOutline = atom(false); -export const $isModifyingBoundingBox = computed( - [$isTransformingBoundingBox, $isMovingBoundingBox], - (isTransformingBoundingBox, isMovingBoundingBox) => isTransformingBoundingBox || isMovingBoundingBox -); - -export const resetCanvasInteractionState = () => { - $cursorPosition.set(null); - $isDrawing.set(false); - $isMoveBoundingBoxKeyHeld.set(false); - $isMoveStageKeyHeld.set(false); - $isMovingBoundingBox.set(false); - $isMovingStage.set(false); -}; - -export const resetToolInteractionState = () => { - $isTransformingBoundingBox.set(false); - $isMovingBoundingBox.set(false); - $isMovingStage.set(false); -}; - -export const setCanvasInteractionStateMouseOut = () => { - $cursorPosition.set(null); -}; -export const $canvasBaseLayer = atom(null); -export const $canvasStage = atom(null); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts deleted file mode 100644 index 29dc4c9fb8d..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; - -import { selectCanvasSlice } from './canvasSlice'; - -export const isStagingSelector = createSelector( - selectCanvasSlice, - (canvas) => canvas.batchIds.length > 0 || canvas.layerState.stagingArea.images.length > 0 -); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts deleted file mode 100644 index fbb63781662..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ /dev/null @@ -1,728 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; -import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; -import calculateCoordinates from 'features/canvas/util/calculateCoordinates'; -import calculateScale from 'features/canvas/util/calculateScale'; -import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants'; -import floorCoordinates from 'features/canvas/util/floorCoordinates'; -import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import { modelChanged } from 'features/parameters/store/generationSlice'; -import type { PayloadActionWithOptimalDimension } from 'features/parameters/store/types'; -import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; -import type { IRect, Vector2d } from 'konva/lib/types'; -import { clamp } from 'lodash-es'; -import type { RgbaColor } from 'react-colorful'; -import { queueApi } from 'services/api/endpoints/queue'; -import type { ImageDTO } from 'services/api/types'; -import { socketQueueItemStatusChanged } from 'services/events/actions'; - -import type { - BoundingBoxScaleMethod, - CanvasBaseLine, - CanvasImage, - CanvasLayer, - CanvasLayerState, - CanvasMaskLine, - CanvasState, - CanvasTool, - Dimensions, -} from './canvasTypes'; -import { isCanvasAnyLine, isCanvasMaskLine } from './canvasTypes'; -import { CANVAS_GRID_SIZE_FINE } from './constants'; - -/** - * The maximum history length to keep in the past/future layer states. - */ -const MAX_HISTORY = 100; - -const initialLayerState: CanvasLayerState = { - objects: [], - stagingArea: { - images: [], - selectedImageIndex: -1, - }, -}; - -const initialCanvasState: CanvasState = { - _version: 1, - boundingBoxCoordinates: { x: 0, y: 0 }, - boundingBoxDimensions: { width: 512, height: 512 }, - boundingBoxScaleMethod: 'auto', - brushColor: { r: 90, g: 90, b: 255, a: 1 }, - brushSize: 50, - colorPickerColor: { r: 90, g: 90, b: 255, a: 1 }, - futureLayerStates: [], - isMaskEnabled: true, - layer: 'base', - layerState: initialLayerState, - maskColor: { r: 255, g: 90, b: 90, a: 1 }, - pastLayerStates: [], - scaledBoundingBoxDimensions: { width: 512, height: 512 }, - shouldAntialias: true, - shouldAutoSave: false, - shouldCropToBoundingBoxOnSave: false, - shouldDarkenOutsideBoundingBox: false, - shouldFitImageSize: true, - shouldInvertBrushSizeScrollDirection: false, - shouldLockBoundingBox: false, - shouldPreserveMaskedArea: false, - shouldRestrictStrokesToBox: true, - shouldShowBoundingBox: true, - shouldShowCanvasDebugInfo: false, - shouldShowGrid: true, - shouldShowIntermediates: true, - shouldShowStagingImage: true, - shouldShowStagingOutline: true, - shouldSnapToGrid: true, - stageCoordinates: { x: 0, y: 0 }, - stageDimensions: { width: 0, height: 0 }, - stageScale: 1, - batchIds: [], - aspectRatio: { - id: '1:1', - value: 1, - isLocked: false, - }, -}; - -const setBoundingBoxDimensionsReducer = ( - state: CanvasState, - payload: Partial, - optimalDimension: number -) => { - const boundingBoxDimensions = payload; - const newDimensions = { - ...state.boundingBoxDimensions, - ...boundingBoxDimensions, - }; - state.boundingBoxDimensions = newDimensions; - if (state.boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions(newDimensions, optimalDimension); - state.scaledBoundingBoxDimensions = scaledDimensions; - } -}; - -export const canvasSlice = createSlice({ - name: 'canvas', - initialState: initialCanvasState, - reducers: { - setLayer: (state, action: PayloadAction) => { - state.layer = action.payload; - }, - setMaskColor: (state, action: PayloadAction) => { - state.maskColor = action.payload; - }, - setBrushColor: (state, action: PayloadAction) => { - state.brushColor = action.payload; - }, - setBrushSize: (state, action: PayloadAction) => { - state.brushSize = action.payload; - }, - clearMask: (state) => { - pushToPrevLayerStates(state); - state.layerState.objects = state.layerState.objects.filter((obj) => !isCanvasMaskLine(obj)); - state.futureLayerStates = []; - state.shouldPreserveMaskedArea = false; - }, - toggleShouldInvertMask: (state) => { - state.shouldPreserveMaskedArea = !state.shouldPreserveMaskedArea; - }, - toggleShouldShowMask: (state) => { - state.isMaskEnabled = !state.isMaskEnabled; - }, - setShouldPreserveMaskedArea: (state, action: PayloadAction) => { - state.shouldPreserveMaskedArea = action.payload; - }, - setIsMaskEnabled: (state, action: PayloadAction) => { - state.isMaskEnabled = action.payload; - state.layer = action.payload ? 'mask' : 'base'; - }, - setInitialCanvasImage: { - reducer: (state, action: PayloadActionWithOptimalDimension) => { - const { width, height, image_name } = action.payload; - const { optimalDimension } = action.meta; - const { stageDimensions, shouldFitImageSize } = state; - - const newBoundingBoxDimensions = shouldFitImageSize - ? { - width: roundDownToMultiple(width, CANVAS_GRID_SIZE_FINE), - height: roundDownToMultiple(height, CANVAS_GRID_SIZE_FINE), - } - : { - width: roundDownToMultiple(clamp(width, CANVAS_GRID_SIZE_FINE, optimalDimension), CANVAS_GRID_SIZE_FINE), - height: roundDownToMultiple( - clamp(height, CANVAS_GRID_SIZE_FINE, optimalDimension), - CANVAS_GRID_SIZE_FINE - ), - }; - - const newBoundingBoxCoordinates = { - x: roundToMultiple(width / 2 - newBoundingBoxDimensions.width / 2, CANVAS_GRID_SIZE_FINE), - y: roundToMultiple(height / 2 - newBoundingBoxDimensions.height / 2, CANVAS_GRID_SIZE_FINE), - }; - - if (state.boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions(newBoundingBoxDimensions, optimalDimension); - state.scaledBoundingBoxDimensions = scaledDimensions; - } - - state.boundingBoxDimensions = newBoundingBoxDimensions; - state.boundingBoxCoordinates = newBoundingBoxCoordinates; - - pushToPrevLayerStates(state); - - state.layerState = { - ...deepClone(initialLayerState), - objects: [ - { - kind: 'image', - layer: 'base', - x: 0, - y: 0, - width, - height, - imageName: image_name, - }, - ], - }; - state.futureLayerStates = []; - - const newScale = calculateScale( - stageDimensions.width, - stageDimensions.height, - width, - height, - STAGE_PADDING_PERCENTAGE - ); - - const newCoordinates = calculateCoordinates( - stageDimensions.width, - stageDimensions.height, - 0, - 0, - width, - height, - newScale - ); - state.stageScale = newScale; - state.stageCoordinates = newCoordinates; - }, - prepare: (payload: ImageDTO, optimalDimension: number) => ({ - payload, - meta: { - optimalDimension, - }, - }), - }, - setBoundingBoxCoordinates: (state, action: PayloadAction) => { - state.boundingBoxCoordinates = floorCoordinates(action.payload); - }, - setStageCoordinates: (state, action: PayloadAction) => { - state.stageCoordinates = action.payload; - }, - setStageScale: (state, action: PayloadAction) => { - state.stageScale = action.payload; - }, - setShouldDarkenOutsideBoundingBox: (state, action: PayloadAction) => { - state.shouldDarkenOutsideBoundingBox = action.payload; - }, - setShouldInvertBrushSizeScrollDirection: (state, action: PayloadAction) => { - state.shouldInvertBrushSizeScrollDirection = action.payload; - }, - clearCanvasHistory: (state) => { - state.pastLayerStates = []; - state.futureLayerStates = []; - }, - setShouldLockBoundingBox: (state, action: PayloadAction) => { - state.shouldLockBoundingBox = action.payload; - }, - setShouldShowBoundingBox: (state, action: PayloadAction) => { - state.shouldShowBoundingBox = action.payload; - }, - canvasBatchIdAdded: (state, action: PayloadAction) => { - state.batchIds.push(action.payload); - }, - canvasBatchIdsReset: (state) => { - state.batchIds = []; - }, - stagingAreaInitialized: ( - state, - action: PayloadAction<{ - boundingBox: IRect; - }> - ) => { - const { boundingBox } = action.payload; - - state.layerState.stagingArea = { - boundingBox, - images: [], - selectedImageIndex: -1, - }; - }, - addImageToStagingArea: (state, action: PayloadAction) => { - const image = action.payload; - - if (!image || !state.layerState.stagingArea.boundingBox) { - return; - } - - pushToPrevLayerStates(state); - - state.layerState.stagingArea.images.push({ - kind: 'image', - layer: 'base', - ...state.layerState.stagingArea.boundingBox, - imageName: image.image_name, - }); - - state.layerState.stagingArea.selectedImageIndex = state.layerState.stagingArea.images.length - 1; - - state.futureLayerStates = []; - }, - discardStagedImages: (state) => { - pushToPrevLayerStates(state); - resetStagingArea(state); - state.futureLayerStates = []; - }, - discardStagedImage: (state) => { - const { images, selectedImageIndex } = state.layerState.stagingArea; - pushToPrevLayerStates(state); - images.splice(selectedImageIndex, 1); - state.layerState.stagingArea.selectedImageIndex = Math.max(0, images.length - 1); - state.futureLayerStates = []; - }, - addFillRect: (state) => { - const { boundingBoxCoordinates, boundingBoxDimensions, brushColor } = state; - - pushToPrevLayerStates(state); - - state.layerState.objects.push({ - kind: 'fillRect', - layer: 'base', - ...boundingBoxCoordinates, - ...boundingBoxDimensions, - color: brushColor, - }); - - state.futureLayerStates = []; - }, - addEraseRect: (state) => { - const { boundingBoxCoordinates, boundingBoxDimensions } = state; - - pushToPrevLayerStates(state); - - state.layerState.objects.push({ - kind: 'eraseRect', - layer: 'base', - ...boundingBoxCoordinates, - ...boundingBoxDimensions, - }); - - state.futureLayerStates = []; - }, - addLine: (state, action: PayloadAction<{ points: number[]; tool: CanvasTool }>) => { - const { layer, brushColor, brushSize, shouldRestrictStrokesToBox } = state; - const { points, tool } = action.payload; - - if (tool === 'move' || tool === 'colorPicker') { - return; - } - - const newStrokeWidth = brushSize / 2; - - // set & then spread this to only conditionally add the "color" key - const newColor = layer === 'base' && tool === 'brush' ? { color: brushColor } : {}; - - pushToPrevLayerStates(state); - - const newLine: CanvasMaskLine | CanvasBaseLine = { - kind: 'line', - layer, - tool, - strokeWidth: newStrokeWidth, - points, - ...newColor, - }; - - if (shouldRestrictStrokesToBox) { - newLine.clip = { - ...state.boundingBoxCoordinates, - ...state.boundingBoxDimensions, - }; - } - - state.layerState.objects.push(newLine); - - state.futureLayerStates = []; - }, - addPointToCurrentLine: (state, action: PayloadAction) => { - const lastLine = state.layerState.objects.findLast(isCanvasAnyLine); - - if (!lastLine) { - return; - } - - lastLine.points.push(...action.payload); - }, - undo: (state) => { - const targetState = state.pastLayerStates.pop(); - - if (!targetState) { - return; - } - - pushToFutureLayerStates(state); - - state.layerState = targetState; - }, - redo: (state) => { - const targetState = state.futureLayerStates.shift(); - - if (!targetState) { - return; - } - - pushToPrevLayerStates(state); - - state.layerState = targetState; - }, - setShouldShowGrid: (state, action: PayloadAction) => { - state.shouldShowGrid = action.payload; - }, - setShouldSnapToGrid: (state, action: PayloadAction) => { - state.shouldSnapToGrid = action.payload; - }, - setShouldAutoSave: (state, action: PayloadAction) => { - state.shouldAutoSave = action.payload; - }, - setShouldShowIntermediates: (state, action: PayloadAction) => { - state.shouldShowIntermediates = action.payload; - }, - resetCanvas: (state) => { - pushToPrevLayerStates(state); - state.layerState = deepClone(initialLayerState); - state.futureLayerStates = []; - state.boundingBoxCoordinates = { - ...initialCanvasState.boundingBoxCoordinates, - }; - state.boundingBoxDimensions = { - ...initialCanvasState.boundingBoxDimensions, - }; - state.stageScale = calculateScale( - state.stageDimensions.width, - state.stageDimensions.height, - state.boundingBoxDimensions.width, - state.boundingBoxDimensions.height, - STAGE_PADDING_PERCENTAGE - ); - state.stageCoordinates = calculateCoordinates( - state.stageDimensions.width, - state.stageDimensions.height, - 0, - 0, - state.boundingBoxDimensions.width, - state.boundingBoxDimensions.height, - 1 - ); - }, - canvasResized: (state, action: PayloadAction<{ width: number; height: number }>) => { - state.stageDimensions = { - width: Math.floor(action.payload.width), - height: Math.floor(action.payload.height), - }; - }, - resetCanvasView: ( - state, - action: PayloadAction<{ - contentRect: IRect; - shouldScaleTo1?: boolean; - }> - ) => { - const { contentRect, shouldScaleTo1 } = action.payload; - const { - stageDimensions: { width: stageWidth, height: stageHeight }, - } = state; - - const newScale = shouldScaleTo1 - ? 1 - : calculateScale( - stageWidth, - stageHeight, - contentRect.width || state.boundingBoxDimensions.width, - contentRect.height || state.boundingBoxDimensions.height, - STAGE_PADDING_PERCENTAGE - ); - - const newCoordinates = calculateCoordinates( - stageWidth, - stageHeight, - contentRect.x || state.boundingBoxCoordinates.x, - contentRect.y || state.boundingBoxCoordinates.y, - contentRect.width || state.boundingBoxDimensions.width, - contentRect.height || state.boundingBoxDimensions.height, - newScale - ); - - state.stageScale = newScale; - state.stageCoordinates = newCoordinates; - }, - nextStagingAreaImage: (state) => { - if (!state.layerState.stagingArea.images.length) { - return; - } - - const nextIndex = state.layerState.stagingArea.selectedImageIndex + 1; - const lastIndex = state.layerState.stagingArea.images.length - 1; - - state.layerState.stagingArea.selectedImageIndex = nextIndex > lastIndex ? 0 : nextIndex; - }, - prevStagingAreaImage: (state) => { - if (!state.layerState.stagingArea.images.length) { - return; - } - - const prevIndex = state.layerState.stagingArea.selectedImageIndex - 1; - const lastIndex = state.layerState.stagingArea.images.length - 1; - - state.layerState.stagingArea.selectedImageIndex = prevIndex < 0 ? lastIndex : prevIndex; - }, - commitStagingAreaImage: (state) => { - if (!state.layerState.stagingArea.images.length) { - return; - } - - const { images, selectedImageIndex } = state.layerState.stagingArea; - - pushToPrevLayerStates(state); - - const imageToCommit = images[selectedImageIndex]; - - if (imageToCommit) { - state.layerState.objects.push({ - ...imageToCommit, - }); - } - - resetStagingArea(state); - state.futureLayerStates = []; - }, - setBoundingBoxScaleMethod: { - reducer: (state, action: PayloadActionWithOptimalDimension) => { - const boundingBoxScaleMethod = action.payload; - const { optimalDimension } = action.meta; - state.boundingBoxScaleMethod = boundingBoxScaleMethod; - - if (boundingBoxScaleMethod === 'auto') { - const scaledDimensions = getScaledBoundingBoxDimensions(state.boundingBoxDimensions, optimalDimension); - state.scaledBoundingBoxDimensions = scaledDimensions; - } - }, - prepare: (payload: BoundingBoxScaleMethod, optimalDimension: number) => ({ - payload, - meta: { - optimalDimension, - }, - }), - }, - setScaledBoundingBoxDimensions: (state, action: PayloadAction>) => { - state.scaledBoundingBoxDimensions = { - ...state.scaledBoundingBoxDimensions, - ...action.payload, - }; - }, - setBoundingBoxDimensions: { - reducer: (state, action: PayloadActionWithOptimalDimension>) => { - setBoundingBoxDimensionsReducer(state, action.payload, action.meta.optimalDimension); - }, - prepare: (payload: Partial, optimalDimension: number) => ({ - payload, - meta: { - optimalDimension, - }, - }), - }, - setShouldShowStagingImage: (state, action: PayloadAction) => { - state.shouldShowStagingImage = action.payload; - }, - setShouldShowStagingOutline: (state, action: PayloadAction) => { - state.shouldShowStagingOutline = action.payload; - }, - setShouldShowCanvasDebugInfo: (state, action: PayloadAction) => { - state.shouldShowCanvasDebugInfo = action.payload; - }, - setShouldRestrictStrokesToBox: (state, action: PayloadAction) => { - state.shouldRestrictStrokesToBox = action.payload; - }, - setShouldAntialias: (state, action: PayloadAction) => { - state.shouldAntialias = action.payload; - }, - setShouldFitImageSize: (state, action: PayloadAction) => { - state.shouldFitImageSize = action.payload; - }, - setShouldCropToBoundingBoxOnSave: (state, action: PayloadAction) => { - state.shouldCropToBoundingBoxOnSave = action.payload; - }, - setColorPickerColor: (state, action: PayloadAction) => { - state.colorPickerColor = action.payload; - }, - commitColorPickerColor: (state) => { - state.brushColor = { - ...state.colorPickerColor, - a: state.brushColor.a, - }; - }, - setMergedCanvas: (state, action: PayloadAction) => { - pushToPrevLayerStates(state); - - state.futureLayerStates = []; - - state.layerState.objects = [action.payload]; - }, - aspectRatioChanged: (state, action: PayloadAction) => { - state.aspectRatio = action.payload; - }, - }, - extraReducers: (builder) => { - builder.addCase(modelChanged, (state, action) => { - const newModel = action.payload; - if (!newModel || action.meta.previousModel?.base === newModel.base) { - // Model was cleared or the base didn't change - return; - } - const optimalDimension = getOptimalDimension(action.payload); - const { width, height } = state.boundingBoxDimensions; - if (getIsSizeOptimal(width, height, optimalDimension)) { - return; - } - const newSize = calculateNewSize(state.aspectRatio.value, optimalDimension * optimalDimension); - setBoundingBoxDimensionsReducer(state, newSize, optimalDimension); - }); - - builder.addCase(socketQueueItemStatusChanged, (state, action) => { - const batch_status = action.payload.data.batch_status; - if (!state.batchIds.includes(batch_status.batch_id)) { - return; - } - - if (batch_status.in_progress === 0 && batch_status.pending === 0) { - state.batchIds = state.batchIds.filter((id) => id !== batch_status.batch_id); - } - - const queueItemStatus = action.payload.data.status; - if (queueItemStatus === 'canceled' || queueItemStatus === 'failed') { - resetStagingAreaIfEmpty(state); - } - }); - builder.addMatcher(queueApi.endpoints.clearQueue.matchFulfilled, (state) => { - state.batchIds = []; - resetStagingAreaIfEmpty(state); - }); - builder.addMatcher(queueApi.endpoints.cancelByBatchIds.matchFulfilled, (state, action) => { - state.batchIds = state.batchIds.filter((id) => !action.meta.arg.originalArgs.batch_ids.includes(id)); - resetStagingAreaIfEmpty(state); - }); - }, -}); - -export const { - addEraseRect, - addFillRect, - addImageToStagingArea, - addLine, - addPointToCurrentLine, - clearCanvasHistory, - clearMask, - commitColorPickerColor, - commitStagingAreaImage, - discardStagedImages, - discardStagedImage, - nextStagingAreaImage, - prevStagingAreaImage, - redo, - resetCanvas, - resetCanvasView, - setBoundingBoxCoordinates, - setBoundingBoxDimensions, - setBoundingBoxScaleMethod, - setBrushColor, - setBrushSize, - setColorPickerColor, - setInitialCanvasImage, - setIsMaskEnabled, - setLayer, - setMaskColor, - setMergedCanvas, - setShouldAutoSave, - setShouldCropToBoundingBoxOnSave, - setShouldDarkenOutsideBoundingBox, - setShouldInvertBrushSizeScrollDirection, - setShouldPreserveMaskedArea, - setShouldShowBoundingBox, - setShouldShowCanvasDebugInfo, - setShouldShowGrid, - setShouldShowIntermediates, - setShouldShowStagingImage, - setShouldShowStagingOutline, - setShouldSnapToGrid, - setStageCoordinates, - setStageScale, - undo, - setScaledBoundingBoxDimensions, - setShouldRestrictStrokesToBox, - stagingAreaInitialized, - setShouldAntialias, - setShouldFitImageSize, - canvasResized, - canvasBatchIdAdded, - canvasBatchIdsReset, - aspectRatioChanged, -} = canvasSlice.actions; - -export const selectCanvasSlice = (state: RootState) => state.canvas; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateCanvasState = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - state.aspectRatio = initialAspectRatioState; - } - return state; -}; - -export const canvasPersistConfig: PersistConfig = { - name: canvasSlice.name, - initialState: initialCanvasState, - migrate: migrateCanvasState, - persistDenylist: ['shouldShowStagingImage', 'shouldShowStagingOutline'], -}; - -const pushToPrevLayerStates = (state: CanvasState) => { - state.pastLayerStates.push(deepClone(state.layerState)); - if (state.pastLayerStates.length > MAX_HISTORY) { - state.pastLayerStates = state.pastLayerStates.slice(-MAX_HISTORY); - } -}; - -const pushToFutureLayerStates = (state: CanvasState) => { - state.futureLayerStates.unshift(deepClone(state.layerState)); - if (state.futureLayerStates.length > MAX_HISTORY) { - state.futureLayerStates = state.futureLayerStates.slice(0, MAX_HISTORY); - } -}; - -const resetStagingAreaIfEmpty = (state: CanvasState) => { - if (state.batchIds.length === 0 && state.layerState.stagingArea.images.length === 0) { - resetStagingArea(state); - } -}; - -const resetStagingArea = (state: CanvasState) => { - state.layerState.stagingArea = { ...initialCanvasState.layerState.stagingArea }; - state.shouldShowStagingImage = initialCanvasState.shouldShowStagingImage; - state.shouldShowStagingOutline = initialCanvasState.shouldShowStagingOutline; -}; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts deleted file mode 100644 index c41c6f329f3..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { IRect, Vector2d } from 'konva/lib/types'; -import type { RgbaColor } from 'react-colorful'; -import { z } from 'zod'; - -export type CanvasLayer = 'base' | 'mask'; - -const zBoundingBoxScaleMethod = z.enum(['none', 'auto', 'manual']); -export type BoundingBoxScaleMethod = z.infer; -export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod => - zBoundingBoxScaleMethod.safeParse(v).success; - -type CanvasDrawingTool = 'brush' | 'eraser'; - -export type CanvasTool = CanvasDrawingTool | 'move' | 'colorPicker'; - -export type Dimensions = { - width: number; - height: number; -}; - -export type CanvasImage = { - kind: 'image'; - layer: 'base'; - x: number; - y: number; - width: number; - height: number; - imageName: string; -}; - -export type CanvasMaskLine = { - layer: 'mask'; - kind: 'line'; - tool: CanvasDrawingTool; - strokeWidth: number; - points: number[]; - clip?: IRect; -}; - -export type CanvasBaseLine = { - layer: 'base'; - color?: RgbaColor; - kind: 'line'; - tool: CanvasDrawingTool; - strokeWidth: number; - points: number[]; - clip?: IRect; -}; - -type CanvasFillRect = { - kind: 'fillRect'; - layer: 'base'; - x: number; - y: number; - width: number; - height: number; - color: RgbaColor; -}; - -type CanvasEraseRect = { - kind: 'eraseRect'; - layer: 'base'; - x: number; - y: number; - width: number; - height: number; -}; - -type CanvasObject = CanvasImage | CanvasBaseLine | CanvasMaskLine | CanvasFillRect | CanvasEraseRect; - -export type CanvasLayerState = { - objects: CanvasObject[]; - stagingArea: { - images: CanvasImage[]; - selectedImageIndex: number; - boundingBox?: IRect; - }; -}; - -// type guards -export const isCanvasMaskLine = (obj: CanvasObject): obj is CanvasMaskLine => - obj.kind === 'line' && obj.layer === 'mask'; - -export const isCanvasBaseLine = (obj: CanvasObject): obj is CanvasBaseLine => - obj.kind === 'line' && obj.layer === 'base'; - -export const isCanvasBaseImage = (obj: CanvasObject): obj is CanvasImage => - obj.kind === 'image' && obj.layer === 'base'; - -export const isCanvasFillRect = (obj: CanvasObject): obj is CanvasFillRect => - obj.kind === 'fillRect' && obj.layer === 'base'; - -export const isCanvasEraseRect = (obj: CanvasObject): obj is CanvasEraseRect => - obj.kind === 'eraseRect' && obj.layer === 'base'; - -export const isCanvasAnyLine = (obj: CanvasObject): obj is CanvasMaskLine | CanvasBaseLine => obj.kind === 'line'; - -export interface CanvasState { - _version: 1; - boundingBoxCoordinates: Vector2d; - boundingBoxDimensions: Dimensions; - boundingBoxScaleMethod: BoundingBoxScaleMethod; - brushColor: RgbaColor; - brushSize: number; - colorPickerColor: RgbaColor; - futureLayerStates: CanvasLayerState[]; - isMaskEnabled: boolean; - layer: CanvasLayer; - layerState: CanvasLayerState; - maskColor: RgbaColor; - pastLayerStates: CanvasLayerState[]; - scaledBoundingBoxDimensions: Dimensions; - shouldAntialias: boolean; - shouldAutoSave: boolean; - shouldCropToBoundingBoxOnSave: boolean; - shouldDarkenOutsideBoundingBox: boolean; - shouldFitImageSize: boolean; - shouldInvertBrushSizeScrollDirection: boolean; - shouldLockBoundingBox: boolean; - shouldPreserveMaskedArea: boolean; - shouldRestrictStrokesToBox: boolean; - shouldShowBoundingBox: boolean; - shouldShowCanvasDebugInfo: boolean; - shouldShowGrid: boolean; - shouldShowIntermediates: boolean; - shouldShowStagingImage: boolean; - shouldShowStagingOutline: boolean; - shouldSnapToGrid: boolean; - stageCoordinates: Vector2d; - stageDimensions: Dimensions; - stageScale: number; - generationMode?: GenerationMode; - batchIds: string[]; - aspectRatio: AspectRatioState; -} - -export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; diff --git a/invokeai/frontend/web/src/features/canvas/store/constants.ts b/invokeai/frontend/web/src/features/canvas/store/constants.ts deleted file mode 100644 index 450c2461944..00000000000 --- a/invokeai/frontend/web/src/features/canvas/store/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const CANVAS_GRID_SIZE_COARSE = 64; -export const CANVAS_GRID_SIZE_FINE = 8; -export const CANVAS_TAB_TESTID = 'unified-canvas-tab'; diff --git a/invokeai/frontend/web/src/features/canvas/util/blobToDataURL.ts b/invokeai/frontend/web/src/features/canvas/util/blobToDataURL.ts deleted file mode 100644 index f29010c99c2..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/blobToDataURL.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const blobToDataURL = (blob: Blob): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (_e) => resolve(reader.result as string); - reader.onerror = (_e) => reject(reader.error); - reader.onabort = (_e) => reject(new Error('Read aborted')); - reader.readAsDataURL(blob); - }); -}; - -export function imageDataToDataURL(imageData: ImageData): string { - const { width, height } = imageData; - - // Create a canvas to transfer the ImageData to - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - - // Draw the ImageData onto the canvas - const ctx = canvas.getContext('2d'); - if (!ctx) { - throw new Error('Unable to get canvas context'); - } - ctx.putImageData(imageData, 0, 0); - - // Convert the canvas to a data URL (base64) - return canvas.toDataURL(); -} diff --git a/invokeai/frontend/web/src/features/canvas/util/calculateCoordinates.ts b/invokeai/frontend/web/src/features/canvas/util/calculateCoordinates.ts deleted file mode 100644 index fe9c14b2ba8..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/calculateCoordinates.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Vector2d } from 'konva/lib/types'; - -const calculateCoordinates = ( - containerWidth: number, - containerHeight: number, - containerX: number, - containerY: number, - contentWidth: number, - contentHeight: number, - scale: number -): Vector2d => { - const x = Math.floor(containerWidth / 2 - (containerX + contentWidth / 2) * scale); - const y = Math.floor(containerHeight / 2 - (containerY + contentHeight / 2) * scale); - return { x, y }; -}; - -export default calculateCoordinates; diff --git a/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts b/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts deleted file mode 100644 index 255cb2850b2..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/calculateScale.ts +++ /dev/null @@ -1,14 +0,0 @@ -const calculateScale = ( - containerWidth: number, - containerHeight: number, - contentWidth: number, - contentHeight: number, - padding = 0.95 -): number => { - const scaleX = (containerWidth * padding) / contentWidth; - const scaleY = (containerHeight * padding) / contentHeight; - const scaleFit = Math.min(1, Math.min(scaleX, scaleY)); - return scaleFit ? scaleFit : 1; -}; - -export default calculateScale; diff --git a/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts b/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts deleted file mode 100644 index 44220c8ba42..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Gets a Blob from a canvas. - */ -export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise => - new Promise((resolve, reject) => { - canvas.toBlob((blob) => { - if (blob) { - resolve(blob); - return; - } - reject('Unable to create Blob'); - }); - }); diff --git a/invokeai/frontend/web/src/features/canvas/util/colorToString.ts b/invokeai/frontend/web/src/features/canvas/util/colorToString.ts deleted file mode 100644 index 25d79fed5aa..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/colorToString.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { RgbaColor, RgbColor } from 'react-colorful'; - -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/features/canvas/util/constants.ts b/invokeai/frontend/web/src/features/canvas/util/constants.ts deleted file mode 100644 index 3291732ecc4..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/constants.ts +++ /dev/null @@ -1,14 +0,0 @@ -// canvas wheel zoom exponential scale factor -export const CANVAS_SCALE_BY = 0.999; - -// minimum (furthest-zoomed-out) scale -export const MIN_CANVAS_SCALE = 0.1; - -// maximum (furthest-zoomed-in) scale -export const MAX_CANVAS_SCALE = 20; - -// padding given to initial image/bounding box when stage view is reset -export const STAGE_PADDING_PERCENTAGE = 0.95; - -export const COLOR_PICKER_SIZE = 30; -export const COLOR_PICKER_STROKE_RADIUS = 10; diff --git a/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts b/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts deleted file mode 100644 index d0a71dee40c..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/createMaskStage.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { CanvasMaskLine } from 'features/canvas/store/canvasTypes'; -import Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; - -/** - * Creates a stage from array of mask objects. - * We cannot just convert the mask layer to a blob because it uses a texture with transparent areas. - * So instead we create a new stage with the mask layer and composite it onto a white background. - */ -const createMaskStage = async ( - lines: CanvasMaskLine[], - boundingBox: IRect, - shouldInvertMask: boolean -): Promise => { - // create an offscreen canvas and add the mask to it - const { width, height } = boundingBox; - - const offscreenContainer = document.createElement('div'); - - const maskStage = new Konva.Stage({ - container: offscreenContainer, - width: width, - height: height, - }); - - const baseLayer = new Konva.Layer(); - const maskLayer = new Konva.Layer(); - - // composite the image onto the mask layer - baseLayer.add( - new Konva.Rect({ - ...boundingBox, - fill: shouldInvertMask ? 'black' : 'white', - }) - ); - - lines.forEach((line) => - maskLayer.add( - new Konva.Line({ - points: line.points, - stroke: shouldInvertMask ? 'white' : 'black', - strokeWidth: line.strokeWidth * 2, - tension: 0, - lineCap: 'round', - lineJoin: 'round', - shadowForStrokeEnabled: false, - globalCompositeOperation: line.tool === 'brush' ? 'source-over' : 'destination-out', - }) - ) - ); - - maskStage.add(baseLayer); - maskStage.add(maskLayer); - - // you'd think we can't do this until we finish with the maskStage, but we can - offscreenContainer.remove(); - - return maskStage; -}; - -export default createMaskStage; diff --git a/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts b/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts deleted file mode 100644 index d19cbe46127..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Gets an ImageData object from an image dataURL by drawing it to a canvas. - */ -export const dataURLToImageData = async (dataURL: string, width: number, height: number): Promise => - new Promise((resolve, reject) => { - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - const image = new Image(); - - if (!ctx) { - canvas.remove(); - reject('Unable to get context'); - return; - } - - image.onload = function () { - ctx.drawImage(image, 0, 0); - canvas.remove(); - resolve(ctx.getImageData(0, 0, width, height)); - }; - - image.src = dataURL; - }); diff --git a/invokeai/frontend/web/src/features/canvas/util/downloadBlob.ts b/invokeai/frontend/web/src/features/canvas/util/downloadBlob.ts deleted file mode 100644 index 837e76c9980..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/downloadBlob.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** Download a blob as a file */ -export const downloadBlob = (blob: Blob, fileName: string) => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = fileName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - a.remove(); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/floorCoordinates.ts b/invokeai/frontend/web/src/features/canvas/util/floorCoordinates.ts deleted file mode 100644 index 49088683324..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/floorCoordinates.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Vector2d } from 'konva/lib/types'; - -const floorCoordinates = (coord: Vector2d): Vector2d => { - return { - x: Math.floor(coord.x), - y: Math.floor(coord.y), - }; -}; - -export default floorCoordinates; diff --git a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts b/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts deleted file mode 100644 index d37d6440087..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore'; - -import { konvaNodeToBlob } from './konvaNodeToBlob'; - -/** - * Get the canvas base layer blob, with or without bounding box according to `shouldCropToBoundingBoxOnSave` - */ -export const getBaseLayerBlob = async (state: RootState, alwaysUseBoundingBox: boolean = false) => { - const canvasBaseLayer = $canvasBaseLayer.get(); - - if (!canvasBaseLayer) { - throw new Error('Problem getting base layer blob'); - } - - const { shouldCropToBoundingBoxOnSave, boundingBoxCoordinates, boundingBoxDimensions } = state.canvas; - - const clonedBaseLayer = canvasBaseLayer.clone(); - - clonedBaseLayer.scale({ x: 1, y: 1 }); - - const absPos = clonedBaseLayer.getAbsolutePosition(); - - const boundingBox = - shouldCropToBoundingBoxOnSave || alwaysUseBoundingBox - ? { - x: boundingBoxCoordinates.x + absPos.x, - y: boundingBoxCoordinates.y + absPos.y, - width: boundingBoxDimensions.width, - height: boundingBoxDimensions.height, - } - : clonedBaseLayer.getClientRect(); - - return konvaNodeToBlob(clonedBaseLayer, boundingBox); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts b/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts deleted file mode 100644 index d17096cb71b..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { logger } from 'app/logging/logger'; -import { $canvasBaseLayer, $canvasStage } from 'features/canvas/store/canvasNanostore'; -import type { CanvasLayerState, Dimensions } from 'features/canvas/store/canvasTypes'; -import { isCanvasMaskLine } from 'features/canvas/store/canvasTypes'; -import { konvaNodeToImageData } from 'features/canvas/util/konvaNodeToImageData'; -import type { Vector2d } from 'konva/lib/types'; - -import createMaskStage from './createMaskStage'; -import { konvaNodeToBlob } from './konvaNodeToBlob'; - -/** - * Gets Blob and ImageData objects for the base and mask layers - */ -export const getCanvasData = async ( - layerState: CanvasLayerState, - boundingBoxCoordinates: Vector2d, - boundingBoxDimensions: Dimensions, - isMaskEnabled: boolean, - shouldPreserveMaskedArea: boolean -) => { - const log = logger('canvas'); - - const canvasBaseLayer = $canvasBaseLayer.get(); - const canvasStage = $canvasStage.get(); - - if (!canvasBaseLayer || !canvasStage) { - log.error('Unable to find canvas / stage'); - return; - } - - const boundingBox = { - ...boundingBoxCoordinates, - ...boundingBoxDimensions, - }; - - // Clone the base layer so we don't affect the visible base layer - const clonedBaseLayer = canvasBaseLayer.clone(); - - // Scale it to 100% so we get full resolution - clonedBaseLayer.scale({ x: 1, y: 1 }); - - // absolute position is needed to get the bounding box coords relative to the base layer - const absPos = clonedBaseLayer.getAbsolutePosition(); - - const offsetBoundingBox = { - x: boundingBox.x + absPos.x, - y: boundingBox.y + absPos.y, - width: boundingBox.width, - height: boundingBox.height, - }; - - // For the base layer, use the offset boundingBox - const baseBlob = await konvaNodeToBlob(clonedBaseLayer, offsetBoundingBox); - const baseImageData = await konvaNodeToImageData(clonedBaseLayer, offsetBoundingBox); - - // For the mask layer, use the normal boundingBox - const maskStage = await createMaskStage( - isMaskEnabled ? layerState.objects.filter(isCanvasMaskLine) : [], // only include mask lines, and only if mask is enabled - boundingBox, - shouldPreserveMaskedArea - ); - const maskBlob = await konvaNodeToBlob(maskStage, boundingBox); - const maskImageData = await konvaNodeToImageData(maskStage, boundingBox); - - return { - baseBlob, - baseImageData, - maskBlob, - maskImageData, - }; -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getCanvasGenerationMode.ts b/invokeai/frontend/web/src/features/canvas/util/getCanvasGenerationMode.ts deleted file mode 100644 index f0a34649862..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getCanvasGenerationMode.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { areAnyPixelsBlack, getImageDataTransparency } from 'common/util/arrayBuffer'; -import type { GenerationMode } from 'features/canvas/store/canvasTypes'; - -export const getCanvasGenerationMode = (baseImageData: ImageData, maskImageData: ImageData): GenerationMode => { - const { isPartiallyTransparent: baseIsPartiallyTransparent, isFullyTransparent: baseIsFullyTransparent } = - getImageDataTransparency(baseImageData.data); - - // check mask for black - const doesMaskHaveBlackPixels = areAnyPixelsBlack(maskImageData.data); - - if (baseIsPartiallyTransparent) { - if (baseIsFullyTransparent) { - return 'txt2img'; - } - - return 'outpaint'; - } else { - if (doesMaskHaveBlackPixels) { - return 'inpaint'; - } - - return 'img2img'; - } -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getColoredMaskSVG.ts b/invokeai/frontend/web/src/features/canvas/util/getColoredMaskSVG.ts deleted file mode 100644 index 47e1100447a..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getColoredMaskSVG.ts +++ /dev/null @@ -1,81 +0,0 @@ -export const getColoredMaskSVG = (color: string) => { - return `data:image/svg+xml;utf8, - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`.replaceAll('black', color); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts b/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts deleted file mode 100644 index a5fbc999222..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore'; - -import { konvaNodeToBlob } from './konvaNodeToBlob'; - -/** - * Gets the canvas base layer blob, without bounding box - */ -export const getFullBaseLayerBlob = async () => { - const canvasBaseLayer = $canvasBaseLayer.get(); - - if (!canvasBaseLayer) { - return; - } - - const clonedBaseLayer = canvasBaseLayer.clone(); - - clonedBaseLayer.scale({ x: 1, y: 1 }); - - return konvaNodeToBlob(clonedBaseLayer, clonedBaseLayer.getClientRect()); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts b/invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts deleted file mode 100644 index de38d12cf5f..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getScaledBoundingBoxDimensions.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { roundToMultiple } from 'common/util/roundDownToMultiple'; -import type { Dimensions } from 'features/canvas/store/canvasTypes'; -import { CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/constants'; - -const getScaledBoundingBoxDimensions = (dimensions: Dimensions, optimalDimension: number) => { - const { width, height } = dimensions; - - const scaledDimensions = { width, height }; - const targetArea = optimalDimension * optimalDimension; - const aspectRatio = width / height; - let currentArea = width * height; - let maxDimension = optimalDimension - CANVAS_GRID_SIZE_FINE; - while (currentArea < targetArea) { - maxDimension += CANVAS_GRID_SIZE_FINE; - if (width === height) { - scaledDimensions.width = optimalDimension; - scaledDimensions.height = optimalDimension; - break; - } else { - if (aspectRatio > 1) { - scaledDimensions.width = maxDimension; - scaledDimensions.height = roundToMultiple(maxDimension / aspectRatio, CANVAS_GRID_SIZE_FINE); - } else if (aspectRatio < 1) { - scaledDimensions.height = maxDimension; - scaledDimensions.width = roundToMultiple(maxDimension * aspectRatio, CANVAS_GRID_SIZE_FINE); - } - currentArea = scaledDimensions.width * scaledDimensions.height; - } - } - - return scaledDimensions; -}; - -export default getScaledBoundingBoxDimensions; diff --git a/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts b/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts deleted file mode 100644 index 1250f66d525..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/getScaledCursorPosition.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Stage } from 'konva/lib/Stage'; - -const getScaledCursorPosition = (stage: Stage) => { - const pointerPosition = stage.getPointerPosition(); - - const stageTransform = stage.getAbsoluteTransform().copy(); - - if (!pointerPosition || !stageTransform) { - return; - } - - const scaledCursorPosition = stageTransform.invert().point(pointerPosition); - - return { - x: scaledCursorPosition.x, - y: scaledCursorPosition.y, - }; -}; - -export default getScaledCursorPosition; diff --git a/invokeai/frontend/web/src/features/canvas/util/isInteractiveTarget.ts b/invokeai/frontend/web/src/features/canvas/util/isInteractiveTarget.ts deleted file mode 100644 index 74a09aa8e48..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/isInteractiveTarget.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isInputElement } from 'common/util/isInputElement'; - -export const isInteractiveTarget = (target: EventTarget | null) => { - if (target instanceof HTMLElement) { - return ( - target.tabIndex > -1 || - isInputElement(target) || - ['dialog', 'alertdialog'].includes(target.getAttribute('role') ?? '') - ); - } - - return false; -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToBlob.ts b/invokeai/frontend/web/src/features/canvas/util/konvaNodeToBlob.ts deleted file mode 100644 index e16988ea23b..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToBlob.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; - -import { canvasToBlob } from './canvasToBlob'; - -/** - * Converts a Konva node to a Blob - * @param node - The Konva node to convert to a Blob - * @param boundingBox - The bounding box to crop to - * @returns A Promise that resolves with Blob of the node cropped to the bounding box - */ -export const konvaNodeToBlob = async (node: Konva.Node, boundingBox: IRect): Promise => { - return await canvasToBlob(node.toCanvas(boundingBox)); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToImageData.ts b/invokeai/frontend/web/src/features/canvas/util/konvaNodeToImageData.ts deleted file mode 100644 index 3b5780ae16a..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/konvaNodeToImageData.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type Konva from 'konva'; -import type { IRect } from 'konva/lib/types'; - -import { dataURLToImageData } from './dataURLToImageData'; - -/** - * Converts a Konva node to an ImageData object - * @param node - The Konva node to convert to an ImageData object - * @param boundingBox - The bounding box to crop to - * @returns A Promise that resolves with ImageData object of the node cropped to the bounding box - */ -export const konvaNodeToImageData = async (node: Konva.Node, boundingBox: IRect): Promise => { - // get a dataURL of the bbox'd region - const dataURL = node.toDataURL(boundingBox); - - return await dataURLToImageData(dataURL, boundingBox.width, boundingBox.height); -}; diff --git a/invokeai/frontend/web/src/features/canvas/util/roundToHundreth.ts b/invokeai/frontend/web/src/features/canvas/util/roundToHundreth.ts deleted file mode 100644 index 1b05e7f64dd..00000000000 --- a/invokeai/frontend/web/src/features/canvas/util/roundToHundreth.ts +++ /dev/null @@ -1,5 +0,0 @@ -const roundToHundreth = (val: number): number => { - return Math.round(val * 100) / 100; -}; - -export default roundToHundreth; diff --git a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx index 218e2973935..5ac6ffcb7c9 100644 --- a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx +++ b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx @@ -1,42 +1,68 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, ConfirmationAlertDialog, Flex, FormControl, Text } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +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 = createMemoizedSelector( +const selectImagesToChange = createSelector( selectChangeBoardModalSlice, - (changeBoardModal) => changeBoardModal.imagesToChange + (changeBoardModal) => changeBoardModal.image_names +); + +const selectIsModalOpen = createSelector( + selectChangeBoardModalSlice, + (changeBoardModal) => changeBoardModal.isModalOpen ); const ChangeBoardModal = () => { + useAssertSingleton('ChangeBoardModal'); const dispatch = useAppDispatch(); - const [selectedBoard, setSelectedBoard] = useState(); - const { data: boards, isFetching } = useListAllBoardsQuery(); - const isModalOpen = useAppSelector((s) => s.changeBoardModal.isModalOpen); + 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 ?? []).map((board) => ({ - label: board.board_name, - value: board.board_id, - })) - ); - }, [boards, t]); + 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 === selectedBoard), [options, selectedBoard]); + const value = useMemo(() => options.find((o) => o.value === selectedBoardId), [options, selectedBoardId]); const handleClose = useCallback(() => { dispatch(changeBoardReset()); @@ -44,27 +70,28 @@ const ChangeBoardModal = () => { }, [dispatch]); const handleChangeBoard = useCallback(() => { - if (!imagesToChange.length || !selectedBoard) { + if (!selectedBoardId || imagesToChange.length === 0) { return; } - if (selectedBoard === 'none') { - removeImagesFromBoard({ imageDTOs: imagesToChange }); - } else { - addImagesToBoard({ - imageDTOs: imagesToChange, - board_id: selectedBoard, - }); + if (imagesToChange.length) { + if (selectedBoardId === 'none') { + removeImagesFromBoard({ image_names: imagesToChange }); + } else { + addImagesToBoard({ + image_names: imagesToChange, + board_id: selectedBoardId, + }); + } } - setSelectedBoard(null); dispatch(changeBoardReset()); - }, [addImagesToBoard, dispatch, imagesToChange, removeImagesFromBoard, selectedBoard]); + }, [addImagesToBoard, dispatch, imagesToChange, removeImagesFromBoard, selectedBoardId]); const onChange = useCallback((v) => { if (!v) { return; } - setSelectedBoard(v.value); + setSelectedBoardId(v.value); }, []); return ( @@ -75,13 +102,13 @@ const ChangeBoardModal = () => { acceptCallback={handleChangeBoard} acceptButtonText={t('boards.move')} cancelButtonText={t('boards.cancel')} + useInert={false} > {t('boards.movingImagesToBoard', { count: imagesToChange.length, })} - : []), +}); +type ChangeBoardModalState = z.infer; + +const getInitialState = (): ChangeBoardModalState => zChangeBoardModalState.parse({}); -export const changeBoardModalSlice = createSlice({ +const slice = createSlice({ name: 'changeBoardModal', - initialState, + initialState: getInitialState(), reducers: { isModalOpenChanged: (state, action: PayloadAction) => { state.isModalOpen = action.payload; }, - imagesToChangeSelected: (state, action: PayloadAction) => { - state.imagesToChange = action.payload; + imagesToChangeSelected: (state, action: PayloadAction) => { + state.image_names = action.payload; }, changeBoardReset: (state) => { - state.imagesToChange = []; + state.image_names = []; state.isModalOpen = false; }, }, }); -export const { isModalOpenChanged, imagesToChangeSelected, changeBoardReset } = changeBoardModalSlice.actions; +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/changeBoardModal/store/types.ts b/invokeai/frontend/web/src/features/changeBoardModal/store/types.ts deleted file mode 100644 index f1a825480eb..00000000000 --- a/invokeai/frontend/web/src/features/changeBoardModal/store/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { ImageDTO } from 'services/api/types'; - -export type ChangeBoardModalState = { - isModalOpen: boolean; - imagesToChange: ImageDTO[]; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx deleted file mode 100644 index c13783cddd9..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterConfig.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { Box, Flex, FormControl, FormLabel, Icon, IconButton, Switch } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import ParamControlAdapterModel from 'features/controlAdapters/components/parameters/ParamControlAdapterModel'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterType } from 'features/controlAdapters/hooks/useControlAdapterType'; -import { - controlAdapterDuplicated, - controlAdapterIsEnabledChanged, - controlAdapterRemoved, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiCaretUpBold, PiCopyBold, PiTrashSimpleBold } from 'react-icons/pi'; -import { useToggle } from 'react-use'; - -import ControlAdapterImagePreview from './ControlAdapterImagePreview'; -import ControlAdapterProcessorComponent from './ControlAdapterProcessorComponent'; -import ControlAdapterShouldAutoConfig from './ControlAdapterShouldAutoConfig'; -import ControlNetCanvasImageImports from './imports/ControlNetCanvasImageImports'; -import { ParamControlAdapterBeginEnd } from './parameters/ParamControlAdapterBeginEnd'; -import ParamControlAdapterControlMode from './parameters/ParamControlAdapterControlMode'; -import ParamControlAdapterIPMethod from './parameters/ParamControlAdapterIPMethod'; -import ParamControlAdapterProcessorSelect from './parameters/ParamControlAdapterProcessorSelect'; -import ParamControlAdapterResizeMode from './parameters/ParamControlAdapterResizeMode'; -import ParamControlAdapterWeight from './parameters/ParamControlAdapterWeight'; - -const ControlAdapterConfig = (props: { id: string; number: number }) => { - const { id, number } = props; - const controlAdapterType = useControlAdapterType(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const activeTabName = useAppSelector(activeTabNameSelector); - const isEnabled = useControlAdapterIsEnabled(id); - const [isExpanded, toggleIsExpanded] = useToggle(false); - - const handleDelete = useCallback(() => { - dispatch(controlAdapterRemoved({ id })); - }, [id, dispatch]); - - const handleDuplicate = useCallback(() => { - dispatch(controlAdapterDuplicated(id)); - }, [id, dispatch]); - - const handleToggleIsEnabled = useCallback( - (e: ChangeEvent) => { - dispatch( - controlAdapterIsEnabledChanged({ - id, - isEnabled: e.target.checked, - }) - ); - }, - [id, dispatch] - ); - - if (!controlAdapterType) { - return null; - } - - return ( - - - - {t(`controlnet.${controlAdapterType}`, { number })} - - - - - - - - {activeTabName === 'canvas' && } - } - /> - } - /> - - } - /> - - - - - - - - - - {!isExpanded && ( - - - - )} - - - - {isExpanded && ( - <> - - - - - - - - - - )} - - ); -}; - -export default memo(ControlAdapterConfig); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx deleted file mode 100644 index bf1c7dce9f3..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { Box, Flex, Spinner } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIDndImage from 'common/components/IAIDndImage'; -import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; -import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; -import { useControlAdapterControlImage } from 'features/controlAdapters/hooks/useControlAdapterControlImage'; -import { useControlAdapterProcessedControlImage } from 'features/controlAdapters/hooks/useControlAdapterProcessedControlImage'; -import { useControlAdapterProcessorType } from 'features/controlAdapters/hooks/useControlAdapterProcessorType'; -import { - controlAdapterImageChanged, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; -import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi'; -import { - useAddImageToBoardMutation, - useChangeImageIsIntermediateMutation, - useGetImageDTOQuery, - useRemoveImageFromBoardMutation, -} from 'services/api/endpoints/images'; -import type { PostUploadAction } from 'services/api/types'; - -type Props = { - id: string; - isSmall?: boolean; -}; - -const selectPendingControlImages = createMemoizedSelector( - selectControlAdaptersSlice, - (controlAdapters) => controlAdapters.pendingControlImages -); - -const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const controlImageName = useControlAdapterControlImage(id); - const processedControlImageName = useControlAdapterProcessedControlImage(id); - const processorType = useControlAdapterProcessorType(id); - const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); - const isConnected = useAppSelector((s) => s.system.isConnected); - const activeTabName = useAppSelector(activeTabNameSelector); - const optimalDimension = useAppSelector(selectOptimalDimension); - const pendingControlImages = useAppSelector(selectPendingControlImages); - - const [isMouseOverImage, setIsMouseOverImage] = useState(false); - - const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - controlImageName ?? skipToken - ); - - const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - processedControlImageName ?? skipToken - ); - - const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); - const [addToBoard] = useAddImageToBoardMutation(); - const [removeFromBoard] = useRemoveImageFromBoardMutation(); - const handleResetControlImage = useCallback(() => { - dispatch(controlAdapterImageChanged({ id, controlImage: null })); - }, [id, dispatch]); - - const handleSaveControlImage = useCallback(async () => { - if (!processedControlImage) { - return; - } - - await changeIsIntermediate({ - imageDTO: processedControlImage, - is_intermediate: false, - }).unwrap(); - - if (autoAddBoardId !== 'none') { - addToBoard({ - imageDTO: processedControlImage, - board_id: autoAddBoardId, - }); - } else { - removeFromBoard({ imageDTO: processedControlImage }); - } - }, [processedControlImage, changeIsIntermediate, autoAddBoardId, addToBoard, removeFromBoard]); - - const handleSetControlImageToDimensions = useCallback(() => { - if (!controlImage) { - return; - } - - if (activeTabName === 'canvas') { - dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension)); - } else { - const options = { updateAspectRatio: true, clamp: true }; - const { width, height } = calculateNewSize( - controlImage.width / controlImage.height, - optimalDimension * optimalDimension - ); - dispatch(widthChanged({ width, ...options })); - dispatch(heightChanged({ height, ...options })); - } - }, [controlImage, activeTabName, dispatch, optimalDimension]); - - const handleMouseEnter = useCallback(() => { - setIsMouseOverImage(true); - }, []); - - const handleMouseLeave = useCallback(() => { - setIsMouseOverImage(false); - }, []); - - const draggableData = useMemo(() => { - if (controlImage) { - return { - id, - payloadType: 'IMAGE_DTO', - payload: { imageDTO: controlImage }, - }; - } - }, [controlImage, id]); - - const droppableData = useMemo( - () => ({ - id, - actionType: 'SET_CONTROL_ADAPTER_IMAGE', - context: { id }, - }), - [id] - ); - - const postUploadAction = useMemo(() => ({ type: 'SET_CONTROL_ADAPTER_IMAGE', id }), [id]); - - const shouldShowProcessedImage = - controlImage && - processedControlImage && - !isMouseOverImage && - !pendingControlImages.includes(id) && - processorType !== 'none'; - - useEffect(() => { - if (isConnected && (isErrorControlImage || isErrorProcessedControlImage)) { - handleResetControlImage(); - } - }, [handleResetControlImage, isConnected, isErrorControlImage, isErrorProcessedControlImage]); - - return ( - - - - - - - - <> - : undefined} - tooltip={t('controlnet.resetControlImage')} - /> - : undefined} - tooltip={t('controlnet.saveControlImage')} - styleOverrides={saveControlImageStyleOverrides} - /> - : undefined} - tooltip={t('controlnet.setControlImageDimensions')} - styleOverrides={setControlImageDimensionsStyleOverrides} - /> - - - {pendingControlImages.includes(id) && ( - - - - )} - - ); -}; - -export default memo(ControlAdapterImagePreview); - -const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 }; -const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 }; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterProcessorComponent.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterProcessorComponent.tsx deleted file mode 100644 index 2e37d88e272..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterProcessorComponent.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterProcessorNode } from 'features/controlAdapters/hooks/useControlAdapterProcessorNode'; -import { memo } from 'react'; - -import CannyProcessor from './processors/CannyProcessor'; -import ColorMapProcessor from './processors/ColorMapProcessor'; -import ContentShuffleProcessor from './processors/ContentShuffleProcessor'; -import DepthAnyThingProcessor from './processors/DepthAnyThingProcessor'; -import DWOpenposeProcessor from './processors/DWOpenposeProcessor'; -import HedProcessor from './processors/HedProcessor'; -import LineartAnimeProcessor from './processors/LineartAnimeProcessor'; -import LineartProcessor from './processors/LineartProcessor'; -import MediapipeFaceProcessor from './processors/MediapipeFaceProcessor'; -import MidasDepthProcessor from './processors/MidasDepthProcessor'; -import MlsdImageProcessor from './processors/MlsdImageProcessor'; -import NormalBaeProcessor from './processors/NormalBaeProcessor'; -import PidiProcessor from './processors/PidiProcessor'; -import ZoeDepthProcessor from './processors/ZoeDepthProcessor'; - -type Props = { - id: string; -}; - -const ControlAdapterProcessorComponent = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const processorNode = useControlAdapterProcessorNode(id); - - if (!processorNode) { - return null; - } - - if (processorNode.type === 'canny_image_processor') { - return ; - } - - if (processorNode.type === 'color_map_image_processor') { - return ; - } - - if (processorNode.type === 'depth_anything_image_processor') { - return ; - } - - if (processorNode.type === 'hed_image_processor') { - return ; - } - - if (processorNode.type === 'lineart_image_processor') { - return ; - } - - if (processorNode.type === 'content_shuffle_image_processor') { - return ; - } - - if (processorNode.type === 'lineart_anime_image_processor') { - return ; - } - - if (processorNode.type === 'mediapipe_face_processor') { - return ; - } - - if (processorNode.type === 'midas_depth_image_processor') { - return ; - } - - if (processorNode.type === 'mlsd_image_processor') { - return ; - } - - if (processorNode.type === 'normalbae_image_processor') { - return ; - } - - if (processorNode.type === 'dw_openpose_image_processor') { - return ; - } - - if (processorNode.type === 'pidi_image_processor') { - return ; - } - - if (processorNode.type === 'zoe_depth_image_processor') { - return ; - } - - return null; -}; - -export default memo(ControlAdapterProcessorComponent); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterShouldAutoConfig.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterShouldAutoConfig.tsx deleted file mode 100644 index cb3d36c58d9..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterShouldAutoConfig.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterModel } from 'features/controlAdapters/hooks/useControlAdapterModel'; -import { useControlAdapterShouldAutoConfig } from 'features/controlAdapters/hooks/useControlAdapterShouldAutoConfig'; -import { controlAdapterAutoConfigToggled } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isNil } from 'lodash-es'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const ControlAdapterShouldAutoConfig = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const shouldAutoConfig = useControlAdapterShouldAutoConfig(id); - const { modelConfig } = useControlAdapterModel(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const handleShouldAutoConfigChanged = useCallback(() => { - dispatch(controlAdapterAutoConfigToggled({ id, modelConfig })); - }, [id, dispatch, modelConfig]); - - if (isNil(shouldAutoConfig)) { - return null; - } - - return ( - - {t('controlnet.autoConfigure')} - - - ); -}; - -export default memo(ControlAdapterShouldAutoConfig); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/hooks/useProcessorNodeChanged.ts b/invokeai/frontend/web/src/features/controlAdapters/components/hooks/useProcessorNodeChanged.ts deleted file mode 100644 index d76717cbf3e..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/hooks/useProcessorNodeChanged.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useAppDispatch } from 'app/store/storeHooks'; -import { controlAdapterProcessorParamsChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlAdapterProcessorNode } from 'features/controlAdapters/store/types'; -import { useCallback } from 'react'; - -export const useProcessorNodeChanged = () => { - const dispatch = useAppDispatch(); - const handleProcessorNodeChanged = useCallback( - (id: string, params: Partial) => { - dispatch( - controlAdapterProcessorParamsChanged({ - id, - params, - }) - ); - }, - [dispatch] - ); - return handleProcessorNodeChanged; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/imports/ControlNetCanvasImageImports.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/imports/ControlNetCanvasImageImports.tsx deleted file mode 100644 index fada3e3abf2..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/imports/ControlNetCanvasImageImports.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Flex, IconButton } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { canvasImageToControlAdapter, canvasMaskToControlAdapter } from 'features/canvas/store/actions'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiExcludeBold, PiImageSquareBold } from 'react-icons/pi'; - -type ControlNetCanvasImageImportsProps = { - id: string; -}; - -const ControlNetCanvasImageImports = (props: ControlNetCanvasImageImportsProps) => { - const { id } = props; - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const handleImportImageFromCanvas = useCallback(() => { - dispatch(canvasImageToControlAdapter({ id })); - }, [id, dispatch]); - - const handleImportMaskFromCanvas = useCallback(() => { - dispatch(canvasMaskToControlAdapter({ id })); - }, [id, dispatch]); - - return ( - - } - tooltip={t('controlnet.importImageFromCanvas')} - aria-label={t('controlnet.importImageFromCanvas')} - onClick={handleImportImageFromCanvas} - /> - } - tooltip={t('controlnet.importMaskFromCanvas')} - aria-label={t('controlnet.importMaskFromCanvas')} - onClick={handleImportMaskFromCanvas} - /> - - ); -}; - -export default memo(ControlNetCanvasImageImports); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterBeginEnd.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterBeginEnd.tsx deleted file mode 100644 index 245c182b9fb..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterBeginEnd.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { CompositeRangeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterBeginEndStepPct } from 'features/controlAdapters/hooks/useControlAdapterBeginEndStepPct'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { - controlAdapterBeginStepPctChanged, - controlAdapterEndStepPctChanged, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const formatPct = (v: number) => `${Math.round(v * 100)}%`; - -export const ParamControlAdapterBeginEnd = memo(({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const stepPcts = useControlAdapterBeginEndStepPct(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const onChange = useCallback( - (v: [number, number]) => { - dispatch( - controlAdapterBeginStepPctChanged({ - id, - beginStepPct: v[0], - }) - ); - dispatch( - controlAdapterEndStepPctChanged({ - id, - endStepPct: v[1], - }) - ); - }, - [dispatch, id] - ); - - const onReset = useCallback(() => { - dispatch( - controlAdapterBeginStepPctChanged({ - id, - beginStepPct: 0, - }) - ); - dispatch( - controlAdapterEndStepPctChanged({ - id, - endStepPct: 1, - }) - ); - }, [dispatch, id]); - - const value = useMemo<[number, number]>(() => [stepPcts?.beginStepPct ?? 0, stepPcts?.endStepPct ?? 1], [stepPcts]); - - if (!stepPcts) { - return null; - } - - return ( - - - {t('controlnet.beginEndStepPercent')} - - - - ); -}); - -ParamControlAdapterBeginEnd.displayName = 'ParamControlAdapterBeginEnd'; - -const ariaLabel = ['Begin Step %', 'End Step %']; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterControlMode.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterControlMode.tsx deleted file mode 100644 index ea16d6bc1ff..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterControlMode.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterControlMode } from 'features/controlAdapters/hooks/useControlAdapterControlMode'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { controlAdapterControlModeChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlMode } from 'features/controlAdapters/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const ParamControlAdapterControlMode = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const controlMode = useControlAdapterControlMode(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const CONTROL_MODE_DATA = useMemo( - () => [ - { label: t('controlnet.balanced'), value: 'balanced' }, - { label: t('controlnet.prompt'), value: 'more_prompt' }, - { label: t('controlnet.control'), value: 'more_control' }, - { label: t('controlnet.megaControl'), value: 'unbalanced' }, - ], - [t] - ); - - const handleControlModeChange = useCallback( - (v) => { - if (!v) { - return; - } - dispatch( - controlAdapterControlModeChanged({ - id, - controlMode: v.value as ControlMode, - }) - ); - }, - [id, dispatch] - ); - - const value = useMemo( - () => CONTROL_MODE_DATA.filter((o) => o.value === controlMode)[0], - [CONTROL_MODE_DATA, controlMode] - ); - - if (!controlMode) { - return null; - } - - return ( - - - {t('controlnet.controlMode')} - - - - ); -}; - -export default memo(ParamControlAdapterControlMode); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx deleted file mode 100644 index c7aaa9f26c8..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterIPMethod.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterIPMethod } from 'features/controlAdapters/hooks/useControlAdapterIPMethod'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { controlAdapterIPMethodChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { IPMethod } from 'features/controlAdapters/store/types'; -import { isIPMethod } from 'features/controlAdapters/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const ParamControlAdapterIPMethod = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const method = useControlAdapterIPMethod(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const options: { label: string; value: IPMethod }[] = useMemo( - () => [ - { label: t('controlnet.full'), value: 'full' }, - { label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' }, - { label: `${t('controlnet.composition')} (${t('common.beta')})`, value: 'composition' }, - ], - [t] - ); - - const handleIPMethodChanged = useCallback( - (v) => { - if (!isIPMethod(v?.value)) { - return; - } - dispatch( - controlAdapterIPMethodChanged({ - id, - method: v.value, - }) - ); - }, - [id, dispatch] - ); - - const value = useMemo(() => options.find((o) => o.value === method), [options, method]); - - if (!method) { - return null; - } - - return ( - - - {t('controlnet.ipAdapterMethod')} - - - - ); -}; - -export default memo(ParamControlAdapterIPMethod); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx deleted file mode 100644 index 00c7d5859da..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; -import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { useControlAdapterCLIPVisionModel } from 'features/controlAdapters/hooks/useControlAdapterCLIPVisionModel'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterModel } from 'features/controlAdapters/hooks/useControlAdapterModel'; -import { useControlAdapterModels } from 'features/controlAdapters/hooks/useControlAdapterModels'; -import { useControlAdapterType } from 'features/controlAdapters/hooks/useControlAdapterType'; -import { - controlAdapterCLIPVisionModelChanged, - controlAdapterModelChanged, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { CLIPVisionModel } from 'features/controlAdapters/store/types'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import type { - AnyModelConfig, - ControlNetModelConfig, - IPAdapterModelConfig, - T2IAdapterModelConfig, -} from 'services/api/types'; - -type ParamControlAdapterModelProps = { - id: string; -}; - -const selectMainModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model); - -const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => { - const isEnabled = useControlAdapterIsEnabled(id); - const controlAdapterType = useControlAdapterType(id); - const { modelConfig } = useControlAdapterModel(id); - const dispatch = useAppDispatch(); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base); - const currentCLIPVisionModel = useControlAdapterCLIPVisionModel(id); - const mainModel = useAppSelector(selectMainModel); - const { t } = useTranslation(); - - const [modelConfigs, { isLoading }] = useControlAdapterModels(controlAdapterType); - - const _onChange = useCallback( - (modelConfig: ControlNetModelConfig | IPAdapterModelConfig | T2IAdapterModelConfig | null) => { - if (!modelConfig) { - return; - } - dispatch( - controlAdapterModelChanged({ - id, - modelConfig, - }) - ); - }, - [dispatch, id] - ); - - const onCLIPVisionModelChange = useCallback( - (v) => { - if (!v?.value) { - return; - } - dispatch(controlAdapterCLIPVisionModelChanged({ id, clipVisionModel: v.value as CLIPVisionModel })); - }, - [dispatch, id] - ); - - const selectedModel = useMemo( - () => (modelConfig && controlAdapterType ? { ...modelConfig, model_type: controlAdapterType } : null), - [controlAdapterType, modelConfig] - ); - - const getIsDisabled = useCallback( - (model: AnyModelConfig): boolean => { - const isCompatible = currentBaseModel === model.base; - const hasMainModel = Boolean(currentBaseModel); - return !hasMainModel || !isCompatible; - }, - [currentBaseModel] - ); - - const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ - modelConfigs, - onChange: _onChange, - selectedModel, - getIsDisabled, - isLoading, - }); - - const clipVisionOptions = useMemo( - () => [ - { label: 'ViT-H', value: 'ViT-H' }, - { label: 'ViT-G', value: 'ViT-G' }, - ], - [] - ); - - const clipVisionModel = useMemo( - () => clipVisionOptions.find((o) => o.value === currentCLIPVisionModel), - [clipVisionOptions, currentCLIPVisionModel] - ); - - return ( - - - - - - - {modelConfig?.type === 'ip_adapter' && modelConfig.format === 'checkpoint' && ( - - - - )} - - ); -}; - -export default memo(ParamControlAdapterModel); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect.tsx deleted file mode 100644 index 5257a471283..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; -import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterProcessorNode } from 'features/controlAdapters/hooks/useControlAdapterProcessorNode'; -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import { controlAdapterProcessortTypeChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import { configSelector } from 'features/system/store/configSelectors'; -import { map } from 'lodash-es'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const selectOptions = createMemoizedSelector(configSelector, (config) => { - const options: ComboboxOption[] = map(CONTROLNET_PROCESSORS, (p) => ({ - value: p.type, - label: p.label, - })) - .sort((a, b) => - // sort 'none' to the top - a.value === 'none' ? -1 : b.value === 'none' ? 1 : a.label.localeCompare(b.label) - ) - .filter((d) => !config.sd.disabledControlNetProcessors.includes(d.value as ControlAdapterProcessorType)); - - return options; -}); - -const ParamControlAdapterProcessorSelect = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const processorNode = useControlAdapterProcessorNode(id); - const dispatch = useAppDispatch(); - const options = useAppSelector(selectOptions); - const { t } = useTranslation(); - - const onChange = useCallback( - (v) => { - if (!v) { - return; - } - dispatch( - controlAdapterProcessortTypeChanged({ - id, - processorType: v.value as ControlAdapterProcessorType, // TODO: need runtime check... - }) - ); - }, - [id, dispatch] - ); - const value = useMemo(() => options.find((o) => o.value === processorNode?.type), [options, processorNode]); - - if (!processorNode) { - return null; - } - return ( - - - {t('controlnet.processor')} - - - - ); -}; - -export default memo(ParamControlAdapterProcessorSelect); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterResizeMode.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterResizeMode.tsx deleted file mode 100644 index 58b8905f5ac..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterResizeMode.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterResizeMode } from 'features/controlAdapters/hooks/useControlAdapterResizeMode'; -import { controlAdapterResizeModeChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ResizeMode } from 'features/controlAdapters/store/types'; -import { isResizeMode } from 'features/controlAdapters/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -type Props = { - id: string; -}; - -const ParamControlAdapterResizeMode = ({ id }: Props) => { - const isEnabled = useControlAdapterIsEnabled(id); - const resizeMode = useControlAdapterResizeMode(id); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const options: { label: string; value: ResizeMode }[] = useMemo( - () => [ - { label: t('controlnet.resize'), value: 'just_resize' }, - { label: t('controlnet.crop'), value: 'crop_resize' }, - { label: t('controlnet.fill'), value: 'fill_resize' }, - { label: t('controlnet.resizeSimple'), value: 'just_resize_simple' }, - ], - [t] - ); - - const handleResizeModeChange = useCallback( - (v) => { - if (!isResizeMode(v?.value)) { - return; - } - dispatch( - controlAdapterResizeModeChanged({ - id, - resizeMode: v.value, - }) - ); - }, - [id, dispatch] - ); - - const value = useMemo(() => options.find((o) => o.value === resizeMode), [options, resizeMode]); - - if (!resizeMode) { - return null; - } - - return ( - - - {t('controlnet.resizeMode')} - - - - ); -}; - -export default memo(ParamControlAdapterResizeMode); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterWeight.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterWeight.tsx deleted file mode 100644 index 7f45901f538..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterWeight.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; -import { useControlAdapterWeight } from 'features/controlAdapters/hooks/useControlAdapterWeight'; -import { controlAdapterWeightChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isNil } from 'lodash-es'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -type ParamControlAdapterWeightProps = { - id: string; -}; - -const formatValue = (v: number) => v.toFixed(2); - -const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useControlAdapterIsEnabled(id); - const weight = useControlAdapterWeight(id); - const initial = useAppSelector((s) => s.config.sd.ca.weight.initial); - const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.ca.weight.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.ca.weight.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.ca.weight.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.ca.weight.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.ca.weight.fineStep); - - const onChange = useCallback( - (weight: number) => { - dispatch(controlAdapterWeightChanged({ id, weight })); - }, - [dispatch, id] - ); - - if (isNil(weight)) { - // should never happen - return null; - } - - return ( - - - {t('controlnet.weight')} - - - - - ); -}; - -export default memo(ParamControlAdapterWeight); - -const marks = [0, 1, 2]; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/CannyProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/CannyProcessor.tsx deleted file mode 100644 index 0d547b9490b..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/CannyProcessor.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredCannyImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type CannyProcessorProps = { - controlNetId: string; - processorNode: RequiredCannyImageProcessorInvocation; - isEnabled: boolean; -}; - -const CannyProcessor = (props: CannyProcessorProps) => { - const { controlNetId, processorNode, isEnabled } = props; - const { low_threshold, high_threshold, image_resolution, detect_resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - const defaults = useGetDefaultForControlnetProcessor( - 'canny_image_processor' - ) as RequiredCannyImageProcessorInvocation; - - const handleLowThresholdChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { low_threshold: v }); - }, - [controlNetId, processorChanged] - ); - - const handleHighThresholdChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { high_threshold: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.lowThreshold')} - - - - - {t('controlnet.highThreshold')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.detectResolution')} - - - - - ); -}; - -export default memo(CannyProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ColorMapProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/ColorMapProcessor.tsx deleted file mode 100644 index 3dd6bf0aa98..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ColorMapProcessor.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredColorMapImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type ColorMapProcessorProps = { - controlNetId: string; - processorNode: RequiredColorMapImageProcessorInvocation; - isEnabled: boolean; -}; - -const ColorMapProcessor = (props: ColorMapProcessorProps) => { - const { controlNetId, processorNode, isEnabled } = props; - const { color_map_tile_size } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - const defaults = useGetDefaultForControlnetProcessor( - 'color_map_image_processor' - ) as RequiredColorMapImageProcessorInvocation; - - const handleColorMapTileSizeChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { color_map_tile_size: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.colorMapTileSize')} - - - - - ); -}; - -export default memo(ColorMapProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ContentShuffleProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/ContentShuffleProcessor.tsx deleted file mode 100644 index 9677a3a2233..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ContentShuffleProcessor.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredContentShuffleImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredContentShuffleImageProcessorInvocation; - isEnabled: boolean; -}; - -const ContentShuffleProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution, w, h, f } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'content_shuffle_image_processor' - ) as RequiredContentShuffleImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleWChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { w: v }); - }, - [controlNetId, processorChanged] - ); - - const handleHChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { h: v }); - }, - [controlNetId, processorChanged] - ); - - const handleFChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { f: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.w')} - - - - - {t('controlnet.h')} - - - - - {t('controlnet.f')} - - - - - ); -}; - -export default memo(ContentShuffleProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/DWOpenposeProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/DWOpenposeProcessor.tsx deleted file mode 100644 index 6761bfd4e1d..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/DWOpenposeProcessor.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredDWOpenposeImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredDWOpenposeImageProcessorInvocation; - isEnabled: boolean; -}; - -const DWOpenposeProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, draw_body, draw_face, draw_hands } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'dw_openpose_image_processor' - ) as RequiredDWOpenposeImageProcessorInvocation; - - const handleDrawBodyChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { draw_body: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - const handleDrawFaceChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { draw_face: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - const handleDrawHandsChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { draw_hands: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - - {t('controlnet.body')} - - - - {t('controlnet.face')} - - - - {t('controlnet.hands')} - - - - - {t('controlnet.imageResolution')} - - - - - ); -}; - -export default memo(DWOpenposeProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/DepthAnyThingProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/DepthAnyThingProcessor.tsx deleted file mode 100644 index a5b04624378..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/DepthAnyThingProcessor.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import type { ComboboxOnChange } from '@invoke-ai/ui-library'; -import { Combobox, CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { - DepthAnythingModelSize, - RequiredDepthAnythingImageProcessorInvocation, -} from 'features/controlAdapters/store/types'; -import { isDepthAnythingModelSize } from 'features/controlAdapters/store/types'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredDepthAnythingImageProcessorInvocation; - isEnabled: boolean; -}; - -const DepthAnythingProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { model_size, resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'midas_depth_image_processor' - ) as RequiredDepthAnythingImageProcessorInvocation; - - const handleModelSizeChange = useCallback( - (v) => { - if (!isDepthAnythingModelSize(v?.value)) { - return; - } - processorChanged(controlNetId, { - model_size: v.value, - }); - }, - [controlNetId, processorChanged] - ); - - const options: { label: string; value: DepthAnythingModelSize }[] = useMemo( - () => [ - { label: t('controlnet.small'), value: 'small' }, - { label: t('controlnet.base'), value: 'base' }, - { label: t('controlnet.large'), value: 'large' }, - ], - [t] - ); - - const value = useMemo(() => options.filter((o) => o.value === model_size)[0], [options, model_size]); - - const handleResolutionChange = useCallback( - (v: number) => { - processorChanged(controlNetId, { resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleResolutionDefaultChange = useCallback(() => { - processorChanged(controlNetId, { resolution: 512 }); - }, [controlNetId, processorChanged]); - - return ( - - - {t('controlnet.modelSize')} - - - - {t('controlnet.imageResolution')} - - - - - ); -}; - -export default memo(DepthAnythingProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/HedProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/HedProcessor.tsx deleted file mode 100644 index e75d50de363..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/HedProcessor.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredHedImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type HedProcessorProps = { - controlNetId: string; - processorNode: RequiredHedImageProcessorInvocation; - isEnabled: boolean; -}; - -const HedPreprocessor = (props: HedProcessorProps) => { - const { - controlNetId, - processorNode: { detect_resolution, image_resolution, scribble }, - isEnabled, - } = props; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor('hed_image_processor') as RequiredHedImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleScribbleChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { scribble: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.scribble')} - - - - ); -}; - -export default memo(HedPreprocessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartAnimeProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartAnimeProcessor.tsx deleted file mode 100644 index 9849bda7c89..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartAnimeProcessor.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredLineartAnimeImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredLineartAnimeImageProcessorInvocation; - isEnabled: boolean; -}; - -const LineartAnimeProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'lineart_anime_image_processor' - ) as RequiredLineartAnimeImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - ); -}; - -export default memo(LineartAnimeProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartProcessor.tsx deleted file mode 100644 index 51d082eb57f..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/LineartProcessor.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredLineartImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type LineartProcessorProps = { - controlNetId: string; - processorNode: RequiredLineartImageProcessorInvocation; - isEnabled: boolean; -}; - -const LineartProcessor = (props: LineartProcessorProps) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution, coarse } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'lineart_image_processor' - ) as RequiredLineartImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleCoarseChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { coarse: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.coarse')} - - - - ); -}; - -export default memo(LineartProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MediapipeFaceProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/MediapipeFaceProcessor.tsx deleted file mode 100644 index de35d628d7c..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MediapipeFaceProcessor.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredMediapipeFaceProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredMediapipeFaceProcessorInvocation; - isEnabled: boolean; -}; - -const MediapipeFaceProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { max_faces, min_confidence, image_resolution, detect_resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'mediapipe_face_processor' - ) as RequiredMediapipeFaceProcessorInvocation; - - const handleMaxFacesChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { max_faces: v }); - }, - [controlNetId, processorChanged] - ); - - const handleMinConfidenceChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { min_confidence: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.maxFaces')} - - - - - {t('controlnet.minConfidence')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.detectResolution')} - - - - - ); -}; - -export default memo(MediapipeFaceProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MidasDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/MidasDepthProcessor.tsx deleted file mode 100644 index f4089ed48f3..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MidasDepthProcessor.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredMidasDepthImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredMidasDepthImageProcessorInvocation; - isEnabled: boolean; -}; - -const MidasDepthProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { a_mult, bg_th, image_resolution, detect_resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'midas_depth_image_processor' - ) as RequiredMidasDepthImageProcessorInvocation; - - const handleAMultChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { a_mult: v }); - }, - [controlNetId, processorChanged] - ); - - const handleBgThChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { bg_th: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.amult')} - - - - - {t('controlnet.bgth')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.detectResolution')} - - - - - ); -}; - -export default memo(MidasDepthProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MlsdImageProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/MlsdImageProcessor.tsx deleted file mode 100644 index 69fd4f68074..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/MlsdImageProcessor.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredMlsdImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredMlsdImageProcessorInvocation; - isEnabled: boolean; -}; - -const MlsdImageProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution, thr_d, thr_v } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor('mlsd_image_processor') as RequiredMlsdImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleThrDChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { thr_d: v }); - }, - [controlNetId, processorChanged] - ); - - const handleThrVChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { thr_v: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.w')} - - - - - {t('controlnet.h')} - - - - - ); -}; - -export default memo(MlsdImageProcessor); - -const marks0to4096 = [0, 4096]; -const marks0to1 = [0, 1]; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/NormalBaeProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/NormalBaeProcessor.tsx deleted file mode 100644 index 43f7115df51..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/NormalBaeProcessor.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredNormalbaeImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredNormalbaeImageProcessorInvocation; - isEnabled: boolean; -}; - -const NormalBaeProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor( - 'normalbae_image_processor' - ) as RequiredNormalbaeImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - ); -}; - -export default memo(NormalBaeProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/PidiProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/PidiProcessor.tsx deleted file mode 100644 index 763069f769a..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/PidiProcessor.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; -import { useProcessorNodeChanged } from 'features/controlAdapters/components/hooks/useProcessorNodeChanged'; -import { useGetDefaultForControlnetProcessor } from 'features/controlAdapters/hooks/useGetDefaultForControlnetProcessor'; -import type { RequiredPidiImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ProcessorWrapper from './common/ProcessorWrapper'; - -type Props = { - controlNetId: string; - processorNode: RequiredPidiImageProcessorInvocation; - isEnabled: boolean; -}; - -const PidiProcessor = (props: Props) => { - const { controlNetId, processorNode, isEnabled } = props; - const { image_resolution, detect_resolution, scribble, safe } = processorNode; - const processorChanged = useProcessorNodeChanged(); - const { t } = useTranslation(); - - const defaults = useGetDefaultForControlnetProcessor('pidi_image_processor') as RequiredPidiImageProcessorInvocation; - - const handleDetectResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { detect_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleImageResolutionChanged = useCallback( - (v: number) => { - processorChanged(controlNetId, { image_resolution: v }); - }, - [controlNetId, processorChanged] - ); - - const handleScribbleChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { scribble: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - const handleSafeChanged = useCallback( - (e: ChangeEvent) => { - processorChanged(controlNetId, { safe: e.target.checked }); - }, - [controlNetId, processorChanged] - ); - - return ( - - - {t('controlnet.detectResolution')} - - - - - {t('controlnet.imageResolution')} - - - - - {t('controlnet.scribble')} - - - - {t('controlnet.safe')} - - - - ); -}; - -export default memo(PidiProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ZoeDepthProcessor.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/ZoeDepthProcessor.tsx deleted file mode 100644 index 61897a5d27f..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/ZoeDepthProcessor.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { RequiredZoeDepthImageProcessorInvocation } from 'features/controlAdapters/store/types'; -import { memo } from 'react'; - -type Props = { - controlNetId: string; - processorNode: RequiredZoeDepthImageProcessorInvocation; - isEnabled: boolean; -}; - -const ZoeDepthProcessor = (_props: Props) => { - // Has no parameters? - return null; -}; - -export default memo(ZoeDepthProcessor); diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/processors/common/ProcessorWrapper.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/processors/common/ProcessorWrapper.tsx deleted file mode 100644 index 0b99887b539..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/components/processors/common/ProcessorWrapper.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Flex } from '@invoke-ai/ui-library'; -import type { PropsWithChildren } from 'react'; -import { memo } from 'react'; - -type Props = PropsWithChildren; - -const ProcessorWrapper = (props: Props) => { - return ( - - {props.children} - - ); -}; - -export default memo(ProcessorWrapper); diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useAddControlAdapter.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useAddControlAdapter.ts deleted file mode 100644 index 1af2fc81b99..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useAddControlAdapter.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useControlAdapterModels } from 'features/controlAdapters/hooks/useControlAdapterModels'; -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import { controlAdapterAdded } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { type ControlAdapterType, isControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import { useCallback, useMemo } from 'react'; -import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; - -export const useAddControlAdapter = (type: ControlAdapterType) => { - const baseModel = useAppSelector((s) => s.generation.model?.base); - const dispatch = useAppDispatch(); - - const [models] = useControlAdapterModels(type); - - const firstModel: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig | undefined = useMemo(() => { - // prefer to use a model that matches the base model - const firstCompatibleModel = models.filter((m) => (baseModel ? m.base === baseModel : true))[0]; - - if (firstCompatibleModel) { - return firstCompatibleModel; - } - - return models[0]; - }, [baseModel, models]); - - const isDisabled = useMemo(() => !firstModel, [firstModel]); - - const addControlAdapter = useCallback(() => { - if (isDisabled) { - return; - } - - if ( - (type === 'controlnet' || type === 't2i_adapter') && - (firstModel?.type === 'controlnet' || firstModel?.type === 't2i_adapter') - ) { - const defaultPreprocessor = firstModel.default_settings?.preprocessor; - const processorType = isControlAdapterProcessorType(defaultPreprocessor) ? defaultPreprocessor : 'none'; - const processorNode = CONTROLNET_PROCESSORS[processorType].buildDefaults(baseModel); - dispatch( - controlAdapterAdded({ - type, - overrides: { - model: firstModel, - processorType, - processorNode, - }, - }) - ); - return; - } - dispatch( - controlAdapterAdded({ - type, - overrides: { model: firstModel }, - }) - ); - }, [dispatch, firstModel, isDisabled, type, baseModel]); - - return [addControlAdapter, isDisabled] as const; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterBeginEndStepPct.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterBeginEndStepPct.ts deleted file mode 100644 index c9c99fcb2e4..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterBeginEndStepPct.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterBeginEndStepPct = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const cn = selectControlAdapterById(controlAdapters, id); - return cn - ? { - beginStepPct: cn.beginStepPct, - endStepPct: cn.endStepPct, - } - : undefined; - }), - [id] - ); - - const stepPcts = useAppSelector(selector); - - return stepPcts; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterCLIPVisionModel.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterCLIPVisionModel.ts deleted file mode 100644 index 249d2022fe8..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterCLIPVisionModel.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterCLIPVisionModel = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const cn = selectControlAdapterById(controlAdapters, id); - if (cn && cn?.type === 'ip_adapter') { - return cn.clipVisionModel; - } - }), - [id] - ); - - const clipVisionModel = useAppSelector(selector); - - return clipVisionModel; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlImage.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlImage.ts deleted file mode 100644 index c8efdf91253..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlImage.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterControlImage = (id: string) => { - const selector = useMemo( - () => - createSelector( - selectControlAdaptersSlice, - (controlAdapters) => selectControlAdapterById(controlAdapters, id)?.controlImage - ), - [id] - ); - - const controlImageName = useAppSelector(selector); - - return controlImageName; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlMode.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlMode.ts deleted file mode 100644 index 50ddd80156e..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterControlMode.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNet } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterControlMode = (id: string) => { - const selector = useMemo( - () => - createSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - if (ca && isControlNet(ca)) { - return ca.controlMode; - } - return undefined; - }), - [id] - ); - - const controlMode = useAppSelector(selector); - - return controlMode; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIPMethod.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIPMethod.ts deleted file mode 100644 index a179899396d..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIPMethod.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterIPMethod = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const cn = selectControlAdapterById(controlAdapters, id); - if (cn && cn?.type === 'ip_adapter') { - return cn.method; - } - }), - [id] - ); - - const method = useAppSelector(selector); - - return method; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIsEnabled.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIsEnabled.ts deleted file mode 100644 index 58bb956ce39..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterIsEnabled.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterIsEnabled = (id: string) => { - const selector = useMemo( - () => - createSelector( - selectControlAdaptersSlice, - (controlAdapters) => selectControlAdapterById(controlAdapters, id)?.isEnabled ?? false - ), - [id] - ); - - const isEnabled = useAppSelector(selector); - - return isEnabled; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModel.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModel.ts deleted file mode 100644 index 4de2aeac7fb..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModel.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/query'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; -import { useGetModelConfigWithTypeGuard } from 'services/api/hooks/useGetModelConfigWithTypeGuard'; -import { isControlAdapterModelConfig } from 'services/api/types'; - -export const useControlAdapterModel = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector( - selectControlAdaptersSlice, - (controlAdapters) => selectControlAdapterById(controlAdapters, id)?.model?.key - ), - [id] - ); - - const key = useAppSelector(selector); - - const result = useGetModelConfigWithTypeGuard(key ?? skipToken, isControlAdapterModelConfig); - - return result; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModels.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModels.ts deleted file mode 100644 index 4fe5ae78112..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModels.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ControlAdapterType } from 'features/controlAdapters/store/types'; -import { useControlNetModels, useIPAdapterModels, useT2IAdapterModels } from 'services/api/hooks/modelsByType'; - -export const useControlAdapterModels = (type: ControlAdapterType) => { - const controlNetModels = useControlNetModels(); - const t2iAdapterModels = useT2IAdapterModels(); - const ipAdapterModels = useIPAdapterModels(); - - if (type === 'controlnet') { - return controlNetModels; - } - if (type === 't2i_adapter') { - return t2iAdapterModels; - } - if (type === 'ip_adapter') { - return ipAdapterModels; - } - - // Assert that the end of the function is not reachable. - const exhaustiveCheck: never = type; - return exhaustiveCheck; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessedControlImage.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessedControlImage.ts deleted file mode 100644 index a02e1f9ed94..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessedControlImage.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterProcessedControlImage = (id: string) => { - const selector = useMemo( - () => - createSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - - return ca && isControlNetOrT2IAdapter(ca) ? ca.processedControlImage : undefined; - }), - [id] - ); - - const weight = useAppSelector(selector); - - return weight; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorNode.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorNode.ts deleted file mode 100644 index 272af132a68..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorNode.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterProcessorNode = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - - return ca && isControlNetOrT2IAdapter(ca) ? ca.processorNode : undefined; - }), - [id] - ); - - const processorNode = useAppSelector(selector); - - return processorNode; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorType.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorType.ts deleted file mode 100644 index 777bfc05b47..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterProcessorType.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterProcessorType = (id: string) => { - const selector = useMemo( - () => - createSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - - return ca && isControlNetOrT2IAdapter(ca) ? ca.processorType : undefined; - }), - [id] - ); - - const processorType = useAppSelector(selector); - - return processorType; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterResizeMode.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterResizeMode.ts deleted file mode 100644 index c6140bced74..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterResizeMode.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterResizeMode = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - if (ca && isControlNetOrT2IAdapter(ca)) { - return ca.resizeMode; - } - return undefined; - }), - [id] - ); - - const controlMode = useAppSelector(selector); - - return controlMode; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterShouldAutoConfig.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterShouldAutoConfig.ts deleted file mode 100644 index 07fbd2982ca..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterShouldAutoConfig.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useControlAdapterShouldAutoConfig = (id: string) => { - const selector = useMemo( - () => - createSelector(selectControlAdaptersSlice, (controlAdapters) => { - const ca = selectControlAdapterById(controlAdapters, id); - if (ca && isControlNetOrT2IAdapter(ca)) { - return ca.shouldAutoConfig; - } - return undefined; - }), - [id] - ); - - const controlMode = useAppSelector(selector); - - return controlMode; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterType.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterType.ts deleted file mode 100644 index fe818f32875..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterType.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; -import { assert } from 'tsafe'; - -export const useControlAdapterType = (id: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { - const type = selectControlAdapterById(controlAdapters, id)?.type; - assert(type !== undefined, `Control adapter with id ${id} not found`); - return type; - }), - [id] - ); - - const type = useAppSelector(selector); - - return type; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterWeight.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterWeight.ts deleted file mode 100644 index 9e65993fde2..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterWeight.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - selectControlAdapterById, - selectControlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; -import { useMemo } from 'react'; - -export const useControlAdapterWeight = (id: string) => { - const selector = useMemo( - () => - createSelector( - selectControlAdaptersSlice, - (controlAdapters) => selectControlAdapterById(controlAdapters, id)?.weight - ), - [id] - ); - - const weight = useAppSelector(selector); - - return weight; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useGetDefaultForControlnetProcessor.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useGetDefaultForControlnetProcessor.ts deleted file mode 100644 index 99d2e0da8c3..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useGetDefaultForControlnetProcessor.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useAppSelector } from 'app/store/storeHooks'; -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import type { ControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import { useMemo } from 'react'; - -export const useGetDefaultForControlnetProcessor = (processorType: ControlAdapterProcessorType) => { - const baseModel = useAppSelector((s) => s.generation.model?.base); - - const defaults = useMemo(() => { - return CONTROLNET_PROCESSORS[processorType].buildDefaults(baseModel); - }, [baseModel, processorType]); - - return defaults; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/actions.ts b/invokeai/frontend/web/src/features/controlAdapters/store/actions.ts deleted file mode 100644 index 99ea84ed139..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/store/actions.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -export const controlAdapterImageProcessed = createAction<{ - id: string; -}>('controlAdapters/imageProcessed'); diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/constants.ts b/invokeai/frontend/web/src/features/controlAdapters/store/constants.ts deleted file mode 100644 index 152e977e5c6..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/store/constants.ts +++ /dev/null @@ -1,261 +0,0 @@ -import i18n from 'i18next'; -import type { BaseModelType } from 'services/api/types'; - -import type { ControlAdapterProcessorType, RequiredControlAdapterProcessorNode } from './types'; - -type ControlNetProcessorsDict = Record< - ControlAdapterProcessorType, - { - type: ControlAdapterProcessorType | 'none'; - label: string; - description: string; - buildDefaults(baseModel?: BaseModelType): RequiredControlAdapterProcessorNode | { type: 'none' }; - } ->; -/** - * A dict of ControlNet processors, including: - * - type - * - label - * - description - * - default values - * - * TODO: Generate from the OpenAPI schema - */ -export const CONTROLNET_PROCESSORS: ControlNetProcessorsDict = { - none: { - type: 'none', - get label() { - return i18n.t('controlnet.none'); - }, - get description() { - return i18n.t('controlnet.noneDescription'); - }, - buildDefaults: () => ({ - type: 'none', - }), - }, - canny_image_processor: { - type: 'canny_image_processor', - get label() { - return i18n.t('controlnet.canny'); - }, - get description() { - return i18n.t('controlnet.cannyDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'canny_image_processor', - type: 'canny_image_processor', - low_threshold: 100, - high_threshold: 200, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - color_map_image_processor: { - type: 'color_map_image_processor', - get label() { - return i18n.t('controlnet.colorMap'); - }, - get description() { - return i18n.t('controlnet.colorMapDescription'); - }, - buildDefaults: () => ({ - id: 'color_map_image_processor', - type: 'color_map_image_processor', - color_map_tile_size: 64, - }), - }, - content_shuffle_image_processor: { - type: 'content_shuffle_image_processor', - get label() { - return i18n.t('controlnet.contentShuffle'); - }, - get description() { - return i18n.t('controlnet.contentShuffleDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'content_shuffle_image_processor', - type: 'content_shuffle_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - h: baseModel === 'sdxl' ? 1024 : 512, - w: baseModel === 'sdxl' ? 1024 : 512, - f: baseModel === 'sdxl' ? 512 : 256, - }), - }, - depth_anything_image_processor: { - type: 'depth_anything_image_processor', - get label() { - return i18n.t('controlnet.depthAnything'); - }, - get description() { - return i18n.t('controlnet.depthAnythingDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'depth_anything_image_processor', - type: 'depth_anything_image_processor', - model_size: 'small', - resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - hed_image_processor: { - type: 'hed_image_processor', - get label() { - return i18n.t('controlnet.hed'); - }, - get description() { - return i18n.t('controlnet.hedDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'hed_image_processor', - type: 'hed_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - scribble: false, - }), - }, - lineart_anime_image_processor: { - type: 'lineart_anime_image_processor', - get label() { - return i18n.t('controlnet.lineartAnime'); - }, - get description() { - return i18n.t('controlnet.lineartAnimeDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'lineart_anime_image_processor', - type: 'lineart_anime_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - lineart_image_processor: { - type: 'lineart_image_processor', - get label() { - return i18n.t('controlnet.lineart'); - }, - get description() { - return i18n.t('controlnet.lineartDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'lineart_image_processor', - type: 'lineart_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - coarse: false, - }), - }, - mediapipe_face_processor: { - type: 'mediapipe_face_processor', - get label() { - return i18n.t('controlnet.mediapipeFace'); - }, - get description() { - return i18n.t('controlnet.mediapipeFaceDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'mediapipe_face_processor', - type: 'mediapipe_face_processor', - max_faces: 1, - min_confidence: 0.5, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - midas_depth_image_processor: { - type: 'midas_depth_image_processor', - get label() { - return i18n.t('controlnet.depthMidas'); - }, - get description() { - return i18n.t('controlnet.depthMidasDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'midas_depth_image_processor', - type: 'midas_depth_image_processor', - a_mult: 2, - bg_th: 0.1, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - mlsd_image_processor: { - type: 'mlsd_image_processor', - get label() { - return i18n.t('controlnet.mlsd'); - }, - get description() { - return i18n.t('controlnet.mlsdDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'mlsd_image_processor', - type: 'mlsd_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - thr_d: 0.1, - thr_v: 0.1, - }), - }, - normalbae_image_processor: { - type: 'normalbae_image_processor', - get label() { - return i18n.t('controlnet.normalBae'); - }, - get description() { - return i18n.t('controlnet.normalBaeDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'normalbae_image_processor', - type: 'normalbae_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - }), - }, - dw_openpose_image_processor: { - type: 'dw_openpose_image_processor', - get label() { - return i18n.t('controlnet.dwOpenpose'); - }, - get description() { - return i18n.t('controlnet.dwOpenposeDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'dw_openpose_image_processor', - type: 'dw_openpose_image_processor', - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - draw_body: true, - draw_face: false, - draw_hands: false, - }), - }, - pidi_image_processor: { - type: 'pidi_image_processor', - get label() { - return i18n.t('controlnet.pidi'); - }, - get description() { - return i18n.t('controlnet.pidiDescription'); - }, - buildDefaults: (baseModel?: BaseModelType) => ({ - id: 'pidi_image_processor', - type: 'pidi_image_processor', - detect_resolution: baseModel === 'sdxl' ? 1024 : 512, - image_resolution: baseModel === 'sdxl' ? 1024 : 512, - scribble: false, - safe: false, - }), - }, - zoe_depth_image_processor: { - type: 'zoe_depth_image_processor', - get label() { - return i18n.t('controlnet.depthZoe'); - }, - get description() { - return i18n.t('controlnet.depthZoeDescription'); - }, - buildDefaults: () => ({ - id: 'zoe_depth_image_processor', - type: 'zoe_depth_image_processor', - }), - }, -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts deleted file mode 100644 index 8ec397f99c9..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts +++ /dev/null @@ -1,433 +0,0 @@ -import type { PayloadAction, Update } from '@reduxjs/toolkit'; -import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; -import { getSelectorsOptions } from 'app/store/createMemoizedSelector'; -import type { PersistConfig, RootState } from 'app/store/store'; -import { deepClone } from 'common/util/deepClone'; -import { buildControlAdapter } from 'features/controlAdapters/util/buildControlAdapter'; -import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; -import { zModelIdentifierField } from 'features/nodes/types/common'; -import { merge, uniq } from 'lodash-es'; -import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -import { socketInvocationError } from 'services/events/actions'; -import { v4 as uuidv4 } from 'uuid'; - -import { controlAdapterImageProcessed } from './actions'; -import { CONTROLNET_PROCESSORS } from './constants'; -import type { - CLIPVisionModel, - ControlAdapterConfig, - ControlAdapterProcessorType, - ControlAdaptersState, - ControlAdapterType, - ControlMode, - ControlNetConfig, - IPMethod, - RequiredControlAdapterProcessorNode, - ResizeMode, - T2IAdapterConfig, -} from './types'; -import { isControlNet, isControlNetOrT2IAdapter, isIPAdapter, isT2IAdapter } from './types'; - -const caAdapter = createEntityAdapter({ - selectId: (ca) => ca.id, -}); -const caAdapterSelectors = caAdapter.getSelectors(undefined, getSelectorsOptions); - -export const { - selectById: selectControlAdapterById, - selectAll: selectControlAdapterAll, - selectIds: selectControlAdapterIds, -} = caAdapterSelectors; - -const initialControlAdaptersState: ControlAdaptersState = caAdapter.getInitialState<{ - _version: 2; - pendingControlImages: string[]; -}>({ - _version: 2, - pendingControlImages: [], -}); - -export const selectAllControlNets = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters).filter(isControlNet); - -export const selectValidControlNets = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters) - .filter(isControlNet) - .filter( - (ca) => - ca.isEnabled && - ca.model && - (Boolean(ca.processedControlImage) || (ca.processorType === 'none' && Boolean(ca.controlImage))) - ); - -export const selectAllIPAdapters = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters).filter(isIPAdapter); - -export const selectValidIPAdapters = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters) - .filter(isIPAdapter) - .filter((ca) => ca.isEnabled && ca.model && Boolean(ca.controlImage)); - -export const selectAllT2IAdapters = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters).filter(isT2IAdapter); - -export const selectValidT2IAdapters = (controlAdapters: ControlAdaptersState) => - selectControlAdapterAll(controlAdapters) - .filter(isT2IAdapter) - .filter( - (ca) => - ca.isEnabled && - ca.model && - (Boolean(ca.processedControlImage) || (ca.processorType === 'none' && Boolean(ca.controlImage))) - ); - -export const controlAdaptersSlice = createSlice({ - name: 'controlAdapters', - initialState: initialControlAdaptersState, - reducers: { - controlAdapterAdded: { - reducer: ( - state, - action: PayloadAction<{ - id: string; - type: ControlAdapterType; - overrides?: Partial; - }> - ) => { - const { id, type, overrides } = action.payload; - caAdapter.addOne(state, buildControlAdapter(id, type, overrides)); - }, - prepare: ({ type, overrides }: { type: ControlAdapterType; overrides?: Partial }) => { - return { payload: { id: uuidv4(), type, overrides } }; - }, - }, - controlAdapterRecalled: (state, action: PayloadAction) => { - caAdapter.addOne(state, action.payload); - }, - controlAdapterDuplicated: { - reducer: ( - state, - action: PayloadAction<{ - id: string; - newId: string; - }> - ) => { - const { id, newId } = action.payload; - const controlAdapter = selectControlAdapterById(state, id); - if (!controlAdapter) { - return; - } - const newControlAdapter = merge(deepClone(controlAdapter), { - id: newId, - isEnabled: true, - }); - caAdapter.addOne(state, newControlAdapter); - }, - prepare: (id: string) => { - return { payload: { id, newId: uuidv4() } }; - }, - }, - controlAdapterRemoved: (state, action: PayloadAction<{ id: string }>) => { - caAdapter.removeOne(state, action.payload.id); - }, - controlAdapterIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { - const { id, isEnabled } = action.payload; - caAdapter.updateOne(state, { id, changes: { isEnabled } }); - }, - controlAdapterImageChanged: ( - state, - action: PayloadAction<{ - id: string; - controlImage: string | null; - }> - ) => { - const { id, controlImage } = action.payload; - const ca = selectControlAdapterById(state, id); - if (!ca) { - return; - } - - caAdapter.updateOne(state, { - id, - changes: { controlImage, processedControlImage: null }, - }); - - if (controlImage !== null && isControlNetOrT2IAdapter(ca) && ca.processorType !== 'none') { - state.pendingControlImages.push(id); - } - }, - controlAdapterProcessedImageChanged: ( - state, - action: PayloadAction<{ - id: string; - processedControlImage: string | null; - }> - ) => { - const { id, processedControlImage } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn) { - return; - } - - if (!isControlNetOrT2IAdapter(cn)) { - return; - } - - caAdapter.updateOne(state, { - id, - changes: { - processedControlImage, - }, - }); - - state.pendingControlImages = state.pendingControlImages.filter((pendingId) => pendingId !== id); - }, - controlAdapterModelCleared: (state, action: PayloadAction<{ id: string }>) => { - caAdapter.updateOne(state, { - id: action.payload.id, - changes: { model: null }, - }); - }, - controlAdapterModelChanged: ( - state, - action: PayloadAction<{ - id: string; - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig; - }> - ) => { - const { id, modelConfig } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn) { - return; - } - - const model = zModelIdentifierField.parse(modelConfig); - - if (!isControlNetOrT2IAdapter(cn)) { - caAdapter.updateOne(state, { id, changes: { model } }); - return; - } - - const update: Update = { - id, - changes: { model, shouldAutoConfig: true }, - }; - - update.changes.processedControlImage = null; - - if (modelConfig.type === 'ip_adapter') { - // should never happen... - return; - } - - const processor = buildControlAdapterProcessor(modelConfig); - update.changes.processorType = processor.processorType; - update.changes.processorNode = processor.processorNode; - - caAdapter.updateOne(state, update); - }, - controlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { - const { id, weight } = action.payload; - caAdapter.updateOne(state, { id, changes: { weight } }); - }, - controlAdapterBeginStepPctChanged: (state, action: PayloadAction<{ id: string; beginStepPct: number }>) => { - const { id, beginStepPct } = action.payload; - caAdapter.updateOne(state, { id, changes: { beginStepPct } }); - }, - controlAdapterEndStepPctChanged: (state, action: PayloadAction<{ id: string; endStepPct: number }>) => { - const { id, endStepPct } = action.payload; - caAdapter.updateOne(state, { id, changes: { endStepPct } }); - }, - controlAdapterControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlMode }>) => { - const { id, controlMode } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn || !isControlNet(cn)) { - return; - } - caAdapter.updateOne(state, { id, changes: { controlMode } }); - }, - controlAdapterIPMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethod }>) => { - const { id, method } = action.payload; - caAdapter.updateOne(state, { id, changes: { method } }); - }, - controlAdapterCLIPVisionModelChanged: ( - state, - action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModel }> - ) => { - const { id, clipVisionModel } = action.payload; - caAdapter.updateOne(state, { id, changes: { clipVisionModel } }); - }, - controlAdapterResizeModeChanged: ( - state, - action: PayloadAction<{ - id: string; - resizeMode: ResizeMode; - }> - ) => { - const { id, resizeMode } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn || !isControlNetOrT2IAdapter(cn)) { - return; - } - caAdapter.updateOne(state, { id, changes: { resizeMode } }); - }, - controlAdapterProcessorParamsChanged: ( - state, - action: PayloadAction<{ - id: string; - params: Partial; - }> - ) => { - const { id, params } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn || !isControlNetOrT2IAdapter(cn) || !cn.processorNode) { - return; - } - - const processorNode = merge(deepClone(cn.processorNode), params); - - caAdapter.updateOne(state, { - id, - changes: { - shouldAutoConfig: false, - processorNode, - }, - }); - }, - controlAdapterProcessortTypeChanged: ( - state, - action: PayloadAction<{ - id: string; - processorType: ControlAdapterProcessorType; - }> - ) => { - const { id, processorType } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn || !isControlNetOrT2IAdapter(cn)) { - return; - } - - const processorNode = deepClone( - CONTROLNET_PROCESSORS[processorType].buildDefaults(cn.model?.base) - ) as RequiredControlAdapterProcessorNode; - - caAdapter.updateOne(state, { - id, - changes: { - processorType, - processedControlImage: null, - processorNode, - shouldAutoConfig: false, - }, - }); - }, - controlAdapterAutoConfigToggled: ( - state, - action: PayloadAction<{ - id: string; - modelConfig?: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig; - }> - ) => { - const { id, modelConfig } = action.payload; - const cn = selectControlAdapterById(state, id); - if (!cn || !isControlNetOrT2IAdapter(cn) || modelConfig?.type === 'ip_adapter') { - return; - } - const update: Update = { - id, - changes: { shouldAutoConfig: !cn.shouldAutoConfig }, - }; - - if (update.changes.shouldAutoConfig && modelConfig) { - const processor = buildControlAdapterProcessor(modelConfig); - update.changes.processorType = processor.processorType; - update.changes.processorNode = processor.processorNode; - } - - caAdapter.updateOne(state, update); - }, - controlAdaptersReset: () => { - return deepClone(initialControlAdaptersState); - }, - pendingControlImagesCleared: (state) => { - state.pendingControlImages = []; - }, - ipAdaptersReset: (state) => { - selectAllIPAdapters(state).forEach((ca) => { - caAdapter.removeOne(state, ca.id); - }); - }, - controlNetsReset: (state) => { - selectAllControlNets(state).forEach((ca) => { - caAdapter.removeOne(state, ca.id); - }); - }, - t2iAdaptersReset: (state) => { - selectAllT2IAdapters(state).forEach((ca) => { - caAdapter.removeOne(state, ca.id); - }); - }, - }, - extraReducers: (builder) => { - builder.addCase(controlAdapterImageProcessed, (state, action) => { - const cn = selectControlAdapterById(state, action.payload.id); - if (!cn) { - return; - } - if (cn.controlImage !== null) { - state.pendingControlImages = uniq(state.pendingControlImages.concat(action.payload.id)); - } - }); - - builder.addCase(socketInvocationError, (state) => { - state.pendingControlImages = []; - }); - }, -}); - -export const { - controlAdapterAdded, - controlAdapterRecalled, - controlAdapterDuplicated, - controlAdapterRemoved, - controlAdapterImageChanged, - controlAdapterProcessedImageChanged, - controlAdapterIsEnabledChanged, - controlAdapterModelChanged, - controlAdapterCLIPVisionModelChanged, - controlAdapterIPMethodChanged, - controlAdapterWeightChanged, - controlAdapterBeginStepPctChanged, - controlAdapterEndStepPctChanged, - controlAdapterControlModeChanged, - controlAdapterResizeModeChanged, - controlAdapterProcessorParamsChanged, - controlAdapterProcessortTypeChanged, - controlAdaptersReset, - controlAdapterAutoConfigToggled, - pendingControlImagesCleared, - controlAdapterModelCleared, - ipAdaptersReset, - controlNetsReset, - t2iAdaptersReset, -} = controlAdaptersSlice.actions; - -export const selectControlAdaptersSlice = (state: RootState) => state.controlAdapters; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateControlAdaptersState = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - } - if (state._version === 1) { - state = deepClone(initialControlAdaptersState); - } - return state; -}; - -export const controlAdaptersPersistConfig: PersistConfig = { - name: controlAdaptersSlice.name, - initialState: initialControlAdaptersState, - migrate: migrateControlAdaptersState, - persistDenylist: ['pendingControlImages'], -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/types.test.ts b/invokeai/frontend/web/src/features/controlAdapters/store/types.test.ts deleted file mode 100644 index 3bde8bc6c6f..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/store/types.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ControlAdapterProcessorType, zControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import type { Equals } from 'tsafe'; -import { assert } from 'tsafe'; -import { describe, test } from 'vitest'; -import type { z } from 'zod'; - -describe('Control Adapter Types', () => { - test('ControlAdapterProcessorType', () => - assert>>()); -}); diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts b/invokeai/frontend/web/src/features/controlAdapters/store/types.ts deleted file mode 100644 index b76a729263e..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts +++ /dev/null @@ -1,274 +0,0 @@ -import type { EntityState } from '@reduxjs/toolkit'; -import type { - ParameterControlNetModel, - ParameterIPAdapterModel, - ParameterT2IAdapterModel, -} from 'features/parameters/types/parameterSchemas'; -import type { components } from 'services/api/schema'; -import type { Invocation } from 'services/api/types'; -import type { O } from 'ts-toolbelt'; -import { z } from 'zod'; - -/** - * Any ControlNet processor node - */ -export type ControlAdapterProcessorNode = - | Invocation<'canny_image_processor'> - | Invocation<'color_map_image_processor'> - | Invocation<'content_shuffle_image_processor'> - | Invocation<'depth_anything_image_processor'> - | Invocation<'hed_image_processor'> - | Invocation<'lineart_anime_image_processor'> - | Invocation<'lineart_image_processor'> - | Invocation<'mediapipe_face_processor'> - | Invocation<'midas_depth_image_processor'> - | Invocation<'mlsd_image_processor'> - | Invocation<'normalbae_image_processor'> - | Invocation<'dw_openpose_image_processor'> - | Invocation<'pidi_image_processor'> - | Invocation<'zoe_depth_image_processor'>; - -/** - * Any ControlNet processor type - */ -export type ControlAdapterProcessorType = NonNullable; -export const zControlAdapterProcessorType = z.enum([ - 'canny_image_processor', - 'color_map_image_processor', - 'content_shuffle_image_processor', - 'depth_anything_image_processor', - 'hed_image_processor', - 'lineart_anime_image_processor', - 'lineart_image_processor', - 'mediapipe_face_processor', - 'midas_depth_image_processor', - 'mlsd_image_processor', - 'normalbae_image_processor', - 'dw_openpose_image_processor', - 'pidi_image_processor', - 'zoe_depth_image_processor', - 'none', -]); -export const isControlAdapterProcessorType = (v: unknown): v is ControlAdapterProcessorType => - zControlAdapterProcessorType.safeParse(v).success; - -/** - * The Canny processor node, with parameters flagged as required - */ -export type RequiredCannyImageProcessorInvocation = O.Required< - Invocation<'canny_image_processor'>, - 'type' | 'low_threshold' | 'high_threshold' | 'image_resolution' | 'detect_resolution' ->; - -/** - * The Color Map processor node, with parameters flagged as required - */ -export type RequiredColorMapImageProcessorInvocation = O.Required< - Invocation<'color_map_image_processor'>, - 'type' | 'color_map_tile_size' ->; - -/** - * The ContentShuffle processor node, with parameters flagged as required - */ -export type RequiredContentShuffleImageProcessorInvocation = O.Required< - Invocation<'content_shuffle_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' | 'w' | 'h' | 'f' ->; - -/** - * The DepthAnything processor node, with parameters flagged as required - */ -export type RequiredDepthAnythingImageProcessorInvocation = O.Required< - Invocation<'depth_anything_image_processor'>, - 'type' | 'model_size' | 'resolution' | 'offload' ->; - -const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']); -export type DepthAnythingModelSize = z.infer; -export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize => - zDepthAnythingModelSize.safeParse(v).success; - -/** - * The HED processor node, with parameters flagged as required - */ -export type RequiredHedImageProcessorInvocation = O.Required< - Invocation<'hed_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' | 'scribble' ->; - -/** - * The Lineart Anime processor node, with parameters flagged as required - */ -export type RequiredLineartAnimeImageProcessorInvocation = O.Required< - Invocation<'lineart_anime_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' ->; - -/** - * The Lineart processor node, with parameters flagged as required - */ -export type RequiredLineartImageProcessorInvocation = O.Required< - Invocation<'lineart_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' | 'coarse' ->; - -/** - * The MediapipeFace processor node, with parameters flagged as required - */ -export type RequiredMediapipeFaceProcessorInvocation = O.Required< - Invocation<'mediapipe_face_processor'>, - 'type' | 'max_faces' | 'min_confidence' | 'image_resolution' | 'detect_resolution' ->; - -/** - * The MidasDepth processor node, with parameters flagged as required - */ -export type RequiredMidasDepthImageProcessorInvocation = O.Required< - Invocation<'midas_depth_image_processor'>, - 'type' | 'a_mult' | 'bg_th' | 'image_resolution' | 'detect_resolution' ->; - -/** - * The MLSD processor node, with parameters flagged as required - */ -export type RequiredMlsdImageProcessorInvocation = O.Required< - Invocation<'mlsd_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' | 'thr_v' | 'thr_d' ->; - -/** - * The NormalBae processor node, with parameters flagged as required - */ -export type RequiredNormalbaeImageProcessorInvocation = O.Required< - Invocation<'normalbae_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' ->; - -/** - * The DW Openpose processor node, with parameters flagged as required - */ -export type RequiredDWOpenposeImageProcessorInvocation = O.Required< - Invocation<'dw_openpose_image_processor'>, - 'type' | 'image_resolution' | 'draw_body' | 'draw_face' | 'draw_hands' ->; - -/** - * The Pidi processor node, with parameters flagged as required - */ -export type RequiredPidiImageProcessorInvocation = O.Required< - Invocation<'pidi_image_processor'>, - 'type' | 'detect_resolution' | 'image_resolution' | 'safe' | 'scribble' ->; - -/** - * The ZoeDepth processor node, with parameters flagged as required - */ -export type RequiredZoeDepthImageProcessorInvocation = O.Required, 'type'>; - -/** - * Any ControlNet Processor node, with its parameters flagged as required - */ -export type RequiredControlAdapterProcessorNode = - | O.Required< - | RequiredCannyImageProcessorInvocation - | RequiredColorMapImageProcessorInvocation - | RequiredContentShuffleImageProcessorInvocation - | RequiredDepthAnythingImageProcessorInvocation - | RequiredHedImageProcessorInvocation - | RequiredLineartAnimeImageProcessorInvocation - | RequiredLineartImageProcessorInvocation - | RequiredMediapipeFaceProcessorInvocation - | RequiredMidasDepthImageProcessorInvocation - | RequiredMlsdImageProcessorInvocation - | RequiredNormalbaeImageProcessorInvocation - | RequiredDWOpenposeImageProcessorInvocation - | RequiredPidiImageProcessorInvocation - | RequiredZoeDepthImageProcessorInvocation, - 'id' - > - | { type: 'none' }; - -export type ControlMode = NonNullable; - -const zResizeMode = z.enum(['just_resize', 'crop_resize', 'fill_resize', 'just_resize_simple']); -export type ResizeMode = z.infer; -export const isResizeMode = (v: unknown): v is ResizeMode => zResizeMode.safeParse(v).success; - -const zIPMethod = z.enum(['full', 'style', 'composition']); -export type IPMethod = z.infer; -export const isIPMethod = (v: unknown): v is IPMethod => zIPMethod.safeParse(v).success; - -export type ControlNetConfig = { - type: 'controlnet'; - id: string; - isEnabled: boolean; - model: ParameterControlNetModel | null; - weight: number; - beginStepPct: number; - endStepPct: number; - controlMode: ControlMode; - resizeMode: ResizeMode; - controlImage: string | null; - processedControlImage: string | null; - processorType: ControlAdapterProcessorType; - processorNode: RequiredControlAdapterProcessorNode; - shouldAutoConfig: boolean; -}; - -export type T2IAdapterConfig = { - type: 't2i_adapter'; - id: string; - isEnabled: boolean; - model: ParameterT2IAdapterModel | null; - weight: number; - beginStepPct: number; - endStepPct: number; - resizeMode: ResizeMode; - controlImage: string | null; - processedControlImage: string | null; - processorType: ControlAdapterProcessorType; - processorNode: RequiredControlAdapterProcessorNode; - shouldAutoConfig: boolean; -}; - -export type CLIPVisionModel = 'ViT-H' | 'ViT-G'; - -export type IPAdapterConfig = { - type: 'ip_adapter'; - id: string; - isEnabled: boolean; - controlImage: string | null; - model: ParameterIPAdapterModel | null; - clipVisionModel: CLIPVisionModel; - weight: number; - method: IPMethod; - beginStepPct: number; - endStepPct: number; -}; - -export type ControlAdapterConfig = ControlNetConfig | IPAdapterConfig | T2IAdapterConfig; - -export type ControlAdapterType = ControlAdapterConfig['type']; - -export type ControlAdaptersState = EntityState & { - pendingControlImages: string[]; -}; - -export const isControlNet = (controlAdapter: ControlAdapterConfig): controlAdapter is ControlNetConfig => { - return controlAdapter.type === 'controlnet'; -}; - -export const isIPAdapter = (controlAdapter: ControlAdapterConfig): controlAdapter is IPAdapterConfig => { - return controlAdapter.type === 'ip_adapter'; -}; - -export const isT2IAdapter = (controlAdapter: ControlAdapterConfig): controlAdapter is T2IAdapterConfig => { - return controlAdapter.type === 't2i_adapter'; -}; - -export const isControlNetOrT2IAdapter = ( - controlAdapter: ControlAdapterConfig -): controlAdapter is ControlNetConfig | T2IAdapterConfig => { - return isControlNet(controlAdapter) || isT2IAdapter(controlAdapter); -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts b/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts deleted file mode 100644 index ad7bdba3639..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { deepClone } from 'common/util/deepClone'; -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import type { - ControlAdapterConfig, - ControlAdapterType, - ControlNetConfig, - IPAdapterConfig, - RequiredCannyImageProcessorInvocation, - T2IAdapterConfig, -} from 'features/controlAdapters/store/types'; -import { merge } from 'lodash-es'; - -export const initialControlNet: Omit = { - type: 'controlnet', - isEnabled: true, - model: null, - weight: 1, - beginStepPct: 0, - endStepPct: 1, - controlMode: 'balanced', - resizeMode: 'just_resize', - controlImage: null, - processedControlImage: null, - processorType: 'canny_image_processor', - processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation, - shouldAutoConfig: true, -}; - -export const initialT2IAdapter: Omit = { - type: 't2i_adapter', - isEnabled: true, - model: null, - weight: 1, - beginStepPct: 0, - endStepPct: 1, - resizeMode: 'just_resize', - controlImage: null, - processedControlImage: null, - processorType: 'canny_image_processor', - processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation, - shouldAutoConfig: true, -}; - -export const initialIPAdapter: Omit = { - type: 'ip_adapter', - isEnabled: true, - controlImage: null, - model: null, - method: 'full', - clipVisionModel: 'ViT-H', - weight: 1, - beginStepPct: 0, - endStepPct: 1, -}; - -export const buildControlAdapter = ( - id: string, - type: ControlAdapterType, - overrides: Partial = {} -): ControlAdapterConfig => { - switch (type) { - case 'controlnet': - return merge(deepClone(initialControlNet), { id, ...overrides }); - case 't2i_adapter': - return merge(deepClone(initialT2IAdapter), { id, ...overrides }); - case 'ip_adapter': - return merge(deepClone(initialIPAdapter), { id, ...overrides }); - default: - throw new Error(`Unknown control adapter type: ${type}`); - } -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapterProcessor.ts b/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapterProcessor.ts deleted file mode 100644 index 63766b8e6ea..00000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapterProcessor.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; -import { isControlAdapterProcessorType } from 'features/controlAdapters/store/types'; -import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; - -export const buildControlAdapterProcessor = (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { - const defaultPreprocessor = modelConfig.default_settings?.preprocessor; - const processorType = isControlAdapterProcessorType(defaultPreprocessor) ? defaultPreprocessor : 'none'; - const processorNode = CONTROLNET_PROCESSORS[processorType].buildDefaults(modelConfig.base); - - return { processorType, processorNode }; -}; 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/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx deleted file mode 100644 index c7a49da8c7e..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiPlusBold } from 'react-icons/pi'; - -export const AddLayerButton = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const [addCALayer, isAddCALayerDisabled] = useAddCALayer(); - const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); - const [addIILayer, isAddIILayerDisabled] = useAddIILayer(); - const addRGLayer = useCallback(() => { - dispatch(rgLayerAdded()); - }, [dispatch]); - - return ( - - } - variant="ghost" - data-testid="control-layers-add-layer-menu-button" - > - {t('controlLayers.addLayer')} - - - } onClick={addRGLayer}> - {t('controlLayers.regionalGuidanceLayer')} - - } onClick={addCALayer} isDisabled={isAddCALayerDisabled}> - {t('controlLayers.globalControlAdapterLayer')} - - } onClick={addIPALayer} isDisabled={isAddIPALayerDisabled}> - {t('controlLayers.globalIPAdapterLayer')} - - } onClick={addIILayer} isDisabled={isAddIILayerDisabled}> - {t('controlLayers.globalInitialImageLayer')} - - - - ); -}); - -AddLayerButton.displayName = 'AddLayerButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx deleted file mode 100644 index 26d9c8ce69f..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Button, Flex } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { - isRegionalGuidanceLayer, - rgLayerNegativePromptChanged, - rgLayerPositivePromptChanged, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; -import { useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiPlusBold } from 'react-icons/pi'; -import { assert } from 'tsafe'; -type AddPromptButtonProps = { - layerId: string; -}; - -export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); - const selectValidActions = useMemo( - () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = controlLayers.present.layers.find((l) => l.id === layerId); - assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); - return { - canAddPositivePrompt: layer.positivePrompt === null, - canAddNegativePrompt: layer.negativePrompt === null, - }; - }), - [layerId] - ); - const validActions = useAppSelector(selectValidActions); - const addPositivePrompt = useCallback(() => { - dispatch(rgLayerPositivePromptChanged({ layerId, prompt: '' })); - }, [dispatch, layerId]); - const addNegativePrompt = useCallback(() => { - dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); - }, [dispatch, layerId]); - - return ( - - - - - - ); -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx b/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx deleted file mode 100644 index a34250c29f5..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/BrushSize.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { - CompositeNumberInput, - CompositeSlider, - FormControl, - FormLabel, - Popover, - PopoverArrow, - PopoverBody, - PopoverContent, - PopoverTrigger, -} from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { brushSizeChanged, initialControlLayersState } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -const marks = [0, 100, 200, 300]; -const formatPx = (v: number | string) => `${v} px`; - -export const BrushSize = memo(() => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const brushSize = useAppSelector((s) => s.controlLayers.present.brushSize); - const onChange = useCallback( - (v: number) => { - dispatch(brushSizeChanged(Math.round(v))); - }, - [dispatch] - ); - return ( - - {t('controlLayers.brushSize')} - - - - - - - - - - - - - ); -}); - -BrushSize.displayName = 'BrushSize'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx deleted file mode 100644 index 9e71ad943c6..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { CALayerControlAdapterWrapper } from 'features/controlLayers/components/CALayer/CALayerControlAdapterWrapper'; -import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; -import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; -import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; -import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { layerSelected, selectCALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; -import { memo, useCallback } from 'react'; - -import CALayerOpacity from './CALayerOpacity'; - -type Props = { - layerId: string; -}; - -export const CALayer = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected); - const onClick = useCallback(() => { - dispatch(layerSelected(layerId)); - }, [dispatch, layerId]); - const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - - return ( - - - - - - - - - - {isOpen && ( - - - - )} - - ); -}); - -CALayer.displayName = 'CALayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx deleted file mode 100644 index a44ae32c137..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { ControlAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapter'; -import { - caLayerControlModeChanged, - caLayerImageChanged, - caLayerModelChanged, - caLayerProcessedImageChanged, - caLayerProcessorConfigChanged, - caOrIPALayerBeginEndStepPctChanged, - caOrIPALayerWeightChanged, - selectCALayerOrThrow, -} from 'features/controlLayers/store/controlLayersSlice'; -import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import type { CALayerImageDropData } from 'features/dnd/types'; -import { memo, useCallback, useMemo } from 'react'; -import type { - CALayerImagePostUploadAction, - ControlNetModelConfig, - ImageDTO, - T2IAdapterModelConfig, -} from 'services/api/types'; - -type Props = { - layerId: string; -}; - -export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { - const dispatch = useAppDispatch(); - const controlAdapter = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).controlAdapter); - - const onChangeBeginEndStepPct = useCallback( - (beginEndStepPct: [number, number]) => { - dispatch( - caOrIPALayerBeginEndStepPctChanged({ - layerId, - beginEndStepPct, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeControlMode = useCallback( - (controlMode: ControlModeV2) => { - dispatch( - caLayerControlModeChanged({ - layerId, - controlMode, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeWeight = useCallback( - (weight: number) => { - dispatch(caOrIPALayerWeightChanged({ layerId, weight })); - }, - [dispatch, layerId] - ); - - const onChangeProcessorConfig = useCallback( - (processorConfig: ProcessorConfig | null) => { - dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig })); - }, - [dispatch, layerId] - ); - - const onChangeModel = useCallback( - (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { - dispatch( - caLayerModelChanged({ - layerId, - modelConfig, - }) - ); - }, - [dispatch, layerId] - ); - - const onChangeImage = useCallback( - (imageDTO: ImageDTO | null) => { - dispatch(caLayerImageChanged({ layerId, imageDTO })); - }, - [dispatch, layerId] - ); - - const onErrorLoadingImage = useCallback(() => { - dispatch(caLayerImageChanged({ layerId, imageDTO: null })); - }, [dispatch, layerId]); - - const onErrorLoadingProcessedImage = useCallback(() => { - dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null })); - }, [dispatch, layerId]); - - const droppableData = useMemo( - () => ({ - actionType: 'SET_CA_LAYER_IMAGE', - context: { - layerId, - }, - id: layerId, - }), - [layerId] - ); - - const postUploadAction = useMemo( - () => ({ - layerId, - type: 'SET_CA_LAYER_IMAGE', - }), - [layerId] - ); - - return ( - - ); -}); - -CALayerControlAdapterWrapper.displayName = 'CALayerControlAdapterWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx deleted file mode 100644 index 353f8e03072..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerOpacity.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { - CompositeNumberInput, - CompositeSlider, - Flex, - FormControl, - FormLabel, - IconButton, - Popover, - PopoverArrow, - PopoverBody, - PopoverContent, - PopoverTrigger, - Switch, -} from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { stopPropagation } from 'common/util/stopPropagation'; -import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks'; -import { caLayerIsFilterEnabledChanged, caLayerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiDropHalfFill } from 'react-icons/pi'; - -type Props = { - layerId: string; -}; - -const marks = [0, 25, 50, 75, 100]; -const formatPct = (v: number | string) => `${v} %`; - -const CALayerOpacity = ({ layerId }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const { opacity, isFilterEnabled } = useLayerOpacity(layerId); - const onChangeOpacity = useCallback( - (v: number) => { - dispatch(caLayerOpacityChanged({ layerId, opacity: v / 100 })); - }, - [dispatch, layerId] - ); - const onChangeFilter = useCallback( - (e: ChangeEvent) => { - dispatch(caLayerIsFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked })); - }, - [dispatch, layerId] - ); - return ( - - - } - variant="ghost" - onDoubleClick={stopPropagation} - /> - - - - - - - - {t('controlLayers.opacityFilter')} - - - - - {t('controlLayers.opacity')} - - - - - - - - ); -}; - -export default memo(CALayerOpacity); 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..83f3c1b05f0 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd.ts @@ -0,0 +1,88 @@ +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} +