diff --git a/.github/workflows/backport_release.yaml b/.github/workflows/backport_release.yaml new file mode 100644 index 000000000..ede6bde33 --- /dev/null +++ b/.github/workflows/backport_release.yaml @@ -0,0 +1,519 @@ +name: Backport Release + +on: + workflow_dispatch: + inputs: + commit: + description: 'Full 40-char SHA of the tip commit of the backport source branch (the PR head commit that passed tests). The branch is resolved from this SHA and must be unique.' + required: true + type: string + +permissions: + contents: read + pull-requests: read + checks: read + +jobs: + backport-release: + name: Create backport release + runs-on: ubuntu-latest + environment: backport release + + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 + with: + app-id: ${{ secrets.FEN_RELEASE_APP_ID }} + private-key: ${{ secrets.FEN_RELEASE_PRIVATE_KEY }} + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + token: ${{ steps.app-token.outputs.token }} + fetch-depth: 0 + fetch-tags: true + + - name: Configure git + run: | + git config user.name "fen-release[bot]" + git config user.email "fen-release[bot]@users.noreply.github.com" + + - name: Resolve source branch from commit SHA + id: resolve + env: + SOURCE_COMMIT: ${{ inputs.commit }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + + # Require a full 40-char lowercase-hex SHA. Short SHAs are ambiguous + # and we will be comparing this value against API responses (PR head + # SHA, ref tips) that always return the full form. + if [[ ! "${SOURCE_COMMIT}" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::Input commit '${SOURCE_COMMIT}' is not a full 40-char lowercase hex SHA." + exit 1 + fi + + # Fetch all remote branches so we can search for which one(s) point + # at this SHA. `actions/checkout` with fetch-depth: 0 fetches full + # history of the checked-out ref but does not necessarily populate + # every refs/remotes/origin/*, so do it explicitly. + git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*' + + # Verify the commit actually exists in this repo's object DB. + if ! git cat-file -e "${SOURCE_COMMIT}^{commit}" 2>/dev/null; then + echo "::error::Commit ${SOURCE_COMMIT} was not found in the repository." + exit 1 + fi + + # Find every remote branch whose tip == SOURCE_COMMIT. Exactly one + # branch must point at it. If zero, the commit isn't anyone's tip + # (likely stale, force-pushed past, or never the PR head). If more + # than one, the (branch -> SHA) mapping is ambiguous and we refuse + # to guess — the operator must give us a unique branch to release. + mapfile -t matching_branches < <( + git for-each-ref \ + --format='%(refname:strip=3)' \ + --points-at="${SOURCE_COMMIT}" \ + refs/remotes/origin/ \ + | grep -vx 'HEAD' || true + ) + + if [[ "${#matching_branches[@]}" -eq 0 ]]; then + echo "::error::No branch on origin has ${SOURCE_COMMIT} as its tip." + echo "::error::Either the branch was updated after you copied this SHA, or this commit was never the head of a branch." + exit 1 + fi + + if [[ "${#matching_branches[@]}" -gt 1 ]]; then + echo "::error::More than one branch on origin has ${SOURCE_COMMIT} as its tip; cannot pick one:" + for b in "${matching_branches[@]}"; do + echo "::error:: - ${b}" + done + echo "::error::Refusing to proceed with an ambiguous source branch." + exit 1 + fi + + source_branch="${matching_branches[0]}" + + if [[ "${source_branch}" == "${DEFAULT_BRANCH}" ]]; then + echo "::error::Source branch must not be the default branch ('${DEFAULT_BRANCH}')." + exit 1 + fi + + echo "Resolved commit ${SOURCE_COMMIT} to branch '${source_branch}'." + echo "source_branch=${source_branch}" >> "$GITHUB_OUTPUT" + + - name: Determine latest stable release + id: latest + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -euo pipefail + + # List all tags matching vMAJOR.MINOR.PATCH and pick the highest by numeric + # comparison of each component. We DO NOT use `sort -V` because it treats + # v0.19.99 as higher than v0.20.1. + latest_tag="$( + git tag --list 'v[0-9]*.[0-9]*.[0-9]*' \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ + | awk -F'[v.]' '{ printf "%010d %010d %010d %s\n", $2, $3, $4, $0 }' \ + | sort -k1,1n -k2,2n -k3,3n \ + | tail -n1 \ + | awk '{print $4}' + )" + + if [[ -z "${latest_tag}" ]]; then + echo "::error::No stable release tags (vMAJOR.MINOR.PATCH) were found." + exit 1 + fi + + # Parse components + ver="${latest_tag#v}" + major="${ver%%.*}" + rest="${ver#*.}" + minor="${rest%%.*}" + patch="${rest#*.}" + + new_patch=$((patch + 1)) + new_version="v${major}.${minor}.${new_patch}" + release_branch="release/v${major}.${minor}" + + latest_sha="$(git rev-list -n 1 "refs/tags/${latest_tag}")" + + echo "latest_tag=${latest_tag}" >> "$GITHUB_OUTPUT" + echo "latest_sha=${latest_sha}" >> "$GITHUB_OUTPUT" + echo "major=${major}" >> "$GITHUB_OUTPUT" + echo "minor=${minor}" >> "$GITHUB_OUTPUT" + echo "patch=${patch}" >> "$GITHUB_OUTPUT" + echo "new_version=${new_version}" >> "$GITHUB_OUTPUT" + echo "new_version_no_v=${major}.${minor}.${new_patch}" >> "$GITHUB_OUTPUT" + echo "release_branch=${release_branch}" >> "$GITHUB_OUTPUT" + + echo "Latest stable release: ${latest_tag} (${latest_sha})" + echo "New version will be: ${new_version}" + echo "Release branch: ${release_branch}" + + - name: Validate source branch is cut directly from the latest stable release + env: + SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }} + SOURCE_COMMIT: ${{ inputs.commit }} + LATEST_TAG_SHA: ${{ steps.latest.outputs.latest_sha }} + LATEST_TAG: ${{ steps.latest.outputs.latest_tag }} + run: | + set -euo pipefail + + # Use the user-provided SHA directly rather than re-resolving the branch + # tip — the resolve step already proved the branch tip equals SOURCE_COMMIT, + # and pinning to the SHA here makes the rest of the job TOCTOU-safe against + # someone pushing to the branch mid-run. + source_sha="${SOURCE_COMMIT}" + + # Walking first-parent from the source tip must reach LATEST_TAG_SHA. + # We capture rev-list into a variable and grep against a here-string + # rather than piping `rev-list | grep -q`: under `set -o pipefail`, + # `grep -q` would exit on first match and SIGPIPE the still-streaming + # `rev-list`, propagating exit 141 as a spurious "not found". + first_parent_chain="$(git rev-list --first-parent "${source_sha}")" + if ! grep -Fxq "${LATEST_TAG_SHA}" <<< "${first_parent_chain}"; then + echo "::error::Source branch '${SOURCE_BRANCH}' is not cut from '${LATEST_TAG}'." + echo "::error::Its first-parent history does not include ${LATEST_TAG_SHA}." + exit 1 + fi + + # Additionally, every commit added on top of the tag (the set we are + # about to publish) must itself be a descendant of the tag along + # first-parent — i.e. no sibling commits from master sneak in via a + # non-first-parent path. Enforce by requiring that the symmetric + # difference is empty in one direction: commits in source that are + # NOT first-parent-reachable from source starting at the tag. + # We do this by intersecting: + # A = commits reachable from source but not from tag (full DAG) + # B = commits on the first-parent chain from source down to tag + # and requiring A == B. + all_added="$(git rev-list "${LATEST_TAG_SHA}..${source_sha}" | sort)" + first_parent_added="$( + git rev-list --first-parent "${LATEST_TAG_SHA}..${source_sha}" | sort + )" + + if [[ "${all_added}" != "${first_parent_added}" ]]; then + echo "::error::Source branch '${SOURCE_BRANCH}' contains commits not on its first-parent chain from '${LATEST_TAG}'." + echo "::error::This usually means the branch was cut from master (not from the tag) or contains a merge from master." + echo "Commits reachable but not on first-parent chain:" + comm -23 <(printf '%s\n' "${all_added}") <(printf '%s\n' "${first_parent_added}") \ + | while read -r sha; do + echo " $(git log -1 --format='%h %s' "${sha}")" + done + exit 1 + fi + + added_count="$(printf '%s\n' "${all_added}" | grep -c . || true)" + echo "Source branch is cut directly from ${LATEST_TAG} with ${added_count} commit(s) on top." + + - name: Validate PR exists, is open, named correctly, has latest commit, and checks pass + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }} + SOURCE_COMMIT: ${{ inputs.commit }} + NEW_VERSION: ${{ steps.latest.outputs.new_version }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + + expected_title="ComfyUI backport release ${NEW_VERSION}" + + # Find open PRs from this branch into master. The --state open filter + # is load-bearing: a closed/merged PR with passing checks must not be + # accepted as authorization for a new release. + pr_json="$( + gh pr list \ + --repo "${REPO}" \ + --state open \ + --head "${SOURCE_BRANCH}" \ + --base master \ + --json number,title,headRefOid,state \ + --limit 10 + )" + + pr_count="$(echo "${pr_json}" | jq 'length')" + if [[ "${pr_count}" -eq 0 ]]; then + echo "::error::No open PR found from '${SOURCE_BRANCH}' into 'master'. The PR must exist and be open." + exit 1 + fi + + # Pick the PR matching the expected title + pr_number="$(echo "${pr_json}" | jq -r --arg t "${expected_title}" ' + map(select(.title == $t)) | .[0].number // empty + ')" + pr_head_sha="$(echo "${pr_json}" | jq -r --arg t "${expected_title}" ' + map(select(.title == $t)) | .[0].headRefOid // empty + ')" + + if [[ -z "${pr_number}" ]]; then + echo "::error::No open PR from '${SOURCE_BRANCH}' into 'master' is titled '${expected_title}'." + echo "Found PRs:" + echo "${pr_json}" | jq -r '.[] | " #\(.number): \(.title)"' + exit 1 + fi + + # The PR's current head commit must equal the SHA the operator gave us. + # This is what closes the door on releasing stale code: if anyone has + # pushed to the branch since the operator validated tests passed, the + # PR head will have advanced past SOURCE_COMMIT and we abort. (The + # resolve step already proved the branch tip == SOURCE_COMMIT; this + # ties that same SHA to the PR that authorizes the release.) + if [[ "${pr_head_sha}" != "${SOURCE_COMMIT}" ]]; then + echo "::error::PR #${pr_number} head commit is ${pr_head_sha}, but the operator-provided commit is ${SOURCE_COMMIT}." + echo "::error::The PR has new commits since this release was authorized. Re-run with the new head SHA after verifying its checks." + exit 1 + fi + + echo "Found open PR #${pr_number} titled '${expected_title}' at head ${pr_head_sha} (matches operator-provided commit)." + + # Verify all check runs on the head commit have completed successfully. + # A check is considered passing if conclusion is success, neutral, or skipped. + checks_json="$( + gh api \ + --paginate \ + "repos/${REPO}/commits/${pr_head_sha}/check-runs" \ + --jq '.check_runs[] | {name: .name, status: .status, conclusion: .conclusion}' + )" + + if [[ -z "${checks_json}" ]]; then + echo "::error::No check runs found on PR head commit ${pr_head_sha}." + exit 1 + fi + + echo "Check runs on ${pr_head_sha}:" + echo "${checks_json}" | jq -s '.' + + failing="$(echo "${checks_json}" | jq -s ' + map(select( + .status != "completed" + or (.conclusion as $c + | ["success","neutral","skipped"] + | index($c) | not) + )) + ')" + + failing_count="$(echo "${failing}" | jq 'length')" + if [[ "${failing_count}" -gt 0 ]]; then + echo "::error::One or more checks have not passed on PR head commit ${pr_head_sha}:" + echo "${failing}" | jq -r '.[] | " - \(.name): status=\(.status) conclusion=\(.conclusion)"' + exit 1 + fi + + echo "All checks have passed on ${pr_head_sha}." + + - name: Prepare release branch + id: prepare + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REPO: ${{ github.repository }} + RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} + LATEST_TAG: ${{ steps.latest.outputs.latest_tag }} + LATEST_TAG_SHA: ${{ steps.latest.outputs.latest_sha }} + PATCH: ${{ steps.latest.outputs.patch }} + run: | + set -euo pipefail + + # Try to fetch the release branch. If patch == 0, it shouldn't exist yet + # and we'll create it from the latest stable tag. If patch > 0, it must + # already exist and its tip must equal the latest stable tag commit (i.e. + # the previous patch release). + if git ls-remote --exit-code --heads origin "${RELEASE_BRANCH}" >/dev/null 2>&1; then + echo "Release branch '${RELEASE_BRANCH}' already exists on origin." + git fetch origin "refs/heads/${RELEASE_BRANCH}:refs/remotes/origin/${RELEASE_BRANCH}" + git checkout -B "${RELEASE_BRANCH}" "refs/remotes/origin/${RELEASE_BRANCH}" + + current_tip="$(git rev-parse HEAD)" + if [[ "${current_tip}" != "${LATEST_TAG_SHA}" ]]; then + echo "::error::Release branch '${RELEASE_BRANCH}' tip (${current_tip}) is not at the latest stable release '${LATEST_TAG}' (${LATEST_TAG_SHA})." + echo "::error::Refusing to release on top of a divergent branch." + exit 1 + fi + echo "branch_existed=true" >> "$GITHUB_OUTPUT" + else + if [[ "${PATCH}" != "0" ]]; then + echo "::error::Release branch '${RELEASE_BRANCH}' does not exist on origin, but the latest stable release '${LATEST_TAG}' has patch=${PATCH} (>0). This is inconsistent." + exit 1 + fi + echo "Release branch '${RELEASE_BRANCH}' does not exist. Creating from ${LATEST_TAG}." + git checkout -B "${RELEASE_BRANCH}" "refs/tags/${LATEST_TAG}" + echo "branch_existed=false" >> "$GITHUB_OUTPUT" + fi + + - name: Fast-forward merge source branch into release branch + env: + SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }} + SOURCE_COMMIT: ${{ inputs.commit }} + RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} + run: | + set -euo pipefail + + # --ff-only guarantees no merge commit is created. If a fast-forward is + # not possible (i.e. the release branch has commits the source branch + # doesn't), the merge will fail and we abort. Because we already validated + # that the source branch is rooted on the latest stable tag, and the + # release branch tip equals that same tag, this fast-forward should + # always succeed for a well-formed backport branch. + # + # We merge the operator-provided SHA, not the branch ref, so a push to + # the branch in the window between resolve and now cannot smuggle new + # commits into the release. + if ! git merge --ff-only "${SOURCE_COMMIT}"; then + echo "::error::Cannot fast-forward '${RELEASE_BRANCH}' to ${SOURCE_COMMIT} (tip of '${SOURCE_BRANCH}'). A merge commit would be required. Aborting." + exit 1 + fi + + echo "Fast-forwarded '${RELEASE_BRANCH}' to ${SOURCE_COMMIT} (tip of '${SOURCE_BRANCH}')." + + - name: Bump version files + env: + NEW_VERSION_NO_V: ${{ steps.latest.outputs.new_version_no_v }} + run: | + set -euo pipefail + + if [[ ! -f comfyui_version.py ]]; then + echo "::error::comfyui_version.py not found in repo root." + exit 1 + fi + if [[ ! -f pyproject.toml ]]; then + echo "::error::pyproject.toml not found in repo root." + exit 1 + fi + + # Replace the version string in comfyui_version.py. + # Expected format: __version__ = "X.Y.Z" + python3 - "$NEW_VERSION_NO_V" <<'PY' + import re, sys, pathlib + new = sys.argv[1] + + p = pathlib.Path("comfyui_version.py") + src = p.read_text() + new_src, n = re.subn( + r'(__version__\s*=\s*[\'"])[^\'"]+([\'"])', + lambda m: f'{m.group(1)}{new}{m.group(2)}', + src, + count=1, + ) + if n != 1: + sys.exit("Could not find __version__ assignment in comfyui_version.py") + p.write_text(new_src) + + p = pathlib.Path("pyproject.toml") + src = p.read_text() + # Replace the first `version = "..."` inside [project] or [tool.poetry]. + new_src, n = re.subn( + r'(?m)^(version\s*=\s*")[^"]+(")', + lambda m: f'{m.group(1)}{new}{m.group(2)}', + src, + count=1, + ) + if n != 1: + sys.exit("Could not find version assignment in pyproject.toml") + p.write_text(new_src) + PY + + echo "Updated version to ${NEW_VERSION_NO_V} in comfyui_version.py and pyproject.toml." + git --no-pager diff -- comfyui_version.py pyproject.toml + + - name: Commit version bump and tag release + env: + NEW_VERSION: ${{ steps.latest.outputs.new_version }} + run: | + set -euo pipefail + + git add comfyui_version.py pyproject.toml + git commit -m "ComfyUI ${NEW_VERSION}" + + if git rev-parse -q --verify "refs/tags/${NEW_VERSION}" >/dev/null; then + echo "::error::Tag ${NEW_VERSION} already exists locally." + exit 1 + fi + git tag "${NEW_VERSION}" + + - name: Verify tag does not already exist on origin + env: + NEW_VERSION: ${{ steps.latest.outputs.new_version }} + run: | + set -euo pipefail + if git ls-remote --exit-code --tags origin "refs/tags/${NEW_VERSION}" >/dev/null 2>&1; then + echo "::error::Tag ${NEW_VERSION} already exists on origin. Aborting." + exit 1 + fi + + - name: Push release branch and tag + env: + RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} + NEW_VERSION: ${{ steps.latest.outputs.new_version }} + run: | + set -euo pipefail + + # Push the branch first, then the tag. Atomic-ish: if the branch push + # fails we never publish the tag. + git push origin "refs/heads/${RELEASE_BRANCH}:refs/heads/${RELEASE_BRANCH}" + git push origin "refs/tags/${NEW_VERSION}" + + echo "Released ${NEW_VERSION} on ${RELEASE_BRANCH}." + + - name: Delete remote source branch + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REPO: ${{ github.repository }} + SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }} + SOURCE_COMMIT: ${{ inputs.commit }} + RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + + # Belt-and-braces: the resolve step already refuses the default branch, + # but never delete the default or the release branch under any + # circumstances. + if [[ "${SOURCE_BRANCH}" == "${DEFAULT_BRANCH}" || "${SOURCE_BRANCH}" == "${RELEASE_BRANCH}" ]]; then + echo "::error::Refusing to delete '${SOURCE_BRANCH}' (matches default or release branch)." + exit 1 + fi + + # Delete the source branch on origin, but only if its tip is still the + # SHA we released from. If someone pushed new commits to it after we + # resolved it, leave it alone — those commits would be silently lost. + current_tip="$(git ls-remote origin "refs/heads/${SOURCE_BRANCH}" | awk '{print $1}')" + if [[ -z "${current_tip}" ]]; then + echo "Source branch '${SOURCE_BRANCH}' no longer exists on origin; nothing to delete." + exit 0 + fi + if [[ "${current_tip}" != "${SOURCE_COMMIT}" ]]; then + echo "::warning::Source branch '${SOURCE_BRANCH}' tip (${current_tip}) no longer matches released commit (${SOURCE_COMMIT}). Leaving it in place." + exit 0 + fi + + git push origin --delete "refs/heads/${SOURCE_BRANCH}" + echo "Deleted remote branch '${SOURCE_BRANCH}'." + + - name: Summary + if: always() + env: + NEW_VERSION: ${{ steps.latest.outputs.new_version }} + RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} + LATEST_TAG: ${{ steps.latest.outputs.latest_tag }} + SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }} + SOURCE_COMMIT: ${{ inputs.commit }} + run: | + # SOURCE_BRANCH is empty if the resolve step never produced an output + # (e.g. the workflow failed in or before that step). Show a placeholder + # in that case so the summary table still renders cleanly. + source_branch_display="${SOURCE_BRANCH:-(unresolved)}" + { + echo "## Backport release" + echo "" + echo "| Field | Value |" + echo "|---|---|" + echo "| Source commit | \`${SOURCE_COMMIT}\` |" + echo "| Source branch | \`${source_branch_display}\` |" + echo "| Previous stable | \`${LATEST_TAG}\` |" + echo "| New version | \`${NEW_VERSION}\` |" + echo "| Release branch | \`${RELEASE_BRANCH}\` |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml index f501b7b31..bc64ed74d 100644 --- a/.github/workflows/stable-release.yml +++ b/.github/workflows/stable-release.yml @@ -145,6 +145,8 @@ jobs: cp -r ComfyUI/.ci/windows_${{ inputs.rel_name }}_base_files/* ./ cp ../update_comfyui_and_python_dependencies.bat ./update/ + echo 'local-portable' > ComfyUI/.comfy_environment + cd .. "C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=9 -mfb=128 -md=768m -ms=on -mf=BCJ2 ComfyUI_windows_portable.7z ComfyUI_windows_portable diff --git a/.spectral.yaml b/.spectral.yaml index 4bb4a4a94..a4b137628 100644 --- a/.spectral.yaml +++ b/.spectral.yaml @@ -89,3 +89,12 @@ rules: then: field: description function: truthy + +overrides: + # /ws uses HTTP 101 (Switching Protocols) — a legitimate response for a + # WebSocket upgrade, but not a 2xx, so operation-success-response fires + # as a false positive. OpenAPI 3.x has no native WebSocket support. + - files: + - "openapi.yaml#/paths/~1ws" + rules: + operation-success-response: off diff --git a/CODEOWNERS b/CODEOWNERS index 946dbf946..043c0ec75 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,5 @@ -# Admins * @comfyanonymous @kosinkadink @guill @alexisrolland @rattus128 @kijai + +/CODEOWNERS @comfyanonymous +/.ci/ @comfyanonymous +/.github/ @comfyanonymous diff --git a/README.md b/README.md index 0fd317d0a..dc2389266 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ [website-url]: https://www.comfy.org/ [discord-shield]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Fcomfyorg%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&logo=discord&logoColor=white&label=Discord&color=green&suffix=%20total -[discord-url]: https://www.comfy.org/discord +[discord-url]: https://discord.com/invite/comfyorg [twitter-shield]: https://img.shields.io/twitter/follow/ComfyUI [twitter-url]: https://x.com/ComfyUI @@ -38,7 +38,7 @@ ComfyUI is the AI creation engine for visual professionals who demand control over every model, every parameter, and every output. Its powerful and modular node graph interface empowers creatives to generate images, videos, 3D models, audio, and more... - ComfyUI natively supports the latest open-source state of the art models. - API nodes provide access to the best closed source models such as Nano Banana, Seedance, Hunyuan3D, etc. -- It is available on Windows, Linux, and macOS, locally with our desktop application or on our cloud. +- It is available on Windows, Linux, and macOS, locally with our [desktop application](https://www.comfy.org/download), our [portable install](#installing) or on our [cloud](https://www.comfy.org/cloud). - The most sophisticated workflows can be exposed through a simple UI thanks to App Mode. - It integrates seamlessly into production pipelines with our API endpoints. @@ -429,9 +429,11 @@ Use `--tls-keyfile key.pem --tls-certfile cert.pem` to enable TLS/SSL, the app w See also: [https://www.comfy.org/](https://www.comfy.org/) +> _psst — we're hiring!_ Help build ComfyUI: [comfy.org/careers](https://www.comfy.org/careers) + ## Frontend Development -As of August 15, 2024, we have transitioned to a new frontend, which is now hosted in a separate repository: [ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend). This repository now hosts the compiled JS (from TS/Vue) under the `web/` directory. +As of August 15, 2024, we have transitioned to a new frontend, which is now hosted in a separate repository: [ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend). The compiled JS files (from TS/Vue) are published to [pypi](https://pypi.org/project/comfyui-frontend-package) and installed as a dependency in ComfyUI. ### Reporting Issues and Requesting Features diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..299b0067b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,44 @@ +# Security Policy + +## Scope + +ComfyUI is designed to run locally. By default, the server binds to `127.0.0.1`, meaning only the user's own machine can reach it. Our threat model assumes: + +- The user installed ComfyUI through a supported channel: the desktop application, the portable build, or a manual install following the README. +- The user has not installed untrusted custom nodes. Custom nodes are arbitrary Python code and are trusted as much as any other software the user chooses to install. +- Anyone with access to the ComfyUI URL is trusted (a direct consequence of the localhost-only default). +- PyTorch and other dependencies are at the versions we ship or recommend in the README. + +A report is in scope only if it affects a user operating within this threat model. + +## What We Consider a Vulnerability + +We want to hear about issues where a **reasonable user** — someone who does not install random untrusted nodes and who reads UI prompts and warnings before clicking through them — can be harmed by ComfyUI itself. + +The clearest example: a workflow file that such a user might plausibly load and run, using only built-in nodes, that results in **untrusted code execution, arbitrary file read/write outside expected directories, or credential/data exfiltration**. + +When submitting a report, please include a clear description of *why this is a problem for a typical local ComfyUI user*. Reports without this context are difficult to act on. + +## What We Do Not Consider a Security Vulnerability + +Please report the following through our regular [GitHub issues](https://github.com/comfyanonymous/ComfyUI/issues) instead. Filing them as security reports will likely cause them to be deprioritized or closed. + +- **Issues requiring `--listen` or any non-default network exposure.** ComfyUI binds to localhost by default. If a remote attacker needs to reach the server for the attack to work, the user has chosen to expose it and is responsible for securing that deployment (firewall, reverse proxy, authentication, etc.). These are bugs, not vulnerabilities. +- **`torch.load` and related deserialization issues in old PyTorch versions.** These are upstream PyTorch issues. Our distributions ship with — and our documentation recommends — recent PyTorch versions where these are addressed. +- **Vulnerabilities that depend on outdated library versions** that we neither ship nor recommend (e.g., requiring PyTorch 2.6 or older). +- **Issues that require a specific custom node to be installed.** Custom nodes are third-party code. Report these to the maintainer of that node. +- **Crashes, hangs, or resource exhaustion from a loaded workflow.** Annoying, but not a security issue in our model. File a regular bug. +- **Social-engineering scenarios** where the user is expected to ignore an explicit UI warning or prompt. + +## Reporting + +If you believe you have found an issue that falls within the scope above, please report it privately via GitHub's [Report a vulnerability](https://github.com/comfyanonymous/ComfyUI/security/advisories/new) feature rather than opening a public issue. + +Please include: + +1. A description of the vulnerability and the affected component. +2. Reproduction steps, ideally with a minimal workflow file or proof-of-concept. +3. The ComfyUI version, install method (desktop / portable / manual), and OS. +4. An explanation of how this affects a typical local user as described in the threat model. + +We will acknowledge valid reports and coordinate a fix and disclosure timeline with you. diff --git a/app/assets/api/routes.py b/app/assets/api/routes.py index 68126b6a5..6555974e9 100644 --- a/app/assets/api/routes.py +++ b/app/assets/api/routes.py @@ -160,10 +160,12 @@ def _build_asset_response(result: schemas.AssetDetailResult | schemas.UploadResu preview_url = None else: preview_url = _build_preview_url_from_view(result.tags, result.ref.user_metadata) + asset_content_hash = result.asset.hash if result.asset else None return schemas_out.Asset( id=result.ref.id, name=result.ref.name, - asset_hash=result.asset.hash if result.asset else None, + hash=asset_content_hash, + asset_hash=asset_content_hash, size=int(result.asset.size_bytes) if result.asset else None, mime_type=result.asset.mime_type if result.asset else None, tags=result.tags, diff --git a/app/assets/api/schemas_out.py b/app/assets/api/schemas_out.py index d99b1098d..0e748b907 100644 --- a/app/assets/api/schemas_out.py +++ b/app/assets/api/schemas_out.py @@ -10,6 +10,7 @@ class Asset(BaseModel): id: str name: str + hash: str | None = None asset_hash: str | None = None size: int | None = None mime_type: str | None = None diff --git a/app/assets/services/metadata_extract.py b/app/assets/services/metadata_extract.py index a004929bc..bdfe60218 100644 --- a/app/assets/services/metadata_extract.py +++ b/app/assets/services/metadata_extract.py @@ -4,7 +4,6 @@ Tier 1: Filesystem metadata (zero parsing) Tier 2: Safetensors header metadata (fast JSON read only) """ -from __future__ import annotations import json import logging diff --git a/app/custom_node_manager.py b/app/custom_node_manager.py index 281febca9..738af2abd 100644 --- a/app/custom_node_manager.py +++ b/app/custom_node_manager.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import os import folder_paths import glob diff --git a/app/frontend_management.py b/app/frontend_management.py index f753ef0de..8e84e8dd9 100644 --- a/app/frontend_management.py +++ b/app/frontend_management.py @@ -1,4 +1,3 @@ -from __future__ import annotations import argparse import logging import os @@ -27,7 +26,7 @@ def frontend_install_warning_message(): return f""" {get_missing_requirements_message()} -This error is happening because the ComfyUI frontend is no longer shipped as part of the main repo but as a pip package instead. +The ComfyUI frontend is shipped in a pip package so it needs to be updated separately from the ComfyUI code. """.strip() def parse_version(version: str) -> tuple[int, int, int]: @@ -38,40 +37,63 @@ def is_valid_version(version: str) -> bool: pattern = r"^(\d+)\.(\d+)\.(\d+)$" return bool(re.match(pattern, version)) -def get_installed_frontend_version(): - """Get the currently installed frontend package version.""" - frontend_version_str = version("comfyui-frontend-package") - return frontend_version_str - - def get_required_frontend_version(): return get_required_packages_versions().get("comfyui-frontend-package", None) -def check_frontend_version(): - """Check if the frontend version is up to date.""" +COMFY_PACKAGE_VERSIONS = [] +def get_comfy_package_versions(): + """List installed/required versions for every comfy* package in requirements.txt.""" + if COMFY_PACKAGE_VERSIONS: + return COMFY_PACKAGE_VERSIONS.copy() + out = COMFY_PACKAGE_VERSIONS + for name, required in (get_required_packages_versions() or {}).items(): + if not name.startswith("comfy"): + continue + try: + installed = version(name) + except Exception: + installed = None + out.append({"name": name, "installed": installed, "required": required}) + return out.copy() - try: - frontend_version_str = get_installed_frontend_version() - frontend_version = parse_version(frontend_version_str) - required_frontend_str = get_required_frontend_version() - required_frontend = parse_version(required_frontend_str) - if frontend_version < required_frontend: - app.logger.log_startup_warning( - f""" + +def check_comfy_packages_versions(): + """Warn for every comfy* package whose installed version is below requirements.txt.""" + from packaging.version import InvalidVersion, parse as parse_pep440 + outdated_packages = [] + + for pkg in get_comfy_package_versions(): + installed_str = pkg["installed"] + required_str = pkg["required"] + if not installed_str or not required_str: + continue + try: + outdated = parse_pep440(installed_str) < parse_pep440(required_str) + except InvalidVersion as e: + logging.error(f"Failed to check {pkg['name']} version: {e}") + continue + if outdated: + outdated_packages.append((pkg["name"], installed_str, required_str)) + else: + logging.info("{} version: {}".format(pkg["name"], installed_str)) + + if outdated_packages: + package_warnings = "\n".join( + f"Installed {name} version {installed} is lower than the recommended version {required}." + for name, installed, required in outdated_packages + ) + app.logger.log_startup_warning( + f""" ________________________________________________________________________ WARNING WARNING WARNING WARNING WARNING -Installed frontend version {".".join(map(str, frontend_version))} is lower than the recommended version {".".join(map(str, required_frontend))}. +{package_warnings} -{frontend_install_warning_message()} +{get_missing_requirements_message()} ________________________________________________________________________ """.strip() - ) - else: - logging.info("ComfyUI frontend version: {}".format(frontend_version_str)) - except Exception as e: - logging.error(f"Failed to check frontend version: {e}") + ) REQUEST_TIMEOUT = 10 # seconds @@ -201,6 +223,11 @@ class FrontendManager: def get_required_templates_version(cls) -> str: return get_required_packages_versions().get("comfyui-workflow-templates", None) + @classmethod + def get_comfy_package_versions(cls): + """List installed/required versions for every comfy* package in requirements.txt.""" + return get_comfy_package_versions() + @classmethod def default_frontend_path(cls) -> str: try: @@ -341,7 +368,7 @@ comfyui-workflow-templates is not installed. main error source might be request timeout or invalid URL. """ if version_string == DEFAULT_VERSION_STRING: - check_frontend_version() + check_comfy_packages_versions() return cls.default_frontend_path() repo_owner, repo_name, version = cls.parse_version_string(version_string) @@ -403,7 +430,7 @@ comfyui-workflow-templates is not installed. except Exception as e: logging.error("Failed to initialize frontend: %s", e) logging.info("Falling back to the default frontend.") - check_frontend_version() + check_comfy_packages_versions() return cls.default_frontend_path() @classmethod def template_asset_handler(cls): diff --git a/app/logger.py b/app/logger.py index 3d26d98fe..bde815822 100644 --- a/app/logger.py +++ b/app/logger.py @@ -5,6 +5,40 @@ import logging import sys import threading +ANSI_NAMED_COLORS = { + 'black': '\033[30m', + 'red': '\033[31m', + 'green': '\033[32m', + 'yellow': '\033[33m', + 'blue': '\033[34m', + 'magenta': '\033[35m', + 'cyan': '\033[36m', + 'white': '\033[37m', +} + +ANSI_LEVEL_COLORS = { + 'DEBUG': ANSI_NAMED_COLORS['cyan'], + 'INFO': ANSI_NAMED_COLORS['green'], + 'WARNING': ANSI_NAMED_COLORS['yellow'], + 'ERROR': ANSI_NAMED_COLORS['red'], + 'CRITICAL': ANSI_NAMED_COLORS['magenta'], +} + +ANSI_RESET = '\033[0m' +ANSI_BOLD = '\033[1m' + + +class ColoredFormatter(logging.Formatter): + def format(self, record): + color = ANSI_LEVEL_COLORS.get(record.levelname, '') + bold = ANSI_BOLD if record.levelno >= logging.WARNING else '' + level_tag = f"{bold}{color}[{record.levelname}]{ANSI_RESET} " + message = super().format(record) + line_color = ANSI_NAMED_COLORS.get(getattr(record, 'color', ''), '') + if line_color: + return f"{level_tag}{line_color}{message}{ANSI_RESET}" + return level_tag + message + logs = None stdout_interceptor = None stderr_interceptor = None @@ -68,8 +102,10 @@ def setup_logger(log_level: str = 'INFO', capacity: int = 300, use_stdout: bool logger = logging.getLogger() logger.setLevel(log_level) + formatter = ColoredFormatter("%(message)s") + stream_handler = logging.StreamHandler() - stream_handler.setFormatter(logging.Formatter("%(message)s")) + stream_handler.setFormatter(formatter) if use_stdout: # Only errors and critical to stderr @@ -77,7 +113,7 @@ def setup_logger(log_level: str = 'INFO', capacity: int = 300, use_stdout: bool # Lesser to stdout stdout_handler = logging.StreamHandler(sys.stdout) - stdout_handler.setFormatter(logging.Formatter("%(message)s")) + stdout_handler.setFormatter(formatter) stdout_handler.addFilter(lambda record: record.levelno < logging.ERROR) logger.addHandler(stdout_handler) diff --git a/app/model_manager.py b/app/model_manager.py index f124d1117..8f6e34b33 100644 --- a/app/model_manager.py +++ b/app/model_manager.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import os import base64 import json diff --git a/app/node_replace_manager.py b/app/node_replace_manager.py index d9aab5b22..72e8ac2b1 100644 --- a/app/node_replace_manager.py +++ b/app/node_replace_manager.py @@ -1,5 +1,7 @@ from __future__ import annotations +import logging + from aiohttp import web from typing import TYPE_CHECKING, TypedDict @@ -31,8 +33,22 @@ class NodeReplaceManager: self._replacements: dict[str, list[NodeReplace]] = {} def register(self, node_replace: NodeReplace): - """Register a node replacement mapping.""" - self._replacements.setdefault(node_replace.old_node_id, []).append(node_replace) + """Register a node replacement mapping. + + Idempotent: if a replacement with the same (old_node_id, new_node_id) + is already registered, the duplicate is ignored. This prevents stale + entries from accumulating when custom nodes are reloaded in the same + process (e.g. via ComfyUI-Manager). + """ + existing = self._replacements.setdefault(node_replace.old_node_id, []) + for entry in existing: + if entry.new_node_id == node_replace.new_node_id: + logging.debug( + "Node replacement %s -> %s already registered, ignoring duplicate.", + node_replace.old_node_id, node_replace.new_node_id, + ) + return + existing.append(node_replace) def get_replacement(self, old_node_id: str) -> list[NodeReplace] | None: """Get replacements for an old node ID.""" diff --git a/app/user_manager.py b/app/user_manager.py index e18afb71b..7b11e381c 100644 --- a/app/user_manager.py +++ b/app/user_manager.py @@ -1,4 +1,3 @@ -from __future__ import annotations import json import os import re @@ -28,8 +27,8 @@ def get_file_info(path: str, relative_to: str) -> FileInfo: return { "path": os.path.relpath(path, relative_to).replace(os.sep, '/'), "size": os.path.getsize(path), - "modified": os.path.getmtime(path), - "created": os.path.getctime(path) + "modified": int(os.path.getmtime(path) * 1000), + "created": int(os.path.getctime(path) * 1000), } diff --git a/blueprints/Audio Generation (Stable Audio 3 Medium Base).json b/blueprints/Audio Generation (Stable Audio 3 Medium Base).json new file mode 100644 index 000000000..e561fe634 --- /dev/null +++ b/blueprints/Audio Generation (Stable Audio 3 Medium Base).json @@ -0,0 +1,2091 @@ +{ + "revision": 0, + "last_node_id": 52, + "last_link_id": 0, + "nodes": [ + { + "id": 52, + "type": "8b66c757-fe2f-4184-91f3-479a19deb565", + "pos": [ + 370, + 1120 + ], + "size": [ + 420, + 450 + ], + "flags": { + "collapsed": false + }, + "order": 0, + "mode": 0, + "inputs": [ + { + "label": "user_input", + "name": "user_input", + "type": "STRING", + "widget": { + "name": "user_input" + }, + "link": null + }, + { + "label": "duration", + "name": "duration", + "type": "FLOAT", + "widget": { + "name": "duration" + }, + "link": null + }, + { + "label": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "label": "use_reprompt", + "name": "use_reprompt", + "type": "BOOLEAN", + "widget": { + "name": "use_reprompt" + }, + "link": null + }, + { + "label": "reprompt_category", + "name": "category", + "type": "COMBO", + "widget": { + "name": "category" + }, + "link": null + }, + { + "label": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": null + }, + { + "label": "sa_clip", + "name": "sa_clip", + "type": "COMBO", + "widget": { + "name": "sa_clip" + }, + "link": null + }, + { + "label": "qwen_clip", + "name": "qwen_clip", + "type": "COMBO", + "widget": { + "name": "qwen_clip" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "AUDIO", + "name": "AUDIO", + "type": "AUDIO", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "31", + "value" + ], + [ + "36", + "value" + ], + [ + "3", + "seed" + ], + [ + "35", + "value" + ], + [ + "43", + "choice" + ], + [ + "25", + "ckpt_name" + ], + [ + "26", + "clip_name" + ], + [ + "29", + "clip_name" + ] + ] + }, + "widgets_values": [], + "title": "Audio Generation (Stable Audio 3 Medium Base)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "8b66c757-fe2f-4184-91f3-479a19deb565", + "version": 1, + "state": { + "lastGroupId": 8, + "lastNodeId": 56, + "lastLinkId": 84, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Audio Generation (Stable Audio 3 Medium Base)", + "inputNode": { + "id": -10, + "bounding": [ + -810, + 400, + 155.953125, + 208 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1750, + 1041, + 128, + 68 + ] + }, + "inputs": [ + { + "id": "78ae2515-114b-494a-becc-43c7b6c2dc2f", + "name": "user_input", + "type": "STRING", + "linkIds": [ + 68 + ], + "label": "user_input", + "pos": [ + -678.046875, + 424 + ] + }, + { + "id": "5ca95030-aff4-4544-b545-f0d814e0e49a", + "name": "duration", + "type": "FLOAT", + "linkIds": [ + 82 + ], + "label": "duration", + "pos": [ + -678.046875, + 444 + ] + }, + { + "id": "718eb10f-da1a-4cea-a9c7-3040f98fe960", + "name": "seed", + "type": "INT", + "linkIds": [ + 76 + ], + "label": "seed", + "pos": [ + -678.046875, + 464 + ] + }, + { + "id": "dc020099-39e6-4009-9937-408409d71736", + "name": "use_reprompt", + "type": "BOOLEAN", + "linkIds": [ + 83 + ], + "label": "use_reprompt", + "pos": [ + -678.046875, + 484 + ] + }, + { + "id": "edae394c-6324-44d6-8ac5-d8caa5ae2169", + "name": "category", + "type": "COMBO", + "linkIds": [ + 78 + ], + "label": "reprompt_category", + "pos": [ + -678.046875, + 504 + ] + }, + { + "id": "be19b747-6a47-4028-9c30-d52f54a712ea", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 79 + ], + "label": "ckpt_name", + "pos": [ + -678.046875, + 524 + ] + }, + { + "id": "bc9241a2-bc20-4c5d-8cb1-f2958f598642", + "name": "sa_clip", + "type": "COMBO", + "linkIds": [ + 80 + ], + "label": "sa_clip", + "pos": [ + -678.046875, + 544 + ] + }, + { + "id": "a33a2468-6d6d-4cb6-937c-3510bf16ebac", + "name": "qwen_clip", + "type": "COMBO", + "linkIds": [ + 81 + ], + "label": "qwen_clip", + "pos": [ + -678.046875, + 564 + ] + } + ], + "outputs": [ + { + "id": "bbe988dd-5c03-44fd-a965-c712f9204988", + "name": "AUDIO", + "type": "AUDIO", + "linkIds": [ + 27 + ], + "localized_name": "AUDIO", + "pos": [ + 1774, + 1065 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + 620, + 420 + ], + "size": [ + 440, + 140 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 35 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 6 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 12, + "type": "VAEDecodeAudio", + "pos": [ + 1450, + 110 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 13 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 39 + } + ], + "outputs": [ + { + "localized_name": "AUDIO", + "name": "AUDIO", + "type": "AUDIO", + "slot_index": 0, + "links": [ + 27 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecodeAudio", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 11, + "type": "EmptyLatentAudio", + "pos": [ + 630, + 610 + ], + "size": [ + 430, + 140 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "seconds", + "name": "seconds", + "type": "FLOAT", + "widget": { + "name": "seconds" + }, + "link": 50 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 12 + ] + } + ], + "properties": { + "Node name for S&R": "EmptyLatentAudio", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 60, + 1 + ] + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + 1100, + 100 + ], + "size": [ + 320, + 350 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 30 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 4 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 6 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 12 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 76 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 13 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + "randomize", + 50, + 7, + "lcm", + "simple", + 1 + ] + }, + { + "id": 29, + "type": "CLIPLoader", + "pos": [ + 690, + 1580 + ], + "size": [ + 430, + 170 + ], + "flags": {}, + "order": 8, + "mode": 0, + "showAdvanced": false, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 81 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 40 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "models": [ + { + "name": "qwen3.5_2b_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/Qwen3.5/resolve/main/text_encoders/qwen3.5_2b_bf16.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "qwen3.5_2b_bf16.safetensors", + "stable_diffusion", + "default" + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + 610, + 130 + ], + "size": [ + 450, + 240 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 34 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 49 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 4 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 34, + "type": "ComfySwitchNode", + "pos": [ + 210, + 610 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 47 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 46 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 48 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 49 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode" + }, + "widgets_values": [ + false + ] + }, + { + "id": 41, + "type": "ComfyMathExpression", + "pos": [ + 1370, + 1360 + ], + "size": [ + 230, + 80 + ], + "flags": { + "collapsed": true + }, + "order": 16, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT,BOOLEAN", + "link": 56 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 57 + ] + }, + { + "localized_name": "BOOL", + "name": "BOOL", + "type": "BOOLEAN", + "links": null + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "a" + ] + }, + { + "id": 42, + "type": "PreviewAny", + "pos": [ + 1370, + 1310 + ], + "size": [ + 230, + 40 + ], + "flags": { + "collapsed": true + }, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 57 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 58 + ] + } + ], + "properties": { + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 39, + "type": "StringReplace", + "pos": [ + 1040, + 900 + ], + "size": [ + 270, + 280 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": 52 + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 53 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 59 + ] + } + ], + "title": "Text Replace (USER INPUT)", + "properties": { + "Node name for S&R": "StringReplace" + }, + "widgets_values": [ + "", + "USER_INPUT", + "" + ] + }, + { + "id": 28, + "type": "TextGenerate", + "pos": [ + 1200, + 1580 + ], + "size": [ + 430, + 420 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 40 + }, + { + "localized_name": "image", + "name": "image", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "localized_name": "video", + "name": "video", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": null + }, + { + "localized_name": "prompt", + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": 60 + }, + { + "localized_name": "max_length", + "name": "max_length", + "type": "INT", + "widget": { + "name": "max_length" + }, + "link": null + }, + { + "localized_name": "sampling_mode", + "name": "sampling_mode", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "sampling_mode" + }, + "link": null + }, + { + "localized_name": "temperature", + "name": "sampling_mode.temperature", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.temperature" + }, + "link": null + }, + { + "localized_name": "top_k", + "name": "sampling_mode.top_k", + "type": "INT", + "widget": { + "name": "sampling_mode.top_k" + }, + "link": null + }, + { + "localized_name": "top_p", + "name": "sampling_mode.top_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.top_p" + }, + "link": null + }, + { + "localized_name": "min_p", + "name": "sampling_mode.min_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.min_p" + }, + "link": null + }, + { + "localized_name": "repetition_penalty", + "name": "sampling_mode.repetition_penalty", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.repetition_penalty" + }, + "link": null + }, + { + "localized_name": "seed", + "name": "sampling_mode.seed", + "type": "INT", + "widget": { + "name": "sampling_mode.seed" + }, + "link": null + }, + { + "localized_name": "presence_penalty", + "name": "sampling_mode.presence_penalty", + "shape": 7, + "type": "FLOAT", + "widget": { + "name": "sampling_mode.presence_penalty" + }, + "link": null + }, + { + "localized_name": "thinking", + "name": "thinking", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "thinking" + }, + "link": null + }, + { + "localized_name": "use_default_template", + "name": "use_default_template", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "use_default_template" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "generated_text", + "name": "generated_text", + "type": "STRING", + "links": [ + 46, + 84 + ] + } + ], + "properties": { + "Node name for S&R": "TextGenerate", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "", + 256, + "on", + 0.7, + 64, + 0.95, + 0.05, + 1.05, + 0, + 0, + false, + true + ] + }, + { + "id": 31, + "type": "PrimitiveStringMultiline", + "pos": [ + -390, + 160 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "STRING", + "widget": { + "name": "value" + }, + "link": 68 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 47, + 53 + ] + } + ], + "title": "User: short description (USER_INPUT in template)", + "properties": { + "Node name for S&R": "PrimitiveStringMultiline" + }, + "widgets_values": [ + "" + ] + }, + { + "id": 43, + "type": "CustomCombo", + "pos": [ + 140, + 910 + ], + "size": [ + 550, + 320 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "choice", + "name": "choice", + "type": "COMBO", + "widget": { + "name": "choice" + }, + "link": 78 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 65 + ] + }, + { + "localized_name": "INDEX", + "name": "INDEX", + "type": "INT", + "links": null + } + ], + "title": "Custom Combo (Category index)", + "properties": { + "Node name for S&R": "CustomCombo" + }, + "widgets_values": [ + "Music", + 0, + "Music", + "Instrument", + "SFX", + "One-shot", + "" + ] + }, + { + "id": 49, + "type": "JsonExtractString", + "pos": [ + 720, + 1200 + ], + "size": [ + 300, + 180 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "localized_name": "json_string", + "name": "json_string", + "type": "STRING", + "widget": { + "name": "json_string" + }, + "link": null + }, + { + "localized_name": "key", + "name": "key", + "type": "STRING", + "widget": { + "name": "key" + }, + "link": 65 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 66 + ] + } + ], + "properties": { + "Node name for S&R": "JsonExtractString" + }, + "widgets_values": [ + "{\n \"Music\": \"You are an expert musician and musicologist and prompt engineer. Transform the user's input into a detailed, vivid music prompt for a full instrumental track.\\n\\n1. Start with the genre or style and optional adjectives (e.g., upbeat, dreamy, aggressive).\\n2. List the main instruments that define the track.\\n3. Add supporting elements or layers such as pads, harmonics, effects, or field recordings.\\n4. Include rhythm or percussion elements like drums, hi-hats, congas, brushes, or polyrhythms.\\n5. Integrate mood and energy naturally in the sentence (e.g., \\\"creating suspenseful tension\\\" or \\\"bright and uplifting\\\").\\n6. Specify the BPM.\\n7. Specify the track length as an integer in seconds. Use ranges: energetic/dance 120-180s, pop/rock 180-210s, cinematic/ambient 240-300s.\\n8. Combine all elements into one natural, fluid sentence. Avoid semicolons.\\n\\nTemplate:\\nGenre/Style with main instruments, supporting instruments/layers, and rhythm/percussion creating mood/energy. BPM: X. Length: Y seconds\\n\\nExamples:\\n- Jazz ballad with smooth saxophone lead, piano chords, upright bass, brushed drums, and soft strings that swing gently for a warm and cozy evening. BPM: 85. Length: 180 seconds\\n- EDM festival track with pulsing synth leads, plucked arpeggios, layered pads, side-chained bass, punchy kick and snare, and hi-hat rolls creating bright, energetic, and uplifting dance energy. BPM: 128. Length: 150 seconds\\n- Lo-fi hip-hop chill track with mellow electric piano, soft vinyl crackle, subtle synth pads, low-pass filtered drums, percussion loops, and soft plucked bass for a relaxed, dreamy vibe. BPM: 75. Length: 150 seconds\\n- Heavy metal anthem with distorted electric guitars, bass guitar, double bass drums, and cymbal crashes with fast palm-muted riffs creating intense, aggressive energy. BPM: 160. Length: 180 seconds\\n- Melancholic piano piece with soft piano lead, string pads, subtle atmospheric synths, and minimal brush percussion evoking a reflective rainy-day feeling. BPM: 60. Length: 240 seconds\\n- Suspenseful electronic thriller with pulsing bass synth, arpeggiated lead synth, cinematic pads, glitchy percussion, and high string stabs creating dark and tense energy. BPM: 100. Length: 200 seconds\\n- Dreamy ambient soundscape with layered pads, soft bell textures, gentle drones, and wind and water field recordings for ethereal and spacious meditation. BPM: 40. Length: 300 seconds\\n- Fingerpicking acoustic guitar solo with harmonics, subtle reverb, occasional shaker and soft stomp percussion, and soft pad layers for warm intimate storytelling. BPM: 70. Length: 120 seconds\\n- Synthwave 80s retro track with arpeggiated synth leads, analog pads, electric bass, punchy electronic drums, gated reverb snares, and atmospheric FX for nostalgic and vibrant energy. BPM: 110. Length: 180 seconds\\n- Tribal percussion ensemble with congas, djembes, bongos, shakers, and frame drums layered with deep synthetic sub-bass in complex polyrhythms. BPM: 100. Length: 140 seconds\\n- 1920s swing jazz with brass section, upright bass, piano, brushed drums, banjo, clarinet, and soft strings that swing lively for energetic dance vibes. BPM: 110. Length: 180 seconds\\n- Futuristic electronic sci-fi track with pulsing bass synth, evolving lead synths, layered pads, glitch percussion, robotic FX, and sub-bass for tense cinematic energy. BPM: 125. Length: 200 seconds\\n- Ambient underwater soundscape with flowing water textures, soft piano motifs, synth drones, distant bells, and underwater reverb for spacious meditative immersion. BPM: 45. Length: 300 seconds\\n- Horror cinematic track with dissonant strings, eerie piano stabs, cinematic percussion including taiko and low toms, and synth FX producing suspenseful creepy tension. BPM: 90. Length: 240 seconds\\n- Reggae track with offbeat guitar, warm basslines, snare, kick, congas, and horn stabs giving laid-back groovy energy. BPM: 85. Length: 150 seconds\\n- Blues track with soulful electric guitar solos, walking bass, piano, and shuffle drums creating expressive and emotive storytelling. BPM: 90. Length: 180 seconds\\n- Latin salsa with congas, timbales, horns, piano montunos, bass, and layered percussion for vibrant danceable energy. BPM: 120. Length: 210 seconds\\n- Afrobeat track with electric guitar stabs, horns, layered percussion, congas, shakers, bass groove, and synth pads for vibrant rhythmic energy. BPM: 105. Length: 200 seconds\\n- Indie rock track with electric guitar riffs, bass, live drum kit, layered synths, and subtle strings for energetic yet emotional feel. BPM: 110. Length: 180 seconds\\n- Funk groove with slap bass, electric guitar chords, brass stabs, drums, congas, and rhythmic keyboards creating high-energy danceable rhythm. BPM: 105. Length: 180 seconds\\n- Drum and bass track with fast breakbeat drums, deep sub-bass, sharp synth leads, pads, and atmospheric FX for high-energy club motion. BPM: 175. Length: 150 seconds\\n- Dark ambient track with drones, distant bells, low rumbles, soft wind textures, and synth pads producing eerie immersive tension. BPM: 50. Length: 300 seconds\\n- Tropical house track with marimba, steel drums, soft synths, smooth bass, layered percussion, and light piano riffs for sunny chill dance vibes. BPM: 110. Length: 180 seconds\\n- Progressive rock track with electric guitar leads, organ, bass, drum kit, synth layers, and occasional strings for epic layered energy. BPM: 100. Length: 220 seconds\\n- Music box melody with delicate metallic tones and soft resonance, lullaby style, with gentle ambient reverb. BPM: 60. Length: 20 seconds\\n- Soft piano arpeggio with warm felted tone and slow attack, lullaby style, with intimate room ambience. BPM: 60. Length: 30 seconds\\n- Harp gentle plucked pattern with airy resonance, lullaby style, with dreamy reverb tail. BPM: 65. Length: 25 seconds\\n- Acoustic guitar fingerstyle pattern with warm nylon strings and soft dynamics, lullaby style, with subtle room resonance. BPM: 60. Length: 30 seconds\\n- Ambient synth pad with smooth evolving texture and soft harmonics, lullaby style, with wide stereo ambience. BPM: 50. Length: 40 seconds\\n- Early rock piano with walking left-hand bass line, shuffle rhythms, and blues scale improvisations in energetic 1950s boogie-woogie style. BPM: 160. Length: 180 seconds\\n- Trip Hop track with jazzy sampled vibraphone, mid-tempo breakbeat drums, harp, Latin ethnic percussion, and sweeping cinematic strings creating airy, relaxing, soulful lounge vibes. BPM: 90. Length: 180 seconds\\n- Country outlaw cinematic instrumental with blues pedal steel guitar, rustic mandolin, fiddle call-and-response, tape-driven rattly drum kit, autoharp, and soaring accordion solo for raw, emotional southern blues expression. BPM: 85. Length: 200 seconds\\n- Neo Classical track with sweeping string section, elegant horns, and delicate piano creating soothing, hypnotic, modern, soft, and classic mood. BPM: 70. Length: 180 seconds\\n- Art Rock desert track with desolate piano chords, western-themed rhythm guitars, unique lead guitars, rattly vintage drum kit, and supporting bass creating lonely, expansive, beautiful, and strange atmospheres. BPM: 95. Length: 180 seconds\\n- Cinematic Sci-Fi score with dramatic horn section, building marcato strings, gliding bassoon, thunderous cymbals, subdued timpani, and subtle synth drones producing awe-inspiring, uplifting, epic intergalactic energy. BPM: 100. Length: 220 seconds\\n- West Coast Hip Hop instrumental with cascading harp melodies, smooth Rhodes piano chops, vintage boom bap drums, and walking double bass producing raw, street, and soulful block-party vibes. BPM: 92. Length: 180 seconds\\n- Synthwave futuristic track with pulsating synth bass, exciting chords, soaring leads, and reverberating drum machine patterns creating gritty, pounding, and cool energy. BPM: 110. Length: 180 seconds\\n- Breakbeat track with complex percussion, intricate breakbeats, gritty synths, lush pads, and 808 bassline producing fresh, modern, futuristic, and rave-ready energy. BPM: 140. Length: 160 seconds\\n- Lounge Jazz 1960s smooth track with laid-back drums, piano chords, double bass, soft electric piano, subtle flute, and unique percussion creating beautiful, atmospheric, eclectic, retro, and chill vibes. BPM: 85. Length: 180 seconds\\n- Latin Jazz 1950s blissful track with laid-back Latin drums, euphoric piano chords, double bass, orchestral accompaniment, acoustic guitar, and vibraphone producing nostalgic, beautiful, atmospheric, cinematic, and chill mood. BPM: 95. Length: 180 seconds\\n- Acid Jazz 1970s summertime track with smooth electric piano, trippy synth leads, laid-back vintage drum kit, fuzzy electric bass, and uplifting violin producing retro, psychedelic, jazzy, relaxing energy. BPM: 100. Length: 180 seconds\\n- Progressive Soul 1970s track with feel-good piano, psychedelic organ, groovy vintage drum kit with percussion, fuzzy electric bass, and synth strings producing retro, raw, soulful, joyous atmosphere. BPM: 90. Length: 180 seconds\\n- Discotheque 1970s French-inspired track with sultry piano, psychedelic guitars, groovy drum kit, fuzzy electric bass, and melancholic organ producing retro, raw, laid-back, and relaxing mood. BPM: 105. Length: 180 seconds\\n- Soul Jazz 1970s track with expressive saxophone, smooth piano, groovy drum kit, rhythmic upright bass, sweeping strings, and minimal vibraphone producing retro, raw, laid-back, and epic energy. BPM: 95. Length: 180 seconds\\n- Vintage R&B 1970s live studio track with subtle brass, smooth piano, sweeping strings, and minimal drums producing retro, beautiful, uplifting, nostalgic mood. BPM: 85. Length: 180 seconds\\n- 50s Pop track with Latin influence, string section, bold brass, vibraphone, acoustic guitar, flute, ethnic percussion, and brushed drums creating sexy, epic, vintage, retro, melancholic, jazzy, dramatic energy. BPM: 100. Length: 180 seconds\\n- A piece of calm, quiet, mellow, serene music perfect for a peaceful film score, featuring soft modulating piano, ambient sfx and foley, beautiful vibraphone, and subtle synthesizer drones. The mood is cinematic, thoughtful, serene and nostalgic. BPM: 55. Length: 300 seconds\",\n \"Instrument\": \"You are a music metadata expert. Given an instrument, generate a descriptive prompt for a generative audio model.\\n\\n1. Identify the instrument.\\n2. Add playing style or technique.\\n3. Include details about material, timbre, or texture.\\n4. Add musical style or mood. Specify the genre, context, or emotional character.\\n5. Add spatial or production qualities.\\n6. Specify BPM: Always include a BPM appropriate to the style and context.\\n7. Specify length: Provide an integer in seconds (6–20 s for loops, 20–180 s for stems).\\n\\nExamples:\\n- Synth arpeggio loop with bright detuned oscillators. BPM: 120. Length: 8 seconds\\n- Chord stab loop with sharp percussive attack. BPM: 90. Length: 6 seconds\\n- Guitar muted strum loop with tight rhythmic feel. BPM: 100. Length: 8 seconds\\n- Pluck sequence loop with bright resonant tone. BPM: 128. Length: 10 seconds\\n- Marimba and vibraphone percussive loop with resonant wooden and metallic tones. BPM: 110. Length: 12 seconds\\n- Drum loop with deep muffled kick on beat one, snappy rimshot snare on beats two and four with rolling ghost note fills, and tight closed hi-hats with subtle open accents. BPM: 85. Length: 10 seconds\\n- Drum groove loop with brushed snare swinging on the ride, soft feathered kick on downbeats, and light closed hi-hat taps on the upbeats. BPM: 130. Length: 12 seconds\\n- Kick and hi-hat loop with four-on-the-floor punchy kick, tight closed hi-hats on every eighth note, and a sharp dry snare on beats two and four. BPM: 130. Length: 15 seconds\\n- Vinyl crackle drum loop with warm low-pass filtered kick, dusty snare with tape saturation, and shuffled closed hi-hats with subtle vinyl crackle ambiance. BPM: 80. Length: 10 seconds\\n- Ambient pad loop with evolving texture. BPM: 80. Length: 12 seconds\\n- Melodic synth bass groove loop with pumping sidechain feel. BPM: 122. Length: 10 seconds\\n- Melodic Bass slap and pop rhythm loop. BPM: 100. Length: 8 seconds\\n- Acoustic bass walking line loop with natural wooden resonance. BPM: 120. Length: 12 seconds\\n- String pizzicato motif loop, suspenseful, with tight string texture. BPM: 90. Length: 8 seconds\\n- Brass staccato riff loop with sharp bright attack. BPM: 130. Length: 10 seconds\\n- Flute airy melodic loop with wooden headjoint resonance. BPM: 100. Length: 6 seconds\\n- Pan flute ambient loop with breathy timbre. BPM: 75. Length: 8 seconds\\n- Clarinet riff loop with warm smooth reed tone. BPM: 120. Length: 10 seconds\\n- Oboe motif loop, orchestral, with rich double reed resonance. BPM: 80. Length: 8 seconds\\n- Recorder Renaissance motif loop with soft wooden timbre. BPM: 100. Length: 6 seconds\\n- Electric sitar riff loop with buzzing resonant tone. BPM: 90. Length: 10 seconds\\n- Koto plucked motif loop with resonant wooden strings. BPM: 90. Length: 8 seconds\\n- Shamisen folk melody loop with percussive twang. BPM: 100. Length: 8 seconds\\n- Banjo fingerpicking loop with metallic string resonance. BPM: 110. Length: 10 seconds\\n- Mandolin tremolo loop with crisp wooden body tone. BPM: 120. Length: 10 seconds\\n- Acoustic guitar chord vamp loop with natural room resonance. BPM: 110. Length: 12 seconds\\n- Nylon string guitar arpeggio loop with warm, soft timbre. BPM: 90. Length: 15 seconds\\n- Electric guitar riff loop with driven distorted tone. BPM: 130. Length: 10 seconds\\n- Slide guitar melody loop with warm resonant glide. BPM: 100. Length: 12 seconds\\n- Steel guitar slide loop with bright pedal steel tone. BPM: 95. Length: 12 seconds\\n- Harpsichord arpeggio loop with crisp plucked attack. BPM: 120. Length: 10 seconds\\n- Rhodes chord vamp loop with warm electric piano tone. BPM: 100. Length: 12 seconds\\n- Clavinet funky rhythm loop. BPM: 105. Length: 10 seconds\\n- Organ chord vamp loop with full drawbar warmth. BPM: 90. Length: 12 seconds\\n- Drum loop with booming 808 kick on beat one, crisp snare on beat three, and rapid triplet hi-hat rolls with open hat accents for aggressive high-energy feel. BPM: 140. Length: 8 seconds\\n- Breakbeat drum loop with chopped Amen-style snare flurries, driving kick on the one, fast sixteenth-note closed hi-hats, and syncopated open hat accents. BPM: 170. Length: 10 seconds\\n- Glitch percussion loop with stuttered kick transients, randomised snare hits processed with bit-crushing, and erratic hi-hat patterns with pitch-shifted metallic ticks. BPM: 120. Length: 12 seconds\\n- Metallic hits loop with distorted kick impacts, processed metal-plate snare slams, and grinding hi-hat noise bursts for aggressive mechanical texture. BPM: 120. Length: 10 seconds\\n- Timpani hits loop, cinematic, with deep resonant kick-like timpani strikes on beat one, rolling snare-style timpani fills, and no hi-hats for a grand orchestral feel. BPM: 70. Length: 8 seconds\\n- Snare roll loop, dramatic, with accelerating snare drum rolls building from soft to crashing, deep supporting kick pulses, and no hi-hats for maximum impact. BPM: 100. Length: 8 seconds\\n- Accordion motif loop with bright reedy bellows tone. BPM: 100. Length: 10 seconds\\n- Harmonica blues riff loop with expressive reed timbre. BPM: 90. Length: 10 seconds\\n- Trombone riff loop with warm sliding brass tone. BPM: 120. Length: 10 seconds\\n- French horn melodic loop, cinematic. BPM: 80. Length: 12 seconds\\n- Soprano sax ballad loop. BPM: 70. Length: 12 seconds\\n- Alto sax bebop riff loop. BPM: 200. Length: 10 seconds\\n- Electric violin melodic loop with reverb. BPM: 90. Length: 10 seconds\\n- String pad loop with cinematic texture. BPM: 70. Length: 15 seconds\\n- Granular synth evolving texture loop. BPM: 90. Length: 15 seconds\\n- Piano motif loop with soft felt hammer tone. BPM: 80. Length: 10 seconds\\n- Pad and synth loop with lush detuned shimmer. BPM: 85. Length: 12 seconds\\n- Synth lead loop with sidechain pumping compression. BPM: 128. Length: 10 seconds\\n- Analog synth bassline loop with deep warm low-end. BPM: 122. Length: 12 seconds\\n- FM synth lead motif loop with bright metallic shimmer. BPM: 110. Length: 10 seconds\\n- Bass groove loop with tight rhythmic two-bar pattern. BPM: 100. Length: 16 seconds\\n- Acoustic guitar fingerstyle motif loop with warm wood resonance. BPM: 90. Length: 45 seconds\\n- Sombre acoustic guitar motif loop with cavernous reverb, delicate fingerpicking, and expressive melancholic tone. BPM: 70. Length: 45 seconds\\n- Electric guitar rock riff motif loop. BPM: 130. Length: 40 seconds\\n- Vintage electric guitar motif loop, live-recorded in a vintage studio, with expressive and dynamic solo performance. BPM: 90. Length: 40 seconds\\n- Piano chord progression motif loop with rich harmonic movement. BPM: 120. Length: 60 seconds\\n- String ensemble cinematic motif loop with rich wooden resonance. BPM: 80. Length: 120 seconds\\n- Brass ensemble cinematic motif loop with bright metallic timbre. BPM: 90. Length: 90 seconds\\n- Ethnic percussion ensemble motif loop with deep resonant djembe kick tones, slapped snare-like rim hits on congas, and layered shakers and bells providing hi-hat-like rhythmic texture with polyrhythmic patterns. BPM: 100. Length: 90 seconds\\n- Synth ambient motif loop with evolving textures. BPM: 80. Length: 180 seconds\\n- Motif loop with warm dusty vinyl crackle and tape saturation. BPM: 80. Length: 60 seconds\\n- Synth lead and bass motif loop with bright punchy energy. BPM: 128. Length: 90 seconds\\n- Funk band motif loop: bass, drums, guitar. BPM: 100. Length: 90 seconds\\n- Ethnic flute motif for cinematic use. BPM: 80. Length: 30 seconds\\n- Steel drum melodic motif loop with bright metallic resonance. BPM: 110. Length: 20 seconds\\n- Marimba percussive motif loop with resonant wooden tone. BPM: 100. Length: 20 seconds\\n- Vibraphone melodic motif loop with metallic shimmer. BPM: 90. Length: 25 seconds\\n- Piano cinematic motif loop with resonant wooden tone. BPM: 80. Length: 30 seconds\\n- Violin expressive cinematic motif loop with rich wooden resonance. BPM: 75. Length: 25 seconds\\n- Cello expressive motif loop with deep wooden resonance. BPM: 70. Length: 30 seconds\\n- Trumpet expressive motif loop with brassy overtones. BPM: 100. Length: 25 seconds\\n- Sax expressive motif loop with warm reed timbre. BPM: 95. Length: 25 seconds\\n- Ethnic drum ensemble motif loop with booming natural-skin bass drum kicks, sharp hand-slap snare accents on djembes and talking drums, and layered wooden and metal percussion providing rhythmic hi-hat-like patterns. BPM: 95. Length: 30 seconds\\n- Ambient drone motif loop. BPM: 60. Length: 180 seconds\\n- Orchestral tension motif loop. BPM: 90. Length: 150 seconds\\n- Electronic track motif loop with drums, bass, synth. BPM: 128. Length: 180 seconds\",\n \"SFX\": \"You are a professional sound design expert. Convert the user's input into a precise, vivid sound effects description suitable for generative audio models.\\n\\nDescribe clearly:\\n- Sound source\\n- Physical character (texture, timbre, material: metal, wood, glass, concrete, etc.)\\n- Spatial qualities (indoor/outdoor, cave/open field/underwater, dry/reverberant, close-up/distant, echoing/muffled)\\n- Temporal evolution (attack, decay, movement, transitions over time)\\n- Include motion or spatial movement if applicable (passing, approaching, stereo movement)\\n\\nAudio length rules:\\n- Very short sounds (impacts, clicks, gunshots): 1–3 seconds\\n- Medium actions (footsteps, object movement, transitions): 3–6 seconds\\n- Ambience / environments: 6–15 seconds\\n- Always append: Length: X seconds (integer only, no decimals).\\n\\nOutput constraints:\\n- Length: 1–2 dense sentences maximum\\n- Output ONLY the final rewritten prompt\\n- No explanations, no formatting, no quotes\\n- Use concise but dense technical language\\n- Focus strictly on sound effects or ambience\\n- Always append: Length: X seconds (integer only, no decimals).\\n\\nQuality guidelines:\\n- Be specific and avoid vague terms\\n- Prioritize clarity and realism\\n- Combine elements into one coherent scene\\n- Avoid redundancy\\n\\nExamples:\\n- Heavy rain hitting a metal roof during a thunderstorm, distant thunder rumbles, stereo, realistic ambience. Length: 45 seconds\\n- Quiet forest at dawn with birds chirping, soft wind through leaves, distant stream flowing. Length: 60 seconds\\n- Busy city street at night, cars passing, muffled conversations, occasional horn, urban ambience. Length: 50 seconds\\n- Ocean waves crashing against rocky cliffs, strong wind, dramatic and cinematic. Length: 70 seconds\\n- Wooden door creaking open slowly in an old house, echoing interior, eerie tone. Length: 3 seconds\\n- Glass bottle shattering on concrete, sharp impact, scattered fragments. Length: 2 seconds\\n- Footsteps on gravel, steady walking pace, close perspective. Length: 8 seconds\\n- Typing rapidly on a mechanical keyboard, crisp tactile clicks. Length: 5 seconds\\n- Punch impact with deep bass hit, cinematic trailer style. Length: 2 seconds\\n- Car speeding past at high velocity, doppler effect, realistic whoosh. Length: 3 seconds\\n- Object falling from height and hitting ground with a heavy thud. Length: 2 seconds\\n- Sword swing whooshing through air, fast motion, clean metallic tone. Length: 2 seconds\\n- Futuristic laser blast, clean energy pulse, high-tech sound design. Length: 1 seconds\\n- Spaceship engine humming, low frequency rumble, interior perspective. Length: 90 seconds\\n- Magical spell casting, shimmering particles, rising tonal energy. Length: 8 seconds\\n- Teleportation effect, glitchy digital distortion with a soft whoosh. Length: 5 seconds\\n- Dark eerie drone with distant whispers, creepy, slow build tension. Length: 120 seconds\\n- Sudden horror jump scare sting, sharp violin hit, cinematic. Length: 1 second\\n- Metal scraping slowly in a dark tunnel, echoing and ominous. Length: 20 seconds\\n- Explosion with debris scattering, deep bass, cinematic realism. Length: 4 seconds\\n- Building collapsing, rumbling concrete, dust and debris falling. Length: 25 seconds\\n- Fire crackling intensely, wood burning, close-up detail. Length: 80 seconds\\n- Gunshot in a large empty warehouse, loud echo decay. Length: 2 seconds\\n- Retro arcade coin insert sound, 8-bit style. Length: 1 second\\n- Level up chime, bright, rewarding, fantasy RPG style. Length: 2 seconds\\n- Error buzzer, short, digital, UI feedback. Length: 1 second\\n- Menu navigation clicks, soft futuristic interface sounds. Length: 3 seconds\\n- Layered soundscape: rain, thunder, footsteps, and distant sirens all blending naturally. Length: 90 seconds\\n- Rapid sequence of three impacts: metal hit, glass break, wood crack, spaced evenly. Length: 4 seconds\\n- Sound moving from left to right stereo field: passing motorcycle. Length: 5 seconds\\n- Close vs far perspective transition: footsteps approaching then fading away. Length: 6 seconds\\n- Tape stop sub drop, a massive sub-bass note that mimics a vinyl record or tape machine being turned off, the pitch and speed drop simultaneously, causing the high-end harmonics to smear and thicken as the sound grinds to a halt at a sub-sonic frequency. Length: 11 seconds\\n- Gravel and leaves footsteps, the sound of a hard boot stepping onto dry leaves or gravel, crisp and natural with detailed texture. Length: 11 seconds\\n- Ghostship moan, a massive, deep wooden groan with a low-frequency moan, like heavy timber under immense structural tension, swaying slowly, processed with long, dark wooden room reverb for a sense of scale. Length: 11 seconds\\n- Bicycle chain, a continuous metallic whirring sound of a chain moving over sprockets, with individual teeth catching the links, processed with resonant band-pass filter to emphasize metallic singing. Length: 11 seconds\\n- Warp drive, a sound that starts with a massive suck-back of ambient noise, followed by a supersonic crack and high-pitched zing that disappears into the distance, giving the sense of stretching space-time. Length: 11 seconds\\n- Ice cubes, high-pitched musical clinking of hard ice hitting a thin glass, bright resonant ring with subtle liquid sloshing around the edges. Length: 11 seconds\\n- Paper shuffle, the sound of a thick stack of heavy bond paper being squared up on a desk, dry papery thud with a quick fanning sound as air moves between the pages. Length: 11 seconds\\n- Drawer slam, a blunt, powerful thud made by slamming a wooden desk drawer shut, pronounced low-mid body, slightly distorted for aggressive character. Length: 3 seconds\",\n \"One-shot\": \"You are a music metadata expert. Given an instrument or sound, generate a descriptive prompt for a short, isolated one-shot audio sample for music production.\\n\\n1. Identify the instrument or sound source.\\n2. Describe the playing technique or hit type (e.g., pluck, slam, tap, stab).\\n3. Include details about material, timbre, or texture.\\n4. Add spatial or production qualities (dry/wet, room, close-mic).\\n5. Specify length: short integer in seconds (1–11 s).\\n\\nExamples:\\n- Piano key hit with bright percussive attack and resonant wooden body. Length: 2 seconds\\n- Kick drum punchy low-end hit with warm skin resonance. Length: 2 seconds\\n- Snare drum rimshot accent with crisp snare wires. Length: 2 seconds\\n- Acoustic guitar fingerstyle note with warm spruce tone. Length: 3 seconds\\n- Bass pluck with jazzy tone and resonant wooden body. Length: 3 seconds\\n- Electric guitar power chord with distortion. Length: 3 seconds\\n- Metallic glitch percussion hit with sharp metallic texture. Length: 2 seconds\\n- Tabla resonant tone hit with natural skin timbre. Length: 2 seconds\\n- Djembe slap accent with dry wooden resonance. Length: 2 seconds\\n- Synth stab with reverb tail. Length: 3 seconds\\n- Violin expressive note with vibrato and rich wooden resonance. Length: 3 seconds\\n- Cello legato note, cinematic, with warm resonant body. Length: 3 seconds\\n- Trumpet bright accent with slightly brassy overtones. Length: 2 seconds\\n- Melodic saxophone jazz riff with smooth reed timbre and a slight vibrato bend. Length: 3 seconds\\n- Harp pluck with airy tone and resonant strings. Length: 2 seconds\\n- Glockenspiel bell-like note with bright metallic clarity. Length: 2 seconds\\n- Metallic clang sound design hit. Length: 2 seconds\\n- Granular texture hit. Length: 3 seconds\\n- Reversed piano hit. Length: 2 seconds\\n- Synth riser effect. Length: 6 seconds\\n- Percussion impact hit. Length: 2 seconds\\n- Cinematic hit. Length: 2 seconds\\n- Dry clap, a crisp, natural single hand clap recorded in a dead room with an extremely sharp transient and no room reflections. Length: 1 second\\n- Studio hat, a classic, natural recording of 14-inch hi-hats played tightly closed, zero ring, very fast decay. Length: 1 second\\n- Disco open hat, bright 14-inch open hi-hat with long, shimmering decay, perfect for disco or dance grooves. Length: 1 second\\n- Pillow kick, acoustic kick drum muffled with a heavy blanket, producing a short, dry \\\"thump\\\" with almost zero resonance. Length: 1 second\\n- Short 808, punchy 808 kick with sharp, distorted transient and fast-decaying sub-tail. Length: 1 second\\n- Egg shaker, classic plastic egg shaker recorded with a small-diaphragm condenser mic, producing a light, consistent \\\"tick\\\" with very short sustain. Length: 1 second\\n- African drums, dynamic African drums and percussion ensemble with natural acoustic textures. Length: 3 seconds\\n- Latin drums, dynamic Latin drums and percussion ensemble featuring authentic rhythmic patterns. Length: 3 seconds\\n- String quartet, euphoric string quartet with dynamic and emotional playing, full of expressive harmonies and movement. Length: 3 seconds\\n- Piano, nostalgic, atmospheric piano piece with dynamic and emotional performance, intimate and resonant. Length: 3 seconds\\n- Analogue drift pad, warm polyphonic pad with three detuned oscillators (saw + triangle), subtle pitch drift, and lush bucket-brigade chorus for wide, nostalgic stereo image. Length: 11 seconds\\n- Phase distortion bass, Casio CZ-style phase-distorted sine wave warped into a jagged sawtooth for retro synth bass tone. Length: 11 seconds\\n- Vibrato saxophone, bright lyrical alto sax with fast fluttery vibrato, reedy vintage tone, captured with ribbon mic for warm nostalgic sound. Length: 11 seconds\\n- Lofi upright bass, upright bass recorded with ribbon mic in a wooden room, natural air with slightly boxy resonance, tape-saturated for dusty 1950s jazz feel. Length: 2 seconds\"\n}", + "Music" + ] + }, + { + "id": 40, + "type": "StringReplace", + "pos": [ + 1350, + 900 + ], + "size": [ + 260, + 280 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": 59 + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 58 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 60 + ] + } + ], + "title": "Text Replace (AUDIO LENGTH)", + "properties": { + "Node name for S&R": "StringReplace" + }, + "widgets_values": [ + "", + "AUDIO_LENGTH", + "" + ] + }, + { + "id": 38, + "type": "StringReplace", + "pos": [ + 720, + 900 + ], + "size": [ + 290, + 280 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": null + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 66 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 52 + ] + } + ], + "title": "Text Replace (PROMPT TEMPLATE)", + "properties": { + "Node name for S&R": "StringReplace" + }, + "widgets_values": [ + "SYSTEM_PROMPTS\n\nInput: USER_INPUT\nTarget audio length: AUDIO_LENGTH seconds.\nOutput:", + "SYSTEM_PROMPTS", + "" + ] + }, + { + "id": 35, + "type": "PrimitiveBoolean", + "pos": [ + -390, + 570 + ], + "size": [ + 400, + 100 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 83 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 48 + ] + } + ], + "title": "Boolean (Enable_Reprompt)", + "properties": { + "Node name for S&R": "PrimitiveBoolean" + }, + "widgets_values": [ + true + ] + }, + { + "id": 36, + "type": "PrimitiveFloat", + "pos": [ + -390, + 410 + ], + "size": [ + 400, + 110 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "FLOAT", + "widget": { + "name": "value" + }, + "link": 82 + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 50, + 56 + ] + } + ], + "title": "Float (Duration)", + "properties": { + "Node name for S&R": "PrimitiveFloat" + }, + "widgets_values": [ + 150 + ] + }, + { + "id": 25, + "type": "CheckpointLoaderSimple", + "pos": [ + 100, + 130 + ], + "size": [ + 440, + 190 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 79 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 30 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 39 + ] + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "models": [ + { + "name": "stable_audio_3_medium_base.safetensors", + "url": "https://huggingface.co/Comfy-Org/stable-audio-3/resolve/main/checkpoints/stable_audio_3_medium_base.safetensors", + "directory": "checkpoints" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "stable_audio_3_medium_base.safetensors" + ] + }, + { + "id": 26, + "type": "CLIPLoader", + "pos": [ + 100, + 390 + ], + "size": [ + 440, + 170 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 80 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 34, + 35 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "models": [ + { + "name": "t5gemma_b_b_ul2.safetensors", + "url": "https://huggingface.co/Comfy-Org/stable-audio-3/resolve/main/text_encoders/t5gemma_b_b_ul2.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "t5gemma_b_b_ul2.safetensors", + "stable_audio", + "default" + ] + }, + { + "id": 54, + "type": "PreviewAny", + "pos": [ + 1720, + 1580 + ], + "size": [ + 420, + 550 + ], + "flags": {}, + "order": 20, + "mode": 4, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 84 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": null + } + ], + "properties": { + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [ + null, + null, + null + ] + } + ], + "groups": [ + { + "id": 1, + "title": "Loaders: checkpoint & CLIP", + "bounding": [ + 80, + 50, + 485.721654232725, + 527.2848777754299 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 2, + "title": "CLIP encode: conditioning", + "bounding": [ + 600, + 60, + 470, + 510 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 3, + "title": "User inputs: prompt & duration", + "bounding": [ + -400, + 10, + 430, + 740 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 7, + "title": "Reprompt: full branch (template + LLM)", + "bounding": [ + 60, + 780, + 1630, + 1360 + ], + "color": "#444", + "flags": {} + }, + { + "id": 4, + "title": "Reprompt: JSON extract & template fills", + "bounding": [ + 120, + 820, + 1520, + 650 + ], + "color": "#444", + "flags": {} + }, + { + "id": 5, + "title": "Helpers: duration to string", + "bounding": [ + 1340, + 1180, + 280, + 250 + ], + "color": "#444", + "flags": {} + }, + { + "id": 6, + "title": "Reprompt: Qwen TextGenerate", + "bounding": [ + 680, + 1510, + 960, + 614.65625 + ], + "color": "#444", + "flags": {} + }, + { + "id": 8, + "title": "Audio generation: Stable Audio", + "bounding": [ + 60, + 10, + 1627.3616782294932, + 737.0545987464304 + ], + "color": "#3f789e", + "flags": {} + } + ], + "links": [ + { + "id": 35, + "origin_id": 26, + "origin_slot": 0, + "target_id": 7, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 13, + "origin_id": 3, + "origin_slot": 0, + "target_id": 12, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 39, + "origin_id": 25, + "origin_slot": 2, + "target_id": 12, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 50, + "origin_id": 36, + "origin_slot": 0, + "target_id": 11, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 30, + "origin_id": 25, + "origin_slot": 0, + "target_id": 3, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 4, + "origin_id": 6, + "origin_slot": 0, + "target_id": 3, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 6, + "origin_id": 7, + "origin_slot": 0, + "target_id": 3, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 12, + "origin_id": 11, + "origin_slot": 0, + "target_id": 3, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 34, + "origin_id": 26, + "origin_slot": 0, + "target_id": 6, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 49, + "origin_id": 34, + "origin_slot": 0, + "target_id": 6, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 47, + "origin_id": 31, + "origin_slot": 0, + "target_id": 34, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 46, + "origin_id": 28, + "origin_slot": 0, + "target_id": 34, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 48, + "origin_id": 35, + "origin_slot": 0, + "target_id": 34, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 56, + "origin_id": 36, + "origin_slot": 0, + "target_id": 41, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 57, + "origin_id": 41, + "origin_slot": 1, + "target_id": 42, + "target_slot": 0, + "type": "INT" + }, + { + "id": 52, + "origin_id": 38, + "origin_slot": 0, + "target_id": 39, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 53, + "origin_id": 31, + "origin_slot": 0, + "target_id": 39, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 40, + "origin_id": 29, + "origin_slot": 0, + "target_id": 28, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 60, + "origin_id": 40, + "origin_slot": 0, + "target_id": 28, + "target_slot": 4, + "type": "STRING" + }, + { + "id": 65, + "origin_id": 43, + "origin_slot": 0, + "target_id": 49, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 59, + "origin_id": 39, + "origin_slot": 0, + "target_id": 40, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 58, + "origin_id": 42, + "origin_slot": 0, + "target_id": 40, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 66, + "origin_id": 49, + "origin_slot": 0, + "target_id": 38, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 27, + "origin_id": 12, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "AUDIO" + }, + { + "id": 68, + "origin_id": -10, + "origin_slot": 0, + "target_id": 31, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 76, + "origin_id": -10, + "origin_slot": 2, + "target_id": 3, + "target_slot": 4, + "type": "INT" + }, + { + "id": 78, + "origin_id": -10, + "origin_slot": 4, + "target_id": 43, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 79, + "origin_id": -10, + "origin_slot": 5, + "target_id": 25, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 80, + "origin_id": -10, + "origin_slot": 6, + "target_id": 26, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 81, + "origin_id": -10, + "origin_slot": 7, + "target_id": 29, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 82, + "origin_id": -10, + "origin_slot": 1, + "target_id": 36, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 83, + "origin_id": -10, + "origin_slot": 3, + "target_id": 35, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 84, + "origin_id": 28, + "origin_slot": 0, + "target_id": 54, + "target_slot": 0, + "type": "STRING" + } + ], + "extra": {}, + "category": "Audio/Music generation", + "description": "Generates music, instrument loops, sound effects, and one-shots from text using the Stable Audio 3 Medium base checkpoint, with optional Qwen 3.5 category-based prompt expansion (Music, Instrument, SFX, One-shot)." + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Audio Generation (Stable Audio 3 Medium).json b/blueprints/Audio Generation (Stable Audio 3 Medium).json new file mode 100644 index 000000000..30add5b05 --- /dev/null +++ b/blueprints/Audio Generation (Stable Audio 3 Medium).json @@ -0,0 +1,2091 @@ +{ + "revision": 0, + "last_node_id": 52, + "last_link_id": 0, + "nodes": [ + { + "id": 52, + "type": "8b66c757-fe2f-4184-91f3-479a19deb565", + "pos": [ + 370, + 1120 + ], + "size": [ + 420, + 450 + ], + "flags": { + "collapsed": false + }, + "order": 0, + "mode": 0, + "inputs": [ + { + "label": "user_input", + "name": "user_input", + "type": "STRING", + "widget": { + "name": "user_input" + }, + "link": null + }, + { + "label": "duration", + "name": "duration", + "type": "FLOAT", + "widget": { + "name": "duration" + }, + "link": null + }, + { + "label": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "label": "use_reprompt", + "name": "use_reprompt", + "type": "BOOLEAN", + "widget": { + "name": "use_reprompt" + }, + "link": null + }, + { + "label": "reprompt_category", + "name": "category", + "type": "COMBO", + "widget": { + "name": "category" + }, + "link": null + }, + { + "label": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": null + }, + { + "label": "sa_clip", + "name": "sa_clip", + "type": "COMBO", + "widget": { + "name": "sa_clip" + }, + "link": null + }, + { + "label": "qwen_clip", + "name": "qwen_clip", + "type": "COMBO", + "widget": { + "name": "qwen_clip" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "AUDIO", + "name": "AUDIO", + "type": "AUDIO", + "links": [] + } + ], + "title": "Audio Generation (Stable Audio 3 Medium)", + "properties": { + "proxyWidgets": [ + [ + "31", + "value" + ], + [ + "36", + "value" + ], + [ + "3", + "seed" + ], + [ + "35", + "value" + ], + [ + "43", + "choice" + ], + [ + "25", + "ckpt_name" + ], + [ + "26", + "clip_name" + ], + [ + "29", + "clip_name" + ] + ] + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "8b66c757-fe2f-4184-91f3-479a19deb565", + "version": 1, + "state": { + "lastGroupId": 8, + "lastNodeId": 56, + "lastLinkId": 84, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Audio Generation (Stable Audio 3 Medium)", + "inputNode": { + "id": -10, + "bounding": [ + -810, + 400, + 155.953125, + 208 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1750, + 1041, + 128, + 68 + ] + }, + "inputs": [ + { + "id": "78ae2515-114b-494a-becc-43c7b6c2dc2f", + "name": "user_input", + "type": "STRING", + "linkIds": [ + 68 + ], + "label": "user_input", + "pos": [ + -678.046875, + 424 + ] + }, + { + "id": "5ca95030-aff4-4544-b545-f0d814e0e49a", + "name": "duration", + "type": "FLOAT", + "linkIds": [ + 82 + ], + "label": "duration", + "pos": [ + -678.046875, + 444 + ] + }, + { + "id": "718eb10f-da1a-4cea-a9c7-3040f98fe960", + "name": "seed", + "type": "INT", + "linkIds": [ + 76 + ], + "label": "seed", + "pos": [ + -678.046875, + 464 + ] + }, + { + "id": "dc020099-39e6-4009-9937-408409d71736", + "name": "use_reprompt", + "type": "BOOLEAN", + "linkIds": [ + 83 + ], + "label": "use_reprompt", + "pos": [ + -678.046875, + 484 + ] + }, + { + "id": "edae394c-6324-44d6-8ac5-d8caa5ae2169", + "name": "category", + "type": "COMBO", + "linkIds": [ + 78 + ], + "label": "reprompt_category", + "pos": [ + -678.046875, + 504 + ] + }, + { + "id": "be19b747-6a47-4028-9c30-d52f54a712ea", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 79 + ], + "label": "ckpt_name", + "pos": [ + -678.046875, + 524 + ] + }, + { + "id": "bc9241a2-bc20-4c5d-8cb1-f2958f598642", + "name": "sa_clip", + "type": "COMBO", + "linkIds": [ + 80 + ], + "label": "sa_clip", + "pos": [ + -678.046875, + 544 + ] + }, + { + "id": "a33a2468-6d6d-4cb6-937c-3510bf16ebac", + "name": "qwen_clip", + "type": "COMBO", + "linkIds": [ + 81 + ], + "label": "qwen_clip", + "pos": [ + -678.046875, + 564 + ] + } + ], + "outputs": [ + { + "id": "bbe988dd-5c03-44fd-a965-c712f9204988", + "name": "AUDIO", + "type": "AUDIO", + "linkIds": [ + 27 + ], + "localized_name": "AUDIO", + "pos": [ + 1774, + 1065 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + 620, + 420 + ], + "size": [ + 440, + 140 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 35 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 6 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 12, + "type": "VAEDecodeAudio", + "pos": [ + 1450, + 110 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 13 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 39 + } + ], + "outputs": [ + { + "localized_name": "AUDIO", + "name": "AUDIO", + "type": "AUDIO", + "slot_index": 0, + "links": [ + 27 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecodeAudio", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 11, + "type": "EmptyLatentAudio", + "pos": [ + 630, + 610 + ], + "size": [ + 430, + 140 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "seconds", + "name": "seconds", + "type": "FLOAT", + "widget": { + "name": "seconds" + }, + "link": 50 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 12 + ] + } + ], + "properties": { + "Node name for S&R": "EmptyLatentAudio", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 60, + 1 + ] + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + 1100, + 100 + ], + "size": [ + 320, + 350 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 30 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 4 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 6 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 12 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 76 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 13 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + "randomize", + 8, + 1, + "lcm", + "simple", + 1 + ] + }, + { + "id": 29, + "type": "CLIPLoader", + "pos": [ + 690, + 1580 + ], + "size": [ + 430, + 170 + ], + "flags": {}, + "order": 8, + "mode": 0, + "showAdvanced": false, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 81 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 40 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "models": [ + { + "name": "qwen3.5_2b_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/Qwen3.5/resolve/main/text_encoders/qwen3.5_2b_bf16.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "qwen3.5_2b_bf16.safetensors", + "stable_diffusion", + "default" + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + 610, + 130 + ], + "size": [ + 450, + 240 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 34 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 49 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 4 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 34, + "type": "ComfySwitchNode", + "pos": [ + 210, + 610 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 47 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 46 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 48 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 49 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode" + }, + "widgets_values": [ + false + ] + }, + { + "id": 41, + "type": "ComfyMathExpression", + "pos": [ + 1370, + 1360 + ], + "size": [ + 230, + 80 + ], + "flags": { + "collapsed": true + }, + "order": 16, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT,BOOLEAN", + "link": 56 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 57 + ] + }, + { + "localized_name": "BOOL", + "name": "BOOL", + "type": "BOOLEAN", + "links": null + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "a" + ] + }, + { + "id": 42, + "type": "PreviewAny", + "pos": [ + 1370, + 1310 + ], + "size": [ + 230, + 40 + ], + "flags": { + "collapsed": true + }, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 57 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 58 + ] + } + ], + "properties": { + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 39, + "type": "StringReplace", + "pos": [ + 1040, + 900 + ], + "size": [ + 270, + 280 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": 52 + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 53 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 59 + ] + } + ], + "title": "Text Replace (USER INPUT)", + "properties": { + "Node name for S&R": "StringReplace" + }, + "widgets_values": [ + "", + "USER_INPUT", + "" + ] + }, + { + "id": 28, + "type": "TextGenerate", + "pos": [ + 1200, + 1580 + ], + "size": [ + 430, + 420 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 40 + }, + { + "localized_name": "image", + "name": "image", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "localized_name": "video", + "name": "video", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": null + }, + { + "localized_name": "prompt", + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": 60 + }, + { + "localized_name": "max_length", + "name": "max_length", + "type": "INT", + "widget": { + "name": "max_length" + }, + "link": null + }, + { + "localized_name": "sampling_mode", + "name": "sampling_mode", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "sampling_mode" + }, + "link": null + }, + { + "localized_name": "temperature", + "name": "sampling_mode.temperature", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.temperature" + }, + "link": null + }, + { + "localized_name": "top_k", + "name": "sampling_mode.top_k", + "type": "INT", + "widget": { + "name": "sampling_mode.top_k" + }, + "link": null + }, + { + "localized_name": "top_p", + "name": "sampling_mode.top_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.top_p" + }, + "link": null + }, + { + "localized_name": "min_p", + "name": "sampling_mode.min_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.min_p" + }, + "link": null + }, + { + "localized_name": "repetition_penalty", + "name": "sampling_mode.repetition_penalty", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.repetition_penalty" + }, + "link": null + }, + { + "localized_name": "seed", + "name": "sampling_mode.seed", + "type": "INT", + "widget": { + "name": "sampling_mode.seed" + }, + "link": null + }, + { + "localized_name": "presence_penalty", + "name": "sampling_mode.presence_penalty", + "shape": 7, + "type": "FLOAT", + "widget": { + "name": "sampling_mode.presence_penalty" + }, + "link": null + }, + { + "localized_name": "thinking", + "name": "thinking", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "thinking" + }, + "link": null + }, + { + "localized_name": "use_default_template", + "name": "use_default_template", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "use_default_template" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "generated_text", + "name": "generated_text", + "type": "STRING", + "links": [ + 46, + 84 + ] + } + ], + "properties": { + "Node name for S&R": "TextGenerate", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "", + 256, + "on", + 0.7, + 64, + 0.95, + 0.05, + 1.05, + 0, + 0, + false, + true + ] + }, + { + "id": 31, + "type": "PrimitiveStringMultiline", + "pos": [ + -390, + 160 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "STRING", + "widget": { + "name": "value" + }, + "link": 68 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 47, + 53 + ] + } + ], + "title": "User: short description (USER_INPUT in template)", + "properties": { + "Node name for S&R": "PrimitiveStringMultiline" + }, + "widgets_values": [ + "" + ] + }, + { + "id": 43, + "type": "CustomCombo", + "pos": [ + 140, + 910 + ], + "size": [ + 550, + 320 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "choice", + "name": "choice", + "type": "COMBO", + "widget": { + "name": "choice" + }, + "link": 78 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 65 + ] + }, + { + "localized_name": "INDEX", + "name": "INDEX", + "type": "INT", + "links": null + } + ], + "title": "Custom Combo (Category index)", + "properties": { + "Node name for S&R": "CustomCombo" + }, + "widgets_values": [ + "Music", + 0, + "Music", + "Instrument", + "SFX", + "One-shot", + "" + ] + }, + { + "id": 49, + "type": "JsonExtractString", + "pos": [ + 720, + 1200 + ], + "size": [ + 300, + 180 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "localized_name": "json_string", + "name": "json_string", + "type": "STRING", + "widget": { + "name": "json_string" + }, + "link": null + }, + { + "localized_name": "key", + "name": "key", + "type": "STRING", + "widget": { + "name": "key" + }, + "link": 65 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 66 + ] + } + ], + "properties": { + "Node name for S&R": "JsonExtractString" + }, + "widgets_values": [ + "{\n \"Music\": \"You are an expert musician and musicologist and prompt engineer. Transform the user's input into a detailed, vivid music prompt for a full instrumental track.\\n\\n1. Start with the genre or style and optional adjectives (e.g., upbeat, dreamy, aggressive).\\n2. List the main instruments that define the track.\\n3. Add supporting elements or layers such as pads, harmonics, effects, or field recordings.\\n4. Include rhythm or percussion elements like drums, hi-hats, congas, brushes, or polyrhythms.\\n5. Integrate mood and energy naturally in the sentence (e.g., \\\"creating suspenseful tension\\\" or \\\"bright and uplifting\\\").\\n6. Specify the BPM.\\n7. Specify the track length as an integer in seconds. Use ranges: energetic/dance 120-180s, pop/rock 180-210s, cinematic/ambient 240-300s.\\n8. Combine all elements into one natural, fluid sentence. Avoid semicolons.\\n\\nTemplate:\\nGenre/Style with main instruments, supporting instruments/layers, and rhythm/percussion creating mood/energy. BPM: X. Length: Y seconds\\n\\nExamples:\\n- Jazz ballad with smooth saxophone lead, piano chords, upright bass, brushed drums, and soft strings that swing gently for a warm and cozy evening. BPM: 85. Length: 180 seconds\\n- EDM festival track with pulsing synth leads, plucked arpeggios, layered pads, side-chained bass, punchy kick and snare, and hi-hat rolls creating bright, energetic, and uplifting dance energy. BPM: 128. Length: 150 seconds\\n- Lo-fi hip-hop chill track with mellow electric piano, soft vinyl crackle, subtle synth pads, low-pass filtered drums, percussion loops, and soft plucked bass for a relaxed, dreamy vibe. BPM: 75. Length: 150 seconds\\n- Heavy metal anthem with distorted electric guitars, bass guitar, double bass drums, and cymbal crashes with fast palm-muted riffs creating intense, aggressive energy. BPM: 160. Length: 180 seconds\\n- Melancholic piano piece with soft piano lead, string pads, subtle atmospheric synths, and minimal brush percussion evoking a reflective rainy-day feeling. BPM: 60. Length: 240 seconds\\n- Suspenseful electronic thriller with pulsing bass synth, arpeggiated lead synth, cinematic pads, glitchy percussion, and high string stabs creating dark and tense energy. BPM: 100. Length: 200 seconds\\n- Dreamy ambient soundscape with layered pads, soft bell textures, gentle drones, and wind and water field recordings for ethereal and spacious meditation. BPM: 40. Length: 300 seconds\\n- Fingerpicking acoustic guitar solo with harmonics, subtle reverb, occasional shaker and soft stomp percussion, and soft pad layers for warm intimate storytelling. BPM: 70. Length: 120 seconds\\n- Synthwave 80s retro track with arpeggiated synth leads, analog pads, electric bass, punchy electronic drums, gated reverb snares, and atmospheric FX for nostalgic and vibrant energy. BPM: 110. Length: 180 seconds\\n- Tribal percussion ensemble with congas, djembes, bongos, shakers, and frame drums layered with deep synthetic sub-bass in complex polyrhythms. BPM: 100. Length: 140 seconds\\n- 1920s swing jazz with brass section, upright bass, piano, brushed drums, banjo, clarinet, and soft strings that swing lively for energetic dance vibes. BPM: 110. Length: 180 seconds\\n- Futuristic electronic sci-fi track with pulsing bass synth, evolving lead synths, layered pads, glitch percussion, robotic FX, and sub-bass for tense cinematic energy. BPM: 125. Length: 200 seconds\\n- Ambient underwater soundscape with flowing water textures, soft piano motifs, synth drones, distant bells, and underwater reverb for spacious meditative immersion. BPM: 45. Length: 300 seconds\\n- Horror cinematic track with dissonant strings, eerie piano stabs, cinematic percussion including taiko and low toms, and synth FX producing suspenseful creepy tension. BPM: 90. Length: 240 seconds\\n- Reggae track with offbeat guitar, warm basslines, snare, kick, congas, and horn stabs giving laid-back groovy energy. BPM: 85. Length: 150 seconds\\n- Blues track with soulful electric guitar solos, walking bass, piano, and shuffle drums creating expressive and emotive storytelling. BPM: 90. Length: 180 seconds\\n- Latin salsa with congas, timbales, horns, piano montunos, bass, and layered percussion for vibrant danceable energy. BPM: 120. Length: 210 seconds\\n- Afrobeat track with electric guitar stabs, horns, layered percussion, congas, shakers, bass groove, and synth pads for vibrant rhythmic energy. BPM: 105. Length: 200 seconds\\n- Indie rock track with electric guitar riffs, bass, live drum kit, layered synths, and subtle strings for energetic yet emotional feel. BPM: 110. Length: 180 seconds\\n- Funk groove with slap bass, electric guitar chords, brass stabs, drums, congas, and rhythmic keyboards creating high-energy danceable rhythm. BPM: 105. Length: 180 seconds\\n- Drum and bass track with fast breakbeat drums, deep sub-bass, sharp synth leads, pads, and atmospheric FX for high-energy club motion. BPM: 175. Length: 150 seconds\\n- Dark ambient track with drones, distant bells, low rumbles, soft wind textures, and synth pads producing eerie immersive tension. BPM: 50. Length: 300 seconds\\n- Tropical house track with marimba, steel drums, soft synths, smooth bass, layered percussion, and light piano riffs for sunny chill dance vibes. BPM: 110. Length: 180 seconds\\n- Progressive rock track with electric guitar leads, organ, bass, drum kit, synth layers, and occasional strings for epic layered energy. BPM: 100. Length: 220 seconds\\n- Music box melody with delicate metallic tones and soft resonance, lullaby style, with gentle ambient reverb. BPM: 60. Length: 20 seconds\\n- Soft piano arpeggio with warm felted tone and slow attack, lullaby style, with intimate room ambience. BPM: 60. Length: 30 seconds\\n- Harp gentle plucked pattern with airy resonance, lullaby style, with dreamy reverb tail. BPM: 65. Length: 25 seconds\\n- Acoustic guitar fingerstyle pattern with warm nylon strings and soft dynamics, lullaby style, with subtle room resonance. BPM: 60. Length: 30 seconds\\n- Ambient synth pad with smooth evolving texture and soft harmonics, lullaby style, with wide stereo ambience. BPM: 50. Length: 40 seconds\\n- Early rock piano with walking left-hand bass line, shuffle rhythms, and blues scale improvisations in energetic 1950s boogie-woogie style. BPM: 160. Length: 180 seconds\\n- Trip Hop track with jazzy sampled vibraphone, mid-tempo breakbeat drums, harp, Latin ethnic percussion, and sweeping cinematic strings creating airy, relaxing, soulful lounge vibes. BPM: 90. Length: 180 seconds\\n- Country outlaw cinematic instrumental with blues pedal steel guitar, rustic mandolin, fiddle call-and-response, tape-driven rattly drum kit, autoharp, and soaring accordion solo for raw, emotional southern blues expression. BPM: 85. Length: 200 seconds\\n- Neo Classical track with sweeping string section, elegant horns, and delicate piano creating soothing, hypnotic, modern, soft, and classic mood. BPM: 70. Length: 180 seconds\\n- Art Rock desert track with desolate piano chords, western-themed rhythm guitars, unique lead guitars, rattly vintage drum kit, and supporting bass creating lonely, expansive, beautiful, and strange atmospheres. BPM: 95. Length: 180 seconds\\n- Cinematic Sci-Fi score with dramatic horn section, building marcato strings, gliding bassoon, thunderous cymbals, subdued timpani, and subtle synth drones producing awe-inspiring, uplifting, epic intergalactic energy. BPM: 100. Length: 220 seconds\\n- West Coast Hip Hop instrumental with cascading harp melodies, smooth Rhodes piano chops, vintage boom bap drums, and walking double bass producing raw, street, and soulful block-party vibes. BPM: 92. Length: 180 seconds\\n- Synthwave futuristic track with pulsating synth bass, exciting chords, soaring leads, and reverberating drum machine patterns creating gritty, pounding, and cool energy. BPM: 110. Length: 180 seconds\\n- Breakbeat track with complex percussion, intricate breakbeats, gritty synths, lush pads, and 808 bassline producing fresh, modern, futuristic, and rave-ready energy. BPM: 140. Length: 160 seconds\\n- Lounge Jazz 1960s smooth track with laid-back drums, piano chords, double bass, soft electric piano, subtle flute, and unique percussion creating beautiful, atmospheric, eclectic, retro, and chill vibes. BPM: 85. Length: 180 seconds\\n- Latin Jazz 1950s blissful track with laid-back Latin drums, euphoric piano chords, double bass, orchestral accompaniment, acoustic guitar, and vibraphone producing nostalgic, beautiful, atmospheric, cinematic, and chill mood. BPM: 95. Length: 180 seconds\\n- Acid Jazz 1970s summertime track with smooth electric piano, trippy synth leads, laid-back vintage drum kit, fuzzy electric bass, and uplifting violin producing retro, psychedelic, jazzy, relaxing energy. BPM: 100. Length: 180 seconds\\n- Progressive Soul 1970s track with feel-good piano, psychedelic organ, groovy vintage drum kit with percussion, fuzzy electric bass, and synth strings producing retro, raw, soulful, joyous atmosphere. BPM: 90. Length: 180 seconds\\n- Discotheque 1970s French-inspired track with sultry piano, psychedelic guitars, groovy drum kit, fuzzy electric bass, and melancholic organ producing retro, raw, laid-back, and relaxing mood. BPM: 105. Length: 180 seconds\\n- Soul Jazz 1970s track with expressive saxophone, smooth piano, groovy drum kit, rhythmic upright bass, sweeping strings, and minimal vibraphone producing retro, raw, laid-back, and epic energy. BPM: 95. Length: 180 seconds\\n- Vintage R&B 1970s live studio track with subtle brass, smooth piano, sweeping strings, and minimal drums producing retro, beautiful, uplifting, nostalgic mood. BPM: 85. Length: 180 seconds\\n- 50s Pop track with Latin influence, string section, bold brass, vibraphone, acoustic guitar, flute, ethnic percussion, and brushed drums creating sexy, epic, vintage, retro, melancholic, jazzy, dramatic energy. BPM: 100. Length: 180 seconds\\n- A piece of calm, quiet, mellow, serene music perfect for a peaceful film score, featuring soft modulating piano, ambient sfx and foley, beautiful vibraphone, and subtle synthesizer drones. The mood is cinematic, thoughtful, serene and nostalgic. BPM: 55. Length: 300 seconds\",\n \"Instrument\": \"You are a music metadata expert. Given an instrument, generate a descriptive prompt for a generative audio model.\\n\\n1. Identify the instrument.\\n2. Add playing style or technique.\\n3. Include details about material, timbre, or texture.\\n4. Add musical style or mood. Specify the genre, context, or emotional character.\\n5. Add spatial or production qualities.\\n6. Specify BPM: Always include a BPM appropriate to the style and context.\\n7. Specify length: Provide an integer in seconds (6–20 s for loops, 20–180 s for stems).\\n\\nExamples:\\n- Synth arpeggio loop with bright detuned oscillators. BPM: 120. Length: 8 seconds\\n- Chord stab loop with sharp percussive attack. BPM: 90. Length: 6 seconds\\n- Guitar muted strum loop with tight rhythmic feel. BPM: 100. Length: 8 seconds\\n- Pluck sequence loop with bright resonant tone. BPM: 128. Length: 10 seconds\\n- Marimba and vibraphone percussive loop with resonant wooden and metallic tones. BPM: 110. Length: 12 seconds\\n- Drum loop with deep muffled kick on beat one, snappy rimshot snare on beats two and four with rolling ghost note fills, and tight closed hi-hats with subtle open accents. BPM: 85. Length: 10 seconds\\n- Drum groove loop with brushed snare swinging on the ride, soft feathered kick on downbeats, and light closed hi-hat taps on the upbeats. BPM: 130. Length: 12 seconds\\n- Kick and hi-hat loop with four-on-the-floor punchy kick, tight closed hi-hats on every eighth note, and a sharp dry snare on beats two and four. BPM: 130. Length: 15 seconds\\n- Vinyl crackle drum loop with warm low-pass filtered kick, dusty snare with tape saturation, and shuffled closed hi-hats with subtle vinyl crackle ambiance. BPM: 80. Length: 10 seconds\\n- Ambient pad loop with evolving texture. BPM: 80. Length: 12 seconds\\n- Melodic synth bass groove loop with pumping sidechain feel. BPM: 122. Length: 10 seconds\\n- Melodic Bass slap and pop rhythm loop. BPM: 100. Length: 8 seconds\\n- Acoustic bass walking line loop with natural wooden resonance. BPM: 120. Length: 12 seconds\\n- String pizzicato motif loop, suspenseful, with tight string texture. BPM: 90. Length: 8 seconds\\n- Brass staccato riff loop with sharp bright attack. BPM: 130. Length: 10 seconds\\n- Flute airy melodic loop with wooden headjoint resonance. BPM: 100. Length: 6 seconds\\n- Pan flute ambient loop with breathy timbre. BPM: 75. Length: 8 seconds\\n- Clarinet riff loop with warm smooth reed tone. BPM: 120. Length: 10 seconds\\n- Oboe motif loop, orchestral, with rich double reed resonance. BPM: 80. Length: 8 seconds\\n- Recorder Renaissance motif loop with soft wooden timbre. BPM: 100. Length: 6 seconds\\n- Electric sitar riff loop with buzzing resonant tone. BPM: 90. Length: 10 seconds\\n- Koto plucked motif loop with resonant wooden strings. BPM: 90. Length: 8 seconds\\n- Shamisen folk melody loop with percussive twang. BPM: 100. Length: 8 seconds\\n- Banjo fingerpicking loop with metallic string resonance. BPM: 110. Length: 10 seconds\\n- Mandolin tremolo loop with crisp wooden body tone. BPM: 120. Length: 10 seconds\\n- Acoustic guitar chord vamp loop with natural room resonance. BPM: 110. Length: 12 seconds\\n- Nylon string guitar arpeggio loop with warm, soft timbre. BPM: 90. Length: 15 seconds\\n- Electric guitar riff loop with driven distorted tone. BPM: 130. Length: 10 seconds\\n- Slide guitar melody loop with warm resonant glide. BPM: 100. Length: 12 seconds\\n- Steel guitar slide loop with bright pedal steel tone. BPM: 95. Length: 12 seconds\\n- Harpsichord arpeggio loop with crisp plucked attack. BPM: 120. Length: 10 seconds\\n- Rhodes chord vamp loop with warm electric piano tone. BPM: 100. Length: 12 seconds\\n- Clavinet funky rhythm loop. BPM: 105. Length: 10 seconds\\n- Organ chord vamp loop with full drawbar warmth. BPM: 90. Length: 12 seconds\\n- Drum loop with booming 808 kick on beat one, crisp snare on beat three, and rapid triplet hi-hat rolls with open hat accents for aggressive high-energy feel. BPM: 140. Length: 8 seconds\\n- Breakbeat drum loop with chopped Amen-style snare flurries, driving kick on the one, fast sixteenth-note closed hi-hats, and syncopated open hat accents. BPM: 170. Length: 10 seconds\\n- Glitch percussion loop with stuttered kick transients, randomised snare hits processed with bit-crushing, and erratic hi-hat patterns with pitch-shifted metallic ticks. BPM: 120. Length: 12 seconds\\n- Metallic hits loop with distorted kick impacts, processed metal-plate snare slams, and grinding hi-hat noise bursts for aggressive mechanical texture. BPM: 120. Length: 10 seconds\\n- Timpani hits loop, cinematic, with deep resonant kick-like timpani strikes on beat one, rolling snare-style timpani fills, and no hi-hats for a grand orchestral feel. BPM: 70. Length: 8 seconds\\n- Snare roll loop, dramatic, with accelerating snare drum rolls building from soft to crashing, deep supporting kick pulses, and no hi-hats for maximum impact. BPM: 100. Length: 8 seconds\\n- Accordion motif loop with bright reedy bellows tone. BPM: 100. Length: 10 seconds\\n- Harmonica blues riff loop with expressive reed timbre. BPM: 90. Length: 10 seconds\\n- Trombone riff loop with warm sliding brass tone. BPM: 120. Length: 10 seconds\\n- French horn melodic loop, cinematic. BPM: 80. Length: 12 seconds\\n- Soprano sax ballad loop. BPM: 70. Length: 12 seconds\\n- Alto sax bebop riff loop. BPM: 200. Length: 10 seconds\\n- Electric violin melodic loop with reverb. BPM: 90. Length: 10 seconds\\n- String pad loop with cinematic texture. BPM: 70. Length: 15 seconds\\n- Granular synth evolving texture loop. BPM: 90. Length: 15 seconds\\n- Piano motif loop with soft felt hammer tone. BPM: 80. Length: 10 seconds\\n- Pad and synth loop with lush detuned shimmer. BPM: 85. Length: 12 seconds\\n- Synth lead loop with sidechain pumping compression. BPM: 128. Length: 10 seconds\\n- Analog synth bassline loop with deep warm low-end. BPM: 122. Length: 12 seconds\\n- FM synth lead motif loop with bright metallic shimmer. BPM: 110. Length: 10 seconds\\n- Bass groove loop with tight rhythmic two-bar pattern. BPM: 100. Length: 16 seconds\\n- Acoustic guitar fingerstyle motif loop with warm wood resonance. BPM: 90. Length: 45 seconds\\n- Sombre acoustic guitar motif loop with cavernous reverb, delicate fingerpicking, and expressive melancholic tone. BPM: 70. Length: 45 seconds\\n- Electric guitar rock riff motif loop. BPM: 130. Length: 40 seconds\\n- Vintage electric guitar motif loop, live-recorded in a vintage studio, with expressive and dynamic solo performance. BPM: 90. Length: 40 seconds\\n- Piano chord progression motif loop with rich harmonic movement. BPM: 120. Length: 60 seconds\\n- String ensemble cinematic motif loop with rich wooden resonance. BPM: 80. Length: 120 seconds\\n- Brass ensemble cinematic motif loop with bright metallic timbre. BPM: 90. Length: 90 seconds\\n- Ethnic percussion ensemble motif loop with deep resonant djembe kick tones, slapped snare-like rim hits on congas, and layered shakers and bells providing hi-hat-like rhythmic texture with polyrhythmic patterns. BPM: 100. Length: 90 seconds\\n- Synth ambient motif loop with evolving textures. BPM: 80. Length: 180 seconds\\n- Motif loop with warm dusty vinyl crackle and tape saturation. BPM: 80. Length: 60 seconds\\n- Synth lead and bass motif loop with bright punchy energy. BPM: 128. Length: 90 seconds\\n- Funk band motif loop: bass, drums, guitar. BPM: 100. Length: 90 seconds\\n- Ethnic flute motif for cinematic use. BPM: 80. Length: 30 seconds\\n- Steel drum melodic motif loop with bright metallic resonance. BPM: 110. Length: 20 seconds\\n- Marimba percussive motif loop with resonant wooden tone. BPM: 100. Length: 20 seconds\\n- Vibraphone melodic motif loop with metallic shimmer. BPM: 90. Length: 25 seconds\\n- Piano cinematic motif loop with resonant wooden tone. BPM: 80. Length: 30 seconds\\n- Violin expressive cinematic motif loop with rich wooden resonance. BPM: 75. Length: 25 seconds\\n- Cello expressive motif loop with deep wooden resonance. BPM: 70. Length: 30 seconds\\n- Trumpet expressive motif loop with brassy overtones. BPM: 100. Length: 25 seconds\\n- Sax expressive motif loop with warm reed timbre. BPM: 95. Length: 25 seconds\\n- Ethnic drum ensemble motif loop with booming natural-skin bass drum kicks, sharp hand-slap snare accents on djembes and talking drums, and layered wooden and metal percussion providing rhythmic hi-hat-like patterns. BPM: 95. Length: 30 seconds\\n- Ambient drone motif loop. BPM: 60. Length: 180 seconds\\n- Orchestral tension motif loop. BPM: 90. Length: 150 seconds\\n- Electronic track motif loop with drums, bass, synth. BPM: 128. Length: 180 seconds\",\n \"SFX\": \"You are a professional sound design expert. Convert the user's input into a precise, vivid sound effects description suitable for generative audio models.\\n\\nDescribe clearly:\\n- Sound source\\n- Physical character (texture, timbre, material: metal, wood, glass, concrete, etc.)\\n- Spatial qualities (indoor/outdoor, cave/open field/underwater, dry/reverberant, close-up/distant, echoing/muffled)\\n- Temporal evolution (attack, decay, movement, transitions over time)\\n- Include motion or spatial movement if applicable (passing, approaching, stereo movement)\\n\\nAudio length rules:\\n- Very short sounds (impacts, clicks, gunshots): 1–3 seconds\\n- Medium actions (footsteps, object movement, transitions): 3–6 seconds\\n- Ambience / environments: 6–15 seconds\\n- Always append: Length: X seconds (integer only, no decimals).\\n\\nOutput constraints:\\n- Length: 1–2 dense sentences maximum\\n- Output ONLY the final rewritten prompt\\n- No explanations, no formatting, no quotes\\n- Use concise but dense technical language\\n- Focus strictly on sound effects or ambience\\n- Always append: Length: X seconds (integer only, no decimals).\\n\\nQuality guidelines:\\n- Be specific and avoid vague terms\\n- Prioritize clarity and realism\\n- Combine elements into one coherent scene\\n- Avoid redundancy\\n\\nExamples:\\n- Heavy rain hitting a metal roof during a thunderstorm, distant thunder rumbles, stereo, realistic ambience. Length: 45 seconds\\n- Quiet forest at dawn with birds chirping, soft wind through leaves, distant stream flowing. Length: 60 seconds\\n- Busy city street at night, cars passing, muffled conversations, occasional horn, urban ambience. Length: 50 seconds\\n- Ocean waves crashing against rocky cliffs, strong wind, dramatic and cinematic. Length: 70 seconds\\n- Wooden door creaking open slowly in an old house, echoing interior, eerie tone. Length: 3 seconds\\n- Glass bottle shattering on concrete, sharp impact, scattered fragments. Length: 2 seconds\\n- Footsteps on gravel, steady walking pace, close perspective. Length: 8 seconds\\n- Typing rapidly on a mechanical keyboard, crisp tactile clicks. Length: 5 seconds\\n- Punch impact with deep bass hit, cinematic trailer style. Length: 2 seconds\\n- Car speeding past at high velocity, doppler effect, realistic whoosh. Length: 3 seconds\\n- Object falling from height and hitting ground with a heavy thud. Length: 2 seconds\\n- Sword swing whooshing through air, fast motion, clean metallic tone. Length: 2 seconds\\n- Futuristic laser blast, clean energy pulse, high-tech sound design. Length: 1 seconds\\n- Spaceship engine humming, low frequency rumble, interior perspective. Length: 90 seconds\\n- Magical spell casting, shimmering particles, rising tonal energy. Length: 8 seconds\\n- Teleportation effect, glitchy digital distortion with a soft whoosh. Length: 5 seconds\\n- Dark eerie drone with distant whispers, creepy, slow build tension. Length: 120 seconds\\n- Sudden horror jump scare sting, sharp violin hit, cinematic. Length: 1 second\\n- Metal scraping slowly in a dark tunnel, echoing and ominous. Length: 20 seconds\\n- Explosion with debris scattering, deep bass, cinematic realism. Length: 4 seconds\\n- Building collapsing, rumbling concrete, dust and debris falling. Length: 25 seconds\\n- Fire crackling intensely, wood burning, close-up detail. Length: 80 seconds\\n- Gunshot in a large empty warehouse, loud echo decay. Length: 2 seconds\\n- Retro arcade coin insert sound, 8-bit style. Length: 1 second\\n- Level up chime, bright, rewarding, fantasy RPG style. Length: 2 seconds\\n- Error buzzer, short, digital, UI feedback. Length: 1 second\\n- Menu navigation clicks, soft futuristic interface sounds. Length: 3 seconds\\n- Layered soundscape: rain, thunder, footsteps, and distant sirens all blending naturally. Length: 90 seconds\\n- Rapid sequence of three impacts: metal hit, glass break, wood crack, spaced evenly. Length: 4 seconds\\n- Sound moving from left to right stereo field: passing motorcycle. Length: 5 seconds\\n- Close vs far perspective transition: footsteps approaching then fading away. Length: 6 seconds\\n- Tape stop sub drop, a massive sub-bass note that mimics a vinyl record or tape machine being turned off, the pitch and speed drop simultaneously, causing the high-end harmonics to smear and thicken as the sound grinds to a halt at a sub-sonic frequency. Length: 11 seconds\\n- Gravel and leaves footsteps, the sound of a hard boot stepping onto dry leaves or gravel, crisp and natural with detailed texture. Length: 11 seconds\\n- Ghostship moan, a massive, deep wooden groan with a low-frequency moan, like heavy timber under immense structural tension, swaying slowly, processed with long, dark wooden room reverb for a sense of scale. Length: 11 seconds\\n- Bicycle chain, a continuous metallic whirring sound of a chain moving over sprockets, with individual teeth catching the links, processed with resonant band-pass filter to emphasize metallic singing. Length: 11 seconds\\n- Warp drive, a sound that starts with a massive suck-back of ambient noise, followed by a supersonic crack and high-pitched zing that disappears into the distance, giving the sense of stretching space-time. Length: 11 seconds\\n- Ice cubes, high-pitched musical clinking of hard ice hitting a thin glass, bright resonant ring with subtle liquid sloshing around the edges. Length: 11 seconds\\n- Paper shuffle, the sound of a thick stack of heavy bond paper being squared up on a desk, dry papery thud with a quick fanning sound as air moves between the pages. Length: 11 seconds\\n- Drawer slam, a blunt, powerful thud made by slamming a wooden desk drawer shut, pronounced low-mid body, slightly distorted for aggressive character. Length: 3 seconds\",\n \"One-shot\": \"You are a music metadata expert. Given an instrument or sound, generate a descriptive prompt for a short, isolated one-shot audio sample for music production.\\n\\n1. Identify the instrument or sound source.\\n2. Describe the playing technique or hit type (e.g., pluck, slam, tap, stab).\\n3. Include details about material, timbre, or texture.\\n4. Add spatial or production qualities (dry/wet, room, close-mic).\\n5. Specify length: short integer in seconds (1–11 s).\\n\\nExamples:\\n- Piano key hit with bright percussive attack and resonant wooden body. Length: 2 seconds\\n- Kick drum punchy low-end hit with warm skin resonance. Length: 2 seconds\\n- Snare drum rimshot accent with crisp snare wires. Length: 2 seconds\\n- Acoustic guitar fingerstyle note with warm spruce tone. Length: 3 seconds\\n- Bass pluck with jazzy tone and resonant wooden body. Length: 3 seconds\\n- Electric guitar power chord with distortion. Length: 3 seconds\\n- Metallic glitch percussion hit with sharp metallic texture. Length: 2 seconds\\n- Tabla resonant tone hit with natural skin timbre. Length: 2 seconds\\n- Djembe slap accent with dry wooden resonance. Length: 2 seconds\\n- Synth stab with reverb tail. Length: 3 seconds\\n- Violin expressive note with vibrato and rich wooden resonance. Length: 3 seconds\\n- Cello legato note, cinematic, with warm resonant body. Length: 3 seconds\\n- Trumpet bright accent with slightly brassy overtones. Length: 2 seconds\\n- Melodic saxophone jazz riff with smooth reed timbre and a slight vibrato bend. Length: 3 seconds\\n- Harp pluck with airy tone and resonant strings. Length: 2 seconds\\n- Glockenspiel bell-like note with bright metallic clarity. Length: 2 seconds\\n- Metallic clang sound design hit. Length: 2 seconds\\n- Granular texture hit. Length: 3 seconds\\n- Reversed piano hit. Length: 2 seconds\\n- Synth riser effect. Length: 6 seconds\\n- Percussion impact hit. Length: 2 seconds\\n- Cinematic hit. Length: 2 seconds\\n- Dry clap, a crisp, natural single hand clap recorded in a dead room with an extremely sharp transient and no room reflections. Length: 1 second\\n- Studio hat, a classic, natural recording of 14-inch hi-hats played tightly closed, zero ring, very fast decay. Length: 1 second\\n- Disco open hat, bright 14-inch open hi-hat with long, shimmering decay, perfect for disco or dance grooves. Length: 1 second\\n- Pillow kick, acoustic kick drum muffled with a heavy blanket, producing a short, dry \\\"thump\\\" with almost zero resonance. Length: 1 second\\n- Short 808, punchy 808 kick with sharp, distorted transient and fast-decaying sub-tail. Length: 1 second\\n- Egg shaker, classic plastic egg shaker recorded with a small-diaphragm condenser mic, producing a light, consistent \\\"tick\\\" with very short sustain. Length: 1 second\\n- African drums, dynamic African drums and percussion ensemble with natural acoustic textures. Length: 3 seconds\\n- Latin drums, dynamic Latin drums and percussion ensemble featuring authentic rhythmic patterns. Length: 3 seconds\\n- String quartet, euphoric string quartet with dynamic and emotional playing, full of expressive harmonies and movement. Length: 3 seconds\\n- Piano, nostalgic, atmospheric piano piece with dynamic and emotional performance, intimate and resonant. Length: 3 seconds\\n- Analogue drift pad, warm polyphonic pad with three detuned oscillators (saw + triangle), subtle pitch drift, and lush bucket-brigade chorus for wide, nostalgic stereo image. Length: 11 seconds\\n- Phase distortion bass, Casio CZ-style phase-distorted sine wave warped into a jagged sawtooth for retro synth bass tone. Length: 11 seconds\\n- Vibrato saxophone, bright lyrical alto sax with fast fluttery vibrato, reedy vintage tone, captured with ribbon mic for warm nostalgic sound. Length: 11 seconds\\n- Lofi upright bass, upright bass recorded with ribbon mic in a wooden room, natural air with slightly boxy resonance, tape-saturated for dusty 1950s jazz feel. Length: 2 seconds\"\n}", + "Music" + ] + }, + { + "id": 40, + "type": "StringReplace", + "pos": [ + 1350, + 900 + ], + "size": [ + 260, + 280 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": 59 + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 58 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 60 + ] + } + ], + "title": "Text Replace (AUDIO LENGTH)", + "properties": { + "Node name for S&R": "StringReplace" + }, + "widgets_values": [ + "", + "AUDIO_LENGTH", + "" + ] + }, + { + "id": 38, + "type": "StringReplace", + "pos": [ + 720, + 900 + ], + "size": [ + 290, + 280 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": null + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 66 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 52 + ] + } + ], + "title": "Text Replace (PROMPT TEMPLATE)", + "properties": { + "Node name for S&R": "StringReplace" + }, + "widgets_values": [ + "SYSTEM_PROMPTS\n\nInput: USER_INPUT\nTarget audio length: AUDIO_LENGTH seconds.\nOutput:", + "SYSTEM_PROMPTS", + "" + ] + }, + { + "id": 35, + "type": "PrimitiveBoolean", + "pos": [ + -390, + 570 + ], + "size": [ + 400, + 100 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 83 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 48 + ] + } + ], + "title": "Boolean (Enable_Reprompt)", + "properties": { + "Node name for S&R": "PrimitiveBoolean" + }, + "widgets_values": [ + true + ] + }, + { + "id": 36, + "type": "PrimitiveFloat", + "pos": [ + -390, + 410 + ], + "size": [ + 400, + 110 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "FLOAT", + "widget": { + "name": "value" + }, + "link": 82 + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 50, + 56 + ] + } + ], + "title": "Float (Duration)", + "properties": { + "Node name for S&R": "PrimitiveFloat" + }, + "widgets_values": [ + 150 + ] + }, + { + "id": 25, + "type": "CheckpointLoaderSimple", + "pos": [ + 100, + 130 + ], + "size": [ + 440, + 190 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 79 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 30 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 39 + ] + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "models": [ + { + "name": "stable_audio_3_medium.safetensors", + "url": "https://huggingface.co/Comfy-Org/stable-audio-3/resolve/main/checkpoints/stable_audio_3_medium.safetensors", + "directory": "checkpoints" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "stable_audio_3_medium.safetensors" + ] + }, + { + "id": 26, + "type": "CLIPLoader", + "pos": [ + 100, + 390 + ], + "size": [ + 440, + 170 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 80 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 34, + 35 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "models": [ + { + "name": "t5gemma_b_b_ul2.safetensors", + "url": "https://huggingface.co/Comfy-Org/stable-audio-3/resolve/main/text_encoders/t5gemma_b_b_ul2.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "t5gemma_b_b_ul2.safetensors", + "stable_audio", + "default" + ] + }, + { + "id": 54, + "type": "PreviewAny", + "pos": [ + 1720, + 1580 + ], + "size": [ + 420, + 550 + ], + "flags": {}, + "order": 20, + "mode": 4, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 84 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": null + } + ], + "properties": { + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [ + null, + null, + null + ] + } + ], + "groups": [ + { + "id": 1, + "title": "Loaders: checkpoint & CLIP", + "bounding": [ + 80, + 50, + 485.721654232725, + 527.2848777754299 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 2, + "title": "CLIP encode: conditioning", + "bounding": [ + 600, + 60, + 470, + 510 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 3, + "title": "User inputs: prompt & duration", + "bounding": [ + -400, + 10, + 430, + 740 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 7, + "title": "Reprompt: full branch (template + LLM)", + "bounding": [ + 60, + 780, + 1630, + 1360 + ], + "color": "#444", + "flags": {} + }, + { + "id": 4, + "title": "Reprompt: JSON extract & template fills", + "bounding": [ + 120, + 820, + 1520, + 650 + ], + "color": "#444", + "flags": {} + }, + { + "id": 5, + "title": "Helpers: duration to string", + "bounding": [ + 1340, + 1180, + 280, + 250 + ], + "color": "#444", + "flags": {} + }, + { + "id": 6, + "title": "Reprompt: Qwen TextGenerate", + "bounding": [ + 680, + 1510, + 960, + 614.65625 + ], + "color": "#444", + "flags": {} + }, + { + "id": 8, + "title": "Audio generation: Stable Audio", + "bounding": [ + 60, + 10, + 1627.3616782294932, + 737.0545987464304 + ], + "color": "#3f789e", + "flags": {} + } + ], + "links": [ + { + "id": 35, + "origin_id": 26, + "origin_slot": 0, + "target_id": 7, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 13, + "origin_id": 3, + "origin_slot": 0, + "target_id": 12, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 39, + "origin_id": 25, + "origin_slot": 2, + "target_id": 12, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 50, + "origin_id": 36, + "origin_slot": 0, + "target_id": 11, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 30, + "origin_id": 25, + "origin_slot": 0, + "target_id": 3, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 4, + "origin_id": 6, + "origin_slot": 0, + "target_id": 3, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 6, + "origin_id": 7, + "origin_slot": 0, + "target_id": 3, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 12, + "origin_id": 11, + "origin_slot": 0, + "target_id": 3, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 34, + "origin_id": 26, + "origin_slot": 0, + "target_id": 6, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 49, + "origin_id": 34, + "origin_slot": 0, + "target_id": 6, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 47, + "origin_id": 31, + "origin_slot": 0, + "target_id": 34, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 46, + "origin_id": 28, + "origin_slot": 0, + "target_id": 34, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 48, + "origin_id": 35, + "origin_slot": 0, + "target_id": 34, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 56, + "origin_id": 36, + "origin_slot": 0, + "target_id": 41, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 57, + "origin_id": 41, + "origin_slot": 1, + "target_id": 42, + "target_slot": 0, + "type": "INT" + }, + { + "id": 52, + "origin_id": 38, + "origin_slot": 0, + "target_id": 39, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 53, + "origin_id": 31, + "origin_slot": 0, + "target_id": 39, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 40, + "origin_id": 29, + "origin_slot": 0, + "target_id": 28, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 60, + "origin_id": 40, + "origin_slot": 0, + "target_id": 28, + "target_slot": 4, + "type": "STRING" + }, + { + "id": 65, + "origin_id": 43, + "origin_slot": 0, + "target_id": 49, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 59, + "origin_id": 39, + "origin_slot": 0, + "target_id": 40, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 58, + "origin_id": 42, + "origin_slot": 0, + "target_id": 40, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 66, + "origin_id": 49, + "origin_slot": 0, + "target_id": 38, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 27, + "origin_id": 12, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "AUDIO" + }, + { + "id": 68, + "origin_id": -10, + "origin_slot": 0, + "target_id": 31, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 76, + "origin_id": -10, + "origin_slot": 2, + "target_id": 3, + "target_slot": 4, + "type": "INT" + }, + { + "id": 78, + "origin_id": -10, + "origin_slot": 4, + "target_id": 43, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 79, + "origin_id": -10, + "origin_slot": 5, + "target_id": 25, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 80, + "origin_id": -10, + "origin_slot": 6, + "target_id": 26, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 81, + "origin_id": -10, + "origin_slot": 7, + "target_id": 29, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 82, + "origin_id": -10, + "origin_slot": 1, + "target_id": 36, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 83, + "origin_id": -10, + "origin_slot": 3, + "target_id": 35, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 84, + "origin_id": 28, + "origin_slot": 0, + "target_id": 54, + "target_slot": 0, + "type": "STRING" + } + ], + "extra": {}, + "category": "Audio/Music generation", + "description": "Generates music, instrument loops, sound effects, and one-shots from text using Stable Audio 3 Medium, with optional Qwen 3.5 category-based prompt expansion (Music, Instrument, SFX, One-shot)." + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Brightness and Contrast.json b/blueprints/Brightness and Contrast.json index 90bfe999d..78fc52f29 100644 --- a/blueprints/Brightness and Contrast.json +++ b/blueprints/Brightness and Contrast.json @@ -431,9 +431,10 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adjusts image brightness and contrast using a real-time GPU fragment shader." } ] }, "extra": {} -} +} \ No newline at end of file diff --git a/blueprints/Canny to Image (Z-Image-Turbo).json b/blueprints/Canny to Image (Z-Image-Turbo).json index ff9717308..903d372b1 100644 --- a/blueprints/Canny to Image (Z-Image-Turbo).json +++ b/blueprints/Canny to Image (Z-Image-Turbo).json @@ -162,7 +162,7 @@ }, "revision": 0, "config": {}, - "name": "local-Canny to Image (Z-Image-Turbo)", + "name": "Canny to Image (Z-Image-Turbo)", "inputNode": { "id": -10, "bounding": [ @@ -1553,7 +1553,8 @@ "VHS_MetadataImage": true, "VHS_KeepIntermediate": true }, - "category": "Image generation and editing/Canny to image" + "category": "Image generation and editing/Conditioned", + "description": "Generates an image from a Canny edge map using Z-Image-Turbo, with text conditioning." } ] }, @@ -1574,4 +1575,4 @@ } }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Canny to Video (LTX 2.0).json b/blueprints/Canny to Video (LTX 2.0).json index fae8321b9..ed602b521 100644 --- a/blueprints/Canny to Video (LTX 2.0).json +++ b/blueprints/Canny to Video (LTX 2.0).json @@ -192,7 +192,7 @@ }, "revision": 0, "config": {}, - "name": "local-Canny to Video (LTX 2.0)", + "name": "Canny to Video (LTX 2.0)", "inputNode": { "id": -10, "bounding": [ @@ -3600,7 +3600,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Canny to video" + "category": "Video generation and editing/Conditioned", + "description": "Generates video from Canny edge maps using LTX-2, with optional synchronized audio." } ] }, @@ -3616,4 +3617,4 @@ } }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Chromatic Aberration.json b/blueprints/Chromatic Aberration.json index ae8037b1b..893fb1190 100644 --- a/blueprints/Chromatic Aberration.json +++ b/blueprints/Chromatic Aberration.json @@ -377,8 +377,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adds lens-style chromatic aberration (color fringing) using a real-time GPU fragment shader." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Color Adjustment.json b/blueprints/Color Adjustment.json index 622bf28af..5abbf8baa 100644 --- a/blueprints/Color Adjustment.json +++ b/blueprints/Color Adjustment.json @@ -596,7 +596,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adjusts saturation, temperature, tint, and vibrance using a real-time GPU fragment shader." } ] } diff --git a/blueprints/Color Balance.json b/blueprints/Color Balance.json index 21d6319ed..d921eab37 100644 --- a/blueprints/Color Balance.json +++ b/blueprints/Color Balance.json @@ -1129,7 +1129,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Balances colors across shadows, midtones, and highlights using a real-time GPU fragment shader." } ] } diff --git a/blueprints/Color Curves.json b/blueprints/Color Curves.json index 1461cf396..b9bfb7029 100644 --- a/blueprints/Color Curves.json +++ b/blueprints/Color Curves.json @@ -608,7 +608,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Fine-tunes tone and color with per-channel curve adjustments using a real-time GPU fragment shader." } ] } diff --git a/blueprints/ControlNet (Z-Image-Turbo).json b/blueprints/ControlNet (Z-Image-Turbo).json new file mode 100644 index 000000000..160ee11e2 --- /dev/null +++ b/blueprints/ControlNet (Z-Image-Turbo).json @@ -0,0 +1,1412 @@ +{ + "revision": 0, + "last_node_id": 85, + "last_link_id": 0, + "nodes": [ + { + "id": 85, + "type": "d2e76ecf-6e84-4b8c-8913-48efc09ec1c4", + "pos": [ + 440, + 1220 + ], + "size": [ + 480, + 0 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "label": "control_image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": null + }, + { + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + }, + { + "label": "patch_model", + "name": "name", + "type": "COMBO", + "widget": { + "name": "name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "title": "ControlNet (Z-Image-Turbo)", + "properties": { + "proxyWidgets": [ + [ + "83", + "text" + ], + [ + "79", + "seed" + ], + [ + "74", + "unet_name" + ], + [ + "73", + "clip_name" + ], + [ + "75", + "vae_name" + ], + [ + "76", + "name" + ], + [ + "79", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "d2e76ecf-6e84-4b8c-8913-48efc09ec1c4", + "version": 1, + "state": { + "lastGroupId": 9, + "lastNodeId": 85, + "lastLinkId": 87, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "ControlNet (Z-Image-Turbo)", + "inputNode": { + "id": -10, + "bounding": [ + -500, + 620, + 120, + 180 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1390, + 1100, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "fbbb968e-d3cf-40e4-b3ce-7abb074e5bd8", + "name": "image", + "type": "IMAGE", + "linkIds": [ + 65, + 80 + ], + "localized_name": "image", + "label": "control_image", + "pos": [ + -400, + 640 + ] + }, + { + "id": "c1b19877-5417-4580-aea1-44439c70c1dd", + "name": "text", + "type": "STRING", + "linkIds": [ + 81 + ], + "pos": [ + -400, + 660 + ] + }, + { + "id": "b5671515-bc7a-4be5-b1e7-d4f0f68907d6", + "name": "seed", + "type": "INT", + "linkIds": [ + 83 + ], + "pos": [ + -400, + 680 + ] + }, + { + "id": "2838be23-8034-4f16-87a5-d29d790e8391", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 84 + ], + "pos": [ + -400, + 700 + ] + }, + { + "id": "8a6643b5-8f78-41ff-bbc6-e87b95459706", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 85 + ], + "pos": [ + -400, + 720 + ] + }, + { + "id": "b103dc94-8ca7-456b-a809-414d7e341a1b", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 86 + ], + "pos": [ + -400, + 740 + ] + }, + { + "id": "4a7d65af-f0fd-4a5c-832a-bdc0d15b1f30", + "name": "name", + "type": "COMBO", + "linkIds": [ + 87 + ], + "label": "patch_model", + "pos": [ + -400, + 760 + ] + } + ], + "outputs": [ + { + "id": "ccb7fa39-4a3d-4eb2-8fd2-91d08fad9570", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 45 + ], + "localized_name": "IMAGE", + "pos": [ + 1410, + 1120 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 73, + "type": "CLIPLoader", + "pos": [ + 20, + 500 + ], + "size": [ + 270, + 150 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 85 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 44 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "qwen_3_4b.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors", + "directory": "text_encoders" + } + ] + }, + "widgets_values": [ + "qwen_3_4b.safetensors", + "lumina2", + "default" + ] + }, + { + "id": 74, + "type": "UNETLoader", + "pos": [ + 20, + 320 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 84 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 79 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "UNETLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "z_image_turbo_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/diffusion_models/z_image_turbo_bf16.safetensors", + "directory": "diffusion_models" + } + ] + }, + "widgets_values": [ + "z_image_turbo_bf16.safetensors", + "default" + ] + }, + { + "id": 75, + "type": "VAELoader", + "pos": [ + 20, + 760 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 86 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 39, + 70 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAELoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "ae.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/vae/ae.safetensors", + "directory": "vae" + } + ] + }, + "widgets_values": [ + "ae.safetensors" + ] + }, + { + "id": 76, + "type": "ModelPatchLoader", + "pos": [ + 20, + 940 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "name", + "name": "name", + "type": "COMBO", + "widget": { + "name": "name" + }, + "link": 87 + } + ], + "outputs": [ + { + "localized_name": "MODEL_PATCH", + "name": "MODEL_PATCH", + "type": "MODEL_PATCH", + "links": [ + 74 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.51", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ModelPatchLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "Z-Image-Turbo-Fun-Controlnet-Union.safetensors", + "url": "https://huggingface.co/alibaba-pai/Z-Image-Turbo-Fun-Controlnet-Union/resolve/main/Z-Image-Turbo-Fun-Controlnet-Union.safetensors", + "directory": "model_patches" + } + ] + }, + "widgets_values": [ + "Z-Image-Turbo-Fun-Controlnet-Union.safetensors" + ] + }, + { + "id": 77, + "type": "VAEDecode", + "pos": [ + 940, + 1100 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 38 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 39 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 45 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEDecode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 78, + "type": "ModelSamplingAuraFlow", + "pos": [ + 910, + 270 + ], + "size": [ + 290, + 110 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 69 + }, + { + "localized_name": "shift", + "name": "shift", + "type": "FLOAT", + "widget": { + "name": "shift" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 40 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ModelSamplingAuraFlow", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 3 + ] + }, + { + "id": 79, + "type": "KSampler", + "pos": [ + 910, + 430 + ], + "size": [ + 300, + 570 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 40 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 41 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 42 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 78 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 83 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 38 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "KSampler", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 729703840979498, + "randomize", + 8, + 1, + "res_multistep", + "simple", + 1 + ] + }, + { + "id": 80, + "type": "ConditioningZeroOut", + "pos": [ + 610, + 830 + ], + "size": [ + 230, + 80 + ], + "flags": { + "collapsed": true + }, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 36 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 42 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ConditioningZeroOut", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 81, + "type": "QwenImageDiffsynthControlnet", + "pos": [ + 490, + 970 + ], + "size": [ + 290, + 200 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 79 + }, + { + "localized_name": "model_patch", + "name": "model_patch", + "type": "MODEL_PATCH", + "link": 74 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 70 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 65 + }, + { + "localized_name": "mask", + "name": "mask", + "shape": 7, + "type": "MASK", + "link": null + }, + { + "localized_name": "strength", + "name": "strength", + "type": "FLOAT", + "widget": { + "name": "strength" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 69 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.76", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "QwenImageDiffsynthControlnet", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 82, + "type": "EmptySD3LatentImage", + "pos": [ + 40, + 1200 + ], + "size": [ + 260, + 170 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 76 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 77 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 78 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "EmptySD3LatentImage", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 83, + "type": "CLIPTextEncode", + "pos": [ + 430, + 310 + ], + "size": [ + 400, + 440 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 44 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 81 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 36, + 41 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 84, + "type": "GetImageSize", + "pos": [ + 50, + 1410 + ], + "size": [ + 230, + 120 + ], + "flags": { + "collapsed": true + }, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 80 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 76 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": [ + 77 + ] + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.76", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "GetImageSize", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + } + ], + "groups": [ + { + "id": 3, + "title": "Prompt", + "bounding": [ + 410, + 230, + 440, + 630 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Model", + "bounding": [ + -50, + 230, + 430, + 840 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 8, + "title": "Apple ControlNet", + "bounding": [ + 410, + 890, + 440, + 330 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 9, + "title": "Image Size", + "bounding": [ + -50, + 1100, + 430, + 350 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 38, + "origin_id": 79, + "origin_slot": 0, + "target_id": 77, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 39, + "origin_id": 75, + "origin_slot": 0, + "target_id": 77, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 69, + "origin_id": 81, + "origin_slot": 0, + "target_id": 78, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 40, + "origin_id": 78, + "origin_slot": 0, + "target_id": 79, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 41, + "origin_id": 83, + "origin_slot": 0, + "target_id": 79, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 42, + "origin_id": 80, + "origin_slot": 0, + "target_id": 79, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 78, + "origin_id": 82, + "origin_slot": 0, + "target_id": 79, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 36, + "origin_id": 83, + "origin_slot": 0, + "target_id": 80, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 79, + "origin_id": 74, + "origin_slot": 0, + "target_id": 81, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 74, + "origin_id": 76, + "origin_slot": 0, + "target_id": 81, + "target_slot": 1, + "type": "MODEL_PATCH" + }, + { + "id": 70, + "origin_id": 75, + "origin_slot": 0, + "target_id": 81, + "target_slot": 2, + "type": "VAE" + }, + { + "id": 76, + "origin_id": 84, + "origin_slot": 0, + "target_id": 82, + "target_slot": 0, + "type": "INT" + }, + { + "id": 77, + "origin_id": 84, + "origin_slot": 1, + "target_id": 82, + "target_slot": 1, + "type": "INT" + }, + { + "id": 44, + "origin_id": 73, + "origin_slot": 0, + "target_id": 83, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 65, + "origin_id": -10, + "origin_slot": 0, + "target_id": 81, + "target_slot": 3, + "type": "IMAGE" + }, + { + "id": 80, + "origin_id": -10, + "origin_slot": 0, + "target_id": 84, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 45, + "origin_id": 77, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 81, + "origin_id": -10, + "origin_slot": 1, + "target_id": 83, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 83, + "origin_id": -10, + "origin_slot": 2, + "target_id": 79, + "target_slot": 4, + "type": "INT" + }, + { + "id": 84, + "origin_id": -10, + "origin_slot": 3, + "target_id": 74, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 85, + "origin_id": -10, + "origin_slot": 4, + "target_id": 73, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 86, + "origin_id": -10, + "origin_slot": 5, + "target_id": 75, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 87, + "origin_id": -10, + "origin_slot": 6, + "target_id": 76, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/Conditioned", + "description": "Generates images from a text prompt and ControlNet conditioning (e.g. depth, canny) using Z-Image-Turbo." + } + ] + }, + "extra": { + "ue_links": [] + } +} \ No newline at end of file diff --git a/blueprints/Crop Images 2x2.json b/blueprints/Crop Images 2x2.json index 2aa42cfc3..99b89b608 100644 --- a/blueprints/Crop Images 2x2.json +++ b/blueprints/Crop Images 2x2.json @@ -1609,7 +1609,8 @@ } ], "extra": {}, - "category": "Image Tools/Crop" + "category": "Image Tools/Crop", + "description": "Splits an image into a 2×2 grid of four equal tiles." } ] }, diff --git a/blueprints/Crop Images 3x3.json b/blueprints/Crop Images 3x3.json index 3a3615ac8..6ac636da4 100644 --- a/blueprints/Crop Images 3x3.json +++ b/blueprints/Crop Images 3x3.json @@ -2946,7 +2946,8 @@ } ], "extra": {}, - "category": "Image Tools/Crop" + "category": "Image Tools/Crop", + "description": "Splits an image into a 3×3 grid of nine equal tiles." } ] }, diff --git a/blueprints/Depth to Image (Z-Image-Turbo).json b/blueprints/Depth to Image (Z-Image-Turbo).json index 4f69a8149..2790827a3 100644 --- a/blueprints/Depth to Image (Z-Image-Turbo).json +++ b/blueprints/Depth to Image (Z-Image-Turbo).json @@ -1579,7 +1579,8 @@ "VHS_MetadataImage": true, "VHS_KeepIntermediate": true }, - "category": "Image generation and editing/Depth to image" + "category": "Image generation and editing/Conditioned", + "description": "Generates an image from a depth map using Z-Image-Turbo with text conditioning." }, { "id": "458bdf3c-4b58-421c-af50-c9c663a4d74c", @@ -2461,7 +2462,8 @@ ] }, "workflowRendererVersion": "LG" - } + }, + "description": "Estimates a monocular depth map from an input image using the Lotus depth estimation model." } ] }, diff --git a/blueprints/Depth to Video (ltx 2.0).json b/blueprints/Depth to Video (ltx 2.0).json index f15212520..56912de51 100644 --- a/blueprints/Depth to Video (ltx 2.0).json +++ b/blueprints/Depth to Video (ltx 2.0).json @@ -4233,7 +4233,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Depth to video" + "category": "Video generation and editing/Conditioned", + "description": "Generates depth-controlled video with LTX-2: motion and structure follow a depth-reference video alongside text prompting, optional first-frame image conditioning, with optional synchronized audio." }, { "id": "38b60539-50a7-42f9-a5fe-bdeca26272e2", @@ -5192,7 +5193,8 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Estimates a monocular depth map from an input image using the Lotus depth estimation model." } ] }, diff --git a/blueprints/Edge-Preserving Blur.json b/blueprints/Edge-Preserving Blur.json index 18012beb1..fbda9f126 100644 --- a/blueprints/Edge-Preserving Blur.json +++ b/blueprints/Edge-Preserving Blur.json @@ -450,9 +450,10 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Blur" + "category": "Image Tools/Blur", + "description": "Applies bilateral (edge-preserving) blur to soften images while retaining detail." } ] }, "extra": {} -} +} \ No newline at end of file diff --git a/blueprints/Film Grain.json b/blueprints/Film Grain.json index a680b3ece..3226ea9aa 100644 --- a/blueprints/Film Grain.json +++ b/blueprints/Film Grain.json @@ -580,8 +580,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adds procedural film grain texture for a cinematic look via GPU fragment shader." } ] } -} +} \ No newline at end of file diff --git a/blueprints/First-Last-Frame to Video (LTX-2.3).json b/blueprints/First-Last-Frame to Video (LTX-2.3).json index 8ec9ed61a..4cae2dc24 100644 --- a/blueprints/First-Last-Frame to Video (LTX-2.3).json +++ b/blueprints/First-Last-Frame to Video (LTX-2.3).json @@ -3350,7 +3350,8 @@ } ], "extra": {}, - "category": "Video generation and editing/First-Last-Frame to Video" + "category": "Video generation and editing/Conditioned", + "description": "Generates a video interpolating between first and last keyframes using LTX-2.3." } ] }, diff --git a/blueprints/First-Last-Frame to Video.json b/blueprints/First-Last-Frame to Video.json new file mode 100644 index 000000000..d76e1e045 --- /dev/null +++ b/blueprints/First-Last-Frame to Video.json @@ -0,0 +1,3361 @@ +{ + "revision": 0, + "last_node_id": 227, + "last_link_id": 0, + "nodes": [ + { + "id": 227, + "type": "283e4561-61a2-4538-b960-265736eb041f", + "pos": [ + 620, + 3140 + ], + "size": [ + 540, + 0 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "first_frame", + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": null + }, + { + "label": "last_frame", + "localized_name": "input_1", + "name": "input_1", + "type": "IMAGE,MASK", + "link": null + }, + { + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "label": "width", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + }, + { + "label": "height", + "name": "value_1", + "type": "INT", + "widget": { + "name": "value_1" + }, + "link": null + }, + { + "label": "duration", + "name": "value_2", + "type": "INT", + "widget": { + "name": "value_2" + }, + "link": null + }, + { + "label": "fps", + "name": "value_3", + "type": "INT", + "widget": { + "name": "value_3" + }, + "link": null + }, + { + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": null + }, + { + "label": "ckpt_name", + "name": "ckpt_name_1", + "type": "COMBO", + "widget": { + "name": "ckpt_name_1" + }, + "link": null + }, + { + "name": "text_encoder", + "type": "COMBO", + "widget": { + "name": "text_encoder" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [] + } + ], + "title": "First-Last-Frame to Video", + "properties": { + "proxyWidgets": [ + [ + "222", + "text" + ], + [ + "215", + "value" + ], + [ + "216", + "value" + ], + [ + "198", + "value" + ], + [ + "205", + "value" + ], + [ + "196", + "noise_seed" + ], + [ + "224", + "ckpt_name" + ], + [ + "225", + "text_encoder" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.7" + } + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "283e4561-61a2-4538-b960-265736eb041f", + "version": 1, + "state": { + "lastGroupId": 22, + "lastNodeId": 227, + "lastLinkId": 276, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "First-Last-Frame to Video", + "inputNode": { + "id": -10, + "bounding": [ + 270, + 3100, + 120, + 240 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 3620, + 3120, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "6fe179c4-d96f-4383-b202-844f6de4922e", + "name": "input", + "type": "IMAGE,MASK", + "linkIds": [ + 251 + ], + "localized_name": "input", + "label": "first_frame", + "pos": [ + 370, + 3120 + ] + }, + { + "id": "e80df1ae-5f39-4f86-91bd-0467635e2f2d", + "name": "input_1", + "type": "IMAGE,MASK", + "linkIds": [ + 253 + ], + "localized_name": "input_1", + "label": "last_frame", + "pos": [ + 370, + 3140 + ] + }, + { + "id": "433148fa-bf73-4ab1-81d9-09e2e38ed861", + "name": "text", + "type": "STRING", + "linkIds": [ + 265 + ], + "pos": [ + 370, + 3160 + ] + }, + { + "id": "36915bc8-a6ed-4d48-8619-e0e8723228e9", + "name": "value", + "type": "INT", + "linkIds": [ + 266 + ], + "label": "width", + "pos": [ + 370, + 3180 + ] + }, + { + "id": "425a36b8-91ab-41b7-81e9-496eba064ec8", + "name": "value_1", + "type": "INT", + "linkIds": [ + 267 + ], + "label": "height", + "pos": [ + 370, + 3200 + ] + }, + { + "id": "0c9e003b-bd07-4b7d-aa6d-789e138ed161", + "name": "value_2", + "type": "INT", + "linkIds": [ + 268 + ], + "label": "duration", + "pos": [ + 370, + 3220 + ] + }, + { + "id": "581b52ff-21c5-4774-ac2a-8f69a7e09e2e", + "name": "value_3", + "type": "INT", + "linkIds": [ + 269 + ], + "label": "fps", + "pos": [ + 370, + 3240 + ] + }, + { + "id": "d03cc171-45da-4658-99aa-77252bbcf522", + "name": "noise_seed", + "type": "INT", + "linkIds": [ + 270 + ], + "pos": [ + 370, + 3260 + ] + }, + { + "id": "e68e61c8-905e-43ac-8c76-65ac52270a08", + "name": "ckpt_name_1", + "type": "COMBO", + "linkIds": [ + 272, + 275, + 276 + ], + "label": "ckpt_name", + "pos": [ + 370, + 3280 + ] + }, + { + "id": "5d065f3b-891b-499f-950b-c2df0be24536", + "name": "text_encoder", + "type": "COMBO", + "linkIds": [ + 273 + ], + "pos": [ + 370, + 3300 + ] + } + ], + "outputs": [ + { + "id": "0c8c2dc0-c67c-4bc2-9e57-6aa00db2e3a9", + "name": "VIDEO", + "type": "VIDEO", + "linkIds": [ + 252 + ], + "localized_name": "VIDEO", + "pos": [ + 3640, + 3140 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 195, + "type": "LTXVPreprocess", + "pos": [ + 1480, + 3780 + ], + "size": [ + 230, + 110 + ], + "flags": { + "collapsed": false + }, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 203 + }, + { + "localized_name": "img_compression", + "name": "img_compression", + "type": "INT", + "widget": { + "name": "img_compression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "output_image", + "name": "output_image", + "type": "IMAGE", + "links": [ + 229 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVPreprocess", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 25 + ] + }, + { + "id": 196, + "type": "RandomNoise", + "pos": [ + 1990, + 2320 + ], + "size": [ + 280, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "noise_seed", + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": 270 + } + ], + "outputs": [ + { + "localized_name": "NOISE", + "name": "NOISE", + "type": "NOISE", + "links": [ + 246 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": { + "noise_seed": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "RandomNoise", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 315253765879496, + "randomize" + ] + }, + { + "id": 197, + "type": "LTXVEmptyLatentAudio", + "pos": [ + 2090, + 3820 + ], + "size": [ + 280, + 170 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "audio_vae", + "name": "audio_vae", + "type": "VAE", + "link": 205 + }, + { + "localized_name": "frames_number", + "name": "frames_number", + "type": "INT", + "widget": { + "name": "frames_number" + }, + "link": 262 + }, + { + "localized_name": "frame_rate", + "name": "frame_rate", + "type": "INT", + "widget": { + "name": "frame_rate" + }, + "link": 207 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "Latent", + "name": "Latent", + "type": "LATENT", + "links": [ + 245 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.68", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVEmptyLatentAudio", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 97, + 25, + 1 + ] + }, + { + "id": 198, + "type": "PrimitiveInt", + "pos": [ + 760, + 3650 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 268 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 260 + ] + } + ], + "title": "Duration", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 5, + "fixed" + ] + }, + { + "id": 199, + "type": "LTXVPreprocess", + "pos": [ + 1480, + 3340 + ], + "size": [ + 230, + 110 + ], + "flags": { + "collapsed": false + }, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 210 + }, + { + "localized_name": "img_compression", + "name": "img_compression", + "type": "INT", + "widget": { + "name": "img_compression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "output_image", + "name": "output_image", + "type": "IMAGE", + "links": [ + 240 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVPreprocess", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 25 + ] + }, + { + "id": 200, + "type": "LTXVCropGuides", + "pos": [ + 2820, + 2450 + ], + "size": [ + 280, + 120 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 213 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 214 + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "link": 215 + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "links": [] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "links": [] + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "links": [ + 211 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.8.2", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + }, + "Node name for S&R": "LTXVCropGuides", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 201, + "type": "EmptyLTXVLatentVideo", + "pos": [ + 2090, + 3580 + ], + "size": [ + 280, + 200 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 218 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 219 + }, + { + "localized_name": "length", + "name": "length", + "type": "INT", + "widget": { + "name": "length" + }, + "link": 263 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 239 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.60", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "EmptyLTXVLatentVideo", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 768, + 512, + 97, + 1 + ] + }, + { + "id": 202, + "type": "LTXVConditioning", + "pos": [ + 2090, + 3400 + ], + "size": [ + 280, + 130 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 221 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 222 + }, + { + "localized_name": "frame_rate", + "name": "frame_rate", + "type": "FLOAT", + "widget": { + "name": "frame_rate" + }, + "link": 223 + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "links": [ + 236 + ] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "links": [ + 237 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.56", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVConditioning", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 25 + ] + }, + { + "id": 203, + "type": "GetImageSize", + "pos": [ + 1480, + 3500 + ], + "size": [ + 230, + 130 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 224 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 218 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": [ + 219 + ] + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": [] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "GetImageSize", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 204, + "type": "LTXVAddGuide", + "pos": [ + 2750, + 3700 + ], + "size": [ + 280, + 240 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 225 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 226 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 227 + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "link": 228 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 229 + }, + { + "localized_name": "frame_idx", + "name": "frame_idx", + "type": "INT", + "widget": { + "name": "frame_idx" + }, + "link": null + }, + { + "localized_name": "strength", + "name": "strength", + "type": "FLOAT", + "widget": { + "name": "strength" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "links": [ + 213, + 242 + ] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "links": [ + 214, + 243 + ] + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "links": [ + 244 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.12.3", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVAddGuide", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + -1, + 0.7 + ] + }, + { + "id": 205, + "type": "PrimitiveInt", + "pos": [ + 760, + 3800 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 269 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 207, + 235, + 261 + ] + } + ], + "title": "Frame Rate(int)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 25, + "fixed" + ] + }, + { + "id": 206, + "type": "LTXVAddGuide", + "pos": [ + 2750, + 3430 + ], + "size": [ + 280, + 240 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 236 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 237 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 238 + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "link": 239 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 240 + }, + { + "localized_name": "frame_idx", + "name": "frame_idx", + "type": "INT", + "widget": { + "name": "frame_idx" + }, + "link": null + }, + { + "localized_name": "strength", + "name": "strength", + "type": "FLOAT", + "widget": { + "name": "strength" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "links": [ + 225 + ] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "links": [ + 226 + ] + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "links": [ + 228 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.12.3", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVAddGuide", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + 0.7 + ] + }, + { + "id": 207, + "type": "CFGGuider", + "pos": [ + 1990, + 2500 + ], + "size": [ + 280, + 160 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 241 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 242 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 243 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "links": [ + 247 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CFGGuider", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 208, + "type": "SamplerEulerAncestral", + "pos": [ + 1990, + 2720 + ], + "size": [ + 280, + 120 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "eta", + "name": "eta", + "type": "FLOAT", + "widget": { + "name": "eta" + }, + "link": null + }, + { + "localized_name": "s_noise", + "name": "s_noise", + "type": "FLOAT", + "widget": { + "name": "s_noise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "SAMPLER", + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 248 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "SamplerEulerAncestral", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + 1 + ] + }, + { + "id": 209, + "type": "ManualSigmas", + "pos": [ + 1990, + 2910 + ], + "size": [ + 280, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "STRING", + "widget": { + "name": "sigmas" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "SIGMAS", + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 249 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ManualSigmas", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "1., 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875, 0.0" + ] + }, + { + "id": 210, + "type": "LTXVConcatAVLatent", + "pos": [ + 1990, + 3090 + ], + "size": [ + 280, + 100 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "video_latent", + "name": "video_latent", + "type": "LATENT", + "link": 244 + }, + { + "localized_name": "audio_latent", + "name": "audio_latent", + "type": "LATENT", + "link": 245 + } + ], + "outputs": [ + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "links": [ + 250 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVConcatAVLatent", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 211, + "type": "SamplerCustomAdvanced", + "pos": [ + 2460, + 2330 + ], + "size": [ + 230, + 170 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 246 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 247 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 248 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 249 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 250 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "links": [] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "links": [ + 204 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "SamplerCustomAdvanced", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 212, + "type": "ComfyMathExpression", + "pos": [ + 760, + 3970 + ], + "size": [ + 230, + 170 + ], + "flags": { + "collapsed": true + }, + "order": 17, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 235 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 223, + 234 + ] + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.17.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ComfyMathExpression", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "a" + ] + }, + { + "id": 213, + "type": "ResizeImageMaskNode", + "pos": [ + 1130, + 3340 + ], + "size": [ + 280, + 160 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": 251 + }, + { + "localized_name": "resize_type", + "name": "resize_type", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "resize_type" + }, + "link": null + }, + { + "localized_name": "width", + "name": "resize_type.width", + "type": "INT", + "widget": { + "name": "resize_type.width" + }, + "link": 208 + }, + { + "localized_name": "height", + "name": "resize_type.height", + "type": "INT", + "widget": { + "name": "resize_type.height" + }, + "link": 209 + }, + { + "localized_name": "crop", + "name": "resize_type.crop", + "type": "COMBO", + "widget": { + "name": "resize_type.crop" + }, + "link": null + }, + { + "localized_name": "scale_method", + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "resized", + "name": "resized", + "type": "*", + "links": [ + 210, + 224 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": { + "resize_type.width": true, + "resize_type.height": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ResizeImageMaskNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "scale dimensions", + 640, + 360, + "center", + "nearest-exact" + ] + }, + { + "id": 214, + "type": "ResizeImageMaskNode", + "pos": [ + 1130, + 3780 + ], + "size": [ + 280, + 160 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": 253 + }, + { + "localized_name": "resize_type", + "name": "resize_type", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "resize_type" + }, + "link": null + }, + { + "localized_name": "width", + "name": "resize_type.width", + "type": "INT", + "widget": { + "name": "resize_type.width" + }, + "link": 201 + }, + { + "localized_name": "height", + "name": "resize_type.height", + "type": "INT", + "widget": { + "name": "resize_type.height" + }, + "link": 202 + }, + { + "localized_name": "crop", + "name": "resize_type.crop", + "type": "COMBO", + "widget": { + "name": "resize_type.crop" + }, + "link": null + }, + { + "localized_name": "scale_method", + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "resized", + "name": "resized", + "type": "*", + "links": [ + 203 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": { + "resize_type.width": true, + "resize_type.height": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ResizeImageMaskNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "scale dimensions", + 640, + 360, + "center", + "nearest-exact" + ] + }, + { + "id": 215, + "type": "PrimitiveInt", + "pos": [ + 760, + 3340 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 266 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 201, + 208 + ] + } + ], + "title": "Width", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1280, + "fixed" + ] + }, + { + "id": 216, + "type": "PrimitiveInt", + "pos": [ + 760, + 3490 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 267 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 202, + 209 + ] + } + ], + "title": "height", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 720, + "fixed" + ] + }, + { + "id": 217, + "type": "CLIPTextEncode", + "pos": [ + 1320, + 2870 + ], + "size": [ + 590, + 200 + ], + "flags": { + "collapsed": false + }, + "order": 22, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 230 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 222 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.56", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "blurry, out of focus, overexposed, underexposed, low contrast, washed out colors, excessive noise, grainy texture, poor lighting, flickering, motion blur, distorted proportions, unnatural skin tones, deformed facial features, asymmetrical face, missing facial features, extra limbs, disfigured hands, wrong hand count, artifacts around text, unreadable text on shirt or hat, incorrect lettering on cap (“PNTR”), incorrect t-shirt slogan (“JUST DO IT”), missing microphone, misplaced microphone, inconsistent perspective, camera shake, incorrect depth of field, background too sharp, background clutter, distracting reflections, harsh shadows, inconsistent lighting direction, color banding, cartoonish rendering, 3D CGI look, unrealistic materials, uncanny valley effect, incorrect ethnicity, wrong gender, exaggerated expressions, smiling, laughing, exaggerated sadness, wrong gaze direction, eyes looking at camera, mismatched lip sync, silent or muted audio, distorted voice, robotic voice, echo, background noise, off-sync audio, missing sniff sounds, incorrect dialogue, added dialogue, repetitive speech, jittery movement, awkward pauses, incorrect timing, unnatural transitions, inconsistent framing, tilted camera, missing door or shelves, missing shallow depth of field, flat lighting, inconsistent tone, cinematic oversaturation, stylized filters, or AI artifacts." + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 218, + "type": "CreateVideo", + "pos": [ + 3280, + 2320 + ], + "size": [ + 280, + 130 + ], + "flags": {}, + "order": 23, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 232 + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 233 + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 234 + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [ + 252 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.14.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CreateVideo", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 24 + ] + }, + { + "id": 219, + "type": "VAEDecodeTiled", + "pos": [ + 2820, + 2630 + ], + "size": [ + 280, + 200 + ], + "flags": {}, + "order": 24, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 211 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 212 + }, + { + "localized_name": "tile_size", + "name": "tile_size", + "type": "INT", + "widget": { + "name": "tile_size" + }, + "link": null + }, + { + "localized_name": "overlap", + "name": "overlap", + "type": "INT", + "widget": { + "name": "overlap" + }, + "link": null + }, + { + "localized_name": "temporal_size", + "name": "temporal_size", + "type": "INT", + "widget": { + "name": "temporal_size" + }, + "link": null + }, + { + "localized_name": "temporal_overlap", + "name": "temporal_overlap", + "type": "INT", + "widget": { + "name": "temporal_overlap" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 232 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEDecodeTiled", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 768, + 64, + 4096, + 64 + ] + }, + { + "id": 220, + "type": "LTXVAudioVAEDecode", + "pos": [ + 2820, + 2920 + ], + "size": [ + 280, + 100 + ], + "flags": {}, + "order": 25, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 216 + }, + { + "label": "Audio VAE", + "localized_name": "audio_vae", + "name": "audio_vae", + "type": "VAE", + "link": 217 + } + ], + "outputs": [ + { + "localized_name": "Audio", + "name": "Audio", + "type": "AUDIO", + "links": [ + 233 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVAudioVAEDecode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 221, + "type": "LTXVSeparateAVLatent", + "pos": [ + 2460, + 2580 + ], + "size": [ + 250, + 100 + ], + "flags": {}, + "order": 26, + "mode": 0, + "inputs": [ + { + "localized_name": "av_latent", + "name": "av_latent", + "type": "LATENT", + "link": 204 + } + ], + "outputs": [ + { + "localized_name": "video_latent", + "name": "video_latent", + "type": "LATENT", + "links": [ + 215 + ] + }, + { + "localized_name": "audio_latent", + "name": "audio_latent", + "type": "LATENT", + "links": [ + 216 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.5.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LTXVSeparateAVLatent", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 222, + "type": "CLIPTextEncode", + "pos": [ + 1310, + 2380 + ], + "size": [ + 620, + 420 + ], + "flags": {}, + "order": 27, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 231 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 265 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 221 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.56", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.5.2", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 223, + "type": "CheckpointLoaderSimple", + "pos": [ + 770, + 2380 + ], + "size": [ + 420, + 160 + ], + "flags": {}, + "order": 28, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 276 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 241 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 212, + 227, + 238 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.10.0", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + }, + "Node name for S&R": "CheckpointLoaderSimple", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "ltx-2.3-22b-distilled-fp8.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-distilled-fp8.safetensors", + "directory": "checkpoints" + } + ] + }, + "widgets_values": [ + "ltx-2.3-22b-distilled-fp8.safetensors" + ] + }, + { + "id": 224, + "type": "LTXVAudioVAELoader", + "pos": [ + 770, + 2660 + ], + "size": [ + 420, + 110 + ], + "flags": {}, + "order": 29, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 272 + } + ], + "outputs": [ + { + "localized_name": "Audio VAE", + "name": "Audio VAE", + "type": "VAE", + "links": [ + 205, + 217 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.10.0", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + }, + "Node name for S&R": "LTXVAudioVAELoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "ltx-2.3-22b-distilled-fp8.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-distilled-fp8.safetensors", + "directory": "checkpoints" + } + ] + }, + "widgets_values": [ + "ltx-2.3-22b-distilled-fp8.safetensors" + ] + }, + { + "id": 225, + "type": "LTXAVTextEncoderLoader", + "pos": [ + 770, + 2890 + ], + "size": [ + 410, + 160 + ], + "flags": {}, + "order": 30, + "mode": 0, + "inputs": [ + { + "localized_name": "text_encoder", + "name": "text_encoder", + "type": "COMBO", + "widget": { + "name": "text_encoder" + }, + "link": 273 + }, + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 275 + }, + { + "localized_name": "device", + "name": "device", + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 230, + 231 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.10.0", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.5.2" + }, + "Node name for S&R": "LTXAVTextEncoderLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "gemma_3_12B_it_fp4_mixed.safetensors", + "url": "https://huggingface.co/Comfy-Org/ltx-2/resolve/main/split_files/text_encoders/gemma_3_12B_it_fp4_mixed.safetensors", + "directory": "text_encoders" + }, + { + "name": "ltx-2.3-22b-distilled-fp8.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-distilled-fp8.safetensors", + "directory": "checkpoints" + } + ] + }, + "widgets_values": [ + "gemma_3_12B_it_fp4_mixed.safetensors", + "ltx-2.3-22b-distilled-fp8.safetensors", + "default" + ] + }, + { + "id": 226, + "type": "ComfyMathExpression", + "pos": [ + 760, + 4020 + ], + "size": [ + 400, + 200 + ], + "flags": { + "collapsed": true + }, + "order": 31, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 260 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": 261 + }, + { + "label": "c", + "localized_name": "values.c", + "name": "values.c", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 262, + 263 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {}, + "version": "7.7" + }, + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "a * b + 1" + ] + } + ], + "groups": [ + { + "id": 1, + "title": "Conditioning", + "bounding": [ + 1850, + 3250, + 1370, + 800 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "Settings", + "bounding": [ + 730, + 3250, + 290, + 800 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "FIrst Frame", + "bounding": [ + 1050, + 3250, + 770, + 400 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Last Frame", + "bounding": [ + 1050, + 3680, + 770, + 370 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 5, + "title": "Model", + "bounding": [ + 730, + 2240, + 500, + 980 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 6, + "title": "Prompt", + "bounding": [ + 1260, + 2240, + 680, + 980 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 7, + "title": "Sampling", + "bounding": [ + 1970, + 2240, + 770, + 980 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 8, + "title": "Decoding", + "bounding": [ + 2770, + 2240, + 450, + 980 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 203, + "origin_id": 214, + "origin_slot": 0, + "target_id": 195, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 205, + "origin_id": 224, + "origin_slot": 0, + "target_id": 197, + "target_slot": 0, + "type": "VAE" + }, + { + "id": 207, + "origin_id": 205, + "origin_slot": 0, + "target_id": 197, + "target_slot": 2, + "type": "INT" + }, + { + "id": 210, + "origin_id": 213, + "origin_slot": 0, + "target_id": 199, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 213, + "origin_id": 204, + "origin_slot": 0, + "target_id": 200, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 214, + "origin_id": 204, + "origin_slot": 1, + "target_id": 200, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 215, + "origin_id": 221, + "origin_slot": 0, + "target_id": 200, + "target_slot": 2, + "type": "LATENT" + }, + { + "id": 218, + "origin_id": 203, + "origin_slot": 0, + "target_id": 201, + "target_slot": 0, + "type": "INT" + }, + { + "id": 219, + "origin_id": 203, + "origin_slot": 1, + "target_id": 201, + "target_slot": 1, + "type": "INT" + }, + { + "id": 221, + "origin_id": 222, + "origin_slot": 0, + "target_id": 202, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 222, + "origin_id": 217, + "origin_slot": 0, + "target_id": 202, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 223, + "origin_id": 212, + "origin_slot": 0, + "target_id": 202, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 224, + "origin_id": 213, + "origin_slot": 0, + "target_id": 203, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 225, + "origin_id": 206, + "origin_slot": 0, + "target_id": 204, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 226, + "origin_id": 206, + "origin_slot": 1, + "target_id": 204, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 227, + "origin_id": 223, + "origin_slot": 2, + "target_id": 204, + "target_slot": 2, + "type": "VAE" + }, + { + "id": 228, + "origin_id": 206, + "origin_slot": 2, + "target_id": 204, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 229, + "origin_id": 195, + "origin_slot": 0, + "target_id": 204, + "target_slot": 4, + "type": "IMAGE" + }, + { + "id": 236, + "origin_id": 202, + "origin_slot": 0, + "target_id": 206, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 237, + "origin_id": 202, + "origin_slot": 1, + "target_id": 206, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 238, + "origin_id": 223, + "origin_slot": 2, + "target_id": 206, + "target_slot": 2, + "type": "VAE" + }, + { + "id": 239, + "origin_id": 201, + "origin_slot": 0, + "target_id": 206, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 240, + "origin_id": 199, + "origin_slot": 0, + "target_id": 206, + "target_slot": 4, + "type": "IMAGE" + }, + { + "id": 241, + "origin_id": 223, + "origin_slot": 0, + "target_id": 207, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 242, + "origin_id": 204, + "origin_slot": 0, + "target_id": 207, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 243, + "origin_id": 204, + "origin_slot": 1, + "target_id": 207, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 244, + "origin_id": 204, + "origin_slot": 2, + "target_id": 210, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 245, + "origin_id": 197, + "origin_slot": 0, + "target_id": 210, + "target_slot": 1, + "type": "LATENT" + }, + { + "id": 246, + "origin_id": 196, + "origin_slot": 0, + "target_id": 211, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 247, + "origin_id": 207, + "origin_slot": 0, + "target_id": 211, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 248, + "origin_id": 208, + "origin_slot": 0, + "target_id": 211, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 249, + "origin_id": 209, + "origin_slot": 0, + "target_id": 211, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 250, + "origin_id": 210, + "origin_slot": 0, + "target_id": 211, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 235, + "origin_id": 205, + "origin_slot": 0, + "target_id": 212, + "target_slot": 0, + "type": "INT" + }, + { + "id": 208, + "origin_id": 215, + "origin_slot": 0, + "target_id": 213, + "target_slot": 2, + "type": "INT" + }, + { + "id": 209, + "origin_id": 216, + "origin_slot": 0, + "target_id": 213, + "target_slot": 3, + "type": "INT" + }, + { + "id": 201, + "origin_id": 215, + "origin_slot": 0, + "target_id": 214, + "target_slot": 2, + "type": "INT" + }, + { + "id": 202, + "origin_id": 216, + "origin_slot": 0, + "target_id": 214, + "target_slot": 3, + "type": "INT" + }, + { + "id": 230, + "origin_id": 225, + "origin_slot": 0, + "target_id": 217, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 232, + "origin_id": 219, + "origin_slot": 0, + "target_id": 218, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 233, + "origin_id": 220, + "origin_slot": 0, + "target_id": 218, + "target_slot": 1, + "type": "AUDIO" + }, + { + "id": 234, + "origin_id": 212, + "origin_slot": 0, + "target_id": 218, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 211, + "origin_id": 200, + "origin_slot": 2, + "target_id": 219, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 212, + "origin_id": 223, + "origin_slot": 2, + "target_id": 219, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 216, + "origin_id": 221, + "origin_slot": 1, + "target_id": 220, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 217, + "origin_id": 224, + "origin_slot": 0, + "target_id": 220, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 204, + "origin_id": 211, + "origin_slot": 1, + "target_id": 221, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 231, + "origin_id": 225, + "origin_slot": 0, + "target_id": 222, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 251, + "origin_id": -10, + "origin_slot": 0, + "target_id": 213, + "target_slot": 0, + "type": "IMAGE,MASK" + }, + { + "id": 253, + "origin_id": -10, + "origin_slot": 1, + "target_id": 214, + "target_slot": 0, + "type": "IMAGE,MASK" + }, + { + "id": 252, + "origin_id": 218, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 260, + "origin_id": 198, + "origin_slot": 0, + "target_id": 226, + "target_slot": 0, + "type": "INT" + }, + { + "id": 261, + "origin_id": 205, + "origin_slot": 0, + "target_id": 226, + "target_slot": 1, + "type": "INT" + }, + { + "id": 262, + "origin_id": 226, + "origin_slot": 1, + "target_id": 197, + "target_slot": 1, + "type": "INT" + }, + { + "id": 263, + "origin_id": 226, + "origin_slot": 1, + "target_id": 201, + "target_slot": 2, + "type": "INT" + }, + { + "id": 265, + "origin_id": -10, + "origin_slot": 2, + "target_id": 222, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 266, + "origin_id": -10, + "origin_slot": 3, + "target_id": 215, + "target_slot": 0, + "type": "INT" + }, + { + "id": 267, + "origin_id": -10, + "origin_slot": 4, + "target_id": 216, + "target_slot": 0, + "type": "INT" + }, + { + "id": 268, + "origin_id": -10, + "origin_slot": 5, + "target_id": 198, + "target_slot": 0, + "type": "INT" + }, + { + "id": 269, + "origin_id": -10, + "origin_slot": 6, + "target_id": 205, + "target_slot": 0, + "type": "INT" + }, + { + "id": 270, + "origin_id": -10, + "origin_slot": 7, + "target_id": 196, + "target_slot": 0, + "type": "INT" + }, + { + "id": 272, + "origin_id": -10, + "origin_slot": 8, + "target_id": 224, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 273, + "origin_id": -10, + "origin_slot": 9, + "target_id": 225, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 275, + "origin_id": -10, + "origin_slot": 8, + "target_id": 225, + "target_slot": 1, + "type": "COMBO" + }, + { + "id": 276, + "origin_id": -10, + "origin_slot": 8, + "target_id": 223, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": {}, + "category": "Video generation and editing/FLF2V", + "description": "Generates a video that interpolates between the first and last keyframes using LTX-2.3, including optional audio." + } + ] + }, + "extra": { + "ue_links": [] + } +} \ No newline at end of file diff --git a/blueprints/Frame Interpolation.json b/blueprints/Frame Interpolation.json new file mode 100644 index 000000000..8e183de7e --- /dev/null +++ b/blueprints/Frame Interpolation.json @@ -0,0 +1,858 @@ +{ + "revision": 0, + "last_node_id": 16, + "last_link_id": 0, + "nodes": [ + { + "id": 16, + "type": "022693be-2baa-4009-870a-28921508a7ef", + "pos": [ + -2990, + -3240 + ], + "size": [ + 410, + 200 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": null + }, + { + "label": "multiplier", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + }, + { + "label": "enable_fps_multiplier", + "name": "value_1", + "type": "BOOLEAN", + "widget": { + "name": "value_1" + }, + "link": null + }, + { + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": null + } + ], + "outputs": [ + { + "label": "VIDEO", + "name": "VIDEO_1", + "type": "VIDEO", + "links": [] + }, + { + "name": "IMAGE", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "proxyWidgets": [ + [ + "9", + "value" + ], + [ + "13", + "value" + ], + [ + "1", + "model_name" + ] + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [], + "title": "Frame Interpolation" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "022693be-2baa-4009-870a-28921508a7ef", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 17, + "lastLinkId": 28, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Frame Interpolation", + "inputNode": { + "id": -10, + "bounding": [ + -2810, + -3070, + 159.7421875, + 120 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -1270, + -3075, + 120, + 80 + ] + }, + "inputs": [ + { + "id": "05e31c51-dcb6-4a1e-9651-1b9ad4f7a287", + "name": "video", + "type": "VIDEO", + "linkIds": [ + 2 + ], + "localized_name": "video", + "pos": [ + -2670.2578125, + -3050 + ] + }, + { + "id": "feecb409-7d1c-4a99-9c63-50c5fecdd3c9", + "name": "value", + "type": "INT", + "linkIds": [ + 22 + ], + "label": "multiplier", + "pos": [ + -2670.2578125, + -3030 + ] + }, + { + "id": "0b8a861b-b581-4068-9e8c-f8d15daf1ca6", + "name": "value_1", + "type": "BOOLEAN", + "linkIds": [ + 23 + ], + "label": "enable_fps_multiplier", + "pos": [ + -2670.2578125, + -3010 + ] + }, + { + "id": "a22b101e-8773-4e17-a297-7ee3aae09162", + "name": "model_name", + "type": "COMBO", + "linkIds": [ + 24 + ], + "pos": [ + -2670.2578125, + -2990 + ] + } + ], + "outputs": [ + { + "id": "ef2ada05-d5aa-492a-9394-6c3e71e39ebb", + "name": "VIDEO_1", + "type": "VIDEO", + "linkIds": [ + 26 + ], + "label": "VIDEO", + "pos": [ + -1250, + -3055 + ] + }, + { + "id": "5aacc622-2a07-4983-b31c-e04461f7f953", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 28 + ], + "pos": [ + -1250, + -3035 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 1, + "type": "FrameInterpolationModelLoader", + "pos": [ + -2510, + -3370 + ], + "size": [ + 370, + 90 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "model_name", + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": 24 + } + ], + "outputs": [ + { + "localized_name": "INTERP_MODEL", + "name": "INTERP_MODEL", + "type": "INTERP_MODEL", + "links": [ + 1 + ] + } + ], + "properties": { + "Node name for S&R": "FrameInterpolationModelLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3", + "models": [ + { + "name": "film_net_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/frame_interpolation/resolve/main/frame_interpolation/film_net_fp16.safetensors", + "directory": "frame_interpolation" + } + ] + }, + "widgets_values": [ + "film_net_fp16.safetensors" + ] + }, + { + "id": 2, + "type": "FrameInterpolate", + "pos": [ + -2040, + -3370 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "interp_model", + "name": "interp_model", + "type": "INTERP_MODEL", + "link": 1 + }, + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 3 + }, + { + "localized_name": "multiplier", + "name": "multiplier", + "type": "INT", + "widget": { + "name": "multiplier" + }, + "link": 8 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 4, + 28 + ] + } + ], + "properties": { + "Node name for S&R": "FrameInterpolate", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + 2 + ] + }, + { + "id": 5, + "type": "CreateVideo", + "pos": [ + -1600, + -3370 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 4 + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 5 + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 12 + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [ + 26 + ] + } + ], + "properties": { + "Node name for S&R": "CreateVideo", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + 30 + ] + }, + { + "id": 9, + "type": "PrimitiveInt", + "pos": [ + -2500, + -2970 + ], + "size": [ + 270, + 90 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 22 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 8, + 19 + ] + } + ], + "title": "Int (Multiplier)", + "properties": { + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + 2, + "fixed" + ] + }, + { + "id": 10, + "type": "ComfySwitchNode", + "pos": [ + -1610, + -3120 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 11 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 13 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 15 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 12 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + true + ] + }, + { + "id": 13, + "type": "PrimitiveBoolean", + "pos": [ + -2500, + -2770 + ], + "size": [ + 310, + 90 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 23 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 15 + ] + } + ], + "title": "Boolean (Apply multiplier to FPS?)", + "properties": { + "Node name for S&R": "PrimitiveBoolean", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + true + ] + }, + { + "id": 3, + "type": "GetVideoComponents", + "pos": [ + -2500, + -3170 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 2 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 3 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": [ + 5 + ] + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": [ + 11, + 18 + ] + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + } + }, + { + "id": 11, + "type": "ComfyMathExpression", + "pos": [ + -2090, + -3070 + ], + "size": [ + 400, + 210 + ], + "flags": { + "collapsed": false + }, + "order": 6, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 18 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": 19 + }, + { + "label": "c", + "localized_name": "values.c", + "name": "values.c", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 13 + ] + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": null + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.19.3" + }, + "widgets_values": [ + "min(abs(b), 16) * a" + ] + } + ], + "groups": [], + "links": [ + { + "id": 1, + "origin_id": 1, + "origin_slot": 0, + "target_id": 2, + "target_slot": 0, + "type": "INTERP_MODEL" + }, + { + "id": 3, + "origin_id": 3, + "origin_slot": 0, + "target_id": 2, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 8, + "origin_id": 9, + "origin_slot": 0, + "target_id": 2, + "target_slot": 2, + "type": "INT" + }, + { + "id": 4, + "origin_id": 2, + "origin_slot": 0, + "target_id": 5, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 5, + "origin_id": 3, + "origin_slot": 1, + "target_id": 5, + "target_slot": 1, + "type": "AUDIO" + }, + { + "id": 12, + "origin_id": 10, + "origin_slot": 0, + "target_id": 5, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 11, + "origin_id": 3, + "origin_slot": 2, + "target_id": 10, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 13, + "origin_id": 11, + "origin_slot": 0, + "target_id": 10, + "target_slot": 1, + "type": "FLOAT" + }, + { + "id": 15, + "origin_id": 13, + "origin_slot": 0, + "target_id": 10, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 18, + "origin_id": 3, + "origin_slot": 2, + "target_id": 11, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 19, + "origin_id": 9, + "origin_slot": 0, + "target_id": 11, + "target_slot": 1, + "type": "INT" + }, + { + "id": 2, + "origin_id": -10, + "origin_slot": 0, + "target_id": 3, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 22, + "origin_id": -10, + "origin_slot": 1, + "target_id": 9, + "target_slot": 0, + "type": "INT" + }, + { + "id": 23, + "origin_id": -10, + "origin_slot": 2, + "target_id": 13, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 24, + "origin_id": -10, + "origin_slot": 3, + "target_id": 1, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 26, + "origin_id": 5, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 28, + "origin_id": 2, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "IMAGE" + } + ], + "extra": {}, + "category": "Video Tools", + "description": "Increases video frame rate by synthesizing intermediate frames with a frame interpolation model." + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Geometry Estimation (MoGe).json b/blueprints/Geometry Estimation (MoGe).json new file mode 100644 index 000000000..e6f08bf71 --- /dev/null +++ b/blueprints/Geometry Estimation (MoGe).json @@ -0,0 +1,1266 @@ +{ + "revision": 0, + "last_node_id": 67, + "last_link_id": 0, + "nodes": [ + { + "id": 67, + "type": "936dfaf2-575a-48b5-9e0c-df391319d11f", + "pos": [ + -3950, + 5000 + ], + "size": [ + 430, + 480 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "source_image", + "name": "source_image", + "type": "IMAGE", + "link": null + }, + { + "localized_name": "inference_resolution", + "name": "inference_resolution", + "type": "INT", + "widget": { + "name": "inference_resolution" + }, + "link": null + }, + { + "localized_name": "inference_batch_size", + "name": "inference_batch_size", + "type": "INT", + "widget": { + "name": "inference_batch_size" + }, + "link": null + }, + { + "localized_name": "mesh_frame_index", + "name": "mesh_frame_index", + "type": "INT", + "widget": { + "name": "mesh_frame_index" + }, + "link": null + }, + { + "localized_name": "mesh_decimation", + "name": "mesh_decimation", + "type": "INT", + "widget": { + "name": "mesh_decimation" + }, + "link": null + }, + { + "localized_name": "mesh_gap_threshold", + "name": "mesh_gap_threshold", + "type": "FLOAT", + "widget": { + "name": "mesh_gap_threshold" + }, + "link": null + }, + { + "localized_name": "mesh_texture", + "name": "mesh_texture", + "type": "BOOLEAN", + "widget": { + "name": "mesh_texture" + }, + "link": null + }, + { + "localized_name": "moge_model", + "name": "moge_model", + "type": "COMBO", + "widget": { + "name": "moge_model" + }, + "link": null + }, + { + "label": "auto_resize_input", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "mesh", + "name": "mesh", + "type": "MESH", + "links": [] + }, + { + "localized_name": "normal_opengl", + "name": "normal_opengl", + "type": "IMAGE", + "links": [] + }, + { + "localized_name": "normal_directx", + "name": "normal_directx", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "55", + "resolution_level" + ], + [ + "55", + "batch_size" + ], + [ + "54", + "batch_index" + ], + [ + "54", + "decimation" + ], + [ + "54", + "discontinuity_threshold" + ], + [ + "54", + "texture" + ], + [ + "58", + "model_name" + ], + [ + "66", + "switch" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Geometry Estimation (MoGe)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "936dfaf2-575a-48b5-9e0c-df391319d11f", + "version": 1, + "state": { + "lastGroupId": 1, + "lastNodeId": 69, + "lastLinkId": 91, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Geometry Estimation (MoGe)", + "inputNode": { + "id": -10, + "bounding": [ + -5130, + 5320, + 167.337890625, + 228 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -3090, + 4966, + 131.51953125, + 108 + ] + }, + "inputs": [ + { + "id": "cc8ce79d-ba20-4a25-a51c-c2afcd35e520", + "name": "source_image", + "type": "IMAGE", + "linkIds": [ + 48, + 55, + 56, + 82 + ], + "localized_name": "source_image", + "pos": [ + -4986.662109375, + 5344 + ] + }, + { + "id": "06eefa21-8e60-49f3-9a34-35b081f4ae52", + "name": "inference_resolution", + "type": "INT", + "linkIds": [ + 73 + ], + "localized_name": "inference_resolution", + "pos": [ + -4986.662109375, + 5364 + ] + }, + { + "id": "616638fe-f603-4d10-bae9-fc87c134380f", + "name": "inference_batch_size", + "type": "INT", + "linkIds": [ + 74 + ], + "localized_name": "inference_batch_size", + "pos": [ + -4986.662109375, + 5384 + ] + }, + { + "id": "fcacfca9-7927-4c38-94da-8ab22256325f", + "name": "mesh_frame_index", + "type": "INT", + "linkIds": [ + 75 + ], + "localized_name": "mesh_frame_index", + "pos": [ + -4986.662109375, + 5404 + ] + }, + { + "id": "acbfe7f9-1b69-42c1-8614-4ccf54b28d4e", + "name": "mesh_decimation", + "type": "INT", + "linkIds": [ + 76 + ], + "localized_name": "mesh_decimation", + "pos": [ + -4986.662109375, + 5424 + ] + }, + { + "id": "cd20f9a7-3a0a-4c4c-98d7-96f423867b87", + "name": "mesh_gap_threshold", + "type": "FLOAT", + "linkIds": [ + 77 + ], + "localized_name": "mesh_gap_threshold", + "pos": [ + -4986.662109375, + 5444 + ] + }, + { + "id": "6f5c15f7-7f77-4fc9-b47b-3514467b06b6", + "name": "mesh_texture", + "type": "BOOLEAN", + "linkIds": [ + 78 + ], + "localized_name": "mesh_texture", + "pos": [ + -4986.662109375, + 5464 + ] + }, + { + "id": "65694805-186e-4181-a721-df8b5af49d31", + "name": "moge_model", + "type": "COMBO", + "linkIds": [ + 79 + ], + "localized_name": "moge_model", + "pos": [ + -4986.662109375, + 5484 + ] + }, + { + "id": "badf1be1-53c6-4fc1-b5cd-79ad3daf1674", + "name": "switch", + "type": "BOOLEAN", + "linkIds": [ + 83 + ], + "label": "auto_resize_input", + "pos": [ + -4986.662109375, + 5504 + ] + } + ], + "outputs": [ + { + "id": "3c616ea0-9a4c-4cff-a405-662320229df0", + "name": "mesh", + "type": "MESH", + "linkIds": [ + 34 + ], + "localized_name": "mesh", + "pos": [ + -3066, + 4990 + ] + }, + { + "id": "ff85a763-b7f7-4bcc-9b1d-a4eaf55ad2f9", + "name": "normal_opengl", + "type": "IMAGE", + "linkIds": [ + 62 + ], + "localized_name": "normal_opengl", + "pos": [ + -3066, + 5010 + ] + }, + { + "id": "26b3f88a-0ba0-4d4d-9c7d-0ad76106c844", + "name": "normal_directx", + "type": "IMAGE", + "linkIds": [ + 63 + ], + "localized_name": "normal_directx", + "pos": [ + -3066, + 5030 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 54, + "type": "MoGePointMapToMesh", + "pos": [ + -3440, + 5220 + ], + "size": [ + 290, + 200 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "link": 33 + }, + { + "localized_name": "batch_index", + "name": "batch_index", + "type": "INT", + "widget": { + "name": "batch_index" + }, + "link": 75 + }, + { + "localized_name": "decimation", + "name": "decimation", + "type": "INT", + "widget": { + "name": "decimation" + }, + "link": 76 + }, + { + "localized_name": "discontinuity_threshold", + "name": "discontinuity_threshold", + "type": "FLOAT", + "widget": { + "name": "discontinuity_threshold" + }, + "link": 77 + }, + { + "localized_name": "texture", + "name": "texture", + "type": "BOOLEAN", + "widget": { + "name": "texture" + }, + "link": 78 + } + ], + "outputs": [ + { + "localized_name": "MESH", + "name": "MESH", + "type": "MESH", + "links": [ + 34 + ] + } + ], + "properties": { + "Node name for S&R": "MoGePointMapToMesh", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + 1, + 0.04, + true + ] + }, + { + "id": 55, + "type": "MoGeInference", + "pos": [ + -3790, + 5180 + ], + "size": [ + 270, + 230 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_model", + "name": "moge_model", + "type": "MOGE_MODEL", + "link": 58 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 81 + }, + { + "localized_name": "resolution_level", + "name": "resolution_level", + "type": "INT", + "widget": { + "name": "resolution_level" + }, + "link": 73 + }, + { + "localized_name": "fov_x_degrees", + "name": "fov_x_degrees", + "type": "FLOAT", + "widget": { + "name": "fov_x_degrees" + }, + "link": null + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": 74 + }, + { + "localized_name": "force_projection", + "name": "force_projection", + "type": "BOOLEAN", + "widget": { + "name": "force_projection" + }, + "link": null + }, + { + "localized_name": "apply_mask", + "name": "apply_mask", + "type": "BOOLEAN", + "widget": { + "name": "apply_mask" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "links": [ + 33, + 59, + 60 + ] + } + ], + "properties": { + "Node name for S&R": "MoGeInference", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 9, + 0, + 4, + true, + true + ] + }, + { + "id": 58, + "type": "LoadMoGeModel", + "pos": [ + -4180, + 4910 + ], + "size": [ + 270, + 140 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "model_name", + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": 79 + } + ], + "outputs": [ + { + "localized_name": "MOGE_MODEL", + "name": "MOGE_MODEL", + "type": "MOGE_MODEL", + "links": [ + 58 + ] + } + ], + "properties": { + "Node name for S&R": "LoadMoGeModel", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "models": [ + { + "name": "moge_2_vitl_normal_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/MoGe/resolve/main/geometry_estimation/moge_2_vitl_normal_fp16.safetensors", + "directory": "geometry_estimation" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "moge_2_vitl_normal_fp16.safetensors" + ] + }, + { + "id": 59, + "type": "ComfyMathExpression", + "pos": [ + -4720, + 4910 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT,BOOLEAN", + "link": 49 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": null + }, + { + "localized_name": "BOOL", + "name": "BOOL", + "type": "BOOLEAN", + "links": [ + 53 + ] + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "a > 2048" + ] + }, + { + "id": 60, + "type": "GetImageSize", + "pos": [ + -4980, + 4910 + ], + "size": [ + 230, + 160 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 48 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 49 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": null + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetImageSize", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 61, + "type": "ResizeImagesByLongerEdge", + "pos": [ + -4650, + 5210 + ], + "size": [ + 310, + 110 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 55 + }, + { + "localized_name": "longer_edge", + "name": "longer_edge", + "type": "INT", + "widget": { + "name": "longer_edge" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 54 + ] + } + ], + "properties": { + "Node name for S&R": "ResizeImagesByLongerEdge", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 2048 + ] + }, + { + "id": 62, + "type": "ComfySwitchNode", + "pos": [ + -4180, + 5120 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 56 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 54 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 53 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 80 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 63, + "type": "MoGeRender", + "pos": [ + -3430, + 4890 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "link": 59 + }, + { + "localized_name": "output", + "name": "output", + "type": "COMBO", + "widget": { + "name": "output" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 62 + ] + } + ], + "properties": { + "Node name for S&R": "MoGeRender", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "normal_opengl" + ] + }, + { + "id": 64, + "type": "MoGeRender", + "pos": [ + -3430, + 5050 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "link": 60 + }, + { + "localized_name": "output", + "name": "output", + "type": "COMBO", + "widget": { + "name": "output" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 63 + ] + } + ], + "properties": { + "Node name for S&R": "MoGeRender", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "normal_directx" + ] + }, + { + "id": 66, + "type": "ComfySwitchNode", + "pos": [ + -4160, + 5340 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 82 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 80 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 83 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 81 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + true + ] + } + ], + "groups": [ + { + "id": 1, + "title": "auto_resize_if_width_gt_2048", + "bounding": [ + -5000, + 4840, + 690, + 280 + ], + "color": "#3f789e", + "flags": {} + } + ], + "links": [ + { + "id": 33, + "origin_id": 55, + "origin_slot": 0, + "target_id": 54, + "target_slot": 0, + "type": "MOGE_GEOMETRY" + }, + { + "id": 58, + "origin_id": 58, + "origin_slot": 0, + "target_id": 55, + "target_slot": 0, + "type": "MOGE_MODEL" + }, + { + "id": 49, + "origin_id": 60, + "origin_slot": 0, + "target_id": 59, + "target_slot": 0, + "type": "INT" + }, + { + "id": 54, + "origin_id": 61, + "origin_slot": 0, + "target_id": 62, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 53, + "origin_id": 59, + "origin_slot": 2, + "target_id": 62, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 59, + "origin_id": 55, + "origin_slot": 0, + "target_id": 63, + "target_slot": 0, + "type": "MOGE_GEOMETRY" + }, + { + "id": 60, + "origin_id": 55, + "origin_slot": 0, + "target_id": 64, + "target_slot": 0, + "type": "MOGE_GEOMETRY" + }, + { + "id": 48, + "origin_id": -10, + "origin_slot": 0, + "target_id": 60, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 55, + "origin_id": -10, + "origin_slot": 0, + "target_id": 61, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 56, + "origin_id": -10, + "origin_slot": 0, + "target_id": 62, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 34, + "origin_id": 54, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "MESH" + }, + { + "id": 62, + "origin_id": 63, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 63, + "origin_id": 64, + "origin_slot": 0, + "target_id": -20, + "target_slot": 2, + "type": "IMAGE" + }, + { + "id": 73, + "origin_id": -10, + "origin_slot": 1, + "target_id": 55, + "target_slot": 2, + "type": "INT" + }, + { + "id": 74, + "origin_id": -10, + "origin_slot": 2, + "target_id": 55, + "target_slot": 4, + "type": "INT" + }, + { + "id": 75, + "origin_id": -10, + "origin_slot": 3, + "target_id": 54, + "target_slot": 1, + "type": "INT" + }, + { + "id": 76, + "origin_id": -10, + "origin_slot": 4, + "target_id": 54, + "target_slot": 2, + "type": "INT" + }, + { + "id": 77, + "origin_id": -10, + "origin_slot": 5, + "target_id": 54, + "target_slot": 3, + "type": "FLOAT" + }, + { + "id": 78, + "origin_id": -10, + "origin_slot": 6, + "target_id": 54, + "target_slot": 4, + "type": "BOOLEAN" + }, + { + "id": 79, + "origin_id": -10, + "origin_slot": 7, + "target_id": 58, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 80, + "origin_id": 62, + "origin_slot": 0, + "target_id": 66, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 81, + "origin_id": 66, + "origin_slot": 0, + "target_id": 55, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 82, + "origin_id": -10, + "origin_slot": 0, + "target_id": 66, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 83, + "origin_id": -10, + "origin_slot": 8, + "target_id": 66, + "target_slot": 2, + "type": "BOOLEAN" + } + ], + "category": "3D/Geometry Estimation", + "description": "Estimates 3D scene geometry from an input image using MoGe, outputting a mesh plus OpenGL and DirectX normal maps.", + "extra": {} + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Get Any Video Frame.json b/blueprints/Get Any Video Frame.json new file mode 100644 index 000000000..9ff0f8e6e --- /dev/null +++ b/blueprints/Get Any Video Frame.json @@ -0,0 +1,485 @@ +{ + "revision": 0, + "last_node_id": 98, + "last_link_id": 0, + "nodes": [ + { + "id": 98, + "type": "dca6e78d-fb06-421e-97f7-6ce17a665260", + "pos": [ + -410, + -2230 + ], + "size": [ + 270, + 104 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "video", + "type": "VIDEO", + "link": null + }, + { + "label": "frame_index", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "title": "Get Any Video Frame", + "properties": { + "proxyWidgets": [ + [ + "100", + "value" + ] + ] + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "dca6e78d-fb06-421e-97f7-6ce17a665260", + "version": 1, + "state": { + "lastGroupId": 1, + "lastNodeId": 136, + "lastLinkId": 302, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Get Any Video Frame", + "inputNode": { + "id": -10, + "bounding": [ + 380, + -57, + 120, + 80 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1460, + -57, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "2ceec378-8dcf-4340-8570-155967f59a93", + "name": "video", + "type": "VIDEO", + "linkIds": [ + 4 + ], + "pos": [ + 480, + -37 + ] + }, + { + "id": "819955f6-c686-4896-8032-ff2d0059109a", + "name": "value", + "type": "INT", + "linkIds": [ + 283 + ], + "label": "frame_index", + "pos": [ + 480, + -17 + ] + } + ], + "outputs": [ + { + "id": "1ab0684d-6a44-45b6-8aa4-a0b971a1d41e", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 5 + ], + "pos": [ + 1480, + -37 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 1, + "type": "GetVideoComponents", + "pos": [ + 560, + -150 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 4 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 1, + 2 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": null + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents" + } + }, + { + "id": 2, + "type": "GetImageSize", + "pos": [ + 560, + 50 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 1 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": null + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": null + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": [ + 285 + ] + } + ], + "properties": { + "Node name for S&R": "GetImageSize" + } + }, + { + "id": 3, + "type": "ImageFromBatch", + "pos": [ + 1130, + -150 + ], + "size": [ + 270, + 140 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 2 + }, + { + "localized_name": "batch_index", + "name": "batch_index", + "type": "INT", + "widget": { + "name": "batch_index" + }, + "link": 286 + }, + { + "localized_name": "length", + "name": "length", + "type": "INT", + "widget": { + "name": "length" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 5 + ] + } + ], + "properties": { + "Node name for S&R": "ImageFromBatch" + }, + "widgets_values": [ + 0, + 1 + ] + }, + { + "id": 99, + "type": "ComfyMathExpression", + "pos": [ + 910, + 100 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 284 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": 285 + }, + { + "label": "c", + "localized_name": "values.c", + "name": "values.c", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 286 + ] + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "min(max(int(a if a >= 0 else b + a), 0), b - 1)" + ] + }, + { + "id": 100, + "type": "PrimitiveInt", + "pos": [ + 560, + 250 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 283 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 284 + ] + } + ], + "properties": { + "Node name for S&R": "PrimitiveInt" + }, + "widgets_values": [ + 0, + "fixed" + ] + } + ], + "groups": [], + "links": [ + { + "id": 1, + "origin_id": 1, + "origin_slot": 0, + "target_id": 2, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 2, + "origin_id": 1, + "origin_slot": 0, + "target_id": 3, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 4, + "origin_id": -10, + "origin_slot": 0, + "target_id": 1, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 5, + "origin_id": 3, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 283, + "origin_id": -10, + "origin_slot": 1, + "target_id": 100, + "target_slot": 0, + "type": "INT" + }, + { + "id": 284, + "origin_id": 100, + "origin_slot": 0, + "target_id": 99, + "target_slot": 0, + "type": "INT" + }, + { + "id": 285, + "origin_id": 2, + "origin_slot": 2, + "target_id": 99, + "target_slot": 1, + "type": "INT" + }, + { + "id": 286, + "origin_id": 99, + "origin_slot": 1, + "target_id": 3, + "target_slot": 1, + "type": "INT" + } + ], + "extra": {}, + "category": "Video Tools", + "description": "Extracts one image frame from a video at a chosen index, with optional trim and FPS control." + } + ] + }, + "extra": { + "ds": { + "scale": 1.197015527856339, + "offset": [ + -168.76833554248222, + 540.6638955283997 + ] + }, + "frontendVersion": "1.42.8" + } +} \ No newline at end of file diff --git a/blueprints/Glow.json b/blueprints/Glow.json index 1dafb2d35..2bbfdee51 100644 --- a/blueprints/Glow.json +++ b/blueprints/Glow.json @@ -575,8 +575,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adds a glow/bloom effect around bright image areas via GPU fragment shader." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Hue and Saturation.json b/blueprints/Hue and Saturation.json index 1a2df8937..cddf0154a 100644 --- a/blueprints/Hue and Saturation.json +++ b/blueprints/Hue and Saturation.json @@ -752,8 +752,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adjusts hue, saturation, and lightness of an image using a real-time GPU fragment shader." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Image Blur.json b/blueprints/Image Blur.json index 3c7a784b0..0ca8d9931 100644 --- a/blueprints/Image Blur.json +++ b/blueprints/Image Blur.json @@ -374,7 +374,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Blur" + "category": "Image Tools/Blur", + "description": "Applies Gaussian, Box, or Radial blur to soften images and create stylized depth or motion effects." } ] } diff --git a/blueprints/Image Captioning (gemini).json b/blueprints/Image Captioning (gemini).json index 98cfb8999..9005e5191 100644 --- a/blueprints/Image Captioning (gemini).json +++ b/blueprints/Image Captioning (gemini).json @@ -310,8 +310,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Text generation/Image Captioning" + "category": "Image Tools", + "description": "Generates descriptive captions for images using Google's Gemini multimodal LLM." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Image Channels.json b/blueprints/Image Channels.json index 9c7b675b2..b6fdff5be 100644 --- a/blueprints/Image Channels.json +++ b/blueprints/Image Channels.json @@ -315,8 +315,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Manipulates individual RGBA channels for masking, compositing, and channel effects." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Image to Depth Map (Lotus).json b/blueprints/Image Depth Estimation (Lotus Depth).json similarity index 92% rename from blueprints/Image to Depth Map (Lotus).json rename to blueprints/Image Depth Estimation (Lotus Depth).json index 089f2cd42..8aa338d0d 100644 --- a/blueprints/Image to Depth Map (Lotus).json +++ b/blueprints/Image Depth Estimation (Lotus Depth).json @@ -1,19 +1,18 @@ { - "id": "6af0a6c1-0161-4528-8685-65776e838d44", "revision": 0, - "last_node_id": 75, - "last_link_id": 245, + "last_node_id": 76, + "last_link_id": 0, "nodes": [ { - "id": 75, - "type": "488652fd-6edf-4d06-8f9f-4d84d3a34eaf", + "id": 76, + "type": "96338968-1242-4f02-b6a1-d496af4bcffe", "pos": [ - 600, - 830 + 670, + 1280 ], "size": [ 400, - 110 + 201.3125 ], "flags": {}, "order": 0, @@ -59,47 +58,44 @@ "links": [] } ], + "title": "Image Depth Estimation (Lotus Depth)", "properties": { "proxyWidgets": [ [ - "-1", + "28", "sigma" ], [ - "-1", + "10", "unet_name" ], [ - "-1", + "14", "vae_name" ] ], "cnr_id": "comfy-core", "ver": "0.14.1" }, - "widgets_values": [ - 999.0000000000002, - "lotus-depth-d-v1-1.safetensors", - "vae-ft-mse-840000-ema-pruned.safetensors" - ] + "widgets_values": [] } ], "links": [], - "groups": [], + "version": 0.4, "definitions": { "subgraphs": [ { - "id": "488652fd-6edf-4d06-8f9f-4d84d3a34eaf", + "id": "96338968-1242-4f02-b6a1-d496af4bcffe", "version": 1, "state": { "lastGroupId": 1, - "lastNodeId": 75, + "lastNodeId": 76, "lastLinkId": 245, "lastRerouteId": 0 }, "revision": 0, "config": {}, - "name": "local-Image to Depth Map (Lotus)", + "name": "Image Depth Estimation (Lotus Depth)", "inputNode": { "id": -10, "bounding": [ @@ -191,12 +187,12 @@ "id": 10, "type": "UNETLoader", "pos": [ - 108.05555555555557, - -253.05555555555557 + 110, + -250 ], "size": [ - 254.93706597222226, - 82 + 260, + 90 ], "flags": {}, "order": 4, @@ -234,9 +230,9 @@ } ], "properties": { + "Node name for S&R": "UNETLoader", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "UNETLoader", "models": [ { "name": "lotus-depth-d-v1-1.safetensors", @@ -255,12 +251,12 @@ "id": 18, "type": "DisableNoise", "pos": [ - 607.0641494069639, - -268.33337840371513 + 610, + -270 ], "size": [ - 175, - 33.333333333333336 + 180, + 40 ], "flags": {}, "order": 0, @@ -278,26 +274,25 @@ } ], "properties": { + "Node name for S&R": "DisableNoise", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "DisableNoise", "widget_ue_connectable": {} - }, - "widgets_values": [] + } }, { - "id": 23, + "id": 74, "type": "VAEEncode", "pos": [ 620, 160 ], "size": [ - 175, + 180, 50 ], "flags": {}, - "order": 10, + "order": 11, "mode": 0, "inputs": [ { @@ -325,12 +320,11 @@ } ], "properties": { + "Node name for S&R": "VAEEncode", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "VAEEncode", "widget_ue_connectable": {} - }, - "widgets_values": [] + } }, { "id": 21, @@ -341,7 +335,7 @@ ], "size": [ 210, - 58 + 60 ], "flags": {}, "order": 1, @@ -369,9 +363,9 @@ } ], "properties": { + "Node name for S&R": "KSamplerSelect", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "KSamplerSelect", "widget_ue_connectable": {} }, "widgets_values": [ @@ -386,7 +380,7 @@ -170 ], "size": [ - 175, + 180, 50 ], "flags": {}, @@ -418,12 +412,11 @@ } ], "properties": { + "Node name for S&R": "BasicGuider", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "BasicGuider", "widget_ue_connectable": {} - }, - "widgets_values": [] + } }, { "id": 16, @@ -433,8 +426,8 @@ -130 ], "size": [ - 295.99609375, - 271.65798611111114 + 300, + 280 ], "flags": {}, "order": 6, @@ -490,12 +483,11 @@ } ], "properties": { + "Node name for S&R": "SamplerCustomAdvanced", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "SamplerCustomAdvanced", "widget_ue_connectable": {} - }, - "widgets_values": [] + } }, { "id": 28, @@ -506,10 +498,10 @@ ], "size": [ 210, - 58 + 60 ], "flags": {}, - "order": 11, + "order": 10, "mode": 0, "inputs": [ { @@ -540,9 +532,9 @@ } ], "properties": { + "Node name for S&R": "SetFirstSigma", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "SetFirstSigma", "widget_ue_connectable": {} }, "widgets_values": [ @@ -557,7 +549,7 @@ -120 ], "size": [ - 175, + 180, 50 ], "flags": {}, @@ -589,12 +581,11 @@ } ], "properties": { + "Node name for S&R": "VAEDecode", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "VAEDecode", "widget_ue_connectable": {} - }, - "widgets_values": [] + } }, { "id": 22, @@ -604,8 +595,8 @@ -220 ], "size": [ - 175, - 33.333333333333336 + 180, + 40 ], "flags": {}, "order": 9, @@ -630,12 +621,11 @@ } ], "properties": { + "Node name for S&R": "ImageInvert", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "ImageInvert", "widget_ue_connectable": {} - }, - "widgets_values": [] + } }, { "id": 14, @@ -645,8 +635,8 @@ -90 ], "size": [ - 254.93706597222226, - 58 + 260, + 60 ], "flags": {}, "order": 5, @@ -675,9 +665,9 @@ } ], "properties": { + "Node name for S&R": "VAELoader", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "VAELoader", "models": [ { "name": "vae-ft-mse-840000-ema-pruned.safetensors", @@ -692,15 +682,15 @@ ] }, { - "id": 68, + "id": 75, "type": "LotusConditioning", "pos": [ 400, -150 ], "size": [ - 175, - 33.333333333333336 + 180, + 40 ], "flags": {}, "order": 2, @@ -718,12 +708,11 @@ } ], "properties": { + "Node name for S&R": "LotusConditioning", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "LotusConditioning", "widget_ue_connectable": {} - }, - "widgets_values": [] + } }, { "id": 20, @@ -734,7 +723,7 @@ ], "size": [ 210, - 106 + 110 ], "flags": {}, "order": 8, @@ -786,9 +775,9 @@ } ], "properties": { + "Node name for S&R": "BasicScheduler", "cnr_id": "comfy-core", "ver": "0.3.34", - "Node name for S&R": "BasicScheduler", "widget_ue_connectable": {} }, "widgets_values": [ @@ -850,7 +839,7 @@ }, { "id": 201, - "origin_id": 23, + "origin_id": 74, "origin_slot": 0, "target_id": 16, "target_slot": 4, @@ -866,7 +855,7 @@ }, { "id": 238, - "origin_id": 68, + "origin_id": 75, "origin_slot": 0, "target_id": 19, "target_slot": 1, @@ -892,7 +881,7 @@ "id": 38, "origin_id": 14, "origin_slot": 0, - "target_id": 23, + "target_id": 74, "target_slot": 1, "type": "VAE" }, @@ -908,7 +897,7 @@ "id": 37, "origin_id": -10, "origin_slot": 0, - "target_id": 23, + "target_id": 74, "target_slot": 0, "type": "IMAGE" }, @@ -948,11 +937,11 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Depth to image" + "category": "Conditioning & Preprocessors/Depth", + "description": "Estimates a monocular depth map from an input image using the Lotus depth estimation model." } ] }, - "config": {}, "extra": { "ds": { "scale": 1.3589709866044692, @@ -960,8 +949,6 @@ -138.53613935617864, -786.0629126022195 ] - }, - "workflowRendererVersion": "LG" - }, - "version": 0.4 -} + } + } +} \ No newline at end of file diff --git a/blueprints/Image Depth Estimation (MoGe).json b/blueprints/Image Depth Estimation (MoGe).json new file mode 100644 index 000000000..e2d5d1298 --- /dev/null +++ b/blueprints/Image Depth Estimation (MoGe).json @@ -0,0 +1,1154 @@ +{ + "revision": 0, + "last_node_id": 49, + "last_link_id": 0, + "nodes": [ + { + "id": 49, + "type": "ca1fac5f-abe5-4729-b7fe-2299f6630a65", + "pos": [ + -3970, + 5000 + ], + "size": [ + 430, + 330 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "source_image", + "name": "source_image", + "type": "IMAGE", + "link": null + }, + { + "localized_name": "inference_resolution", + "name": "inference_resolution", + "type": "INT", + "widget": { + "name": "inference_resolution" + }, + "link": null + }, + { + "localized_name": "inference_batch_size", + "name": "inference_batch_size", + "type": "INT", + "widget": { + "name": "inference_batch_size" + }, + "link": null + }, + { + "localized_name": "moge_model", + "name": "moge_model", + "type": "COMBO", + "widget": { + "name": "moge_model" + }, + "link": null + }, + { + "label": "auto_resize_input", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "depth_colored", + "name": "depth_colored", + "type": "IMAGE", + "links": [] + }, + { + "localized_name": "depth", + "name": "depth", + "type": "IMAGE", + "links": [] + }, + { + "name": "MASK", + "type": "MASK", + "links": [] + } + ], + "title": "Image Depth Estimation (MoGe)", + "properties": { + "proxyWidgets": [ + [ + "13", + "resolution_level" + ], + [ + "13", + "batch_size" + ], + [ + "32", + "model_name" + ], + [ + "53", + "switch" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "ca1fac5f-abe5-4729-b7fe-2299f6630a65", + "version": 1, + "state": { + "lastGroupId": 1, + "lastNodeId": 69, + "lastLinkId": 90, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image Depth Estimation (MoGe)", + "description": "Estimates monocular depth from an input image using MoGe, outputting both raw and colorized depth maps plus a mask.", + "inputNode": { + "id": -10, + "bounding": [ + -5130, + 5320, + 167.337890625, + 148 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -3090, + 4966, + 129, + 108 + ] + }, + "inputs": [ + { + "id": "cc8ce79d-ba20-4a25-a51c-c2afcd35e520", + "name": "source_image", + "type": "IMAGE", + "linkIds": [ + 48, + 55, + 56, + 82 + ], + "localized_name": "source_image", + "pos": [ + -4986.662109375, + 5344 + ] + }, + { + "id": "06eefa21-8e60-49f3-9a34-35b081f4ae52", + "name": "inference_resolution", + "type": "INT", + "linkIds": [ + 73 + ], + "localized_name": "inference_resolution", + "pos": [ + -4986.662109375, + 5364 + ] + }, + { + "id": "616638fe-f603-4d10-bae9-fc87c134380f", + "name": "inference_batch_size", + "type": "INT", + "linkIds": [ + 74 + ], + "localized_name": "inference_batch_size", + "pos": [ + -4986.662109375, + 5384 + ] + }, + { + "id": "65694805-186e-4181-a721-df8b5af49d31", + "name": "moge_model", + "type": "COMBO", + "linkIds": [ + 79 + ], + "localized_name": "moge_model", + "pos": [ + -4986.662109375, + 5404 + ] + }, + { + "id": "badf1be1-53c6-4fc1-b5cd-79ad3daf1674", + "name": "switch", + "type": "BOOLEAN", + "linkIds": [ + 83 + ], + "label": "auto_resize_input", + "pos": [ + -4986.662109375, + 5424 + ] + } + ], + "outputs": [ + { + "id": "59c37b52-074f-49fc-9731-483f899c12c4", + "name": "depth_colored", + "type": "IMAGE", + "linkIds": [ + 36 + ], + "localized_name": "depth_colored", + "pos": [ + -3066, + 4990 + ] + }, + { + "id": "f583e936-da5c-4630-9901-391fa605c1f8", + "name": "depth", + "type": "IMAGE", + "linkIds": [ + 40 + ], + "localized_name": "depth", + "pos": [ + -3066, + 5010 + ] + }, + { + "id": "6845b6a1-1980-454a-9451-314f24495c1d", + "name": "MASK", + "type": "MASK", + "linkIds": [ + 86 + ], + "pos": [ + -3066, + 5030 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 13, + "type": "MoGeInference", + "pos": [ + -3790, + 5180 + ], + "size": [ + 270, + 230 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_model", + "name": "moge_model", + "type": "MOGE_MODEL", + "link": 58 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 81 + }, + { + "localized_name": "resolution_level", + "name": "resolution_level", + "type": "INT", + "widget": { + "name": "resolution_level" + }, + "link": 73 + }, + { + "localized_name": "fov_x_degrees", + "name": "fov_x_degrees", + "type": "FLOAT", + "widget": { + "name": "fov_x_degrees" + }, + "link": null + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": 74 + }, + { + "localized_name": "force_projection", + "name": "force_projection", + "type": "BOOLEAN", + "widget": { + "name": "force_projection" + }, + "link": null + }, + { + "localized_name": "apply_mask", + "name": "apply_mask", + "type": "BOOLEAN", + "widget": { + "name": "apply_mask" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "links": [ + 35, + 39, + 61 + ] + } + ], + "properties": { + "Node name for S&R": "MoGeInference", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 3, + 0, + 4, + true, + true + ] + }, + { + "id": 23, + "type": "MoGeRender", + "pos": [ + -3430, + 4870 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "link": 35 + }, + { + "localized_name": "output", + "name": "output", + "type": "COMBO", + "widget": { + "name": "output" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 36 + ] + } + ], + "properties": { + "Node name for S&R": "MoGeRender", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "depth_colored" + ] + }, + { + "id": 25, + "type": "MoGeRender", + "pos": [ + -3430, + 5030 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "link": 39 + }, + { + "localized_name": "output", + "name": "output", + "type": "COMBO", + "widget": { + "name": "output" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 40 + ] + } + ], + "properties": { + "Node name for S&R": "MoGeRender", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "depth" + ] + }, + { + "id": 32, + "type": "LoadMoGeModel", + "pos": [ + -4180, + 4880 + ], + "size": [ + 270, + 140 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "model_name", + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": 79 + } + ], + "outputs": [ + { + "localized_name": "MOGE_MODEL", + "name": "MOGE_MODEL", + "type": "MOGE_MODEL", + "links": [ + 58 + ] + } + ], + "properties": { + "Node name for S&R": "LoadMoGeModel", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "models": [ + { + "name": "moge_2_vitl_normal_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/MoGe/resolve/main/geometry_estimation/moge_2_vitl_normal_fp16.safetensors", + "directory": "geometry_estimation" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "moge_2_vitl_normal_fp16.safetensors" + ] + }, + { + "id": 36, + "type": "ComfyMathExpression", + "pos": [ + -4720, + 4910 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT,BOOLEAN", + "link": 49 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": null + }, + { + "localized_name": "BOOL", + "name": "BOOL", + "type": "BOOLEAN", + "links": [ + 53 + ] + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "a > 2048" + ] + }, + { + "id": 37, + "type": "GetImageSize", + "pos": [ + -4980, + 4910 + ], + "size": [ + 230, + 160 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 48 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 49 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": null + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetImageSize", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 40, + "type": "ResizeImagesByLongerEdge", + "pos": [ + -4650, + 5210 + ], + "size": [ + 310, + 110 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 55 + }, + { + "localized_name": "longer_edge", + "name": "longer_edge", + "type": "INT", + "widget": { + "name": "longer_edge" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 54 + ] + } + ], + "properties": { + "Node name for S&R": "ResizeImagesByLongerEdge", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 2048 + ] + }, + { + "id": 42, + "type": "ComfySwitchNode", + "pos": [ + -4180, + 5060 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 56 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 54 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 53 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 80 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 45, + "type": "MoGeRender", + "pos": [ + -3430, + 5200 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "link": 61 + }, + { + "localized_name": "output", + "name": "output", + "type": "COMBO", + "widget": { + "name": "output" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 85 + ] + } + ], + "properties": { + "Node name for S&R": "MoGeRender", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "mask" + ] + }, + { + "id": 53, + "type": "ComfySwitchNode", + "pos": [ + -4160, + 5340 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 82 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 80 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 83 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 81 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + true + ] + }, + { + "id": 68, + "type": "ImageToMask", + "pos": [ + -3420, + 5360 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 85 + }, + { + "localized_name": "channel", + "name": "channel", + "type": "COMBO", + "widget": { + "name": "channel" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MASK", + "name": "MASK", + "type": "MASK", + "links": [ + 86 + ] + } + ], + "properties": { + "Node name for S&R": "ImageToMask" + }, + "widgets_values": [ + "red" + ] + } + ], + "groups": [ + { + "id": 1, + "title": "auto_resize_if_width_gt_2048", + "bounding": [ + -5000, + 4840, + 690, + 280 + ], + "color": "#3f789e", + "flags": {} + } + ], + "links": [ + { + "id": 58, + "origin_id": 32, + "origin_slot": 0, + "target_id": 13, + "target_slot": 0, + "type": "MOGE_MODEL" + }, + { + "id": 35, + "origin_id": 13, + "origin_slot": 0, + "target_id": 23, + "target_slot": 0, + "type": "MOGE_GEOMETRY" + }, + { + "id": 39, + "origin_id": 13, + "origin_slot": 0, + "target_id": 25, + "target_slot": 0, + "type": "MOGE_GEOMETRY" + }, + { + "id": 49, + "origin_id": 37, + "origin_slot": 0, + "target_id": 36, + "target_slot": 0, + "type": "INT" + }, + { + "id": 54, + "origin_id": 40, + "origin_slot": 0, + "target_id": 42, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 53, + "origin_id": 36, + "origin_slot": 2, + "target_id": 42, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 61, + "origin_id": 13, + "origin_slot": 0, + "target_id": 45, + "target_slot": 0, + "type": "MOGE_GEOMETRY" + }, + { + "id": 48, + "origin_id": -10, + "origin_slot": 0, + "target_id": 37, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 55, + "origin_id": -10, + "origin_slot": 0, + "target_id": 40, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 56, + "origin_id": -10, + "origin_slot": 0, + "target_id": 42, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 36, + "origin_id": 23, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 40, + "origin_id": 25, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 73, + "origin_id": -10, + "origin_slot": 1, + "target_id": 13, + "target_slot": 2, + "type": "INT" + }, + { + "id": 74, + "origin_id": -10, + "origin_slot": 2, + "target_id": 13, + "target_slot": 4, + "type": "INT" + }, + { + "id": 79, + "origin_id": -10, + "origin_slot": 3, + "target_id": 32, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 80, + "origin_id": 42, + "origin_slot": 0, + "target_id": 53, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 81, + "origin_id": 53, + "origin_slot": 0, + "target_id": 13, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 82, + "origin_id": -10, + "origin_slot": 0, + "target_id": 53, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 83, + "origin_id": -10, + "origin_slot": 4, + "target_id": 53, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 85, + "origin_id": 45, + "origin_slot": 0, + "target_id": 68, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 86, + "origin_id": 68, + "origin_slot": 0, + "target_id": -20, + "target_slot": 2, + "type": "MASK" + } + ], + "extra": {}, + "category": "Conditioning & Preprocessors/Depth" + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Image Edit (FireRed Image Edit 1.1).json b/blueprints/Image Edit (FireRed Image Edit 1.1).json index c34246ce6..b82c7d18b 100644 --- a/blueprints/Image Edit (FireRed Image Edit 1.1).json +++ b/blueprints/Image Edit (FireRed Image Edit 1.1).json @@ -1,18 +1,18 @@ { "revision": 0, - "last_node_id": 172, + "last_node_id": 213, "last_link_id": 0, "nodes": [ { - "id": 172, - "type": "edf73971-14ee-4d39-b58e-46ce2a89d3d0", + "id": 213, + "type": "e35fbbeb-d7b1-46d1-a74e-959517d0fb1a", "pos": [ - 30, - 200 + -700, + -470 ], "size": [ 500, - 570 + 0 ], "flags": {}, "order": 2, @@ -105,44 +105,44 @@ "properties": { "proxyWidgets": [ [ - "118", + "208", "prompt" ], [ - "153", + "207", "value" ], [ - "130", + "210", "seed" ], [ - "128", + "205", "unet_name" ], [ - "115", + "203", "clip_name" ], [ - "116", + "202", "vae_name" ], [ - "151", + "204", "lora_name" ], [ - "130", + "210", "control_after_generate" ] ], + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -160,12 +160,12 @@ "definitions": { "subgraphs": [ { - "id": "edf73971-14ee-4d39-b58e-46ce2a89d3d0", + "id": "e35fbbeb-d7b1-46d1-a74e-959517d0fb1a", "version": 1, "state": { "lastGroupId": 8, - "lastNodeId": 174, - "lastLinkId": 376, + "lastNodeId": 213, + "lastLinkId": 378, "lastRerouteId": 0 }, "revision": 0, @@ -183,8 +183,8 @@ "outputNode": { "id": -20, "bounding": [ - 1147.5, - -1215, + 1860, + -1340, 120, 60 ] @@ -327,26 +327,26 @@ ], "localized_name": "IMAGE", "pos": [ - 1167.5, - -1195 + 1880, + -1320 ] } ], "widgets": [], "nodes": [ { - "id": 120, + "id": 193, "type": "ModelSamplingAuraFlow", "pos": [ - 1060, - -1760 + 1010, + -1680 ], "size": [ 290, 110 ], "flags": {}, - "order": 8, + "order": 4, "mode": 0, "inputs": [ { @@ -376,13 +376,13 @@ } ], "properties": { + "Node name for S&R": "ModelSamplingAuraFlow", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "ModelSamplingAuraFlow", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -396,7 +396,7 @@ ] }, { - "id": 154, + "id": 194, "type": "ComfySwitchNode", "pos": [ 680, @@ -407,7 +407,7 @@ 140 ], "flags": {}, - "order": 16, + "order": 5, "mode": 0, "inputs": [ { @@ -444,13 +444,13 @@ ], "title": "Switch (Model)", "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "ComfySwitchNode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -464,7 +464,7 @@ ] }, { - "id": 155, + "id": 195, "type": "PrimitiveInt", "pos": [ 190, @@ -500,13 +500,13 @@ ], "title": "Int (Steps)", "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "PrimitiveInt", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -521,18 +521,18 @@ ] }, { - "id": 123, + "id": 196, "type": "CFGNorm", "pos": [ - 1060, - -1590 + 1010, + -1510 ], "size": [ 290, 110 ], "flags": {}, - "order": 9, + "order": 6, "mode": 0, "inputs": [ { @@ -562,13 +562,13 @@ } ], "properties": { + "Node name for S&R": "CFGNorm", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "CFGNorm", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -582,7 +582,7 @@ ] }, { - "id": 164, + "id": 197, "type": "ComfySwitchNode", "pos": [ 680, @@ -593,7 +593,7 @@ 130 ], "flags": {}, - "order": 18, + "order": 7, "mode": 0, "inputs": [ { @@ -630,13 +630,13 @@ ], "title": "Switch (CFG)", "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "ComfySwitchNode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -650,7 +650,7 @@ ] }, { - "id": 156, + "id": 198, "type": "PrimitiveInt", "pos": [ 190, @@ -686,13 +686,13 @@ ], "title": "Float (Steps)", "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "PrimitiveInt", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -707,7 +707,7 @@ ] }, { - "id": 162, + "id": 199, "type": "PrimitiveFloat", "pos": [ 190, @@ -743,13 +743,13 @@ ], "title": "Float (CFG)", "properties": { + "Node name for S&R": "PrimitiveFloat", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "PrimitiveFloat", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -763,7 +763,7 @@ ] }, { - "id": 163, + "id": 200, "type": "PrimitiveFloat", "pos": [ 190, @@ -799,13 +799,13 @@ ], "title": "Float (CFG)", "properties": { + "Node name for S&R": "PrimitiveFloat", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "PrimitiveFloat", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -819,7 +819,7 @@ ] }, { - "id": 157, + "id": 201, "type": "ComfySwitchNode", "pos": [ 680, @@ -830,7 +830,7 @@ 130 ], "flags": {}, - "order": 17, + "order": 8, "mode": 0, "inputs": [ { @@ -867,13 +867,13 @@ ], "title": "Switch (Steps)", "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "ComfySwitchNode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -887,11 +887,11 @@ ] }, { - "id": 116, + "id": 202, "type": "VAELoader", "pos": [ - -950, - -1040 + -960, + -1100 ], "size": [ 400, @@ -900,7 +900,7 @@ "flags": { "collapsed": false }, - "order": 5, + "order": 9, "mode": 0, "inputs": [ { @@ -928,45 +928,45 @@ } ], "properties": { - "ue_properties": { - "widget_ue_connectable": {}, - "input_ue_unconnectable": {} - }, + "Node name for S&R": "VAELoader", "cnr_id": "comfy-core", "ver": "0.5.1", - "Node name for S&R": "VAELoader", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, "models": [ { "name": "qwen_image_vae.safetensors", "url": "https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.0-ComfyUI/resolve/main/qwen_image_vae.safetensors", "directory": "vae" } - ] + ], + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 }, "widgets_values": [ "qwen_image_vae.safetensors" ] }, { - "id": 115, + "id": 203, "type": "CLIPLoader", "pos": [ -960, - -1370 + -1400 ], "size": [ 400, 150 ], "flags": {}, - "order": 4, + "order": 10, "mode": 0, "inputs": [ { @@ -1010,27 +1010,27 @@ } ], "properties": { - "ue_properties": { - "widget_ue_connectable": {}, - "input_ue_unconnectable": {} - }, + "Node name for S&R": "CLIPLoader", "cnr_id": "comfy-core", "ver": "0.5.1", - "Node name for S&R": "CLIPLoader", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, "models": [ { "name": "qwen_2.5_vl_7b_fp8_scaled.safetensors", "url": "https://huggingface.co/Comfy-Org/HunyuanVideo_1.5_repackaged/resolve/main/split_files/text_encoders/qwen_2.5_vl_7b_fp8_scaled.safetensors", "directory": "text_encoders" } - ] + ], + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 }, "widgets_values": [ "qwen_2.5_vl_7b_fp8_scaled.safetensors", @@ -1039,7 +1039,7 @@ ] }, { - "id": 151, + "id": 204, "type": "LoraLoaderModelOnly", "pos": [ 100, @@ -1050,7 +1050,7 @@ 140 ], "flags": {}, - "order": 14, + "order": 11, "mode": 0, "inputs": [ { @@ -1089,27 +1089,27 @@ } ], "properties": { - "ue_properties": { - "widget_ue_connectable": {}, - "input_ue_unconnectable": {} - }, + "Node name for S&R": "LoraLoaderModelOnly", "cnr_id": "comfy-core", "ver": "0.15.1", - "Node name for S&R": "LoraLoaderModelOnly", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, "models": [ { "name": "FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors", "url": "https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.0-ComfyUI/resolve/main/FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors", "directory": "loras" } - ] + ], + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 }, "widgets_values": [ "FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors", @@ -1117,7 +1117,7 @@ ] }, { - "id": 128, + "id": 205, "type": "UNETLoader", "pos": [ -960, @@ -1163,27 +1163,27 @@ } ], "properties": { - "ue_properties": { - "widget_ue_connectable": {}, - "input_ue_unconnectable": {} - }, + "Node name for S&R": "UNETLoader", "cnr_id": "comfy-core", "ver": "0.5.1", - "Node name for S&R": "UNETLoader", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, "models": [ { "name": "FireRed-Image-Edit-1.1-transformer.safetensors", "url": "https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.1-ComfyUI/resolve/main/FireRed-Image-Edit-1.1-transformer.safetensors", "directory": "diffusion_models" } - ] + ], + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 }, "widgets_values": [ "FireRed-Image-Edit-1.1-transformer.safetensors", @@ -1191,7 +1191,7 @@ ] }, { - "id": 125, + "id": 206, "type": "VAEEncode", "pos": [ -390, @@ -1202,7 +1202,7 @@ 100 ], "flags": {}, - "order": 10, + "order": 13, "mode": 0, "inputs": [ { @@ -1229,13 +1229,13 @@ } ], "properties": { + "Node name for S&R": "VAEEncode", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "VAEEncode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1246,7 +1246,7 @@ } }, { - "id": 153, + "id": 207, "type": "PrimitiveBoolean", "pos": [ 160, @@ -1257,7 +1257,7 @@ 100 ], "flags": {}, - "order": 15, + "order": 14, "mode": 0, "inputs": [ { @@ -1284,13 +1284,13 @@ ], "title": "Enable Lightning LoRA?", "properties": { + "Node name for S&R": "PrimitiveBoolean", + "cnr_id": "comfy-core", + "ver": "0.15.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.15.1", - "Node name for S&R": "PrimitiveBoolean", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1304,7 +1304,7 @@ ] }, { - "id": 118, + "id": 208, "type": "TextEncodeQwenImageEditPlus", "pos": [ -480, @@ -1315,7 +1315,7 @@ 370 ], "flags": {}, - "order": 7, + "order": 15, "mode": 0, "inputs": [ { @@ -1374,13 +1374,13 @@ ], "title": "TextEncodeQwenImageEditPlus (Positive)", "properties": { + "Node name for S&R": "TextEncodeQwenImageEditPlus", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "TextEncodeQwenImageEditPlus", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1396,7 +1396,7 @@ "bgcolor": "#353" }, { - "id": 117, + "id": 209, "type": "TextEncodeQwenImageEditPlus", "pos": [ -470, @@ -1407,7 +1407,7 @@ 290 ], "flags": {}, - "order": 6, + "order": 16, "mode": 0, "inputs": [ { @@ -1465,13 +1465,13 @@ } ], "properties": { + "Node name for S&R": "TextEncodeQwenImageEditPlus", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "TextEncodeQwenImageEditPlus", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1487,18 +1487,18 @@ "bgcolor": "#535" }, { - "id": 130, + "id": 210, "type": "KSampler", "pos": [ - 1060, - -1420 + 1010, + -1340 ], "size": [ 270, 480 ], "flags": {}, - "order": 13, + "order": 17, "mode": 0, "inputs": [ { @@ -1591,13 +1591,13 @@ } ], "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "KSampler", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1617,11 +1617,11 @@ ] }, { - "id": 126, + "id": 211, "type": "VAEDecode", "pos": [ - 1360, - -1420 + 1440, + -1340 ], "size": [ 230, @@ -1630,7 +1630,7 @@ "flags": { "collapsed": false }, - "order": 11, + "order": 18, "mode": 0, "inputs": [ { @@ -1658,13 +1658,13 @@ } ], "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.5.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} }, - "cnr_id": "comfy-core", - "ver": "0.5.1", - "Node name for S&R": "VAEDecode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1675,7 +1675,7 @@ } }, { - "id": 174, + "id": 212, "type": "ResizeImageMaskNode", "pos": [ -900, @@ -1736,18 +1736,18 @@ } ], "properties": { + "Node name for S&R": "ResizeImageMaskNode", + "cnr_id": "comfy-core", + "ver": "0.18.1", "ue_properties": { "widget_ue_connectable": {}, "input_ue_unconnectable": {} - }, - "cnr_id": "comfy-core", - "ver": "0.18.1", - "Node name for S&R": "ResizeImageMaskNode" + } }, "widgets_values": [ "scale total pixels", 1, - "area" + "lanczos" ] } ], @@ -1808,207 +1808,207 @@ "links": [ { "id": 326, - "origin_id": 154, + "origin_id": 194, "origin_slot": 0, - "target_id": 120, + "target_id": 193, "target_slot": 0, "type": "MODEL" }, { "id": 324, - "origin_id": 128, + "origin_id": 205, "origin_slot": 0, - "target_id": 154, + "target_id": 194, "target_slot": 0, "type": "MODEL" }, { "id": 325, - "origin_id": 151, + "origin_id": 204, "origin_slot": 0, - "target_id": 154, + "target_id": 194, "target_slot": 1, "type": "MODEL" }, { "id": 323, - "origin_id": 153, + "origin_id": 207, "origin_slot": 0, - "target_id": 154, + "target_id": 194, "target_slot": 2, "type": "BOOLEAN" }, { "id": 294, - "origin_id": 120, + "origin_id": 193, "origin_slot": 0, - "target_id": 123, + "target_id": 196, "target_slot": 0, "type": "MODEL" }, { "id": 333, - "origin_id": 162, + "origin_id": 199, "origin_slot": 0, - "target_id": 164, + "target_id": 197, "target_slot": 0, "type": "FLOAT" }, { "id": 334, - "origin_id": 163, + "origin_id": 200, "origin_slot": 0, - "target_id": 164, + "target_id": 197, "target_slot": 1, "type": "FLOAT" }, { "id": 336, - "origin_id": 153, + "origin_id": 207, "origin_slot": 0, - "target_id": 164, + "target_id": 197, "target_slot": 2, "type": "BOOLEAN" }, { "id": 329, - "origin_id": 155, + "origin_id": 195, "origin_slot": 0, - "target_id": 157, + "target_id": 201, "target_slot": 0, "type": "INT" }, { "id": 337, - "origin_id": 156, + "origin_id": 198, "origin_slot": 0, - "target_id": 157, + "target_id": 201, "target_slot": 1, "type": "INT" }, { "id": 330, - "origin_id": 153, + "origin_id": 207, "origin_slot": 0, - "target_id": 157, + "target_id": 201, "target_slot": 2, "type": "BOOLEAN" }, { "id": 297, - "origin_id": 115, + "origin_id": 203, "origin_slot": 0, - "target_id": 117, + "target_id": 209, "target_slot": 0, "type": "CLIP" }, { "id": 299, - "origin_id": 116, + "origin_id": 202, "origin_slot": 0, - "target_id": 117, + "target_id": 209, "target_slot": 1, "type": "VAE" }, { "id": 316, - "origin_id": 128, + "origin_id": 205, "origin_slot": 0, - "target_id": 151, + "target_id": 204, "target_slot": 0, "type": "MODEL" }, { "id": 296, - "origin_id": 115, + "origin_id": 203, "origin_slot": 0, - "target_id": 118, + "target_id": 208, "target_slot": 0, "type": "CLIP" }, { "id": 298, - "origin_id": 116, + "origin_id": 202, "origin_slot": 0, - "target_id": 118, + "target_id": 208, "target_slot": 1, "type": "VAE" }, { "id": 300, - "origin_id": 116, + "origin_id": 202, "origin_slot": 0, - "target_id": 125, + "target_id": 206, "target_slot": 1, "type": "VAE" }, { "id": 295, - "origin_id": 123, + "origin_id": 196, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 0, "type": "MODEL" }, { "id": 312, - "origin_id": 118, + "origin_id": 208, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 1, "type": "CONDITIONING" }, { "id": 313, - "origin_id": 117, + "origin_id": 209, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 2, "type": "CONDITIONING" }, { "id": 303, - "origin_id": 125, + "origin_id": 206, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 3, "type": "LATENT" }, { "id": 345, - "origin_id": 157, + "origin_id": 201, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 5, "type": "INT" }, { "id": 335, - "origin_id": 164, + "origin_id": 197, "origin_slot": 0, - "target_id": 130, + "target_id": 210, "target_slot": 6, "type": "FLOAT" }, { "id": 273, - "origin_id": 130, + "origin_id": 210, "origin_slot": 0, - "target_id": 126, + "target_id": 211, "target_slot": 0, "type": "LATENT" }, { "id": 314, - "origin_id": 116, + "origin_id": 202, "origin_slot": 0, - "target_id": 126, + "target_id": 211, "target_slot": 1, "type": "VAE" }, { "id": 292, - "origin_id": 126, + "origin_id": 211, "origin_slot": 0, "target_id": -20, "target_slot": 0, @@ -2018,7 +2018,7 @@ "id": 355, "origin_id": -10, "origin_slot": 1, - "target_id": 118, + "target_id": 208, "target_slot": 3, "type": "IMAGE" }, @@ -2026,7 +2026,7 @@ "id": 356, "origin_id": -10, "origin_slot": 1, - "target_id": 117, + "target_id": 209, "target_slot": 3, "type": "IMAGE" }, @@ -2034,7 +2034,7 @@ "id": 357, "origin_id": -10, "origin_slot": 2, - "target_id": 118, + "target_id": 208, "target_slot": 4, "type": "IMAGE" }, @@ -2042,7 +2042,7 @@ "id": 358, "origin_id": -10, "origin_slot": 2, - "target_id": 117, + "target_id": 209, "target_slot": 4, "type": "IMAGE" }, @@ -2050,7 +2050,7 @@ "id": 359, "origin_id": -10, "origin_slot": 3, - "target_id": 118, + "target_id": 208, "target_slot": 5, "type": "STRING" }, @@ -2058,31 +2058,31 @@ "id": 364, "origin_id": -10, "origin_slot": 4, - "target_id": 153, + "target_id": 207, "target_slot": 0, "type": "BOOLEAN" }, { "id": 368, - "origin_id": 174, + "origin_id": 212, "origin_slot": 0, - "target_id": 125, + "target_id": 206, "target_slot": 0, "type": "IMAGE" }, { "id": 369, - "origin_id": 174, + "origin_id": 212, "origin_slot": 0, - "target_id": 118, + "target_id": 208, "target_slot": 2, "type": "IMAGE" }, { "id": 370, - "origin_id": 174, + "origin_id": 212, "origin_slot": 0, - "target_id": 117, + "target_id": 209, "target_slot": 2, "type": "IMAGE" }, @@ -2090,7 +2090,7 @@ "id": 371, "origin_id": -10, "origin_slot": 0, - "target_id": 174, + "target_id": 212, "target_slot": 0, "type": "IMAGE" }, @@ -2098,7 +2098,7 @@ "id": 372, "origin_id": -10, "origin_slot": 5, - "target_id": 130, + "target_id": 210, "target_slot": 4, "type": "INT" }, @@ -2106,7 +2106,7 @@ "id": 373, "origin_id": -10, "origin_slot": 6, - "target_id": 128, + "target_id": 205, "target_slot": 0, "type": "COMBO" }, @@ -2114,7 +2114,7 @@ "id": 374, "origin_id": -10, "origin_slot": 7, - "target_id": 115, + "target_id": 203, "target_slot": 0, "type": "COMBO" }, @@ -2122,7 +2122,7 @@ "id": 375, "origin_id": -10, "origin_slot": 8, - "target_id": 116, + "target_id": 202, "target_slot": 0, "type": "COMBO" }, @@ -2130,7 +2130,7 @@ "id": 376, "origin_id": -10, "origin_slot": 9, - "target_id": 151, + "target_id": 204, "target_slot": 1, "type": "COMBO" } @@ -2138,7 +2138,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Edit image" + "category": "Image generation and editing/Edit image", + "description": "Edits images via text instructions using FireRed Image Edit 1.1, a diffusion-based instruction-following editing model." } ] }, diff --git a/blueprints/Image Edit (Flux.2 Dev).json b/blueprints/Image Edit (Flux.2 Dev).json new file mode 100644 index 000000000..92827bf17 --- /dev/null +++ b/blueprints/Image Edit (Flux.2 Dev).json @@ -0,0 +1,2050 @@ +{ + "revision": 0, + "last_node_id": 139, + "last_link_id": 0, + "nodes": [ + { + "id": 139, + "type": "41b0c117-7470-454c-914e-b8742dc06d62", + "pos": [ + -650, + 570 + ], + "size": [ + 400, + 0 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "image", + "localized_name": "pixels", + "name": "pixels", + "type": "IMAGE", + "link": null + }, + { + "label": "prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + }, + { + "label": "enable_turbo_mode", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": null + }, + { + "label": "turbo_lora", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "123", + "text" + ], + [ + "129", + "unet_name" + ], + [ + "124", + "clip_name" + ], + [ + "121", + "vae_name" + ], + [ + "138", + "value" + ], + [ + "128", + "lora_name" + ], + [ + "125", + "noise_seed" + ], + [ + "125", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": { + "text": true, + "value": true, + "lora_name": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Image Edit (Flux.2 Dev)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "41b0c117-7470-454c-914e-b8742dc06d62", + "version": 1, + "state": { + "lastGroupId": 8, + "lastNodeId": 139, + "lastLinkId": 194, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image Edit (Flux.2 Dev)", + "inputNode": { + "id": -10, + "bounding": [ + -1520, + 400, + 151.744140625, + 180 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1240, + 420, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "fc74acd5-30a9-410b-abb5-4a4171ba3d25", + "name": "pixels", + "type": "IMAGE", + "linkIds": [ + 126, + 169 + ], + "localized_name": "pixels", + "label": "image", + "pos": [ + -1388.255859375, + 420 + ] + }, + { + "id": "3e69affa-397b-4d52-82d7-68dfcef9e761", + "name": "text", + "type": "STRING", + "linkIds": [ + 168 + ], + "label": "prompt", + "pos": [ + -1388.255859375, + 440 + ] + }, + { + "id": "2f016a8a-fb3e-4cb9-97f2-a991defe4fa2", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 177 + ], + "pos": [ + -1388.255859375, + 460 + ] + }, + { + "id": "799b9dc7-0c90-4b19-9a13-e01d896bea1f", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 178 + ], + "pos": [ + -1388.255859375, + 480 + ] + }, + { + "id": "e58a83c9-1b93-4378-9598-f24068820313", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 179 + ], + "pos": [ + -1388.255859375, + 500 + ] + }, + { + "id": "8335a4a9-0ce4-4e67-a641-1c9d7a762977", + "name": "value", + "type": "BOOLEAN", + "linkIds": [ + 191 + ], + "label": "enable_turbo_mode", + "pos": [ + -1388.255859375, + 520 + ] + }, + { + "id": "890b22b4-44a7-4707-912a-ca8b4ee7b7c9", + "name": "lora_name", + "type": "COMBO", + "linkIds": [ + 192 + ], + "label": "turbo_lora", + "pos": [ + -1388.255859375, + 540 + ] + } + ], + "outputs": [ + { + "id": "3eaa05d6-4960-4a7c-bf2a-8b585fbb7c9c", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 9 + ], + "localized_name": "IMAGE", + "pos": [ + 1260, + 440 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 118, + "type": "Flux2Scheduler", + "pos": [ + 540, + 430 + ], + "size": [ + 230, + 170 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 188 + }, + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 170 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 172 + } + ], + "outputs": [ + { + "localized_name": "SIGMAS", + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 132 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "Flux2Scheduler", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 20, + 1248, + 832 + ] + }, + { + "id": 119, + "type": "BasicGuider", + "pos": [ + 530, + 120 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 185 + }, + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 166 + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "slot_index": 0, + "links": [ + 30 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "BasicGuider", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 120, + "type": "KSamplerSelect", + "pos": [ + 530, + 270 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "SAMPLER", + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 19 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "KSamplerSelect", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "euler" + ] + }, + { + "id": 121, + "type": "VAELoader", + "pos": [ + -970, + 390 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 179 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "slot_index": 0, + "links": [ + 127, + 159 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAELoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "full_encoder_small_decoder.safetensors", + "url": "https://huggingface.co/black-forest-labs/FLUX.2-small-decoder/resolve/main/full_encoder_small_decoder.safetensors", + "directory": "vae" + } + ] + }, + "widgets_values": [ + "full_encoder_small_decoder.safetensors" + ] + }, + { + "id": 122, + "type": "SamplerCustomAdvanced", + "pos": [ + 790, + -50 + ], + "size": [ + 280, + 170 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 37 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 30 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 19 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 132 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 161 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "slot_index": 0, + "links": [ + 24 + ] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "SamplerCustomAdvanced", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 123, + "type": "CLIPTextEncode", + "pos": [ + -630, + -50 + ], + "size": [ + 430, + 360 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 117 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 168 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 41 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 124, + "type": "CLIPLoader", + "pos": [ + -970, + 160 + ], + "size": [ + 300, + 150 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 178 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 117 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "mistral_3_small_flux2_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/text_encoders/mistral_3_small_flux2_bf16.safetensors", + "directory": "text_encoders" + } + ] + }, + "widgets_values": [ + "mistral_3_small_flux2_bf16.safetensors", + "flux2", + "default" + ] + }, + { + "id": 125, + "type": "RandomNoise", + "pos": [ + 530, + -50 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "noise_seed", + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "NOISE", + "name": "NOISE", + "type": "NOISE", + "links": [ + 37 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "RandomNoise", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 342971778941390, + "randomize" + ] + }, + { + "id": 126, + "type": "VAEDecode", + "pos": [ + 830, + 410 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 24 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 159 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 9 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEDecode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 127, + "type": "FluxGuidance", + "pos": [ + -520, + 390 + ], + "size": [ + 320, + 110 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 41 + }, + { + "localized_name": "guidance", + "name": "guidance", + "type": "FLOAT", + "widget": { + "name": "guidance" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 144 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "FluxGuidance", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 4 + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 128, + "type": "LoraLoaderModelOnly", + "pos": [ + -150, + 200 + ], + "size": [ + 300, + 140 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 181 + }, + { + "localized_name": "lora_name", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": 192 + }, + { + "localized_name": "strength_model", + "name": "strength_model", + "type": "FLOAT", + "widget": { + "name": "strength_model" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 183 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LoraLoaderModelOnly", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "Flux_2-Turbo-LoRA_comfyui.safetensors", + "url": "https://huggingface.co/ByteZSzn/Flux.2-Turbo-ComfyUI/resolve/main/Flux_2-Turbo-LoRA_comfyui.safetensors", + "directory": "loras" + } + ] + }, + "widgets_values": [ + "Flux_2-Turbo-LoRA_comfyui.safetensors", + 1 + ] + }, + { + "id": 129, + "type": "UNETLoader", + "pos": [ + -970, + -40 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 177 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 181, + 184 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "UNETLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "flux2_dev_fp8mixed.safetensors", + "url": "https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/diffusion_models/flux2_dev_fp8mixed.safetensors", + "directory": "diffusion_models" + } + ] + }, + "widgets_values": [ + "flux2_dev_fp8mixed.safetensors", + "default" + ] + }, + { + "id": 130, + "type": "ComfySwitchNode", + "pos": [ + 220, + 10 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 184 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 183 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 190 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 185 + ] + } + ], + "title": "Switch(model)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 131, + "type": "PrimitiveInt", + "pos": [ + -150, + 430 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 186 + ] + } + ], + "title": "Steps", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 8, + "fixed" + ] + }, + { + "id": 132, + "type": "PrimitiveInt", + "pos": [ + -150, + -50 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 187 + ] + } + ], + "title": "Steps", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 20, + "fixed" + ] + }, + { + "id": 133, + "type": "ComfySwitchNode", + "pos": [ + 220, + 280 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 187 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 186 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 189 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 188 + ] + } + ], + "title": "Switch(steps)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 134, + "type": "EmptyFlux2LatentImage", + "pos": [ + 530, + 790 + ], + "size": [ + 270, + 170 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 171 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 173 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 161 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "EmptyFlux2LatentImage", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1248, + 832, + 1 + ] + }, + { + "id": 135, + "type": "GetImageSize", + "pos": [ + -100, + 810 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 169 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 170, + 171 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": [ + 172, + 173 + ] + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "GetImageSize", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 136, + "type": "VAEEncode", + "pos": [ + -910, + 600 + ], + "size": [ + 230, + 100 + ], + "flags": { + "collapsed": true + }, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "pixels", + "name": "pixels", + "type": "IMAGE", + "link": 126 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 127 + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 125 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 137, + "type": "ReferenceLatent", + "pos": [ + -470, + 580 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 144 + }, + { + "localized_name": "latent", + "name": "latent", + "shape": 7, + "type": "LATENT", + "link": 125 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 166 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ReferenceLatent", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 138, + "type": "PrimitiveBoolean", + "pos": [ + -130, + 640 + ], + "size": [ + 270, + 100 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 191 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 189, + 190 + ] + } + ], + "title": "Enable 8 steps lora", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.18.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveBoolean", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + } + ], + "groups": [ + { + "id": 1, + "title": "Models", + "bounding": [ + -980, + -120, + 320, + 640 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "Custom sampler", + "bounding": [ + 520, + -120, + 590, + 740 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "Image size", + "bounding": [ + 510, + 690, + 590, + 290 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Prompt", + "bounding": [ + -640, + -120, + 450, + 640 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 7, + "title": "Original", + "bounding": [ + -160, + -120, + 340, + 230 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 8, + "title": "8 Steps LoRA", + "bounding": [ + -160, + 130, + 340, + 430 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 41, + "origin_id": 123, + "origin_slot": 0, + "target_id": 127, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 144, + "origin_id": 127, + "origin_slot": 0, + "target_id": 137, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 125, + "origin_id": 136, + "origin_slot": 0, + "target_id": 137, + "target_slot": 1, + "type": "LATENT" + }, + { + "id": 37, + "origin_id": 125, + "origin_slot": 0, + "target_id": 122, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 30, + "origin_id": 119, + "origin_slot": 0, + "target_id": 122, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 19, + "origin_id": 120, + "origin_slot": 0, + "target_id": 122, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 132, + "origin_id": 118, + "origin_slot": 0, + "target_id": 122, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 161, + "origin_id": 134, + "origin_slot": 0, + "target_id": 122, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 24, + "origin_id": 122, + "origin_slot": 0, + "target_id": 126, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 159, + "origin_id": 121, + "origin_slot": 0, + "target_id": 126, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 117, + "origin_id": 124, + "origin_slot": 0, + "target_id": 123, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 127, + "origin_id": 121, + "origin_slot": 0, + "target_id": 136, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 126, + "origin_id": -10, + "origin_slot": 0, + "target_id": 136, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 9, + "origin_id": 126, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 166, + "origin_id": 137, + "origin_slot": 0, + "target_id": 119, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 168, + "origin_id": -10, + "origin_slot": 1, + "target_id": 123, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 169, + "origin_id": -10, + "origin_slot": 0, + "target_id": 135, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 170, + "origin_id": 135, + "origin_slot": 0, + "target_id": 118, + "target_slot": 1, + "type": "INT" + }, + { + "id": 171, + "origin_id": 135, + "origin_slot": 0, + "target_id": 134, + "target_slot": 0, + "type": "INT" + }, + { + "id": 172, + "origin_id": 135, + "origin_slot": 1, + "target_id": 118, + "target_slot": 2, + "type": "INT" + }, + { + "id": 173, + "origin_id": 135, + "origin_slot": 1, + "target_id": 134, + "target_slot": 1, + "type": "INT" + }, + { + "id": 177, + "origin_id": -10, + "origin_slot": 2, + "target_id": 129, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 178, + "origin_id": -10, + "origin_slot": 3, + "target_id": 124, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 179, + "origin_id": -10, + "origin_slot": 4, + "target_id": 121, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 181, + "origin_id": 129, + "origin_slot": 0, + "target_id": 128, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 183, + "origin_id": 128, + "origin_slot": 0, + "target_id": 130, + "target_slot": 1, + "type": "MODEL" + }, + { + "id": 184, + "origin_id": 129, + "origin_slot": 0, + "target_id": 130, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 185, + "origin_id": 130, + "origin_slot": 0, + "target_id": 119, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 186, + "origin_id": 131, + "origin_slot": 0, + "target_id": 133, + "target_slot": 1, + "type": "INT" + }, + { + "id": 187, + "origin_id": 132, + "origin_slot": 0, + "target_id": 133, + "target_slot": 0, + "type": "INT" + }, + { + "id": 188, + "origin_id": 133, + "origin_slot": 0, + "target_id": 118, + "target_slot": 0, + "type": "INT" + }, + { + "id": 189, + "origin_id": 138, + "origin_slot": 0, + "target_id": 133, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 190, + "origin_id": 138, + "origin_slot": 0, + "target_id": 130, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 191, + "origin_id": -10, + "origin_slot": 5, + "target_id": 138, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 192, + "origin_id": -10, + "origin_slot": 6, + "target_id": 128, + "target_slot": 1, + "type": "COMBO" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/Edit image", + "description": "Edits an image from text instructions using Flux.2 [dev], with guidance, schedulers, and optional Turbo LoRAs." + } + ] + }, + "extra": { + "ue_links": [] + } +} \ No newline at end of file diff --git a/blueprints/Image Edit (Flux.2 Klein 4B).json b/blueprints/Image Edit (Flux.2 Klein 4B).json index 6f2f7dc01..7f6fa7a4b 100644 --- a/blueprints/Image Edit (Flux.2 Klein 4B).json +++ b/blueprints/Image Edit (Flux.2 Klein 4B).json @@ -1472,7 +1472,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Edit image" + "category": "Image generation and editing/Edit image", + "description": "Edits an input image via text instructions using FLUX.2 [klein] 4B." }, { "id": "6007e698-2ebd-4917-84d8-299b35d7b7ab", @@ -1821,7 +1822,8 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Applies reference image conditioning for style/identity transfer (Flux.2 Klein 4B)." } ] }, @@ -1837,4 +1839,4 @@ } }, "version": 0.4 -} \ No newline at end of file +} diff --git a/blueprints/Image Edit (LongCat Image Edit).json b/blueprints/Image Edit (LongCat Image Edit).json index 5b4eb18f0..de1c155a2 100644 --- a/blueprints/Image Edit (LongCat Image Edit).json +++ b/blueprints/Image Edit (LongCat Image Edit).json @@ -1417,7 +1417,8 @@ } ], "extra": {}, - "category": "Image generation and editing/Edit image" + "category": "Image generation and editing/Edit image", + "description": "Edits images via text instructions using LongCat Image Edit, an instruction-following image editing diffusion model." } ] }, diff --git a/blueprints/Image Edit (Qwen 2509).json b/blueprints/Image Edit (Qwen 2509).json new file mode 100644 index 000000000..f7be322a0 --- /dev/null +++ b/blueprints/Image Edit (Qwen 2509).json @@ -0,0 +1,1947 @@ +{ + "revision": 0, + "last_node_id": 433, + "last_link_id": 0, + "nodes": [ + { + "id": 433, + "type": "eba40a3a-f6c5-48ac-b58e-55525d06b373", + "pos": [ + 90, + -160 + ], + "size": [ + 390, + 610 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "image", + "name": "image", + "type": "IMAGE", + "link": null + }, + { + "label": "image2 (optional)", + "name": "image2", + "type": "IMAGE", + "link": null + }, + { + "label": "image3 (optional)", + "name": "image3", + "type": "IMAGE", + "link": null + }, + { + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "label": "enable_turbo_mode", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "111", + "prompt" + ], + [ + "3", + "seed" + ], + [ + "443", + "value" + ], + [ + "37", + "unet_name" + ], + [ + "38", + "clip_name" + ], + [ + "39", + "vae_name" + ], + [ + "3", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.3.62" + }, + "widgets_values": [], + "title": "Image Edit (Qwen 2509)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "eba40a3a-f6c5-48ac-b58e-55525d06b373", + "version": 1, + "state": { + "lastGroupId": 51, + "lastNodeId": 468, + "lastLinkId": 731, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image Edit (Qwen 2509)", + "inputNode": { + "id": -10, + "bounding": [ + -1160, + 280, + 151.744140625, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 2030, + -20, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "d5089bd3-63bc-4a24-b478-6565ed2364e3", + "name": "image", + "type": "IMAGE", + "linkIds": [ + 248 + ], + "label": "image", + "pos": [ + -1028.255859375, + 300 + ] + }, + { + "id": "9e80fff0-ed0a-439f-a16e-a4a6cc1eb601", + "name": "image2", + "type": "IMAGE", + "linkIds": [ + 235, + 236 + ], + "label": "image2 (optional)", + "pos": [ + -1028.255859375, + 320 + ] + }, + { + "id": "49d98fd6-01b5-440b-8603-579252fd7fef", + "name": "image3", + "type": "IMAGE", + "linkIds": [ + 237, + 238 + ], + "label": "image3 (optional)", + "pos": [ + -1028.255859375, + 340 + ] + }, + { + "id": "5de32f24-a7b5-4423-b772-72824005f585", + "name": "prompt", + "type": "STRING", + "linkIds": [ + 244 + ], + "pos": [ + -1028.255859375, + 360 + ] + }, + { + "id": "85fb3d74-7881-4c71-bc8c-624be5eedc3d", + "name": "seed", + "type": "INT", + "linkIds": [ + 718 + ], + "pos": [ + -1028.255859375, + 380 + ] + }, + { + "id": "b0c828de-d7eb-42a3-8dfb-4f53360d4fc9", + "name": "value", + "type": "BOOLEAN", + "linkIds": [ + 719 + ], + "label": "enable_turbo_mode", + "pos": [ + -1028.255859375, + 400 + ] + }, + { + "id": "072baa05-5551-4a98-bd66-015a36833ac2", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 720 + ], + "pos": [ + -1028.255859375, + 420 + ] + }, + { + "id": "d2891d11-b336-4750-9742-b93717c9ae39", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 721 + ], + "pos": [ + -1028.255859375, + 440 + ] + }, + { + "id": "4218135f-5128-4b7e-8572-92cc55615793", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 722 + ], + "pos": [ + -1028.255859375, + 460 + ] + } + ], + "outputs": [ + { + "id": "c4ebfc18-de83-4361-8e42-767c3c8c25c0", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 110 + ], + "localized_name": "IMAGE", + "pos": [ + 2050, + 0 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 75, + "type": "CFGNorm", + "pos": [ + 1080, + 30 + ], + "size": [ + 290, + 110 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 141 + }, + { + "localized_name": "strength", + "name": "strength", + "type": "FLOAT", + "widget": { + "name": "strength" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "patched_model", + "name": "patched_model", + "type": "MODEL", + "links": [ + 186 + ] + } + ], + "properties": { + "Node name for S&R": "CFGNorm", + "cnr_id": "comfy-core", + "ver": "0.3.50", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": { + "strength": true + } + } + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 39, + "type": "VAELoader", + "pos": [ + -730, + 410 + ], + "size": [ + 330, + 110 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 722 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "slot_index": 0, + "links": [ + 76, + 168, + 206, + 207 + ] + } + ], + "properties": { + "Node name for S&R": "VAELoader", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "models": [ + { + "name": "qwen_image_vae.safetensors", + "url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "qwen_image_vae.safetensors" + ] + }, + { + "id": 38, + "type": "CLIPLoader", + "pos": [ + -730, + 150 + ], + "size": [ + 330, + 150 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 721 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "slot_index": 0, + "links": [ + 204, + 205 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "models": [ + { + "name": "qwen_2.5_vl_7b_fp8_scaled.safetensors", + "url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/text_encoders/qwen_2.5_vl_7b_fp8_scaled.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "qwen_2.5_vl_7b_fp8_scaled.safetensors", + "qwen_image", + "default" + ] + }, + { + "id": 37, + "type": "UNETLoader", + "pos": [ + -730, + -60 + ], + "size": [ + 330, + 110 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 720 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 184, + 710 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "models": [ + { + "name": "qwen_image_edit_2509_fp8_e4m3fn.safetensors", + "url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_2509_fp8_e4m3fn.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "qwen_image_edit_2509_fp8_e4m3fn.safetensors", + "default" + ] + }, + { + "id": 110, + "type": "TextEncodeQwenImageEditPlus", + "pos": [ + -240, + 320 + ], + "size": [ + 400, + 240 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 204 + }, + { + "localized_name": "vae", + "name": "vae", + "shape": 7, + "type": "VAE", + "link": 206 + }, + { + "localized_name": "image1", + "name": "image1", + "shape": 7, + "type": "IMAGE", + "link": 251 + }, + { + "localized_name": "image2", + "name": "image2", + "shape": 7, + "type": "IMAGE", + "link": 236 + }, + { + "localized_name": "image3", + "name": "image3", + "shape": 7, + "type": "IMAGE", + "link": 238 + }, + { + "localized_name": "prompt", + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 210 + ] + } + ], + "properties": { + "Node name for S&R": "TextEncodeQwenImageEditPlus", + "cnr_id": "comfy-core", + "ver": "0.3.59" + }, + "widgets_values": [ + "" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 66, + "type": "ModelSamplingAuraFlow", + "pos": [ + 1070, + -120 + ], + "size": [ + 290, + 110 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 708 + }, + { + "localized_name": "shift", + "name": "shift", + "type": "FLOAT", + "widget": { + "name": "shift" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 141 + ] + } + ], + "properties": { + "Node name for S&R": "ModelSamplingAuraFlow", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + 3 + ] + }, + { + "id": 111, + "type": "TextEncodeQwenImageEditPlus", + "pos": [ + -250, + -70 + ], + "size": [ + 410, + 330 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 205 + }, + { + "localized_name": "vae", + "name": "vae", + "shape": 7, + "type": "VAE", + "link": 207 + }, + { + "localized_name": "image1", + "name": "image1", + "shape": 7, + "type": "IMAGE", + "link": 250 + }, + { + "localized_name": "image2", + "name": "image2", + "shape": 7, + "type": "IMAGE", + "link": 235 + }, + { + "localized_name": "image3", + "name": "image3", + "shape": 7, + "type": "IMAGE", + "link": 237 + }, + { + "localized_name": "prompt", + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": 244 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 211 + ] + } + ], + "properties": { + "Node name for S&R": "TextEncodeQwenImageEditPlus", + "cnr_id": "comfy-core", + "ver": "0.3.59" + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 88, + "type": "VAEEncode", + "pos": [ + -70, + 640 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "pixels", + "name": "pixels", + "type": "IMAGE", + "link": 249 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 168 + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 246 + ] + } + ], + "properties": { + "Node name for S&R": "VAEEncode", + "cnr_id": "comfy-core", + "ver": "0.3.50", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {} + } + } + }, + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 1590, + -60 + ], + "size": [ + 230, + 100 + ], + "flags": { + "collapsed": false + }, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 128 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 76 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 110 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + } + }, + { + "id": 89, + "type": "LoraLoaderModelOnly", + "pos": [ + 320, + 300 + ], + "size": [ + 300, + 140 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 184 + }, + { + "localized_name": "lora_name", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": null + }, + { + "localized_name": "strength_model", + "name": "strength_model", + "type": "FLOAT", + "widget": { + "name": "strength_model" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 709 + ] + } + ], + "properties": { + "Node name for S&R": "LoraLoaderModelOnly", + "cnr_id": "comfy-core", + "ver": "0.3.50", + "models": [ + { + "name": "Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors", + "url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors", + "directory": "loras" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": { + "lora_name": true, + "strength_model": true + } + } + }, + "widgets_values": [ + "Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors", + 1 + ] + }, + { + "id": 117, + "type": "FluxKontextImageScale", + "pos": [ + -680, + 630 + ], + "size": [ + 230, + 80 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 248 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 249, + 250, + 251 + ] + } + ], + "properties": { + "Node name for S&R": "FluxKontextImageScale" + } + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + 1070, + 210 + ], + "size": [ + 300, + 590 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 186 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 211 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 210 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 246 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 718 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 707 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": 706 + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 128 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.3.48", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + 973414316252139, + "randomize", + 4, + 1, + "euler", + "simple", + 1 + ] + }, + { + "id": 436, + "type": "PrimitiveInt", + "pos": [ + 320, + 500 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 713 + ] + } + ], + "title": "Steps", + "properties": { + "Node name for S&R": "PrimitiveInt" + }, + "widgets_values": [ + 4, + "fixed" + ] + }, + { + "id": 437, + "type": "PrimitiveFloat", + "pos": [ + 320, + 670 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "FLOAT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 714 + ] + } + ], + "title": "CFG", + "properties": { + "Node name for S&R": "PrimitiveFloat" + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 438, + "type": "PrimitiveInt", + "pos": [ + 320, + -100 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 711 + ] + } + ], + "title": "Steps", + "properties": { + "Node name for S&R": "PrimitiveInt" + }, + "widgets_values": [ + 20, + "fixed" + ] + }, + { + "id": 439, + "type": "PrimitiveFloat", + "pos": [ + 320, + 70 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "FLOAT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 712 + ] + } + ], + "title": "CFG", + "properties": { + "Node name for S&R": "PrimitiveFloat" + }, + "widgets_values": [ + 4 + ] + }, + { + "id": 440, + "type": "ComfySwitchNode", + "pos": [ + 750, + -80 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 710 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 709 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 715 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 708 + ] + } + ], + "title": "Switch (Model)", + "properties": { + "Node name for S&R": "ComfySwitchNode" + }, + "widgets_values": [ + false + ] + }, + { + "id": 441, + "type": "ComfySwitchNode", + "pos": [ + 730, + 340 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 711 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 713 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 716 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 707 + ] + } + ], + "title": "Switch (Steps)", + "properties": { + "Node name for S&R": "ComfySwitchNode" + }, + "widgets_values": [ + false + ] + }, + { + "id": 442, + "type": "ComfySwitchNode", + "pos": [ + 730, + 520 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 712 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 714 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 717 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 706 + ] + } + ], + "title": "Switch (CFG)", + "properties": { + "Node name for S&R": "ComfySwitchNode" + }, + "widgets_values": [ + false + ] + }, + { + "id": 443, + "type": "PrimitiveBoolean", + "pos": [ + 330, + 850 + ], + "size": [ + 270, + 100 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 719 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 715, + 716, + 717 + ] + } + ], + "title": "Enable Lightning LoRA", + "properties": { + "Node name for S&R": "PrimitiveBoolean" + }, + "widgets_values": [ + true + ] + }, + { + "id": 444, + "type": "MarkdownNote", + "pos": [ + 240, + -500 + ], + "size": [ + 450, + 310 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [], + "title": "Note: KSampler settings", + "properties": {}, + "widgets_values": [ + "You can test and find the best setting by yourself. The following table is for reference.\n| Parameters | Qwen Team | Comfy Original | with 4steps LoRA |\n|--------|---------|------------|---------------------------|\n| Steps | 50 | 20 | 4 |\n| CFG | 4.0 | 2.5 | 1.0 |" + ], + "color": "#432", + "bgcolor": "#000" + } + ], + "groups": [ + { + "id": 1, + "title": "Step1 - Load models", + "bounding": [ + -770, + -170, + 410, + 750 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "Step 4 - Prompt", + "bounding": [ + -330, + -170, + 570, + 750 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 50, + "title": "Lightning LoRA", + "bounding": [ + 270, + 220, + 390, + 570 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 51, + "title": "Original Settings", + "bounding": [ + 270, + -170, + 390, + 360 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 141, + "origin_id": 66, + "origin_slot": 0, + "target_id": 75, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 128, + "origin_id": 3, + "origin_slot": 0, + "target_id": 8, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 76, + "origin_id": 39, + "origin_slot": 0, + "target_id": 8, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 184, + "origin_id": 37, + "origin_slot": 0, + "target_id": 89, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 186, + "origin_id": 75, + "origin_slot": 0, + "target_id": 3, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 211, + "origin_id": 111, + "origin_slot": 0, + "target_id": 3, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 210, + "origin_id": 110, + "origin_slot": 0, + "target_id": 3, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 168, + "origin_id": 39, + "origin_slot": 0, + "target_id": 88, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 204, + "origin_id": 38, + "origin_slot": 0, + "target_id": 110, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 206, + "origin_id": 39, + "origin_slot": 0, + "target_id": 110, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 205, + "origin_id": 38, + "origin_slot": 0, + "target_id": 111, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 207, + "origin_id": 39, + "origin_slot": 0, + "target_id": 111, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 110, + "origin_id": 8, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 235, + "origin_id": -10, + "origin_slot": 1, + "target_id": 111, + "target_slot": 3, + "type": "IMAGE" + }, + { + "id": 236, + "origin_id": -10, + "origin_slot": 1, + "target_id": 110, + "target_slot": 3, + "type": "IMAGE" + }, + { + "id": 237, + "origin_id": -10, + "origin_slot": 2, + "target_id": 111, + "target_slot": 4, + "type": "IMAGE" + }, + { + "id": 238, + "origin_id": -10, + "origin_slot": 2, + "target_id": 110, + "target_slot": 4, + "type": "IMAGE" + }, + { + "id": 244, + "origin_id": -10, + "origin_slot": 3, + "target_id": 111, + "target_slot": 5, + "type": "STRING" + }, + { + "id": 246, + "origin_id": 88, + "origin_slot": 0, + "target_id": 3, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 248, + "origin_id": -10, + "origin_slot": 0, + "target_id": 117, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 249, + "origin_id": 117, + "origin_slot": 0, + "target_id": 88, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 250, + "origin_id": 117, + "origin_slot": 0, + "target_id": 111, + "target_slot": 2, + "type": "IMAGE" + }, + { + "id": 251, + "origin_id": 117, + "origin_slot": 0, + "target_id": 110, + "target_slot": 2, + "type": "IMAGE" + }, + { + "id": 706, + "origin_id": 442, + "origin_slot": 0, + "target_id": 3, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 707, + "origin_id": 441, + "origin_slot": 0, + "target_id": 3, + "target_slot": 5, + "type": "INT" + }, + { + "id": 708, + "origin_id": 440, + "origin_slot": 0, + "target_id": 66, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 709, + "origin_id": 89, + "origin_slot": 0, + "target_id": 440, + "target_slot": 1, + "type": "MODEL" + }, + { + "id": 710, + "origin_id": 37, + "origin_slot": 0, + "target_id": 440, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 711, + "origin_id": 438, + "origin_slot": 0, + "target_id": 441, + "target_slot": 0, + "type": "INT" + }, + { + "id": 712, + "origin_id": 439, + "origin_slot": 0, + "target_id": 442, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 713, + "origin_id": 436, + "origin_slot": 0, + "target_id": 441, + "target_slot": 1, + "type": "INT" + }, + { + "id": 714, + "origin_id": 437, + "origin_slot": 0, + "target_id": 442, + "target_slot": 1, + "type": "FLOAT" + }, + { + "id": 715, + "origin_id": 443, + "origin_slot": 0, + "target_id": 440, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 716, + "origin_id": 443, + "origin_slot": 0, + "target_id": 441, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 717, + "origin_id": 443, + "origin_slot": 0, + "target_id": 442, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 718, + "origin_id": -10, + "origin_slot": 4, + "target_id": 3, + "target_slot": 4, + "type": "INT" + }, + { + "id": 719, + "origin_id": -10, + "origin_slot": 5, + "target_id": 443, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 720, + "origin_id": -10, + "origin_slot": 6, + "target_id": 37, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 721, + "origin_id": -10, + "origin_slot": 7, + "target_id": 38, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 722, + "origin_id": -10, + "origin_slot": 8, + "target_id": 39, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/Edit image", + "description": "Edits images from text instructions using Qwen-Image-Edit-2509 with optional Lightning LoRA for few-step sampling." + } + ] + }, + "extra": {} +} diff --git a/blueprints/Image Edit (Qwen 2511).json b/blueprints/Image Edit (Qwen 2511).json index 582171fa0..1aa7e5765 100644 --- a/blueprints/Image Edit (Qwen 2511).json +++ b/blueprints/Image Edit (Qwen 2511).json @@ -132,7 +132,7 @@ }, "revision": 0, "config": {}, - "name": "local-Image Edit (Qwen 2511)", + "name": "Image Edit (Qwen 2511)", "inputNode": { "id": -10, "bounding": [ @@ -1468,7 +1468,8 @@ "VHS_MetadataImage": true, "VHS_KeepIntermediate": true }, - "category": "Image generation and editing/Edit image" + "category": "Image generation and editing/Edit image", + "description": "Edits images via text instructions using Qwen-Image-Edit-2511 with improved character consistency and integrated LoRA." } ] }, @@ -1489,4 +1490,4 @@ } }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Image Face Detection (Mediapipe).json b/blueprints/Image Face Detection (Mediapipe).json new file mode 100644 index 000000000..e2548d485 --- /dev/null +++ b/blueprints/Image Face Detection (Mediapipe).json @@ -0,0 +1,779 @@ +{ + "revision": 0, + "last_node_id": 33, + "last_link_id": 0, + "nodes": [ + { + "id": 33, + "type": "6062babb-b649-4a71-be9e-20ebce567744", + "pos": [ + -450, + 4240 + ], + "size": [ + 420, + 400 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": null + }, + { + "name": "face_landmarker", + "type": "FACE_LANDMARKER", + "link": null + }, + { + "name": "detector_variant", + "type": "COMBO", + "widget": { + "name": "detector_variant" + }, + "link": null + }, + { + "name": "num_faces", + "type": "INT", + "widget": { + "name": "num_faces" + }, + "link": null + }, + { + "label": "custom_face_oval", + "name": "regions.face_oval", + "type": "BOOLEAN", + "widget": { + "name": "regions.face_oval" + }, + "link": null + }, + { + "label": "custom_lips", + "name": "regions.lips", + "type": "BOOLEAN", + "widget": { + "name": "regions.lips" + }, + "link": null + }, + { + "label": "custom_left_eye", + "name": "regions.left_eye", + "type": "BOOLEAN", + "widget": { + "name": "regions.left_eye" + }, + "link": null + }, + { + "label": "custom_right_eye", + "name": "regions.right_eye", + "type": "BOOLEAN", + "widget": { + "name": "regions.right_eye" + }, + "link": null + }, + { + "label": "custom_irises", + "name": "regions.irises", + "type": "BOOLEAN", + "widget": { + "name": "regions.irises" + }, + "link": null + }, + { + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "face_landmarks", + "name": "face_landmarks", + "type": "FACE_LANDMARKS", + "links": [] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [] + }, + { + "label": "mask", + "name": "MASK_1", + "type": "MASK", + "links": [] + } + ], + "title": "Image Face Detection (Mediapipe)", + "properties": { + "proxyWidgets": [ + [ + "11", + "detector_variant" + ], + [ + "11", + "num_faces" + ], + [ + "20", + "regions.face_oval" + ], + [ + "20", + "regions.lips" + ], + [ + "20", + "regions.left_eye" + ], + [ + "20", + "regions.right_eye" + ], + [ + "20", + "regions.irises" + ], + [ + "2", + "model_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.22.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "6062babb-b649-4a71-be9e-20ebce567744", + "version": 1, + "state": { + "lastGroupId": 2, + "lastNodeId": 158, + "lastLinkId": 140, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image Face Detection (Mediapipe)", + "description": "Detects facial landmarks from an image using MediaPipe, outputting landmark data, face bounding boxes, and an optional face-region mask.", + "inputNode": { + "id": -10, + "bounding": [ + -710, + 4300, + 148.880859375, + 248 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 140, + 4480, + 137.677734375, + 108 + ] + }, + "inputs": [ + { + "id": "705dc1ae-6dc9-4155-92df-52f816ad451e", + "name": "image", + "type": "IMAGE", + "linkIds": [ + 60 + ], + "localized_name": "image", + "pos": [ + -585.119140625, + 4324 + ] + }, + { + "id": "d6277190-732c-4604-b7cd-d3a9588bf761", + "name": "face_landmarker", + "type": "FACE_LANDMARKER", + "linkIds": [ + 74 + ], + "pos": [ + -585.119140625, + 4344 + ] + }, + { + "id": "ac473a08-6a86-42a7-b460-e70c6c5e1e2b", + "name": "detector_variant", + "type": "COMBO", + "linkIds": [ + 75 + ], + "pos": [ + -585.119140625, + 4364 + ] + }, + { + "id": "1bec2252-ca2d-496e-8a33-33a61d21f897", + "name": "num_faces", + "type": "INT", + "linkIds": [ + 76 + ], + "pos": [ + -585.119140625, + 4384 + ] + }, + { + "id": "17994fa2-0ea0-4c9b-a70a-19789c459c80", + "name": "regions.face_oval", + "type": "BOOLEAN", + "linkIds": [ + 77 + ], + "label": "custom_face_oval", + "pos": [ + -585.119140625, + 4404 + ] + }, + { + "id": "1c6c5893-2aee-4c37-b702-15ef2e20d863", + "name": "regions.lips", + "type": "BOOLEAN", + "linkIds": [ + 78 + ], + "label": "custom_lips", + "pos": [ + -585.119140625, + 4424 + ] + }, + { + "id": "f353fcea-4b6f-42a1-8fdd-32b3aa1e1f09", + "name": "regions.left_eye", + "type": "BOOLEAN", + "linkIds": [ + 79 + ], + "label": "custom_left_eye", + "pos": [ + -585.119140625, + 4444 + ] + }, + { + "id": "1387e121-c1fb-4522-8f0d-43459e11dd86", + "name": "regions.right_eye", + "type": "BOOLEAN", + "linkIds": [ + 80 + ], + "label": "custom_right_eye", + "pos": [ + -585.119140625, + 4464 + ] + }, + { + "id": "14acb0a0-d1f4-48f3-ba31-811b26236ef9", + "name": "regions.irises", + "type": "BOOLEAN", + "linkIds": [ + 81 + ], + "label": "custom_irises", + "pos": [ + -585.119140625, + 4484 + ] + }, + { + "id": "25a82859-87de-42c8-8431-09948665546e", + "name": "model_name", + "type": "COMBO", + "linkIds": [ + 86 + ], + "pos": [ + -585.119140625, + 4504 + ] + } + ], + "outputs": [ + { + "id": "d2ba3f92-e8b1-49c3-9590-cfad56c54cf4", + "name": "face_landmarks", + "type": "FACE_LANDMARKS", + "linkIds": [ + 44 + ], + "localized_name": "face_landmarks", + "pos": [ + 164, + 4504 + ] + }, + { + "id": "4f356bb0-d4c4-4f93-b4cf-0845a65c4e6d", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 25 + ], + "localized_name": "bboxes", + "pos": [ + 164, + 4524 + ] + }, + { + "id": "f6309e1d-6397-4363-b38f-778a122abc51", + "name": "MASK_1", + "type": "MASK", + "linkIds": [ + 83 + ], + "label": "mask", + "pos": [ + 164, + 4544 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 11, + "type": "MediaPipeFaceLandmarker", + "pos": [ + -280, + 4280 + ], + "size": [ + 350, + 220 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "face_detection_model", + "name": "face_detection_model", + "type": "FACE_DETECTION_MODEL", + "link": 66 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 60 + }, + { + "localized_name": "detector_variant", + "name": "detector_variant", + "type": "COMBO", + "widget": { + "name": "detector_variant" + }, + "link": 75 + }, + { + "localized_name": "num_faces", + "name": "num_faces", + "type": "INT", + "widget": { + "name": "num_faces" + }, + "link": 76 + }, + { + "localized_name": "min_confidence", + "name": "min_confidence", + "type": "FLOAT", + "widget": { + "name": "min_confidence" + }, + "link": null + }, + { + "localized_name": "missing_frame_fallback", + "name": "missing_frame_fallback", + "type": "COMBO", + "widget": { + "name": "missing_frame_fallback" + }, + "link": null + }, + { + "name": "face_landmarker", + "type": "FACE_LANDMARKER", + "link": 74 + } + ], + "outputs": [ + { + "localized_name": "face_landmarks", + "name": "face_landmarks", + "type": "FACE_LANDMARKS", + "links": [ + 44, + 46 + ] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [ + 25 + ] + } + ], + "properties": { + "Node name for S&R": "MediaPipeFaceLandmarker", + "cnr_id": "comfy-core", + "ver": "0.22.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "full", + 0, + 0.5, + "empty" + ] + }, + { + "id": 2, + "type": "LoadMediaPipeFaceLandmarker", + "pos": [ + -290, + 4060 + ], + "size": [ + 350, + 140 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "model_name", + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": 86 + } + ], + "outputs": [ + { + "localized_name": "FACE_DETECTION_MODEL", + "name": "FACE_DETECTION_MODEL", + "type": "FACE_DETECTION_MODEL", + "links": [ + 66 + ] + } + ], + "properties": { + "Node name for S&R": "LoadMediaPipeFaceLandmarker", + "cnr_id": "comfy-core", + "ver": "0.22.0", + "models": [ + { + "name": "mediapipe_face_fp32.safetensors", + "url": "https://huggingface.co/Comfy-Org/mediapipe/resolve/main/detection/mediapipe_face_fp32.safetensors", + "directory": "detection" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "mediapipe_face_fp32.safetensors" + ] + }, + { + "id": 20, + "type": "MediaPipeFaceMask", + "pos": [ + -290, + 4560 + ], + "size": [ + 360, + 180 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "face_landmarks", + "name": "face_landmarks", + "type": "FACE_LANDMARKS", + "link": 46 + }, + { + "localized_name": "regions", + "name": "regions", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "regions" + }, + "link": null + }, + { + "localized_name": "regions.face_oval", + "name": "regions.face_oval", + "type": "BOOLEAN", + "widget": { + "name": "regions.face_oval" + }, + "link": 77 + }, + { + "localized_name": "regions.lips", + "name": "regions.lips", + "type": "BOOLEAN", + "widget": { + "name": "regions.lips" + }, + "link": 78 + }, + { + "localized_name": "regions.left_eye", + "name": "regions.left_eye", + "type": "BOOLEAN", + "widget": { + "name": "regions.left_eye" + }, + "link": 79 + }, + { + "localized_name": "regions.right_eye", + "name": "regions.right_eye", + "type": "BOOLEAN", + "widget": { + "name": "regions.right_eye" + }, + "link": 80 + }, + { + "localized_name": "regions.irises", + "name": "regions.irises", + "type": "BOOLEAN", + "widget": { + "name": "regions.irises" + }, + "link": 81 + } + ], + "outputs": [ + { + "localized_name": "MASK", + "name": "MASK", + "type": "MASK", + "links": [ + 83 + ] + } + ], + "properties": { + "Node name for S&R": "MediaPipeFaceMask", + "cnr_id": "comfy-core", + "ver": "0.22.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "custom", + true, + false, + false, + false, + false + ] + } + ], + "groups": [], + "links": [ + { + "id": 66, + "origin_id": 2, + "origin_slot": 0, + "target_id": 11, + "target_slot": 0, + "type": "FACE_DETECTION_MODEL" + }, + { + "id": 46, + "origin_id": 11, + "origin_slot": 0, + "target_id": 20, + "target_slot": 0, + "type": "FACE_LANDMARKS" + }, + { + "id": 60, + "origin_id": -10, + "origin_slot": 0, + "target_id": 11, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 44, + "origin_id": 11, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "FACE_LANDMARKS" + }, + { + "id": 25, + "origin_id": 11, + "origin_slot": 1, + "target_id": -20, + "target_slot": 1, + "type": "BOUNDING_BOX" + }, + { + "id": 74, + "origin_id": -10, + "origin_slot": 1, + "target_id": 11, + "target_slot": 6, + "type": "FACE_LANDMARKER" + }, + { + "id": 75, + "origin_id": -10, + "origin_slot": 2, + "target_id": 11, + "target_slot": 2, + "type": "COMBO" + }, + { + "id": 76, + "origin_id": -10, + "origin_slot": 3, + "target_id": 11, + "target_slot": 3, + "type": "INT" + }, + { + "id": 77, + "origin_id": -10, + "origin_slot": 4, + "target_id": 20, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 78, + "origin_id": -10, + "origin_slot": 5, + "target_id": 20, + "target_slot": 3, + "type": "BOOLEAN" + }, + { + "id": 79, + "origin_id": -10, + "origin_slot": 6, + "target_id": 20, + "target_slot": 4, + "type": "BOOLEAN" + }, + { + "id": 80, + "origin_id": -10, + "origin_slot": 7, + "target_id": 20, + "target_slot": 5, + "type": "BOOLEAN" + }, + { + "id": 81, + "origin_id": -10, + "origin_slot": 8, + "target_id": 20, + "target_slot": 6, + "type": "BOOLEAN" + }, + { + "id": 83, + "origin_id": 20, + "origin_slot": 0, + "target_id": -20, + "target_slot": 2, + "type": "MASK" + }, + { + "id": 86, + "origin_id": -10, + "origin_slot": 9, + "target_id": 2, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": {}, + "category": "Conditioning & Preprocessors/Face Detection" + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Image Inpainting (Flux.1 Fill Dev).json b/blueprints/Image Inpainting (Flux.1 Fill Dev).json index d40d63594..c1326ed3d 100644 --- a/blueprints/Image Inpainting (Flux.1 Fill Dev).json +++ b/blueprints/Image Inpainting (Flux.1 Fill Dev).json @@ -1188,7 +1188,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Inpaint image" + "category": "Image generation and editing/Inpaint image", + "description": "Inpaints masked image regions using Flux.1 fill [dev], Black Forest Labs' inpainting/outpainting model." } ] }, @@ -1202,4 +1203,4 @@ }, "ue_links": [] } -} \ No newline at end of file +} diff --git a/blueprints/Image Inpainting (Qwen-image).json b/blueprints/Image Inpainting (Qwen-image).json index 95b2909fa..a06d57e19 100644 --- a/blueprints/Image Inpainting (Qwen-image).json +++ b/blueprints/Image Inpainting (Qwen-image).json @@ -1548,7 +1548,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Inpaint image" + "category": "Image generation and editing/Inpaint image", + "description": "Inpaints masked regions using Qwen-Image, extending its multilingual text rendering to inpainting tasks." }, { "id": "56a1f603-fbd2-40ed-94ef-c9ecbd96aca8", @@ -1907,7 +1908,8 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Expands and softens mask edges to reduce visible seams after image processing." } ] }, diff --git a/blueprints/Image Levels.json b/blueprints/Image Levels.json index ef256a1aa..1a1b18932 100644 --- a/blueprints/Image Levels.json +++ b/blueprints/Image Levels.json @@ -742,9 +742,10 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Color adjust" + "category": "Image Tools/Color adjust", + "description": "Adjusts black point, white point, and gamma for tonal range control via GPU shader." } ] }, "extra": {} -} +} \ No newline at end of file diff --git a/blueprints/Image Outpainting (Qwen-Image).json b/blueprints/Image Outpainting (Qwen-Image).json index 218fdc775..6c07227c0 100644 --- a/blueprints/Image Outpainting (Qwen-Image).json +++ b/blueprints/Image Outpainting (Qwen-Image).json @@ -1919,7 +1919,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Outpaint image" + "category": "Image generation and editing/Outpaint image", + "description": "Outpaints beyond image boundaries using Qwen-Image's outpainting capabilities." }, { "id": "f93c215e-c393-460e-9534-ed2c3d8a652e", @@ -2278,7 +2279,8 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Expands and softens mask edges to reduce visible seams after image processing." }, { "id": "2a4b2cc0-db37-4302-a067-da392f38f06b", @@ -2733,7 +2735,8 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Scales both image and mask together while preserving alignment for editing workflows." } ] }, diff --git a/blueprints/Image Segmentation (SAM3).json b/blueprints/Image Segmentation (SAM3).json new file mode 100644 index 000000000..a2ef40ac8 --- /dev/null +++ b/blueprints/Image Segmentation (SAM3).json @@ -0,0 +1,714 @@ +{ + "revision": 0, + "last_node_id": 99, + "last_link_id": 0, + "nodes": [ + { + "id": 99, + "type": "6e7ab3ea-96aa-470f-9b94-3d9d0e01f481", + "pos": [ + -1630, + -3270 + ], + "size": [ + 290, + 370 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": null + }, + { + "label": "object", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "bboxes", + "type": "BOUNDING_BOX", + "link": null + }, + { + "name": "positive_coords", + "type": "STRING", + "link": null + }, + { + "name": "negative_coords", + "type": "STRING", + "link": null + }, + { + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": null + }, + { + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": null + }, + { + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": null + }, + { + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "78", + "text" + ], + [ + "75", + "threshold" + ], + [ + "75", + "refine_iterations" + ], + [ + "75", + "individual_masks" + ], + [ + "77", + "ckpt_name" + ] + ], + "ue_properties": { + "widget_ue_connectable": { + "text": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Image Segmentation (SAM3)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "6e7ab3ea-96aa-470f-9b94-3d9d0e01f481", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 113, + "lastLinkId": 283, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image Segmentation (SAM3)", + "inputNode": { + "id": -10, + "bounding": [ + -2260, + -3450, + 136.369140625, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -1130, + -3305, + 120, + 80 + ] + }, + "inputs": [ + { + "id": "a6e75fa2-162a-4af0-a2fd-1e9c899a5ab6", + "name": "image", + "type": "IMAGE", + "linkIds": [ + 264 + ], + "localized_name": "image", + "label": "image", + "pos": [ + -2143.630859375, + -3430 + ] + }, + { + "id": "3cefd304-7631-4ff6-a5a0-5a0ffb120745", + "name": "text", + "type": "STRING", + "linkIds": [ + 265 + ], + "label": "object", + "pos": [ + -2143.630859375, + -3410 + ] + }, + { + "id": "1aec91c5-d8d2-441c-928c-49c14e7e80ed", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 266 + ], + "pos": [ + -2143.630859375, + -3390 + ] + }, + { + "id": "1ec7ce1a-8257-4719-8a81-60ebc8a98899", + "name": "positive_coords", + "type": "STRING", + "linkIds": [ + 267 + ], + "pos": [ + -2143.630859375, + -3370 + ] + }, + { + "id": "c65f8b87-9bd7-48be-9fc2-823431e95019", + "name": "negative_coords", + "type": "STRING", + "linkIds": [ + 268 + ], + "pos": [ + -2143.630859375, + -3350 + ] + }, + { + "id": "bb4ba35a-ccfe-4c37-98e5-d9b0d69585fb", + "name": "threshold", + "type": "FLOAT", + "linkIds": [ + 269 + ], + "pos": [ + -2143.630859375, + -3330 + ] + }, + { + "id": "b1439668-b050-490b-a5dc-fc4052c55666", + "name": "refine_iterations", + "type": "INT", + "linkIds": [ + 270 + ], + "pos": [ + -2143.630859375, + -3310 + ] + }, + { + "id": "86e239e5-c098-4302-b54d-d42a38bc0f89", + "name": "individual_masks", + "type": "BOOLEAN", + "linkIds": [ + 271 + ], + "pos": [ + -2143.630859375, + -3290 + ] + }, + { + "id": "f9e0b9d4-b2f1-4907-a4a5-305656576706", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 272 + ], + "pos": [ + -2143.630859375, + -3270 + ] + } + ], + "outputs": [ + { + "id": "ff50da09-1e59-4a58-9b7f-be1a00aa5913", + "name": "masks", + "type": "MASK", + "linkIds": [ + 231 + ], + "localized_name": "masks", + "pos": [ + -1110, + -3285 + ] + }, + { + "id": "8f622e40-8528-4078-b7d3-147e9f872194", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 232 + ], + "localized_name": "bboxes", + "pos": [ + -1110, + -3265 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 75, + "type": "SAM3_Detect", + "pos": [ + -1470, + -3460 + ], + "size": [ + 270, + 260 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "label": "model", + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 237 + }, + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 264 + }, + { + "label": "conditioning", + "localized_name": "conditioning", + "name": "conditioning", + "shape": 7, + "type": "CONDITIONING", + "link": 200 + }, + { + "label": "bboxes", + "localized_name": "bboxes", + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": 266 + }, + { + "label": "positive_coords", + "localized_name": "positive_coords", + "name": "positive_coords", + "shape": 7, + "type": "STRING", + "link": 267 + }, + { + "label": "negative_coords", + "localized_name": "negative_coords", + "name": "negative_coords", + "shape": 7, + "type": "STRING", + "link": 268 + }, + { + "localized_name": "threshold", + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": 269 + }, + { + "localized_name": "refine_iterations", + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": 270 + }, + { + "localized_name": "individual_masks", + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": 271 + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [ + 231 + ] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [ + 232 + ] + } + ], + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "cnr_id": "comfy-core", + "ver": "0.19.3", + "Node name for S&R": "SAM3_Detect", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0.5, + 2, + false + ] + }, + { + "id": 77, + "type": "CheckpointLoaderSimple", + "pos": [ + -1970, + -3200 + ], + "size": [ + 330, + 140 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 272 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 237 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 240 + ] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": null + } + ], + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "cnr_id": "comfy-core", + "ver": "0.19.3", + "Node name for S&R": "CheckpointLoaderSimple", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "sam3.1_multiplex_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors", + "directory": "checkpoints" + } + ] + }, + "widgets_values": [ + "sam3.1_multiplex_fp16.safetensors" + ] + }, + { + "id": 78, + "type": "CLIPTextEncode", + "pos": [ + -2000, + -3000 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 240 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 265 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 200 + ] + } + ], + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "cnr_id": "comfy-core", + "ver": "0.19.3", + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ] + } + ], + "groups": [], + "links": [ + { + "id": 237, + "origin_id": 77, + "origin_slot": 0, + "target_id": 75, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 200, + "origin_id": 78, + "origin_slot": 0, + "target_id": 75, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 240, + "origin_id": 77, + "origin_slot": 1, + "target_id": 78, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 231, + "origin_id": 75, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 232, + "origin_id": 75, + "origin_slot": 1, + "target_id": -20, + "target_slot": 1, + "type": "BOUNDING_BOX" + }, + { + "id": 264, + "origin_id": -10, + "origin_slot": 0, + "target_id": 75, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 265, + "origin_id": -10, + "origin_slot": 1, + "target_id": 78, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 266, + "origin_id": -10, + "origin_slot": 2, + "target_id": 75, + "target_slot": 3, + "type": "BOUNDING_BOX" + }, + { + "id": 267, + "origin_id": -10, + "origin_slot": 3, + "target_id": 75, + "target_slot": 4, + "type": "STRING" + }, + { + "id": 268, + "origin_id": -10, + "origin_slot": 4, + "target_id": 75, + "target_slot": 5, + "type": "STRING" + }, + { + "id": 269, + "origin_id": -10, + "origin_slot": 5, + "target_id": 75, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 270, + "origin_id": -10, + "origin_slot": 6, + "target_id": 75, + "target_slot": 7, + "type": "INT" + }, + { + "id": 271, + "origin_id": -10, + "origin_slot": 7, + "target_id": 75, + "target_slot": 8, + "type": "BOOLEAN" + }, + { + "id": 272, + "origin_id": -10, + "origin_slot": 8, + "target_id": 77, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": {}, + "category": "Conditioning & Preprocessors/Segmentation & Mask", + "description": "Segments images into masks using Meta SAM3 from text prompts, points, or boxes." + } + ] + }, + "extra": { + "ue_links": [] + } +} diff --git a/blueprints/Image Upscale(Z-image-Turbo).json b/blueprints/Image Upscale(Z-image-Turbo).json index 0d2b6e240..25d2838a8 100644 --- a/blueprints/Image Upscale(Z-image-Turbo).json +++ b/blueprints/Image Upscale(Z-image-Turbo).json @@ -141,7 +141,7 @@ }, "revision": 0, "config": {}, - "name": "local-Image Upscale(Z-image-Turbo)", + "name": "Image Upscale (Z-image-Turbo)", "inputNode": { "id": -10, "bounding": [ @@ -1302,7 +1302,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Enhance" + "category": "Image generation and editing/Upscale", + "description": "Upscales images to higher resolution using Z-Image-Turbo." } ] }, @@ -1311,4 +1312,4 @@ "workflowRendererVersion": "LG" }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Image to Layers(Qwen-Image-Layered).json b/blueprints/Image to Layers(Qwen-Image-Layered).json index 8a525e7a5..7b44f0563 100644 --- a/blueprints/Image to Layers(Qwen-Image-Layered).json +++ b/blueprints/Image to Layers(Qwen-Image-Layered).json @@ -1586,7 +1586,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Image to layers" + "category": "Image generation and editing/Image to layers", + "description": "Decomposes an image into variable-resolution RGBA layers for independent editing using Qwen-Image-Layered." } ] }, diff --git a/blueprints/Image to Model (Hunyuan3d 2.1).json b/blueprints/Image to Model (Hunyuan3d 2.1).json index 4705603a8..ee5552656 100644 --- a/blueprints/Image to Model (Hunyuan3d 2.1).json +++ b/blueprints/Image to Model (Hunyuan3d 2.1).json @@ -72,7 +72,7 @@ }, "revision": 0, "config": {}, - "name": "local-Image to Model (Hunyuan3d 2.1)", + "name": "Image to 3D Model (Hunyuan3d 2.1)", "inputNode": { "id": -10, "bounding": [ @@ -765,7 +765,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "3D/Image to 3D Model" + "category": "3D/Image to 3D Model", + "description": "Generates 3D mesh models from a single input image using Hunyuan3D 2.0/2.1." } ] }, diff --git a/blueprints/Image to Pose Map (SDPose Multi-Person).json b/blueprints/Image to Pose Map (SDPose Multi-Person).json new file mode 100644 index 000000000..38df20775 --- /dev/null +++ b/blueprints/Image to Pose Map (SDPose Multi-Person).json @@ -0,0 +1,1206 @@ +{ + "revision": 0, + "last_node_id": 675, + "last_link_id": 0, + "nodes": [ + { + "id": 675, + "type": "01b6a731-fb78-4070-9a38-c87146da9604", + "pos": [ + -2480, + 3400 + ], + "size": [ + 370, + 590.625 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": null + }, + { + "label": "resize_target_longer_size", + "name": "resize_type.longer_size", + "type": "INT", + "widget": { + "name": "resize_type.longer_size" + }, + "link": null + }, + { + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": null + }, + { + "name": "draw_body", + "type": "BOOLEAN", + "widget": { + "name": "draw_body" + }, + "link": null + }, + { + "name": "draw_hands", + "type": "BOOLEAN", + "widget": { + "name": "draw_hands" + }, + "link": null + }, + { + "name": "draw_face", + "type": "BOOLEAN", + "widget": { + "name": "draw_face" + }, + "link": null + }, + { + "name": "draw_feet", + "type": "BOOLEAN", + "widget": { + "name": "draw_feet" + }, + "link": null + }, + { + "name": "stick_width", + "type": "INT", + "widget": { + "name": "stick_width" + }, + "link": null + }, + { + "name": "face_point_size", + "type": "INT", + "widget": { + "name": "face_point_size" + }, + "link": null + }, + { + "name": "score_threshold", + "type": "FLOAT", + "widget": { + "name": "score_threshold" + }, + "link": null + }, + { + "label": "detect_threshold", + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": null + }, + { + "label": "detect_class", + "name": "class_name", + "type": "COMBO", + "widget": { + "name": "class_name" + }, + "link": null + }, + { + "name": "max_detections", + "type": "INT", + "widget": { + "name": "max_detections" + }, + "link": null + }, + { + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + }, + { + "name": "keypoints", + "type": "POSE_KEYPOINT", + "links": null + }, + { + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "674", + "resize_type.longer_size" + ], + [ + "674", + "scale_method" + ], + [ + "672", + "draw_body" + ], + [ + "672", + "draw_hands" + ], + [ + "672", + "draw_face" + ], + [ + "672", + "draw_feet" + ], + [ + "672", + "stick_width" + ], + [ + "672", + "face_point_size" + ], + [ + "672", + "score_threshold" + ], + [ + "678", + "threshold" + ], + [ + "678", + "class_name" + ], + [ + "678", + "max_detections" + ], + [ + "673", + "ckpt_name" + ], + [ + "677", + "unet_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.15.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Image to Pose Map (SDPose Multi-Person)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "01b6a731-fb78-4070-9a38-c87146da9604", + "version": 1, + "state": { + "lastGroupId": 2, + "lastNodeId": 691, + "lastLinkId": 1740, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image to Pose Map (SDPose Multi-Person)", + "inputNode": { + "id": -10, + "bounding": [ + -3350, + 3410, + 190.8984375, + 348 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -1840, + 3570, + 128, + 108 + ] + }, + "inputs": [ + { + "id": "e24699c3-1356-4634-9eb4-19bb58e5c0b0", + "name": "input", + "type": "IMAGE,MASK", + "linkIds": [ + 1700 + ], + "localized_name": "input", + "pos": [ + -3183.1015625, + 3434 + ] + }, + { + "id": "088eefc1-cd8a-4573-993f-9e4da008a12d", + "name": "resize_type.longer_size", + "type": "INT", + "linkIds": [ + 1704 + ], + "label": "resize_target_longer_size", + "pos": [ + -3183.1015625, + 3454 + ] + }, + { + "id": "b6449bd3-73d4-41c8-b81f-cf8d33f76a2e", + "name": "scale_method", + "type": "COMBO", + "linkIds": [ + 1705 + ], + "pos": [ + -3183.1015625, + 3474 + ] + }, + { + "id": "4cff52ad-ed07-4c97-8803-fcbd89554fd0", + "name": "draw_body", + "type": "BOOLEAN", + "linkIds": [ + 1706 + ], + "pos": [ + -3183.1015625, + 3494 + ] + }, + { + "id": "7af63dce-f7df-4d7e-8215-d7c7f60bf81c", + "name": "draw_hands", + "type": "BOOLEAN", + "linkIds": [ + 1707 + ], + "pos": [ + -3183.1015625, + 3514 + ] + }, + { + "id": "af3a9bce-61f9-4aca-b530-9f65e028b35e", + "name": "draw_face", + "type": "BOOLEAN", + "linkIds": [ + 1708 + ], + "pos": [ + -3183.1015625, + 3534 + ] + }, + { + "id": "4620f6a3-2c85-4b79-ad8f-35d0326b568f", + "name": "draw_feet", + "type": "BOOLEAN", + "linkIds": [ + 1709 + ], + "pos": [ + -3183.1015625, + 3554 + ] + }, + { + "id": "fee5d0c9-8d4b-4934-81d8-ba2206dc56cb", + "name": "stick_width", + "type": "INT", + "linkIds": [ + 1710 + ], + "pos": [ + -3183.1015625, + 3574 + ] + }, + { + "id": "aafdd060-ba81-4324-a9cc-b656e1ebc133", + "name": "face_point_size", + "type": "INT", + "linkIds": [ + 1711 + ], + "pos": [ + -3183.1015625, + 3594 + ] + }, + { + "id": "514c5503-f9e6-4d23-b1ae-1d3291acb2a3", + "name": "score_threshold", + "type": "FLOAT", + "linkIds": [ + 1712 + ], + "pos": [ + -3183.1015625, + 3614 + ] + }, + { + "id": "4eb3e4ea-7a36-4511-8483-0d12aadd32f7", + "name": "threshold", + "type": "FLOAT", + "linkIds": [ + 1718 + ], + "label": "detect_threshold", + "pos": [ + -3183.1015625, + 3634 + ] + }, + { + "id": "c76a7a05-81e6-4b17-a9e0-85f47a5844f2", + "name": "class_name", + "type": "COMBO", + "linkIds": [ + 1719 + ], + "label": "detect_class", + "pos": [ + -3183.1015625, + 3654 + ] + }, + { + "id": "4417e988-6e80-4236-be31-4c179037f5a2", + "name": "max_detections", + "type": "INT", + "linkIds": [ + 1720 + ], + "pos": [ + -3183.1015625, + 3674 + ] + }, + { + "id": "7d7c4a0b-0d1b-4c98-942b-f90548d2a492", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 1721 + ], + "pos": [ + -3183.1015625, + 3694 + ] + }, + { + "id": "4d75122c-2c14-452a-98fe-d1545d3e012a", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 1722 + ], + "pos": [ + -3183.1015625, + 3714 + ] + } + ], + "outputs": [ + { + "id": "f05ed8cc-9403-4f14-8085-4364b06f8a48", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 1701 + ], + "localized_name": "IMAGE", + "pos": [ + -1816, + 3594 + ] + }, + { + "id": "4b64118e-3cef-4eeb-9dad-4cd09cfd63a2", + "name": "keypoints", + "type": "POSE_KEYPOINT", + "linkIds": [ + 1725 + ], + "pos": [ + -1816, + 3614 + ] + }, + { + "id": "a27f7e34-dcbc-4fb0-a4e1-2c5fc423ca5f", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 1726 + ], + "pos": [ + -1816, + 3634 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 671, + "type": "SDPoseKeypointExtractor", + "pos": [ + -2550, + 3080 + ], + "size": [ + 270, + 180 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 1696 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 1697 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 1698 + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": 1717 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "keypoints", + "name": "keypoints", + "type": "POSE_KEYPOINT", + "links": [ + 1699, + 1725 + ] + } + ], + "properties": { + "Node name for S&R": "SDPoseKeypointExtractor", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 16 + ] + }, + { + "id": 674, + "type": "ResizeImageMaskNode", + "pos": [ + -2970, + 3580 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": 1700 + }, + { + "localized_name": "resize_type", + "name": "resize_type", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "resize_type" + }, + "link": null + }, + { + "localized_name": "resize_type.longer_size", + "name": "resize_type.longer_size", + "type": "INT", + "widget": { + "name": "resize_type.longer_size" + }, + "link": 1704 + }, + { + "localized_name": "scale_method", + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": 1705 + } + ], + "outputs": [ + { + "localized_name": "resized", + "name": "resized", + "type": "*", + "links": [ + 1698, + 1716 + ] + } + ], + "properties": { + "Node name for S&R": "ResizeImageMaskNode", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "scale longer dimension", + 1024, + "lanczos" + ] + }, + { + "id": 672, + "type": "SDPoseDrawKeypoints", + "pos": [ + -2540, + 3590 + ], + "size": [ + 270, + 280 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "keypoints", + "name": "keypoints", + "type": "POSE_KEYPOINT", + "link": 1699 + }, + { + "localized_name": "draw_body", + "name": "draw_body", + "type": "BOOLEAN", + "widget": { + "name": "draw_body" + }, + "link": 1706 + }, + { + "localized_name": "draw_hands", + "name": "draw_hands", + "type": "BOOLEAN", + "widget": { + "name": "draw_hands" + }, + "link": 1707 + }, + { + "localized_name": "draw_face", + "name": "draw_face", + "type": "BOOLEAN", + "widget": { + "name": "draw_face" + }, + "link": 1708 + }, + { + "localized_name": "draw_feet", + "name": "draw_feet", + "type": "BOOLEAN", + "widget": { + "name": "draw_feet" + }, + "link": 1709 + }, + { + "localized_name": "stick_width", + "name": "stick_width", + "type": "INT", + "widget": { + "name": "stick_width" + }, + "link": 1710 + }, + { + "localized_name": "face_point_size", + "name": "face_point_size", + "type": "INT", + "widget": { + "name": "face_point_size" + }, + "link": 1711 + }, + { + "localized_name": "score_threshold", + "name": "score_threshold", + "type": "FLOAT", + "widget": { + "name": "score_threshold" + }, + "link": 1712 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 1701 + ] + } + ], + "properties": { + "Node name for S&R": "SDPoseDrawKeypoints", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + true, + true, + true, + true, + 4, + 2, + 0.5 + ] + }, + { + "id": 673, + "type": "CheckpointLoaderSimple", + "pos": [ + -3040, + 3080 + ], + "size": [ + 390, + 190 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 1721 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 1696 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 1697 + ] + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "models": [ + { + "name": "sdpose_wholebody_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/SDPose/resolve/main/checkpoints/sdpose_wholebody_fp16.safetensors", + "directory": "checkpoints" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "sdpose_wholebody_fp16.safetensors" + ] + }, + { + "id": 677, + "type": "UNETLoader", + "pos": [ + -3030, + 3330 + ], + "size": [ + 370, + 140 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 1722 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 1715 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.14.1", + "models": [ + { + "name": "rt_detr_v4-x-hgnet_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/SDPose/resolve/main/diffusion_models/rt_detr_v4-x-hgnet_fp16.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "rt_detr_v4-x-hgnet_fp16.safetensors", + "default" + ] + }, + { + "id": 678, + "type": "RTDETR_detect", + "pos": [ + -2540, + 3320 + ], + "size": [ + 270, + 200 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "label": "model", + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 1715 + }, + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 1716 + }, + { + "localized_name": "threshold", + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": 1718 + }, + { + "localized_name": "class_name", + "name": "class_name", + "type": "COMBO", + "widget": { + "name": "class_name" + }, + "link": 1719 + }, + { + "localized_name": "max_detections", + "name": "max_detections", + "type": "INT", + "widget": { + "name": "max_detections" + }, + "link": 1720 + } + ], + "outputs": [ + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [ + 1717, + 1726 + ] + } + ], + "properties": { + "Node name for S&R": "RTDETR_detect", + "cnr_id": "comfy-core", + "ver": "0.15.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0.5, + "person", + 1 + ] + } + ], + "groups": [], + "links": [ + { + "id": 1696, + "origin_id": 673, + "origin_slot": 0, + "target_id": 671, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 1697, + "origin_id": 673, + "origin_slot": 2, + "target_id": 671, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 1698, + "origin_id": 674, + "origin_slot": 0, + "target_id": 671, + "target_slot": 2, + "type": "IMAGE" + }, + { + "id": 1699, + "origin_id": 671, + "origin_slot": 0, + "target_id": 672, + "target_slot": 0, + "type": "POSE_KEYPOINT" + }, + { + "id": 1700, + "origin_id": -10, + "origin_slot": 0, + "target_id": 674, + "target_slot": 0, + "type": "IMAGE,MASK" + }, + { + "id": 1701, + "origin_id": 672, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 1704, + "origin_id": -10, + "origin_slot": 1, + "target_id": 674, + "target_slot": 2, + "type": "INT" + }, + { + "id": 1705, + "origin_id": -10, + "origin_slot": 2, + "target_id": 674, + "target_slot": 3, + "type": "COMBO" + }, + { + "id": 1706, + "origin_id": -10, + "origin_slot": 3, + "target_id": 672, + "target_slot": 1, + "type": "BOOLEAN" + }, + { + "id": 1707, + "origin_id": -10, + "origin_slot": 4, + "target_id": 672, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 1708, + "origin_id": -10, + "origin_slot": 5, + "target_id": 672, + "target_slot": 3, + "type": "BOOLEAN" + }, + { + "id": 1709, + "origin_id": -10, + "origin_slot": 6, + "target_id": 672, + "target_slot": 4, + "type": "BOOLEAN" + }, + { + "id": 1710, + "origin_id": -10, + "origin_slot": 7, + "target_id": 672, + "target_slot": 5, + "type": "INT" + }, + { + "id": 1711, + "origin_id": -10, + "origin_slot": 8, + "target_id": 672, + "target_slot": 6, + "type": "INT" + }, + { + "id": 1712, + "origin_id": -10, + "origin_slot": 9, + "target_id": 672, + "target_slot": 7, + "type": "FLOAT" + }, + { + "id": 1715, + "origin_id": 677, + "origin_slot": 0, + "target_id": 678, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 1716, + "origin_id": 674, + "origin_slot": 0, + "target_id": 678, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 1717, + "origin_id": 678, + "origin_slot": 0, + "target_id": 671, + "target_slot": 3, + "type": "BOUNDING_BOX" + }, + { + "id": 1718, + "origin_id": -10, + "origin_slot": 10, + "target_id": 678, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 1719, + "origin_id": -10, + "origin_slot": 11, + "target_id": 678, + "target_slot": 3, + "type": "COMBO" + }, + { + "id": 1720, + "origin_id": -10, + "origin_slot": 12, + "target_id": 678, + "target_slot": 4, + "type": "INT" + }, + { + "id": 1721, + "origin_id": -10, + "origin_slot": 13, + "target_id": 673, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 1722, + "origin_id": -10, + "origin_slot": 14, + "target_id": 677, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 1725, + "origin_id": 671, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "POSE_KEYPOINT" + }, + { + "id": 1726, + "origin_id": 678, + "origin_slot": 0, + "target_id": -20, + "target_slot": 2, + "type": "BOUNDING_BOX" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Conditioning & Preprocessors/Pose", + "description": "Detects multiple people in an image and outputs per-person pose keypoints, skeleton renders, and bounding boxes using SDPose." + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Image to Pose Map (SDPose-OOD).json b/blueprints/Image to Pose Map (SDPose-OOD).json new file mode 100644 index 000000000..76ee9ff4e --- /dev/null +++ b/blueprints/Image to Pose Map (SDPose-OOD).json @@ -0,0 +1,888 @@ +{ + "revision": 0, + "last_node_id": 675, + "last_link_id": 0, + "nodes": [ + { + "id": 675, + "type": "01b6a731-fb78-4070-9a38-c87146da9604", + "pos": [ + -2480, + 3400 + ], + "size": [ + 360, + 433.3125 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": null + }, + { + "label": "resize_target_longer_size", + "name": "resize_type.longer_size", + "type": "INT", + "widget": { + "name": "resize_type.longer_size" + }, + "link": null + }, + { + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": null + }, + { + "name": "draw_body", + "type": "BOOLEAN", + "widget": { + "name": "draw_body" + }, + "link": null + }, + { + "name": "draw_hands", + "type": "BOOLEAN", + "widget": { + "name": "draw_hands" + }, + "link": null + }, + { + "name": "draw_face", + "type": "BOOLEAN", + "widget": { + "name": "draw_face" + }, + "link": null + }, + { + "name": "draw_feet", + "type": "BOOLEAN", + "widget": { + "name": "draw_feet" + }, + "link": null + }, + { + "name": "stick_width", + "type": "INT", + "widget": { + "name": "stick_width" + }, + "link": null + }, + { + "name": "face_point_size", + "type": "INT", + "widget": { + "name": "face_point_size" + }, + "link": null + }, + { + "name": "score_threshold", + "type": "FLOAT", + "widget": { + "name": "score_threshold" + }, + "link": null + }, + { + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": null + }, + { + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + }, + { + "name": "keypoints", + "type": "POSE_KEYPOINT", + "links": null + } + ], + "properties": { + "proxyWidgets": [ + [ + "674", + "resize_type.longer_size" + ], + [ + "674", + "scale_method" + ], + [ + "672", + "draw_body" + ], + [ + "672", + "draw_hands" + ], + [ + "672", + "draw_face" + ], + [ + "672", + "draw_feet" + ], + [ + "672", + "stick_width" + ], + [ + "672", + "face_point_size" + ], + [ + "672", + "score_threshold" + ], + [ + "673", + "ckpt_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "title": "Image to Pose Map (SDPose-OOD)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "01b6a731-fb78-4070-9a38-c87146da9604", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 676, + "lastLinkId": 1715, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image to Pose Map (SDPose-OOD)", + "inputNode": { + "id": -10, + "bounding": [ + -3290, + 3590, + 190.8984375, + 288 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -1756.2451602089645, + 3366, + 128, + 88 + ] + }, + "inputs": [ + { + "id": "e24699c3-1356-4634-9eb4-19bb58e5c0b0", + "name": "input", + "type": "IMAGE,MASK", + "linkIds": [ + 1700 + ], + "localized_name": "input", + "pos": [ + -3123.1015625, + 3614 + ] + }, + { + "id": "088eefc1-cd8a-4573-993f-9e4da008a12d", + "name": "resize_type.longer_size", + "type": "INT", + "linkIds": [ + 1704 + ], + "label": "resize_target_longer_size", + "pos": [ + -3123.1015625, + 3634 + ] + }, + { + "id": "b6449bd3-73d4-41c8-b81f-cf8d33f76a2e", + "name": "scale_method", + "type": "COMBO", + "linkIds": [ + 1705 + ], + "pos": [ + -3123.1015625, + 3654 + ] + }, + { + "id": "4cff52ad-ed07-4c97-8803-fcbd89554fd0", + "name": "draw_body", + "type": "BOOLEAN", + "linkIds": [ + 1706 + ], + "pos": [ + -3123.1015625, + 3674 + ] + }, + { + "id": "7af63dce-f7df-4d7e-8215-d7c7f60bf81c", + "name": "draw_hands", + "type": "BOOLEAN", + "linkIds": [ + 1707 + ], + "pos": [ + -3123.1015625, + 3694 + ] + }, + { + "id": "af3a9bce-61f9-4aca-b530-9f65e028b35e", + "name": "draw_face", + "type": "BOOLEAN", + "linkIds": [ + 1708 + ], + "pos": [ + -3123.1015625, + 3714 + ] + }, + { + "id": "4620f6a3-2c85-4b79-ad8f-35d0326b568f", + "name": "draw_feet", + "type": "BOOLEAN", + "linkIds": [ + 1709 + ], + "pos": [ + -3123.1015625, + 3734 + ] + }, + { + "id": "fee5d0c9-8d4b-4934-81d8-ba2206dc56cb", + "name": "stick_width", + "type": "INT", + "linkIds": [ + 1710 + ], + "pos": [ + -3123.1015625, + 3754 + ] + }, + { + "id": "aafdd060-ba81-4324-a9cc-b656e1ebc133", + "name": "face_point_size", + "type": "INT", + "linkIds": [ + 1711 + ], + "pos": [ + -3123.1015625, + 3774 + ] + }, + { + "id": "514c5503-f9e6-4d23-b1ae-1d3291acb2a3", + "name": "score_threshold", + "type": "FLOAT", + "linkIds": [ + 1712 + ], + "pos": [ + -3123.1015625, + 3794 + ] + }, + { + "id": "ae46de61-2cc6-483e-8ee9-87e4144a2ffa", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 1713 + ], + "pos": [ + -3123.1015625, + 3814 + ] + }, + { + "id": "41bec0c6-dffa-4c78-9289-ee678715ae54", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 1714 + ], + "pos": [ + -3123.1015625, + 3834 + ] + } + ], + "outputs": [ + { + "id": "f05ed8cc-9403-4f14-8085-4364b06f8a48", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 1701 + ], + "localized_name": "IMAGE", + "pos": [ + -1732.2451602089645, + 3390 + ] + }, + { + "id": "29a6584e-4685-4986-8ffd-e6d8539953fd", + "name": "keypoints", + "type": "POSE_KEYPOINT", + "linkIds": [ + 1715 + ], + "pos": [ + -1732.2451602089645, + 3410 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 671, + "type": "SDPoseKeypointExtractor", + "pos": [ + -2470, + 3250 + ], + "size": [ + 270, + 180 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 1696 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 1697 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 1698 + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": 1714 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "keypoints", + "name": "keypoints", + "type": "POSE_KEYPOINT", + "links": [ + 1699, + 1715 + ] + } + ], + "properties": { + "Node name for S&R": "SDPoseKeypointExtractor", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 16 + ] + }, + { + "id": 674, + "type": "ResizeImageMaskNode", + "pos": [ + -2960, + 3490 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": 1700 + }, + { + "localized_name": "resize_type", + "name": "resize_type", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "resize_type" + }, + "link": null + }, + { + "localized_name": "resize_type.longer_size", + "name": "resize_type.longer_size", + "type": "INT", + "widget": { + "name": "resize_type.longer_size" + }, + "link": 1704 + }, + { + "localized_name": "scale_method", + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": 1705 + } + ], + "outputs": [ + { + "localized_name": "resized", + "name": "resized", + "type": "*", + "links": [ + 1698 + ] + } + ], + "properties": { + "Node name for S&R": "ResizeImageMaskNode", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "scale longer dimension", + 1024, + "area" + ] + }, + { + "id": 672, + "type": "SDPoseDrawKeypoints", + "pos": [ + -2120, + 3260 + ], + "size": [ + 270, + 280 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "keypoints", + "name": "keypoints", + "type": "POSE_KEYPOINT", + "link": 1699 + }, + { + "localized_name": "draw_body", + "name": "draw_body", + "type": "BOOLEAN", + "widget": { + "name": "draw_body" + }, + "link": 1706 + }, + { + "localized_name": "draw_hands", + "name": "draw_hands", + "type": "BOOLEAN", + "widget": { + "name": "draw_hands" + }, + "link": 1707 + }, + { + "localized_name": "draw_face", + "name": "draw_face", + "type": "BOOLEAN", + "widget": { + "name": "draw_face" + }, + "link": 1708 + }, + { + "localized_name": "draw_feet", + "name": "draw_feet", + "type": "BOOLEAN", + "widget": { + "name": "draw_feet" + }, + "link": 1709 + }, + { + "localized_name": "stick_width", + "name": "stick_width", + "type": "INT", + "widget": { + "name": "stick_width" + }, + "link": 1710 + }, + { + "localized_name": "face_point_size", + "name": "face_point_size", + "type": "INT", + "widget": { + "name": "face_point_size" + }, + "link": 1711 + }, + { + "localized_name": "score_threshold", + "name": "score_threshold", + "type": "FLOAT", + "widget": { + "name": "score_threshold" + }, + "link": 1712 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 1701 + ] + } + ], + "properties": { + "Node name for S&R": "SDPoseDrawKeypoints", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + true, + true, + true, + true, + 4, + 2, + 0.5 + ] + }, + { + "id": 673, + "type": "CheckpointLoaderSimple", + "pos": [ + -2960, + 3250 + ], + "size": [ + 390, + 190 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 1713 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 1696 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 1697 + ] + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "models": [ + { + "name": "sdpose_wholebody_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/SDPose/resolve/main/checkpoints/sdpose_wholebody_fp16.safetensors", + "directory": "checkpoints" + } + ], + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sdpose_wholebody_fp16.safetensors" + ] + } + ], + "groups": [], + "links": [ + { + "id": 1696, + "origin_id": 673, + "origin_slot": 0, + "target_id": 671, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 1697, + "origin_id": 673, + "origin_slot": 2, + "target_id": 671, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 1698, + "origin_id": 674, + "origin_slot": 0, + "target_id": 671, + "target_slot": 2, + "type": "IMAGE" + }, + { + "id": 1699, + "origin_id": 671, + "origin_slot": 0, + "target_id": 672, + "target_slot": 0, + "type": "POSE_KEYPOINT" + }, + { + "id": 1700, + "origin_id": -10, + "origin_slot": 0, + "target_id": 674, + "target_slot": 0, + "type": "IMAGE,MASK" + }, + { + "id": 1701, + "origin_id": 672, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 1704, + "origin_id": -10, + "origin_slot": 1, + "target_id": 674, + "target_slot": 2, + "type": "INT" + }, + { + "id": 1705, + "origin_id": -10, + "origin_slot": 2, + "target_id": 674, + "target_slot": 3, + "type": "COMBO" + }, + { + "id": 1706, + "origin_id": -10, + "origin_slot": 3, + "target_id": 672, + "target_slot": 1, + "type": "BOOLEAN" + }, + { + "id": 1707, + "origin_id": -10, + "origin_slot": 4, + "target_id": 672, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 1708, + "origin_id": -10, + "origin_slot": 5, + "target_id": 672, + "target_slot": 3, + "type": "BOOLEAN" + }, + { + "id": 1709, + "origin_id": -10, + "origin_slot": 6, + "target_id": 672, + "target_slot": 4, + "type": "BOOLEAN" + }, + { + "id": 1710, + "origin_id": -10, + "origin_slot": 7, + "target_id": 672, + "target_slot": 5, + "type": "INT" + }, + { + "id": 1711, + "origin_id": -10, + "origin_slot": 8, + "target_id": 672, + "target_slot": 6, + "type": "INT" + }, + { + "id": 1712, + "origin_id": -10, + "origin_slot": 9, + "target_id": 672, + "target_slot": 7, + "type": "FLOAT" + }, + { + "id": 1713, + "origin_id": -10, + "origin_slot": 10, + "target_id": 673, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 1714, + "origin_id": -10, + "origin_slot": 11, + "target_id": 671, + "target_slot": 3, + "type": "BOUNDING_BOX" + }, + { + "id": 1715, + "origin_id": 671, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "POSE_KEYPOINT" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Conditioning & Preprocessors/Pose", + "description": "Extracts human pose keypoints and stick-figure visuals from an image using SDPose-OOD, with optional bounding-box input per subject." + } + ] + }, + "extra": { + "ue_links": [] + } +} \ No newline at end of file diff --git a/blueprints/Image to Video (LTX-2.3).json b/blueprints/Image to Video (LTX-2.3).json index 86a601130..3db524ea0 100644 --- a/blueprints/Image to Video (LTX-2.3).json +++ b/blueprints/Image to Video (LTX-2.3).json @@ -4223,7 +4223,8 @@ "extra": { "workflowRendererVersion": "Vue-corrected" }, - "category": "Video generation and editing/Image to video" + "category": "Video generation and editing/Image to video", + "description": "Generates video from a single input image using LTX-2.3." } ] }, diff --git a/blueprints/Image to Video (Wan 2.2).json b/blueprints/Image to Video (Wan 2.2).json index a8dafd3c9..a24adcfb6 100644 --- a/blueprints/Image to Video (Wan 2.2).json +++ b/blueprints/Image to Video (Wan 2.2).json @@ -206,7 +206,7 @@ }, "revision": 0, "config": {}, - "name": "local-Image to Video (Wan 2.2)", + "name": "Image to Video (Wan 2.2)", "inputNode": { "id": -10, "bounding": [ @@ -2027,7 +2027,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Image to video" + "category": "Video generation and editing/Image to video", + "description": "Image-to-video with Wan 2.2 using a start image plus text prompt to extend motion from the still frame." } ] }, diff --git a/blueprints/Merge Videos.json b/blueprints/Merge Videos.json new file mode 100644 index 000000000..689e6ec16 --- /dev/null +++ b/blueprints/Merge Videos.json @@ -0,0 +1,1219 @@ +{ + "revision": 0, + "last_node_id": 26, + "last_link_id": 0, + "nodes": [ + { + "id": 26, + "type": "32e6dbcc-e2d7-45c0-a245-fc74b8271dfb", + "pos": [ + -980, + 480 + ], + "size": [ + 290, + 190 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "label": "base_video", + "localized_name": "clip_to_resize", + "name": "clip_to_resize", + "type": "VIDEO", + "link": null + }, + { + "label": "second_video", + "localized_name": "base_video", + "name": "base_video", + "type": "VIDEO", + "link": null + }, + { + "label": "pad_second_video", + "localized_name": "pad_second_video", + "name": "pad_second_video", + "type": "BOOLEAN", + "widget": { + "name": "pad_second_video" + }, + "link": null + }, + { + "name": "interpolation", + "type": "COMBO", + "widget": { + "name": "interpolation" + }, + "link": null + }, + { + "name": "padding_color", + "type": "COMBO", + "widget": { + "name": "padding_color" + }, + "link": null + }, + { + "label": "drop_audio", + "localized_name": "drop_audio", + "name": "drop_audio", + "type": "BOOLEAN", + "widget": { + "name": "drop_audio" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "merged_video", + "name": "merged_video", + "type": "VIDEO", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "28", + "value" + ], + [ + "6", + "interpolation" + ], + [ + "6", + "padding_color" + ], + [ + "11", + "value" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Merge Videos" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "32e6dbcc-e2d7-45c0-a245-fc74b8271dfb", + "version": 1, + "state": { + "lastGroupId": 2, + "lastNodeId": 34, + "lastLinkId": 75, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Merge Videos", + "inputNode": { + "id": -10, + "bounding": [ + -1990, + 700, + 152.5546875, + 168 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1210, + 614, + 128, + 68 + ] + }, + "inputs": [ + { + "id": "2fb09e41-c5fa-4654-b9d2-569b59626ec4", + "name": "clip_to_resize", + "type": "VIDEO", + "linkIds": [ + 50 + ], + "localized_name": "clip_to_resize", + "label": "base_video", + "pos": [ + -1861.4453125, + 724 + ] + }, + { + "id": "017f8d09-7900-4dc9-b95c-0cab31bcde7d", + "name": "base_video", + "type": "VIDEO", + "linkIds": [ + 51 + ], + "localized_name": "base_video", + "label": "second_video", + "pos": [ + -1861.4453125, + 744 + ] + }, + { + "id": "a39894ce-1785-4037-b39c-b40d2e470c43", + "name": "pad_second_video", + "type": "BOOLEAN", + "linkIds": [ + 59 + ], + "localized_name": "pad_second_video", + "label": "pad_second_video", + "pos": [ + -1861.4453125, + 764 + ] + }, + { + "id": "b4fb86cb-8d87-4193-8533-88a57df50e18", + "name": "interpolation", + "type": "COMBO", + "linkIds": [ + 60 + ], + "pos": [ + -1861.4453125, + 784 + ] + }, + { + "id": "2413a2e2-cfdc-4d1d-9e2e-81e7acdf35e3", + "name": "padding_color", + "type": "COMBO", + "linkIds": [ + 62 + ], + "pos": [ + -1861.4453125, + 804 + ] + }, + { + "id": "338b1e09-0efb-424f-949b-e730a0aa8527", + "name": "drop_audio", + "type": "BOOLEAN", + "linkIds": [ + 63 + ], + "localized_name": "drop_audio", + "label": "drop_audio", + "pos": [ + -1861.4453125, + 824 + ] + } + ], + "outputs": [ + { + "id": "be99efc6-7fb3-4059-93d0-136dc8cc8faf", + "name": "merged_video", + "type": "VIDEO", + "linkIds": [ + 16 + ], + "localized_name": "merged_video", + "pos": [ + 1234, + 638 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 11, + "type": "PrimitiveBoolean", + "pos": [ + -990, + 1230 + ], + "size": [ + 270, + 80 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 63 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 14 + ] + } + ], + "properties": { + "Node name for S&R": "PrimitiveBoolean", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 10, + "type": "EmptyAudio", + "pos": [ + -990, + 1060 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "duration", + "name": "duration", + "type": "FLOAT", + "widget": { + "name": "duration" + }, + "link": null + }, + { + "localized_name": "sample_rate", + "name": "sample_rate", + "type": "INT", + "widget": { + "name": "sample_rate" + }, + "link": null + }, + { + "localized_name": "channels", + "name": "channels", + "type": "INT", + "widget": { + "name": "channels" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "AUDIO", + "name": "AUDIO", + "type": "AUDIO", + "links": [ + 22 + ] + } + ], + "properties": { + "Node name for S&R": "EmptyAudio", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 60, + 44100, + 2 + ] + }, + { + "id": 3, + "type": "ComfySwitchNode", + "pos": [ + -370, + 1010 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 21 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 22 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 14 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 12 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 6, + "type": "ResizeAndPadImage", + "pos": [ + -400, + 440 + ], + "size": [ + 270, + 210 + ], + "flags": {}, + "order": 4, + "mode": 0, + "showAdvanced": true, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 39 + }, + { + "localized_name": "target_width", + "name": "target_width", + "type": "INT", + "widget": { + "name": "target_width" + }, + "link": 4 + }, + { + "localized_name": "target_height", + "name": "target_height", + "type": "INT", + "widget": { + "name": "target_height" + }, + "link": 5 + }, + { + "localized_name": "padding_color", + "name": "padding_color", + "type": "COMBO", + "widget": { + "name": "padding_color" + }, + "link": 62 + }, + { + "localized_name": "interpolation", + "name": "interpolation", + "type": "COMBO", + "widget": { + "name": "interpolation" + }, + "link": 60 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 75 + ] + } + ], + "properties": { + "Node name for S&R": "ResizeAndPadImage", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 512, + 512, + "white", + "lanczos" + ] + }, + { + "id": 8, + "type": "CreateVideo", + "pos": [ + 880, + 280 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 19 + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 12 + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 15 + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [ + 16 + ] + } + ], + "properties": { + "Node name for S&R": "CreateVideo", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 30 + ] + }, + { + "id": 9, + "type": "AudioMerge", + "pos": [ + -990, + 890 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "audio1", + "name": "audio1", + "type": "AUDIO", + "link": 9 + }, + { + "localized_name": "audio2", + "name": "audio2", + "type": "AUDIO", + "link": 10 + }, + { + "localized_name": "merge_method", + "name": "merge_method", + "type": "COMBO", + "widget": { + "name": "merge_method" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "AUDIO", + "name": "AUDIO", + "type": "AUDIO", + "links": [ + 21 + ] + } + ], + "properties": { + "Node name for S&R": "AudioMerge", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "add" + ] + }, + { + "id": 2, + "type": "GetVideoComponents", + "pos": [ + -1590, + 460 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 51 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 39, + 54 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": [ + 9 + ] + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 27, + "type": "ComfySwitchNode", + "pos": [ + 60, + 70 + ], + "size": [ + 280, + 130 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 54 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 75 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 56 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 55 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 1, + "type": "GetVideoComponents", + "pos": [ + -1600, + 30 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 50 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 3, + 17 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": [ + 10 + ] + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": [ + 15 + ] + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 7, + "type": "GetImageSize", + "pos": [ + -1000, + 480 + ], + "size": [ + 260, + 110 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 3 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 4 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": [ + 5 + ] + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetImageSize", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 28, + "type": "PrimitiveBoolean", + "pos": [ + -1590, + 190 + ], + "size": [ + 270, + 80 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 59 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 56 + ] + } + ], + "properties": { + "Node name for S&R": "PrimitiveBoolean", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 13, + "type": "BatchImagesNode", + "pos": [ + 530, + 10 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "label": "image0", + "localized_name": "images.image0", + "name": "images.image0", + "type": "IMAGE", + "link": 17 + }, + { + "label": "image1", + "localized_name": "images.image1", + "name": "images.image1", + "shape": 7, + "type": "IMAGE", + "link": 55 + }, + { + "label": "image2", + "localized_name": "images.image2", + "name": "images.image2", + "shape": 7, + "type": "IMAGE", + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 19 + ] + } + ], + "properties": { + "Node name for S&R": "BatchImagesNode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + } + ], + "groups": [ + { + "id": 1, + "title": "Audio", + "bounding": [ + -1000, + 820, + 915, + 496 + ], + "color": "#3f789e", + "flags": {} + } + ], + "links": [ + { + "id": 21, + "origin_id": 9, + "origin_slot": 0, + "target_id": 3, + "target_slot": 0, + "type": "AUDIO" + }, + { + "id": 22, + "origin_id": 10, + "origin_slot": 0, + "target_id": 3, + "target_slot": 1, + "type": "AUDIO" + }, + { + "id": 14, + "origin_id": 11, + "origin_slot": 0, + "target_id": 3, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 9, + "origin_id": 2, + "origin_slot": 1, + "target_id": 9, + "target_slot": 0, + "type": "AUDIO" + }, + { + "id": 10, + "origin_id": 1, + "origin_slot": 1, + "target_id": 9, + "target_slot": 1, + "type": "AUDIO" + }, + { + "id": 39, + "origin_id": 2, + "origin_slot": 0, + "target_id": 6, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 4, + "origin_id": 7, + "origin_slot": 0, + "target_id": 6, + "target_slot": 1, + "type": "INT" + }, + { + "id": 5, + "origin_id": 7, + "origin_slot": 1, + "target_id": 6, + "target_slot": 2, + "type": "INT" + }, + { + "id": 3, + "origin_id": 1, + "origin_slot": 0, + "target_id": 7, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 17, + "origin_id": 1, + "origin_slot": 0, + "target_id": 13, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 19, + "origin_id": 13, + "origin_slot": 0, + "target_id": 8, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 12, + "origin_id": 3, + "origin_slot": 0, + "target_id": 8, + "target_slot": 1, + "type": "AUDIO" + }, + { + "id": 15, + "origin_id": 1, + "origin_slot": 2, + "target_id": 8, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 16, + "origin_id": 8, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 50, + "origin_id": -10, + "origin_slot": 0, + "target_id": 1, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 51, + "origin_id": -10, + "origin_slot": 1, + "target_id": 2, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 54, + "origin_id": 2, + "origin_slot": 0, + "target_id": 27, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 55, + "origin_id": 27, + "origin_slot": 0, + "target_id": 13, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 56, + "origin_id": 28, + "origin_slot": 0, + "target_id": 27, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 59, + "origin_id": -10, + "origin_slot": 2, + "target_id": 28, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 60, + "origin_id": -10, + "origin_slot": 3, + "target_id": 6, + "target_slot": 4, + "type": "COMBO" + }, + { + "id": 62, + "origin_id": -10, + "origin_slot": 4, + "target_id": 6, + "target_slot": 3, + "type": "COMBO" + }, + { + "id": 63, + "origin_id": -10, + "origin_slot": 5, + "target_id": 11, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 75, + "origin_id": 6, + "origin_slot": 0, + "target_id": 27, + "target_slot": 1, + "type": "IMAGE" + } + ], + "extra": {}, + "category": "Video Tools", + "description": "Concatenates two videos end-to-end with optional resize, letterbox padding, and audio merge or drop." + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Pose to Image (Z-Image-Turbo).json b/blueprints/Pose to Image (Z-Image-Turbo).json index a55410ba4..92ee80907 100644 --- a/blueprints/Pose to Image (Z-Image-Turbo).json +++ b/blueprints/Pose to Image (Z-Image-Turbo).json @@ -134,7 +134,7 @@ }, "revision": 0, "config": {}, - "name": "local-Pose to Image (Z-Image-Turbo)", + "name": "Pose to Image (Z-Image-Turbo)", "inputNode": { "id": -10, "bounding": [ @@ -1298,7 +1298,8 @@ "VHS_MetadataImage": true, "VHS_KeepIntermediate": true }, - "category": "Image generation and editing/Pose to image" + "category": "Image generation and editing/Conditioned", + "description": "Generates an image from pose keypoints using Z-Image-Turbo with text conditioning." } ] }, @@ -1319,4 +1320,4 @@ } }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Pose to Video (LTX 2.0).json b/blueprints/Pose to Video (LTX 2.0).json index 580900bc0..04eb69972 100644 --- a/blueprints/Pose to Video (LTX 2.0).json +++ b/blueprints/Pose to Video (LTX 2.0).json @@ -3870,7 +3870,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Pose to video" + "category": "Video generation and editing/Conditioned", + "description": "Generates video from pose reference frames using LTX-2, with optional synchronized audio." } ] }, diff --git a/blueprints/Prompt Enhance.json b/blueprints/Prompt Enhance.json index 5e57548ff..e3a77a73b 100644 --- a/blueprints/Prompt Enhance.json +++ b/blueprints/Prompt Enhance.json @@ -270,9 +270,10 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Text generation/Prompt enhance" + "category": "Text Tools", + "description": "Expands short text prompts into detailed descriptions using a text generation model for better generation quality." } ] }, "extra": {} -} +} \ No newline at end of file diff --git a/blueprints/Remove Background (BiRefNet).json b/blueprints/Remove Background (BiRefNet).json new file mode 100644 index 000000000..9ec441e51 --- /dev/null +++ b/blueprints/Remove Background (BiRefNet).json @@ -0,0 +1,397 @@ +{ + "revision": 0, + "last_node_id": 19, + "last_link_id": 0, + "nodes": [ + { + "id": 19, + "type": "5b40ca21-ba1a-41d5-b403-4d2d7acdc195", + "pos": [ + -6411.330578108367, + 1940.2638932730042 + ], + "size": [ + 349.609375, + 145.9375 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": null + }, + { + "name": "bg_removal_name", + "type": "COMBO", + "widget": { + "name": "bg_removal_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + }, + { + "name": "mask", + "type": "MASK", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "14", + "bg_removal_name" + ] + ] + }, + "widgets_values": [], + "title": "Remove Background (BiRefNet)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "5b40ca21-ba1a-41d5-b403-4d2d7acdc195", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 21, + "lastLinkId": 16, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Remove Background (BiRefNet)", + "description": "Removes or replaces image backgrounds using BiRefNet segmentation and alpha compositing.", + "inputNode": { + "id": -10, + "bounding": [ + -6728.534070722246, + 1475.2619799128663, + 150.9140625, + 88 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -6169.049695722246, + 1475.2619799128663, + 128, + 88 + ] + }, + "inputs": [ + { + "id": "7bc321cd-df31-4c39-aaf7-7f0d01326189", + "name": "image", + "type": "IMAGE", + "linkIds": [ + 5, + 7 + ], + "localized_name": "image", + "pos": [ + -6601.620008222246, + 1499.2619799128663 + ] + }, + { + "id": "e89d2cd8-daa3-4e29-8a69-851db85072cb", + "name": "bg_removal_name", + "type": "COMBO", + "linkIds": [ + 12 + ], + "pos": [ + -6601.620008222246, + 1519.2619799128663 + ] + } + ], + "outputs": [ + { + "id": "16e7863c-4c38-46c2-aa74-e82991fbfe8d", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 8 + ], + "localized_name": "IMAGE", + "pos": [ + -6145.049695722246, + 1499.2619799128663 + ] + }, + { + "id": "f7240c19-5b80-406e-a8e2-9b12440ee2d6", + "name": "mask", + "type": "MASK", + "linkIds": [ + 11 + ], + "pos": [ + -6145.049695722246, + 1519.2619799128663 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 13, + "type": "RemoveBackground", + "pos": [ + -6536.764823982709, + 1444.9963409012412 + ], + "size": [ + 302.25, + 72 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 5 + }, + { + "localized_name": "bg_removal_model", + "name": "bg_removal_model", + "type": "BACKGROUND_REMOVAL", + "link": 3 + } + ], + "outputs": [ + { + "localized_name": "mask", + "name": "mask", + "type": "MASK", + "links": [ + 4, + 11 + ] + } + ], + "properties": { + "Node name for S&R": "RemoveBackground" + } + }, + { + "id": 14, + "type": "LoadBackgroundRemovalModel", + "pos": [ + -6540.534070722246, + 1302.223464635445 + ], + "size": [ + 311.484375, + 85.515625 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "bg_removal_name", + "name": "bg_removal_name", + "type": "COMBO", + "widget": { + "name": "bg_removal_name" + }, + "link": 12 + } + ], + "outputs": [ + { + "localized_name": "bg_model", + "name": "bg_model", + "type": "BACKGROUND_REMOVAL", + "links": [ + 3 + ] + } + ], + "properties": { + "Node name for S&R": "LoadBackgroundRemovalModel", + "models": [ + { + "name": "birefnet.safetensors", + "url": "https://huggingface.co/Comfy-Org/BiRefNet/resolve/main/background_removal/birefnet.safetensors", + "directory": "background_removal" + } + ] + }, + "widgets_values": [ + "birefnet.safetensors" + ] + }, + { + "id": 15, + "type": "InvertMask", + "pos": [ + -6532.446160529669, + 1571.1111286839914 + ], + "size": [ + 285.984375, + 48 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "mask", + "name": "mask", + "type": "MASK", + "link": 4 + } + ], + "outputs": [ + { + "localized_name": "MASK", + "name": "MASK", + "type": "MASK", + "links": [ + 6 + ] + } + ], + "properties": { + "Node name for S&R": "InvertMask" + } + }, + { + "id": 16, + "type": "JoinImageWithAlpha", + "pos": [ + -6527.4370171636665, + 1674.3004951902876 + ], + "size": [ + 284.96875, + 72 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 7 + }, + { + "localized_name": "alpha", + "name": "alpha", + "type": "MASK", + "link": 6 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 8 + ] + } + ], + "properties": { + "Node name for S&R": "JoinImageWithAlpha" + } + } + ], + "groups": [], + "links": [ + { + "id": 3, + "origin_id": 14, + "origin_slot": 0, + "target_id": 13, + "target_slot": 1, + "type": "BACKGROUND_REMOVAL" + }, + { + "id": 4, + "origin_id": 13, + "origin_slot": 0, + "target_id": 15, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 6, + "origin_id": 15, + "origin_slot": 0, + "target_id": 16, + "target_slot": 1, + "type": "MASK" + }, + { + "id": 5, + "origin_id": -10, + "origin_slot": 0, + "target_id": 13, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 7, + "origin_id": -10, + "origin_slot": 0, + "target_id": 16, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 8, + "origin_id": 16, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 11, + "origin_id": 13, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "MASK" + }, + { + "id": 12, + "origin_id": -10, + "origin_slot": 1, + "target_id": 14, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": {}, + "category": "Image Tools/Background Removal" + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Select Per-Line Text by Index.json b/blueprints/Select Per-Line Text by Index.json new file mode 100644 index 000000000..8a4020d50 --- /dev/null +++ b/blueprints/Select Per-Line Text by Index.json @@ -0,0 +1,485 @@ +{ + "revision": 0, + "last_node_id": 10, + "last_link_id": 0, + "nodes": [ + { + "id": 10, + "type": "3fb7557a-470d-4983-9d8c-6d5caa9788f0", + "pos": [ + -250, + 8590 + ], + "size": [ + 280, + 360 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "text_per_line", + "name": "text_per_line", + "type": "STRING", + "widget": { + "name": "text_per_line" + }, + "link": null + }, + { + "localized_name": "index", + "name": "index", + "type": "INT", + "widget": { + "name": "index" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "selected_line", + "name": "selected_line", + "type": "STRING", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "2", + "string" + ], + [ + "3", + "value" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.19.0", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "title": "Select Per-Line Text by Index" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "3fb7557a-470d-4983-9d8c-6d5caa9788f0", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 10, + "lastLinkId": 14, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Select Per-Line Text by Index", + "inputNode": { + "id": -10, + "bounding": [ + -990, + 8595, + 128, + 88 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 710, + 8585, + 128, + 68 + ] + }, + "inputs": [ + { + "id": "75417d82-a934-4ac9-b667-d8dcd5a3bfb3", + "name": "text_per_line", + "type": "STRING", + "linkIds": [ + 13 + ], + "localized_name": "text_per_line", + "pos": [ + -886, + 8619 + ] + }, + { + "id": "46e69a73-1804-4ca6-9175-31445bf0be96", + "name": "index", + "type": "INT", + "linkIds": [ + 14 + ], + "localized_name": "index", + "pos": [ + -886, + 8639 + ] + } + ], + "outputs": [ + { + "id": "e34e8ad1-84d2-4bd2-a460-eb7de6067c10", + "name": "selected_line", + "type": "STRING", + "linkIds": [ + 10 + ], + "localized_name": "selected_line", + "pos": [ + 734, + 8609 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 1, + "type": "PreviewAny", + "pos": [ + -500, + 8400 + ], + "size": [ + 230, + 180 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 1 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 6 + ] + } + ], + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 2, + "type": "RegexExtract", + "pos": [ + -240, + 8740 + ], + "size": [ + 470, + 460 + ], + "flags": {}, + "order": 1, + "mode": 0, + "showAdvanced": false, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": 13 + }, + { + "localized_name": "regex_pattern", + "name": "regex_pattern", + "type": "STRING", + "widget": { + "name": "regex_pattern" + }, + "link": 9 + }, + { + "localized_name": "mode", + "name": "mode", + "type": "COMBO", + "widget": { + "name": "mode" + }, + "link": null + }, + { + "localized_name": "case_insensitive", + "name": "case_insensitive", + "type": "BOOLEAN", + "widget": { + "name": "case_insensitive" + }, + "link": null + }, + { + "localized_name": "multiline", + "name": "multiline", + "type": "BOOLEAN", + "widget": { + "name": "multiline" + }, + "link": null + }, + { + "localized_name": "dotall", + "name": "dotall", + "type": "BOOLEAN", + "widget": { + "name": "dotall" + }, + "link": null + }, + { + "localized_name": "group_index", + "name": "group_index", + "type": "INT", + "widget": { + "name": "group_index" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 10 + ] + } + ], + "properties": { + "Node name for S&R": "RegexExtract", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "", + "", + "First Group", + false, + false, + false, + 1 + ] + }, + { + "id": 3, + "type": "PrimitiveInt", + "pos": [ + -810, + 8400 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 14 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 1 + ] + } + ], + "title": "Int (line index)", + "properties": { + "Node name for S&R": "Int (line index)", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 0, + "fixed" + ] + }, + { + "id": 8, + "type": "StringReplace", + "pos": [ + -240, + 8400 + ], + "size": [ + 400, + 280 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": null + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 6 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 9 + ] + } + ], + "properties": { + "Node name for S&R": "StringReplace", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "^(?:[^\\n]*\\n){index}([^\\n]*)(?:\\n|$)", + "index", + "" + ] + } + ], + "groups": [], + "links": [ + { + "id": 1, + "origin_id": 3, + "origin_slot": 0, + "target_id": 1, + "target_slot": 0, + "type": "INT" + }, + { + "id": 9, + "origin_id": 8, + "origin_slot": 0, + "target_id": 2, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 6, + "origin_id": 1, + "origin_slot": 0, + "target_id": 8, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 10, + "origin_id": 2, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 13, + "origin_id": -10, + "origin_slot": 0, + "target_id": 2, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 14, + "origin_id": -10, + "origin_slot": 1, + "target_id": 3, + "target_slot": 0, + "type": "INT" + } + ], + "extra": {}, + "category": "Text Tools", + "description": "Selects one line from multiline text by zero-based index for batch or list-driven prompt workflows." + } + ] + }, + "extra": { + "ue_links": [], + "links_added_by_ue": [] + } +} \ No newline at end of file diff --git a/blueprints/Sharpen.json b/blueprints/Sharpen.json index f332400fd..3c4099c6b 100644 --- a/blueprints/Sharpen.json +++ b/blueprints/Sharpen.json @@ -302,8 +302,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Sharpen" + "category": "Image Tools/Sharpen", + "description": "Sharpens image details using a GPU fragment shader for enhanced clarity." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Split Image Grid to Tiles.json b/blueprints/Split Image Grid to Tiles.json new file mode 100644 index 000000000..d1f6e40ef --- /dev/null +++ b/blueprints/Split Image Grid to Tiles.json @@ -0,0 +1,714 @@ +{ + "revision": 0, + "last_node_id": 251, + "last_link_id": 0, + "nodes": [ + { + "id": 251, + "type": "609e1fd1-b731-4b78-89ac-d19b1156b025", + "pos": [ + -1490, + 130 + ], + "size": [ + 230, + 164 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "source_image", + "name": "source_image", + "type": "IMAGE", + "link": null + }, + { + "localized_name": "columns", + "name": "columns", + "type": "INT", + "widget": { + "name": "columns" + }, + "link": null + }, + { + "localized_name": "rows", + "name": "rows", + "type": "INT", + "widget": { + "name": "rows" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "tiles", + "name": "tiles", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "228", + "value" + ], + [ + "252", + "value" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.20.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Split Image Grid to Tiles" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "609e1fd1-b731-4b78-89ac-d19b1156b025", + "version": 1, + "state": { + "lastGroupId": 9, + "lastNodeId": 252, + "lastLinkId": 429, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Split Image Grid to Tiles", + "inputNode": { + "id": -10, + "bounding": [ + -1690, + 260, + 128, + 108 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -510, + 590, + 128, + 68 + ] + }, + "inputs": [ + { + "id": "866ac798-cfbc-450a-b755-e704f86404d9", + "name": "source_image", + "type": "IMAGE", + "linkIds": [ + 386, + 389 + ], + "localized_name": "source_image", + "pos": [ + -1586, + 284 + ] + }, + { + "id": "bc37b1f8-8ab2-4f19-bd00-75d4fbc4feb3", + "name": "columns", + "type": "INT", + "linkIds": [ + 427 + ], + "localized_name": "columns", + "pos": [ + -1586, + 304 + ] + }, + { + "id": "d45915da-e848-43dd-9ccc-e3161e9c99d9", + "name": "rows", + "type": "INT", + "linkIds": [ + 428 + ], + "localized_name": "rows", + "pos": [ + -1586, + 324 + ] + } + ], + "outputs": [ + { + "id": "18bc780f-064b-4038-87c6-67dba71deb08", + "name": "tiles", + "type": "IMAGE", + "linkIds": [ + 394 + ], + "localized_name": "tiles", + "shape": 6, + "pos": [ + -486, + 614 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 225, + "type": "SplitImageToTileList", + "pos": [ + -1010, + 620 + ], + "size": [ + 290, + 170 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 386 + }, + { + "localized_name": "tile_width", + "name": "tile_width", + "type": "INT", + "widget": { + "name": "tile_width" + }, + "link": 403 + }, + { + "localized_name": "tile_height", + "name": "tile_height", + "type": "INT", + "widget": { + "name": "tile_height" + }, + "link": 404 + }, + { + "localized_name": "overlap", + "name": "overlap", + "type": "INT", + "widget": { + "name": "overlap" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "shape": 6, + "type": "IMAGE", + "links": [ + 394 + ] + } + ], + "properties": { + "Node name for S&R": "SplitImageToTileList", + "cnr_id": "comfy-core", + "ver": "0.20.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1024, + 1024, + 0 + ] + }, + { + "id": 231, + "type": "ComfyMathExpression", + "pos": [ + -1080, + 330 + ], + "size": [ + 370, + 190 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT,BOOLEAN", + "link": 390 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": 429 + }, + { + "label": "c", + "localized_name": "values.c", + "name": "values.c", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 404 + ] + }, + { + "localized_name": "BOOL", + "name": "BOOL", + "type": "BOOLEAN", + "links": null + } + ], + "title": "Math Expression (Height)", + "properties": { + "Node name for S&R": "ComfyMathExpression", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "max(1, (int(a) + int(b) - 1) // int(b))" + ] + }, + { + "id": 229, + "type": "ComfyMathExpression", + "pos": [ + -1090, + -30 + ], + "size": [ + 370, + 190 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT,BOOLEAN", + "link": 387 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": 388 + }, + { + "label": "c", + "localized_name": "values.c", + "name": "values.c", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 403 + ] + }, + { + "localized_name": "BOOL", + "name": "BOOL", + "type": "BOOLEAN", + "links": null + } + ], + "title": "Math Expression (Width)", + "properties": { + "Node name for S&R": "ComfyMathExpression", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "max(1, (int(a) + int(b) - 1) // int(b))" + ] + }, + { + "id": 228, + "type": "PrimitiveInt", + "pos": [ + -1380, + 90 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 427 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 388 + ] + } + ], + "title": "Int (grid columns)", + "properties": { + "Node name for S&R": "Int (grid columns)", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 2, + "fixed" + ] + }, + { + "id": 230, + "type": "GetImageSize", + "pos": [ + -1380, + 290 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 389 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 387 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": [ + 390 + ] + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetImageSize", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + } + }, + { + "id": 252, + "type": "PrimitiveInt", + "pos": [ + -1380, + 470 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 428 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 429 + ] + } + ], + "title": "Int (grid rows)", + "properties": { + "Node name for S&R": "Int (grid rows)", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 3, + "fixed" + ] + } + ], + "groups": [], + "links": [ + { + "id": 403, + "origin_id": 229, + "origin_slot": 1, + "target_id": 225, + "target_slot": 1, + "type": "INT" + }, + { + "id": 404, + "origin_id": 231, + "origin_slot": 1, + "target_id": 225, + "target_slot": 2, + "type": "INT" + }, + { + "id": 390, + "origin_id": 230, + "origin_slot": 1, + "target_id": 231, + "target_slot": 0, + "type": "INT" + }, + { + "id": 387, + "origin_id": 230, + "origin_slot": 0, + "target_id": 229, + "target_slot": 0, + "type": "INT" + }, + { + "id": 388, + "origin_id": 228, + "origin_slot": 0, + "target_id": 229, + "target_slot": 1, + "type": "INT" + }, + { + "id": 386, + "origin_id": -10, + "origin_slot": 0, + "target_id": 225, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 389, + "origin_id": -10, + "origin_slot": 0, + "target_id": 230, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 394, + "origin_id": 225, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 427, + "origin_id": -10, + "origin_slot": 1, + "target_id": 228, + "target_slot": 0, + "type": "INT" + }, + { + "id": 428, + "origin_id": -10, + "origin_slot": 2, + "target_id": 252, + "target_slot": 0, + "type": "INT" + }, + { + "id": 429, + "origin_id": 252, + "origin_slot": 0, + "target_id": 231, + "target_slot": 1, + "type": "INT" + } + ], + "extra": {}, + "category": "Image Tools/Crop", + "description": "Splits an image into a configurable columns×rows grid of equal tiles for tiled generation or processing." + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Text to Audio (ACE-Step 1.5).json b/blueprints/Text to Audio (ACE-Step 1.5).json index 206cf16be..5b8b8626f 100644 --- a/blueprints/Text to Audio (ACE-Step 1.5).json +++ b/blueprints/Text to Audio (ACE-Step 1.5).json @@ -222,7 +222,7 @@ }, "revision": 0, "config": {}, - "name": "local-Text to Audio (ACE-Step 1.5)", + "name": "Text to Audio (ACE-Step 1.5)", "inputNode": { "id": -10, "bounding": [ @@ -1502,7 +1502,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Audio/Music generation" + "category": "Audio/Music generation", + "description": "Generates audio/music from text prompts using ACE-Step 1.5, a diffusion-based audio generation model." } ] }, @@ -1518,4 +1519,4 @@ } }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Text to Image (Anima).json b/blueprints/Text to Image (Anima).json new file mode 100644 index 000000000..787908ca9 --- /dev/null +++ b/blueprints/Text to Image (Anima).json @@ -0,0 +1,1085 @@ +{ + "revision": 0, + "last_node_id": 60, + "last_link_id": 0, + "nodes": [ + { + "id": 60, + "type": "a3c0dab6-b250-4585-a0f9-8fb8b074fb2f", + "pos": [ + -10, + 130 + ], + "size": [ + 500, + 640 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "label": "prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": null + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": null + }, + { + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "11", + "text" + ], + [ + "28", + "width" + ], + [ + "28", + "height" + ], + [ + "19", + "steps" + ], + [ + "19", + "cfg" + ], + [ + "19", + "seed" + ], + [ + "44", + "unet_name" + ], + [ + "45", + "clip_name" + ], + [ + "15", + "vae_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Text to Image (Anima)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "a3c0dab6-b250-4585-a0f9-8fb8b074fb2f", + "version": 1, + "state": { + "lastGroupId": 3, + "lastNodeId": 70, + "lastLinkId": 104, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Text to Image (Anima)", + "inputNode": { + "id": -10, + "bounding": [ + -330, + 530, + 120, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1229.9999873482075, + 505, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "4693f350-6ba0-446d-80d4-3038c661d26c", + "name": "text", + "type": "STRING", + "linkIds": [ + 95 + ], + "label": "prompt", + "pos": [ + -230, + 550 + ] + }, + { + "id": "4a7886a9-4ed7-49bb-afc2-977bb78a303d", + "name": "width", + "type": "INT", + "linkIds": [ + 96 + ], + "pos": [ + -230, + 570 + ] + }, + { + "id": "f6c04461-d29e-49e3-8790-07bb662bbbfe", + "name": "height", + "type": "INT", + "linkIds": [ + 97 + ], + "pos": [ + -230, + 590 + ] + }, + { + "id": "7a24f998-3808-4837-8bff-52304ad09fb6", + "name": "steps", + "type": "INT", + "linkIds": [ + 98 + ], + "pos": [ + -230, + 610 + ] + }, + { + "id": "aaa99698-b222-40fe-b946-28067528a85c", + "name": "cfg", + "type": "FLOAT", + "linkIds": [ + 99 + ], + "pos": [ + -230, + 630 + ] + }, + { + "id": "053df9ae-7311-4816-aa23-7fa13c656ced", + "name": "seed", + "type": "INT", + "linkIds": [ + 100 + ], + "pos": [ + -230, + 650 + ] + }, + { + "id": "c59194ea-015c-41a7-8edd-ae7ffc220b63", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 101 + ], + "pos": [ + -230, + 670 + ] + }, + { + "id": "e655aa3b-2db7-4e25-9ea2-61550fa7ae2d", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 102 + ], + "pos": [ + -230, + 690 + ] + }, + { + "id": "94965a7a-74dd-4f5a-87e3-9f87995d554f", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 103 + ], + "pos": [ + -230, + 710 + ] + } + ], + "outputs": [ + { + "id": "ef85ac0a-2152-4232-bfa1-929cfc913718", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 82 + ], + "localized_name": "IMAGE", + "pos": [ + 1249.9999873482075, + 525 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 45, + "type": "CLIPLoader", + "pos": [ + -60, + 380 + ], + "size": [ + 310, + 150 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 102 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 80, + 81 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.11.0", + "models": [ + { + "name": "qwen_3_06b_base.safetensors", + "url": "https://huggingface.co/circlestone-labs/Anima/resolve/main/split_files/text_encoders/qwen_3_06b_base.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "qwen_3_06b_base.safetensors", + "stable_diffusion", + "default" + ] + }, + { + "id": 15, + "type": "VAELoader", + "pos": [ + -50, + 610 + ], + "size": [ + 310, + 100 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 103 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 11 + ] + } + ], + "properties": { + "Node name for S&R": "VAELoader", + "cnr_id": "comfy-core", + "ver": "0.3.40", + "models": [ + { + "name": "qwen_image_vae.safetensors", + "url": "https://huggingface.co/circlestone-labs/Anima/resolve/main/split_files/vae/qwen_image_vae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "qwen_image_vae.safetensors" + ] + }, + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 880, + 840 + ], + "size": [ + 230, + 90 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 10 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 11 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 82 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.3.40", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 28, + "type": "EmptyLatentImage", + "pos": [ + -50, + 830 + ], + "size": [ + 310, + 150 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 96 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 97 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 78 + ] + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage", + "cnr_id": "comfy-core", + "ver": "0.3.40", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 12, + "type": "CLIPTextEncode", + "pos": [ + 330, + 830 + ], + "size": [ + 490, + 140 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 81 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 40 + ] + } + ], + "title": "CLIP Text Encode (Negative Prompt)", + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.65", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "worst quality, low quality, score_1, score_2, score_3, blurry, jpeg artifacts, sepia" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 19, + "type": "KSampler", + "pos": [ + 870, + 120 + ], + "size": [ + 300, + 620 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 79 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 39 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 40 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 78 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 100 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 98 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": 99 + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 10 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.3.40", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + "fixed", + 30, + 4, + "er_sde", + "simple", + 1 + ] + }, + { + "id": 11, + "type": "CLIPTextEncode", + "pos": [ + 320, + 170 + ], + "size": [ + 490, + 610 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 80 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 95 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 39 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.65", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 44, + "type": "UNETLoader", + "pos": [ + -50, + 170 + ], + "size": [ + 310, + 130 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 101 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 79 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.11.0", + "models": [ + { + "name": "anima-base-v1.0.safetensors", + "url": "https://huggingface.co/circlestone-labs/Anima/resolve/main/split_files/diffusion_models/anima-base-v1.0.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "anima-base-v1.0.safetensors", + "default" + ] + } + ], + "groups": [ + { + "id": 1, + "title": "Model", + "bounding": [ + -80, + 80, + 360, + 640 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 2, + "title": "Image Size(1MP)", + "bounding": [ + -80, + 750, + 360, + 240 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 3, + "title": "Prompt", + "bounding": [ + 300, + 80, + 530, + 910 + ], + "color": "#3f789e", + "flags": {} + } + ], + "links": [ + { + "id": 10, + "origin_id": 19, + "origin_slot": 0, + "target_id": 8, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 11, + "origin_id": 15, + "origin_slot": 0, + "target_id": 8, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 81, + "origin_id": 45, + "origin_slot": 0, + "target_id": 12, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 79, + "origin_id": 44, + "origin_slot": 0, + "target_id": 19, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 39, + "origin_id": 11, + "origin_slot": 0, + "target_id": 19, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 40, + "origin_id": 12, + "origin_slot": 0, + "target_id": 19, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 78, + "origin_id": 28, + "origin_slot": 0, + "target_id": 19, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 80, + "origin_id": 45, + "origin_slot": 0, + "target_id": 11, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 82, + "origin_id": 8, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 95, + "origin_id": -10, + "origin_slot": 0, + "target_id": 11, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 96, + "origin_id": -10, + "origin_slot": 1, + "target_id": 28, + "target_slot": 0, + "type": "INT" + }, + { + "id": 97, + "origin_id": -10, + "origin_slot": 2, + "target_id": 28, + "target_slot": 1, + "type": "INT" + }, + { + "id": 98, + "origin_id": -10, + "origin_slot": 3, + "target_id": 19, + "target_slot": 5, + "type": "INT" + }, + { + "id": 99, + "origin_id": -10, + "origin_slot": 4, + "target_id": 19, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 100, + "origin_id": -10, + "origin_slot": 5, + "target_id": 19, + "target_slot": 4, + "type": "INT" + }, + { + "id": 101, + "origin_id": -10, + "origin_slot": 6, + "target_id": 44, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 102, + "origin_id": -10, + "origin_slot": 7, + "target_id": 45, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 103, + "origin_id": -10, + "origin_slot": 8, + "target_id": 15, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": {}, + "category": "Image generation and editing/Text to image" + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Text to Image (Ernie Image Turbo).json b/blueprints/Text to Image (Ernie Image Turbo).json new file mode 100644 index 000000000..4ecdd1883 --- /dev/null +++ b/blueprints/Text to Image (Ernie Image Turbo).json @@ -0,0 +1,2112 @@ +{ + "revision": 0, + "last_node_id": 88, + "last_link_id": 0, + "nodes": [ + { + "id": 88, + "type": "2a4f0815-c4d2-4e8b-9bdf-991a8403889d", + "pos": [ + -120, + 240 + ], + "size": [ + 400, + 540 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "label": "prompt", + "name": "value", + "type": "STRING", + "widget": { + "name": "value" + }, + "link": null + }, + { + "label": "prompt_enhancement", + "name": "value_1", + "type": "BOOLEAN", + "widget": { + "name": "value_1" + }, + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": null + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "label": "prompt_enhancer", + "name": "clip_name_1", + "type": "COMBO", + "widget": { + "name": "clip_name_1" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "94", + "value" + ], + [ + "96", + "value" + ], + [ + "71", + "width" + ], + [ + "71", + "height" + ], + [ + "70", + "seed" + ], + [ + "66", + "unet_name" + ], + [ + "62", + "clip_name" + ], + [ + "98", + "clip_name" + ], + [ + "63", + "vae_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": { + "value": true, + "value_1": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [], + "title": "Text to Image (Ernie Image Turbo)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "2a4f0815-c4d2-4e8b-9bdf-991a8403889d", + "version": 1, + "state": { + "lastGroupId": 7, + "lastNodeId": 103, + "lastLinkId": 134, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Text to Image (Ernie Image Turbo)", + "inputNode": { + "id": -10, + "bounding": [ + -1350, + 370, + 163.50390625, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1110, + 260, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "74a4609c-67df-4ae9-ab96-9ff4e3a1c3b1", + "name": "value", + "type": "STRING", + "linkIds": [ + 128 + ], + "label": "prompt", + "pos": [ + -1206.49609375, + 390 + ] + }, + { + "id": "996f1854-7ae3-450e-821c-a9b5b7c310f9", + "name": "value_1", + "type": "BOOLEAN", + "linkIds": [ + 127 + ], + "label": "prompt_enhancement", + "pos": [ + -1206.49609375, + 410 + ] + }, + { + "id": "71e9c6e8-4285-4543-b1d3-81520088f6a4", + "name": "width", + "type": "INT", + "linkIds": [ + 104, + 129 + ], + "pos": [ + -1206.49609375, + 430 + ] + }, + { + "id": "bdb6cd97-67d9-440c-8c4c-9b7a7540edd0", + "name": "height", + "type": "INT", + "linkIds": [ + 105, + 130 + ], + "pos": [ + -1206.49609375, + 450 + ] + }, + { + "id": "18abb56c-30bf-4de5-83c1-c12376e8d14e", + "name": "seed", + "type": "INT", + "linkIds": [ + 108 + ], + "pos": [ + -1206.49609375, + 470 + ] + }, + { + "id": "e5cd06f9-64ed-4778-97ba-b165f7a79c4e", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 109 + ], + "pos": [ + -1206.49609375, + 490 + ] + }, + { + "id": "06480e4c-4043-489b-ae68-1cf2b4246260", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 110 + ], + "pos": [ + -1206.49609375, + 510 + ] + }, + { + "id": "8d65d01b-16b2-420d-8b7b-42077c2e4976", + "name": "clip_name_1", + "type": "COMBO", + "linkIds": [ + 132 + ], + "label": "prompt_enhancer", + "pos": [ + -1206.49609375, + 530 + ] + }, + { + "id": "697f2fdb-0fd9-4008-a895-0f9ce9e8fd88", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 133 + ], + "pos": [ + -1206.49609375, + 550 + ] + } + ], + "outputs": [ + { + "id": "21d5fbe0-9f91-4d93-8ea8-5bbf2cd5b698", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 84 + ], + "localized_name": "IMAGE", + "pos": [ + 1130, + 280 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 71, + "type": "EmptyFlux2LatentImage", + "pos": [ + -470, + 1050 + ], + "size": [ + 270, + 170 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 104 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 105 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 80 + ] + } + ], + "properties": { + "Node name for S&R": "EmptyFlux2LatentImage", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 66, + "type": "UNETLoader", + "pos": [ + -470, + 320 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 109 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 85 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "ernie-image-turbo.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/diffusion_models/ernie-image-turbo.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ernie-image-turbo.safetensors", + "default" + ] + }, + { + "id": 65, + "type": "VAEDecode", + "pos": [ + 710, + 280 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 73 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 74 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 84 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + } + }, + { + "id": 70, + "type": "KSampler", + "pos": [ + 350, + 280 + ], + "size": [ + 320, + 350 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 85 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 76 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 113 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 80 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 108 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 73 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 423299999918804, + "randomize", + 8, + 1, + "euler", + "simple", + 1 + ] + }, + { + "id": 67, + "type": "CLIPTextEncode", + "pos": [ + -140, + 320 + ], + "size": [ + 410, + 370 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 79 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 131 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 76, + 112 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 62, + "type": "CLIPLoader", + "pos": [ + -470, + 530 + ], + "size": [ + 270, + 150 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 110 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 79 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "ministral-3-3b.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/text_encoders/ministral-3-3b.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ministral-3-3b.safetensors", + "flux2", + "default" + ] + }, + { + "id": 63, + "type": "VAELoader", + "pos": [ + -470, + 780 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 133 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 74 + ] + } + ], + "properties": { + "Node name for S&R": "VAELoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "flux2-vae.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/vae/flux2-vae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "flux2-vae.safetensors" + ] + }, + { + "id": 91, + "type": "ConditioningZeroOut", + "pos": [ + 30, + 760 + ], + "size": [ + 230, + 80 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 112 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 113 + ] + } + ], + "properties": { + "Node name for S&R": "ConditioningZeroOut", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + } + }, + { + "id": 93, + "type": "StringReplace", + "pos": [ + -500, + -650 + ], + "size": [ + 430, + 450 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": null + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 115 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 121 + ] + } + ], + "properties": { + "Node name for S&R": "StringReplace", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "[SYSTEM_PROMPT]你是一个专业的文生图 Prompt 增强助手。你将收到用户的简短图片描述及目标生成分辨率,请据此扩写为一段内容丰富、细节充分的视觉描述,以帮助文生图模型生成高质量的图片。仅输出增强后的描述,不要包含任何解释或前缀。[/SYSTEM_PROMPT][INST]{\"prompt\": \"{prompt}\", \"width\": {width}, \"height\": {height}}[/INST]", + "{prompt}", + "" + ] + }, + { + "id": 94, + "type": "PrimitiveStringMultiline", + "pos": [ + -950, + -660 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "STRING", + "widget": { + "name": "value" + }, + "link": 128 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 115, + 118 + ] + } + ], + "title": "String (Multiline - Prompt)", + "properties": { + "Node name for S&R": "PrimitiveStringMultiline", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ] + }, + { + "id": 95, + "type": "TextGenerate", + "pos": [ + 530, + -660 + ], + "size": [ + 400, + 380 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 116 + }, + { + "localized_name": "image", + "name": "image", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "localized_name": "prompt", + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": 117 + }, + { + "localized_name": "max_length", + "name": "max_length", + "type": "INT", + "widget": { + "name": "max_length" + }, + "link": null + }, + { + "localized_name": "sampling_mode", + "name": "sampling_mode", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "sampling_mode" + }, + "link": null + }, + { + "localized_name": "temperature", + "name": "sampling_mode.temperature", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.temperature" + }, + "link": null + }, + { + "localized_name": "top_k", + "name": "sampling_mode.top_k", + "type": "INT", + "widget": { + "name": "sampling_mode.top_k" + }, + "link": null + }, + { + "localized_name": "top_p", + "name": "sampling_mode.top_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.top_p" + }, + "link": null + }, + { + "localized_name": "min_p", + "name": "sampling_mode.min_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.min_p" + }, + "link": null + }, + { + "localized_name": "repetition_penalty", + "name": "sampling_mode.repetition_penalty", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.repetition_penalty" + }, + "link": null + }, + { + "localized_name": "seed", + "name": "sampling_mode.seed", + "type": "INT", + "widget": { + "name": "sampling_mode.seed" + }, + "link": null + }, + { + "localized_name": "sampling_mode.presence_penalty", + "name": "sampling_mode.presence_penalty", + "shape": 7, + "type": "FLOAT", + "widget": { + "name": "sampling_mode.presence_penalty" + }, + "link": null + }, + { + "localized_name": "thinking", + "name": "thinking", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "thinking" + }, + "link": null + }, + { + "localized_name": "use_default_template", + "name": "use_default_template", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "use_default_template" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "generated_text", + "name": "generated_text", + "type": "STRING", + "links": [ + 119 + ] + } + ], + "properties": { + "Node name for S&R": "TextGenerate", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "", + 2048, + "on", + 0.6, + 64, + 0.8, + 0.05, + 1.05, + 0, + 0, + false, + true + ] + }, + { + "id": 96, + "type": "PrimitiveBoolean", + "pos": [ + -490, + 60 + ], + "size": [ + 270, + 100 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 127 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 120 + ] + } + ], + "title": "Enable prompt enhancement?", + "properties": { + "Node name for S&R": "PrimitiveBoolean", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + true + ] + }, + { + "id": 97, + "type": "ComfySwitchNode", + "pos": [ + 550, + -10 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 118 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 119 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 120 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 131, + 134 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + false + ] + }, + { + "id": 98, + "type": "CLIPLoader", + "pos": [ + -490, + -150 + ], + "size": [ + 510, + 150 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 132 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 116 + ] + } + ], + "title": "Load CLIP (PE)", + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "models": [ + { + "name": "ernie-image-prompt-enhancer.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/text_encoders/ernie-image-prompt-enhancer.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ernie-image-prompt-enhancer.safetensors", + "flux2", + "default" + ] + }, + { + "id": 99, + "type": "PreviewAny", + "pos": [ + -950, + -410 + ], + "size": [ + 400, + 180 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 129 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 122 + ] + } + ], + "title": "Preview as Text (Int to String)", + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 100, + "type": "PreviewAny", + "pos": [ + -950, + -190 + ], + "size": [ + 400, + 180 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 130 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 124 + ] + } + ], + "title": "Preview as Text (Int to String)", + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 101, + "type": "StringReplace", + "pos": [ + -30, + -650 + ], + "size": [ + 230, + 450 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": 121 + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 122 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 123 + ] + } + ], + "properties": { + "Node name for S&R": "StringReplace", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "", + "{width}", + "" + ] + }, + { + "id": 102, + "type": "StringReplace", + "pos": [ + 220, + -650 + ], + "size": [ + 250, + 450 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", + "type": "STRING", + "widget": { + "name": "string" + }, + "link": 123 + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 124 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 117 + ] + } + ], + "properties": { + "Node name for S&R": "StringReplace", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "", + "{height}", + "" + ] + }, + { + "id": 103, + "type": "PreviewAny", + "pos": [ + 970, + -660 + ], + "size": [ + 570, + 790 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 134 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [] + } + ], + "title": "Preview as Text (Int to String)", + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + } + ], + "groups": [ + { + "id": 6, + "title": "Text to Image", + "bounding": [ + -510, + 200, + 1450, + 1060 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "Image Size", + "bounding": [ + -490, + 950, + 300, + 290 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "Prompt", + "bounding": [ + -160, + 250, + 470, + 670 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Model", + "bounding": [ + -490, + 250, + 300, + 670 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 7, + "title": "Prompt Enhancement", + "bounding": [ + -510, + -720, + 1450, + 890 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 73, + "origin_id": 70, + "origin_slot": 0, + "target_id": 65, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 74, + "origin_id": 63, + "origin_slot": 0, + "target_id": 65, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 85, + "origin_id": 66, + "origin_slot": 0, + "target_id": 70, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 76, + "origin_id": 67, + "origin_slot": 0, + "target_id": 70, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 80, + "origin_id": 71, + "origin_slot": 0, + "target_id": 70, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 79, + "origin_id": 62, + "origin_slot": 0, + "target_id": 67, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 84, + "origin_id": 65, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 104, + "origin_id": -10, + "origin_slot": 2, + "target_id": 71, + "target_slot": 0, + "type": "INT" + }, + { + "id": 105, + "origin_id": -10, + "origin_slot": 3, + "target_id": 71, + "target_slot": 1, + "type": "INT" + }, + { + "id": 108, + "origin_id": -10, + "origin_slot": 4, + "target_id": 70, + "target_slot": 4, + "type": "INT" + }, + { + "id": 109, + "origin_id": -10, + "origin_slot": 5, + "target_id": 66, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 110, + "origin_id": -10, + "origin_slot": 6, + "target_id": 62, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 112, + "origin_id": 67, + "origin_slot": 0, + "target_id": 91, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 113, + "origin_id": 91, + "origin_slot": 0, + "target_id": 70, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 115, + "origin_id": 94, + "origin_slot": 0, + "target_id": 93, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 116, + "origin_id": 98, + "origin_slot": 0, + "target_id": 95, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 117, + "origin_id": 102, + "origin_slot": 0, + "target_id": 95, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 118, + "origin_id": 94, + "origin_slot": 0, + "target_id": 97, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 119, + "origin_id": 95, + "origin_slot": 0, + "target_id": 97, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 120, + "origin_id": 96, + "origin_slot": 0, + "target_id": 97, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 121, + "origin_id": 93, + "origin_slot": 0, + "target_id": 101, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 122, + "origin_id": 99, + "origin_slot": 0, + "target_id": 101, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 123, + "origin_id": 101, + "origin_slot": 0, + "target_id": 102, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 124, + "origin_id": 100, + "origin_slot": 0, + "target_id": 102, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 127, + "origin_id": -10, + "origin_slot": 1, + "target_id": 96, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 128, + "origin_id": -10, + "origin_slot": 0, + "target_id": 94, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 129, + "origin_id": -10, + "origin_slot": 2, + "target_id": 99, + "target_slot": 0, + "type": "*" + }, + { + "id": 130, + "origin_id": -10, + "origin_slot": 3, + "target_id": 100, + "target_slot": 0, + "type": "*" + }, + { + "id": 131, + "origin_id": 97, + "origin_slot": 0, + "target_id": 67, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 132, + "origin_id": -10, + "origin_slot": 7, + "target_id": 98, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 133, + "origin_id": -10, + "origin_slot": 8, + "target_id": 63, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 134, + "origin_id": 97, + "origin_slot": 0, + "target_id": 103, + "target_slot": 0, + "type": "STRING" + } + ], + "extra": {}, + "category": "Image generation and editing/Text to image", + "description": "Faster ERNIE Image Turbo variant (~8B DiT, distilled for fewer sampling steps): same strengths in Chinese/English on-image text and layout-heavy graphics as the base ERNIE Image lineup, with bundled encoders and VAE." + } + ] + }, + "extra": { + "ue_links": [] + } +} diff --git a/blueprints/Video Inpaint(Wan2.1 VACE).json b/blueprints/Text to Image (Ernie Image).json similarity index 51% rename from blueprints/Video Inpaint(Wan2.1 VACE).json rename to blueprints/Text to Image (Ernie Image).json index f404e6773..2bab20d69 100644 --- a/blueprints/Video Inpaint(Wan2.1 VACE).json +++ b/blueprints/Text to Image (Ernie Image).json @@ -1,35 +1,39 @@ { - "id": "2f429c60-2e03-4117-908b-31e1fab04bba", "revision": 0, - "last_node_id": 229, - "last_link_id": 366, + "last_node_id": 88, + "last_link_id": 0, "nodes": [ { - "id": 229, - "type": "53a657f3-c9eb-40f2-9ebd-1ed77d25ed67", + "id": 88, + "type": "03921aea-a70e-44b4-bc77-f6bda10f2120", "pos": [ - -230, - 160 + -120, + 240 ], "size": [ 400, - 480 + 540 ], "flags": {}, "order": 0, "mode": 0, "inputs": [ { - "label": "video mask", - "localized_name": "mask", - "name": "mask", - "type": "MASK", + "label": "prompt", + "name": "value", + "type": "STRING", + "widget": { + "name": "value" + }, "link": null }, { - "localized_name": "video", - "name": "video", - "type": "VIDEO", + "label": "prompt_enhancement", + "name": "value_1", + "type": "BOOLEAN", + "widget": { + "name": "value_1" + }, "link": null }, { @@ -49,9 +53,27 @@ "link": null }, { - "label": "reference image", - "name": "reference_image_1", - "type": "IMAGE", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, "link": null }, { @@ -62,14 +84,6 @@ }, "link": null }, - { - "name": "lora_name", - "type": "COMBO", - "widget": { - "name": "lora_name" - }, - "link": null - }, { "name": "clip_name", "type": "COMBO", @@ -78,6 +92,15 @@ }, "link": null }, + { + "label": "prompt_enhancer", + "name": "clip_name_1", + "type": "COMBO", + "widget": { + "name": "clip_name_1" + }, + "link": null + }, { "name": "vae_name", "type": "COMBO", @@ -89,265 +112,312 @@ ], "outputs": [ { - "localized_name": "VIDEO", - "name": "VIDEO", - "type": "VIDEO", + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", "links": [] } ], "properties": { "proxyWidgets": [ [ - "6", - "text" + "78", + "value" ], [ - "-1", + "76", + "value" + ], + [ + "71", "width" ], [ - "-1", + "71", "height" ], [ - "3", + "70", + "steps" + ], + [ + "70", + "cfg" + ], + [ + "70", "seed" ], [ - "3", - "control_after_generate" - ], - [ - "-1", + "66", "unet_name" ], [ - "-1", - "lora_name" - ], - [ - "-1", + "62", "clip_name" ], [ - "-1", + "91", + "clip_name" + ], + [ + "63", "vae_name" ] ], "cnr_id": "comfy-core", - "ver": "0.13.0" + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": { + "value": true, + "value_1": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + } }, - "widgets_values": [ - null, - 720, - 720, - null, - null, - "wan2.1_vace_14B_fp16.safetensors", - "Wan21_CausVid_14B_T2V_lora_rank32.safetensors", - "umt5_xxl_fp8_e4m3fn_scaled.safetensors", - "wan_2.1_vae.safetensors" - ] + "widgets_values": [], + "title": "Text to Image (Ernie Image)" } ], "links": [], - "groups": [], + "version": 0.4, "definitions": { "subgraphs": [ { - "id": "53a657f3-c9eb-40f2-9ebd-1ed77d25ed67", + "id": "03921aea-a70e-44b4-bc77-f6bda10f2120", "version": 1, "state": { - "lastGroupId": 25, - "lastNodeId": 229, - "lastLinkId": 366, + "lastGroupId": 6, + "lastNodeId": 99, + "lastLinkId": 124, "lastRerouteId": 0 }, "revision": 0, "config": {}, - "name": "local-Video Inpaint(Wan2.1 VACE)", + "name": "Text to Image (Ernie Image)", "inputNode": { "id": -10, "bounding": [ - -970, - 800, - 132.54296875, - 220 + -1350, + 370, + 163.50390625, + 260 ] }, "outputNode": { "id": -20, "bounding": [ - 1480, - 535, + 1110, + 260, 120, 60 ] }, "inputs": [ { - "id": "9fdda38d-6aa7-48ad-b425-f493d8aa585c", - "name": "mask", - "type": "MASK", + "id": "504de359-52a4-49aa-b6be-23c1cdb0cbde", + "name": "value", + "type": "STRING", "linkIds": [ - 351, - 335, - 345 + 102 ], - "localized_name": "mask", - "label": "video mask", + "label": "prompt", "pos": [ - -857.45703125, - 820 + -1206.49609375, + 390 ] }, { - "id": "8b1788cc-46d2-4f40-8b33-70fd56b4cb24", - "name": "video", - "type": "VIDEO", + "id": "29f699c6-9263-41f6-b37d-69b9fc3913dd", + "name": "value_1", + "type": "BOOLEAN", "linkIds": [ - 336 + 103 ], - "localized_name": "video", + "label": "prompt_enhancement", "pos": [ - -857.45703125, - 840 + -1206.49609375, + 410 ] }, { - "id": "09393f21-257e-4476-bb02-54899a8252b8", + "id": "968e6213-d1e9-4268-8f47-1d6b9a39a43e", "name": "width", "type": "INT", "linkIds": [ - 355 + 104, + 113 ], "pos": [ - -857.45703125, - 860 + -1206.49609375, + 430 ] }, { - "id": "07a030f7-7eac-4b3f-b8f3-f00ee87b191d", + "id": "181c49ef-740d-4385-aa11-79718951ccb9", "name": "height", "type": "INT", "linkIds": [ - 356 + 105, + 114 ], "pos": [ - -857.45703125, - 880 + -1206.49609375, + 450 ] }, { - "id": "255908d3-6cc9-48fc-b76b-ab9fb72695bc", - "name": "reference_image_1", - "type": "IMAGE", + "id": "1e85f808-66a1-41df-be52-334142b35419", + "name": "steps", + "type": "INT", "linkIds": [ - 361 + 106 ], - "label": "reference image", "pos": [ - -857.45703125, - 900 + -1206.49609375, + 470 ] }, { - "id": "18a5d241-523c-433d-ae05-25b6e69d1e29", + "id": "2806addf-a252-4aa3-a5b7-397ab36dccec", + "name": "cfg", + "type": "FLOAT", + "linkIds": [ + 107 + ], + "pos": [ + -1206.49609375, + 490 + ] + }, + { + "id": "5d036a66-5dc0-4d7c-b9a9-349e454738aa", + "name": "seed", + "type": "INT", + "linkIds": [ + 108 + ], + "pos": [ + -1206.49609375, + 510 + ] + }, + { + "id": "360f9a40-aac5-4e9c-bc98-9d55a4a58be2", "name": "unet_name", "type": "COMBO", "linkIds": [ - 363 + 109 ], "pos": [ - -857.45703125, - 920 + -1206.49609375, + 530 ] }, { - "id": "d7576e1b-da5f-402f-81b2-d37f838b1f8f", - "name": "lora_name", - "type": "COMBO", - "linkIds": [ - 364 - ], - "pos": [ - -857.45703125, - 940 - ] - }, - { - "id": "41676a3e-c710-4723-821e-f651ad3784b1", + "id": "886301c7-6e88-4cec-96fa-8ae20e8340c5", "name": "clip_name", "type": "COMBO", "linkIds": [ - 365 + 110 ], "pos": [ - -857.45703125, - 960 + -1206.49609375, + 550 ] }, { - "id": "41fc878c-9aa6-4c12-bef3-ceda6b094b7c", + "id": "1d73a545-6d01-462f-bc61-966d4b918ff2", + "name": "clip_name_1", + "type": "COMBO", + "linkIds": [ + 120 + ], + "label": "prompt_enhancer", + "pos": [ + -1206.49609375, + 570 + ] + }, + { + "id": "8c61dc8c-e260-4b36-b73e-d36f90a0bbe3", "name": "vae_name", "type": "COMBO", "linkIds": [ - 366 + 121 ], "pos": [ - -857.45703125, - 980 + -1206.49609375, + 590 ] } ], "outputs": [ { - "id": "d4861f39-1011-49dc-80fd-ee318b614a8d", - "name": "VIDEO", - "type": "VIDEO", + "id": "f4cb34c8-4090-4281-b428-7338a339d274", + "name": "IMAGE", + "type": "IMAGE", "linkIds": [ - 129 + 84 ], - "localized_name": "VIDEO", + "localized_name": "IMAGE", "pos": [ - 1500, - 555 + 1130, + 280 ] } ], "widgets": [], "nodes": [ { - "id": 58, - "type": "TrimVideoLatent", + "id": 71, + "type": "EmptyFlux2LatentImage", "pos": [ - 760, - 390 + -460, + 1040 ], "size": [ - 315, - 60 + 270, + 170 ], - "flags": { - "collapsed": false - }, - "order": 13, + "flags": {}, + "order": 6, "mode": 0, "inputs": [ { - "localized_name": "samples", - "name": "samples", - "type": "LATENT", - "link": 116 - }, - { - "localized_name": "trim_amount", - "name": "trim_amount", + "localized_name": "width", + "name": "width", "type": "INT", "widget": { - "name": "trim_amount" + "name": "width" }, - "link": 115 + "link": 104 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 105 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null } ], "outputs": [ @@ -356,14 +426,14 @@ "name": "LATENT", "type": "LATENT", "links": [ - 117 + 80 ] } ], "properties": { + "Node name for S&R": "EmptyFlux2LatentImage", "cnr_id": "comfy-core", - "ver": "0.3.34", - "Node name for S&R": "TrimVideoLatent", + "ver": "0.18.1", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -371,97 +441,48 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65, - "widget_ue_connectable": { - "trim_amount": true + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} } }, "widgets_values": [ - 0 + 1024, + 1024, + 1 ] }, { - "id": 8, - "type": "VAEDecode", + "id": 66, + "type": "UNETLoader", "pos": [ - 770, - 500 + -470, + 320 ], "size": [ - 315, - 46 - ], - "flags": { - "collapsed": false - }, - "order": 11, - "mode": 0, - "inputs": [ - { - "localized_name": "samples", - "name": "samples", - "type": "LATENT", - "link": 117 - }, - { - "localized_name": "vae", - "name": "vae", - "type": "VAE", - "link": 76 - } - ], - "outputs": [ - { - "localized_name": "IMAGE", - "name": "IMAGE", - "type": "IMAGE", - "slot_index": 0, - "links": [ - 139 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.34", - "Node name for S&R": "VAEDecode", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, - "widget_ue_connectable": {} - }, - "widgets_values": [] - }, - { - "id": 48, - "type": "ModelSamplingSD3", - "pos": [ - 400, - 50 - ], - "size": [ - 315, - 58 + 270, + 110 ], "flags": {}, - "order": 9, + "order": 3, "mode": 0, "inputs": [ { - "localized_name": "model", - "name": "model", - "type": "MODEL", - "link": 279 + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 109 }, { - "localized_name": "shift", - "name": "shift", - "type": "FLOAT", + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", "widget": { - "name": "shift" + "name": "weight_dtype" }, "link": null } @@ -471,16 +492,83 @@ "localized_name": "MODEL", "name": "MODEL", "type": "MODEL", + "links": [ + 85 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "ernie-image.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/diffusion_models/ernie-image.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ernie-image.safetensors", + "default" + ] + }, + { + "id": 65, + "type": "VAEDecode", + "pos": [ + 710, + 280 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 73 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 74 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", "slot_index": 0, "links": [ - 280 + 84 ] } ], "properties": { + "Node name for S&R": "VAEDecode", "cnr_id": "comfy-core", - "ver": "0.3.34", - "Node name for S&R": "ModelSamplingSD3", + "ver": "0.3.64", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -488,525 +576,51 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65, - "widget_ue_connectable": {} - }, - "widgets_values": [ - 5 - ] + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + } }, { - "id": 219, - "type": "InvertMask", - "pos": [ - 400, - 990 - ], - "size": [ - 140, - 26 - ], - "flags": {}, - "order": 24, - "mode": 0, - "inputs": [ - { - "localized_name": "mask", - "name": "mask", - "type": "MASK", - "link": 351 - } - ], - "outputs": [ - { - "localized_name": "MASK", - "name": "MASK", - "type": "MASK", - "links": [ - 352 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.40", - "Node name for S&R": "InvertMask" - }, - "widgets_values": [] - }, - { - "id": 216, - "type": "MaskToImage", - "pos": [ - 560, - 990 - ], - "size": [ - 193.2779296875, - 26 - ], - "flags": {}, - "order": 23, - "mode": 0, - "inputs": [ - { - "localized_name": "mask", - "name": "mask", - "type": "MASK", - "link": 352 - } - ], - "outputs": [ - { - "localized_name": "IMAGE", - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 334 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.40", - "Node name for S&R": "MaskToImage" - }, - "widgets_values": [] - }, - { - "id": 213, - "type": "RebatchImages", - "pos": [ - 410, - 690 - ], - "size": [ - 230, - 60 - ], - "flags": {}, - "order": 21, - "mode": 0, - "inputs": [ - { - "localized_name": "images", - "name": "images", - "type": "IMAGE", - "link": 360 - }, - { - "localized_name": "batch_size", - "name": "batch_size", - "type": "INT", - "widget": { - "name": "batch_size" - }, - "link": 340 - } - ], - "outputs": [ - { - "localized_name": "IMAGE", - "name": "IMAGE", - "shape": 6, - "type": "IMAGE", - "links": [ - 333 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.40", - "Node name for S&R": "RebatchImages" - }, - "widgets_values": [ - 1 - ] - }, - { - "id": 68, - "type": "CreateVideo", - "pos": [ - 1150, - 50 - ], - "size": [ - 270, - 78 - ], - "flags": { - "collapsed": false - }, - "order": 14, - "mode": 0, - "inputs": [ - { - "localized_name": "images", - "name": "images", - "type": "IMAGE", - "link": 139 - }, - { - "localized_name": "audio", - "name": "audio", - "shape": 7, - "type": "AUDIO", - "link": 362 - }, - { - "localized_name": "fps", - "name": "fps", - "type": "FLOAT", - "widget": { - "name": "fps" - }, - "link": 353 - } - ], - "outputs": [ - { - "localized_name": "VIDEO", - "name": "VIDEO", - "type": "VIDEO", - "links": [ - 129 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.34", - "Node name for S&R": "CreateVideo", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, - "widget_ue_connectable": {} - }, - "widgets_values": [ - 16 - ] - }, - { - "id": 208, - "type": "ImageCompositeMasked", - "pos": [ - 410, - 790 - ], - "size": [ - 230, - 146 - ], - "flags": {}, - "order": 18, - "mode": 0, - "inputs": [ - { - "localized_name": "destination", - "name": "destination", - "type": "IMAGE", - "link": 333 - }, - { - "localized_name": "source", - "name": "source", - "type": "IMAGE", - "link": 334 - }, - { - "localized_name": "mask", - "name": "mask", - "shape": 7, - "type": "MASK", - "link": 335 - }, - { - "localized_name": "x", - "name": "x", - "type": "INT", - "widget": { - "name": "x" - }, - "link": null - }, - { - "localized_name": "y", - "name": "y", - "type": "INT", - "widget": { - "name": "y" - }, - "link": null - }, - { - "localized_name": "resize_source", - "name": "resize_source", - "type": "BOOLEAN", - "widget": { - "name": "resize_source" - }, - "link": null - } - ], - "outputs": [ - { - "localized_name": "IMAGE", - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 341, - 344 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.40", - "Node name for S&R": "ImageCompositeMasked" - }, - "widgets_values": [ - 0, - 0, - true - ] - }, - { - "id": 214, - "type": "PreviewImage", - "pos": [ - 760, - 690 - ], - "size": [ - 300, - 300 - ], - "flags": {}, - "order": 22, - "mode": 0, - "inputs": [ - { - "localized_name": "images", - "name": "images", - "type": "IMAGE", - "link": 341 - } - ], - "outputs": [], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.40", - "Node name for S&R": "PreviewImage" - }, - "widgets_values": [] - }, - { - "id": 111, - "type": "MaskToImage", - "pos": [ - 20, - 1270 - ], - "size": [ - 240, - 26 - ], - "flags": {}, - "order": 15, - "mode": 0, - "inputs": [ - { - "localized_name": "mask", - "name": "mask", - "type": "MASK", - "link": 345 - } - ], - "outputs": [ - { - "localized_name": "IMAGE", - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 201 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.34", - "Node name for S&R": "MaskToImage", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, - "widget_ue_connectable": {} - }, - "widgets_values": [] - }, - { - "id": 129, - "type": "RepeatImageBatch", - "pos": [ - 20, - 1160 - ], - "size": [ - 240, - 60 - ], - "flags": {}, - "order": 16, - "mode": 0, - "inputs": [ - { - "localized_name": "image", - "name": "image", - "type": "IMAGE", - "link": 201 - }, - { - "localized_name": "amount", - "name": "amount", - "type": "INT", - "widget": { - "name": "amount" - }, - "link": 346 - } - ], - "outputs": [ - { - "localized_name": "IMAGE", - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 202 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.34", - "Node name for S&R": "RepeatImageBatch", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, - "widget_ue_connectable": { - "amount": true - } - }, - "widgets_values": [ - 17 - ] - }, - { - "id": 130, - "type": "ImageToMask", - "pos": [ - 20, - 1050 - ], - "size": [ - 240, - 60 - ], - "flags": {}, - "order": 17, - "mode": 0, - "inputs": [ - { - "localized_name": "image", - "name": "image", - "type": "IMAGE", - "link": 202 - }, - { - "localized_name": "channel", - "name": "channel", - "type": "COMBO", - "widget": { - "name": "channel" - }, - "link": null - } - ], - "outputs": [ - { - "localized_name": "MASK", - "name": "MASK", - "type": "MASK", - "links": [ - 349 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.34", - "Node name for S&R": "ImageToMask", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65, - "widget_ue_connectable": {} - }, - "widgets_values": [ - "red" - ] - }, - { - "id": 3, + "id": 70, "type": "KSampler", "pos": [ - 770, - 50 + 350, + 280 ], "size": [ - 315, - 262 + 320, + 350 ], "flags": {}, - "order": 10, + "order": 5, "mode": 0, "inputs": [ { "localized_name": "model", "name": "model", "type": "MODEL", - "link": 280 + "link": 85 }, { "localized_name": "positive", "name": "positive", "type": "CONDITIONING", - "link": 98 + "link": 76 }, { "localized_name": "negative", "name": "negative", "type": "CONDITIONING", - "link": 99 + "link": 83 }, { "localized_name": "latent_image", "name": "latent_image", "type": "LATENT", - "link": 160 + "link": 80 }, { "localized_name": "seed", @@ -1015,7 +629,7 @@ "widget": { "name": "seed" }, - "link": null + "link": 108 }, { "localized_name": "steps", @@ -1024,7 +638,7 @@ "widget": { "name": "steps" }, - "link": null + "link": 106 }, { "localized_name": "cfg", @@ -1033,7 +647,7 @@ "widget": { "name": "cfg" }, - "link": null + "link": 107 }, { "localized_name": "sampler_name", @@ -1070,14 +684,14 @@ "type": "LATENT", "slot_index": 0, "links": [ - 116 + 73 ] } ], "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.34", "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.3.64", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1085,76 +699,96 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65, - "widget_ue_connectable": {} + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } }, "widgets_values": [ - 584027519362099, + 182596410725960, "randomize", + 20, 4, - 1, - "uni_pc", + "euler", "simple", 1 ] }, { - "id": 224, - "type": "MarkdownNote", - "pos": [ - 420, - -160 - ], - "size": [ - 310, - 110 - ], - "flags": {}, - "order": 0, - "mode": 0, - "inputs": [], - "outputs": [], - "title": "About Video Size", - "properties": {}, - "widgets_values": [ - "| Model | 480P | 720P |\n| ------------------------------------------------------------ | ---- | ---- |\n| [VACE-1.3B](https://huggingface.co/Wan-AI/Wan2.1-VACE-1.3B) | ✅ | ❌ |\n| [VACE-14B](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B) | ✅ | ✅ |" - ], - "color": "#432", - "bgcolor": "#000" - }, - { - "id": 223, - "type": "MarkdownNote", - "pos": [ - 770, - -210 - ], - "size": [ - 303.90106201171875, - 158.5415802001953 - ], - "flags": {}, - "order": 1, - "mode": 0, - "inputs": [], - "outputs": [], - "title": "KSampler Setting", - "properties": {}, - "widgets_values": [ - "## Default\n\n- steps:20\n- cfg:6.0\n\n## For CausVid LoRA\n\n- steps: 2-4\n- cfg: 1.0\n\n" - ], - "color": "#432", - "bgcolor": "#000" - }, - { - "id": 6, + "id": 67, "type": "CLIPTextEncode", "pos": [ - -80, - 60 + -140, + 320 ], "size": [ - 420, - 280 + 410, + 370 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 79 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 100 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 76 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 72, + "type": "CLIPTextEncode", + "pos": [ + -130, + 770 + ], + "size": [ + 390, + 140 ], "flags": {}, "order": 7, @@ -1164,7 +798,7 @@ "localized_name": "clip", "name": "clip", "type": "CLIP", - "link": 74 + "link": 82 }, { "localized_name": "text", @@ -1181,17 +815,15 @@ "localized_name": "CONDITIONING", "name": "CONDITIONING", "type": "CONDITIONING", - "slot_index": 0, "links": [ - 96 + 83 ] } ], - "title": "CLIP Text Encode (Positive Prompt)", "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.34", "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.73", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1199,70 +831,75 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65, - "widget_ue_connectable": {} + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } }, "widgets_values": [ "" ], - "color": "#232", - "bgcolor": "#353" + "color": "#223", + "bgcolor": "#335" }, { - "id": 140, - "type": "UNETLoader", + "id": 83, + "type": "StringReplace", "pos": [ - -505.8336486816406, - 88.22794342041016 + -500, + -640 ], "size": [ - 360, - 82 + 430, + 450 ], "flags": {}, - "order": 2, + "order": 12, "mode": 0, "inputs": [ { - "localized_name": "unet_name", - "name": "unet_name", - "type": "COMBO", + "localized_name": "string", + "name": "string", + "type": "STRING", "widget": { - "name": "unet_name" - }, - "link": 363 - }, - { - "localized_name": "weight_dtype", - "name": "weight_dtype", - "type": "COMBO", - "widget": { - "name": "weight_dtype" + "name": "string" }, "link": null + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" + }, + "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 92 } ], "outputs": [ { - "localized_name": "MODEL", - "name": "MODEL", - "type": "MODEL", - "slot_index": 0, + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", "links": [ - 248 + 115 ] } ], "properties": { + "Node name for S&R": "StringReplace", "cnr_id": "comfy-core", - "ver": "0.3.34", - "Node name for S&R": "UNETLoader", - "models": [ - { - "name": "wan2.1_vace_14B_fp16.safetensors", - "url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_vace_14B_fp16.safetensors", - "directory": "diffusion_models" - } - ], + "ver": "0.18.1", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1270,74 +907,230 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65, - "widget_ue_connectable": {} + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } }, "widgets_values": [ - "wan2.1_vace_14B_fp16.safetensors", - "fp8_e4m3fn_fast" + "[SYSTEM_PROMPT]你是一个专业的文生图 Prompt 增强助手。你将收到用户的简短图片描述及目标生成分辨率,请据此扩写为一段内容丰富、细节充分的视觉描述,以帮助文生图模型生成高质量的图片。仅输出增强后的描述,不要包含任何解释或前缀。[/SYSTEM_PROMPT][INST]{\"prompt\": \"{prompt}\", \"width\": {width}, \"height\": {height}}[/INST]", + "{prompt}", + "" ] }, { - "id": 154, - "type": "LoraLoaderModelOnly", + "id": 78, + "type": "PrimitiveStringMultiline", "pos": [ - -505.8336486816406, - 228.2279510498047 + -950, + -650 ], "size": [ - 360, - 85.11004638671875 + 400, + 200 ], "flags": {}, - "order": 6, + "order": 11, "mode": 0, "inputs": [ { - "localized_name": "model", - "name": "model", - "type": "MODEL", - "link": 248 - }, - { - "localized_name": "lora_name", - "name": "lora_name", - "type": "COMBO", + "localized_name": "value", + "name": "value", + "type": "STRING", "widget": { - "name": "lora_name" + "name": "value" }, - "link": 364 + "link": 102 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 87, + 92 + ] + } + ], + "title": "String (Multiline - Prompt)", + "properties": { + "Node name for S&R": "PrimitiveStringMultiline", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ] + }, + { + "id": 74, + "type": "TextGenerate", + "pos": [ + 530, + -650 + ], + "size": [ + 400, + 380 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 112 }, { - "localized_name": "strength_model", - "name": "strength_model", + "localized_name": "image", + "name": "image", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "localized_name": "prompt", + "name": "prompt", + "type": "STRING", + "widget": { + "name": "prompt" + }, + "link": 119 + }, + { + "localized_name": "max_length", + "name": "max_length", + "type": "INT", + "widget": { + "name": "max_length" + }, + "link": null + }, + { + "localized_name": "sampling_mode", + "name": "sampling_mode", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "sampling_mode" + }, + "link": null + }, + { + "localized_name": "temperature", + "name": "sampling_mode.temperature", "type": "FLOAT", "widget": { - "name": "strength_model" + "name": "sampling_mode.temperature" + }, + "link": null + }, + { + "localized_name": "top_k", + "name": "sampling_mode.top_k", + "type": "INT", + "widget": { + "name": "sampling_mode.top_k" + }, + "link": null + }, + { + "localized_name": "top_p", + "name": "sampling_mode.top_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.top_p" + }, + "link": null + }, + { + "localized_name": "min_p", + "name": "sampling_mode.min_p", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.min_p" + }, + "link": null + }, + { + "localized_name": "repetition_penalty", + "name": "sampling_mode.repetition_penalty", + "type": "FLOAT", + "widget": { + "name": "sampling_mode.repetition_penalty" + }, + "link": null + }, + { + "localized_name": "seed", + "name": "sampling_mode.seed", + "type": "INT", + "widget": { + "name": "sampling_mode.seed" + }, + "link": null + }, + { + "localized_name": "sampling_mode.presence_penalty", + "name": "sampling_mode.presence_penalty", + "shape": 7, + "type": "FLOAT", + "widget": { + "name": "sampling_mode.presence_penalty" + }, + "link": null + }, + { + "localized_name": "thinking", + "name": "thinking", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "thinking" + }, + "link": null + }, + { + "localized_name": "use_default_template", + "name": "use_default_template", + "shape": 7, + "type": "BOOLEAN", + "widget": { + "name": "use_default_template" }, "link": null } ], "outputs": [ { - "localized_name": "MODEL", - "name": "MODEL", - "type": "MODEL", + "localized_name": "generated_text", + "name": "generated_text", + "type": "STRING", "links": [ - 279 + 89 ] } ], "properties": { + "Node name for S&R": "TextGenerate", "cnr_id": "comfy-core", - "ver": "0.3.34", - "Node name for S&R": "LoraLoaderModelOnly", - "models": [ - { - "name": "Wan21_CausVid_14B_T2V_lora_rank32.safetensors", - "url": "https://huggingface.co/Kijai/WanVideo_comfy/resolve/main/Wan21_CausVid_14B_T2V_lora_rank32.safetensors", - "directory": "loras" - } - ], + "ver": "0.18.1", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1345,26 +1138,166 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65, - "widget_ue_connectable": {} + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } }, "widgets_values": [ - "Wan21_CausVid_14B_T2V_lora_rank32.safetensors", - 0.30000000000000004 + "", + 2048, + "on", + 0.6, + 64, + 0.8, + 0.05, + 1.05, + 0, + 0, + false, + true ] }, { - "id": 38, - "type": "CLIPLoader", + "id": 76, + "type": "PrimitiveBoolean", "pos": [ - -499.14141845703125, - 368.0911865234375 + -500, + 60 ], "size": [ - 360, - 106 + 270, + 100 ], "flags": {}, - "order": 3, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 103 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 88 + ] + } + ], + "title": "Enable prompt enhancement?", + "properties": { + "Node name for S&R": "PrimitiveBoolean", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + true + ] + }, + { + "id": 75, + "type": "ComfySwitchNode", + "pos": [ + 530, + 20 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 87 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 89 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 88 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 100, + 124 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + false + ] + }, + { + "id": 62, + "type": "CLIPLoader", + "pos": [ + -460, + 520 + ], + "size": [ + 270, + 150 + ], + "flags": {}, + "order": 0, "mode": 0, "inputs": [ { @@ -1374,7 +1307,7 @@ "widget": { "name": "clip_name" }, - "link": 365 + "link": 110 }, { "localized_name": "type", @@ -1401,21 +1334,20 @@ "localized_name": "CLIP", "name": "CLIP", "type": "CLIP", - "slot_index": 0, "links": [ - 74, - 75 + 79, + 82 ] } ], "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.34", "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", "models": [ { - "name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", - "url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors?download=true", + "name": "ministral-3-3b.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/text_encoders/ministral-3-3b.safetensors", "directory": "text_encoders" } ], @@ -1426,27 +1358,31 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65, - "widget_ue_connectable": {} + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } }, "widgets_values": [ - "umt5_xxl_fp8_e4m3fn_scaled.safetensors", - "wan", + "ministral-3-3b.safetensors", + "flux2", "default" ] }, { - "id": 39, + "id": 63, "type": "VAELoader", "pos": [ - -498.5298156738281, - 517.2576293945312 + -460, + 770 ], "size": [ - 360, - 60 + 270, + 110 ], "flags": {}, - "order": 4, + "order": 1, "mode": 0, "inputs": [ { @@ -1456,7 +1392,7 @@ "widget": { "name": "vae_name" }, - "link": 366 + "link": 121 } ], "outputs": [ @@ -1464,21 +1400,19 @@ "localized_name": "VAE", "name": "VAE", "type": "VAE", - "slot_index": 0, "links": [ - 76, - 101 + 74 ] } ], "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.34", "Node name for S&R": "VAELoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", "models": [ { - "name": "wan_2.1_vae.safetensors", - "url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/vae/wan_2.1_vae.safetensors", + "name": "flux2-vae.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/vae/flux2-vae.safetensors", "directory": "vae" } ], @@ -1489,85 +1423,270 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65, - "widget_ue_connectable": {} + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } }, "widgets_values": [ - "wan_2.1_vae.safetensors" + "flux2-vae.safetensors" ] }, { - "id": 221, - "type": "MarkdownNote", + "id": 91, + "type": "CLIPLoader", "pos": [ - 380, - 1090 + -500, + -150 ], "size": [ - 480, - 170 + 510, + 150 ], "flags": {}, - "order": 5, - "mode": 0, - "inputs": [], - "outputs": [], - "title": "[EN] About video mask", - "properties": { - "widget_ue_connectable": {} - }, - "widgets_values": [ - "Currently, it's difficult to perfectly draw dynamic masks for different frames using only core nodes. However, to avoid requiring users to install additional custom nodes, our templates only use core nodes. You can refer to this implementation idea to achieve video inpainting.\n\nYou can use KJNode’s Points Editor and Sam2Segmentation to create some dynamic mask functions.\n\nCustom node links:\n- [ComfyUI-KJNodes](https://github.com/kijai/ComfyUI-KJNodes)\n- [ComfyUI-segment-anything-2](https://github.com/kijai/ComfyUI-segment-anything-2)" - ], - "color": "#432", - "bgcolor": "#000" - }, - { - "id": 7, - "type": "CLIPTextEncode", - "pos": [ - -80, - 390 - ], - "size": [ - 425.27801513671875, - 180.6060791015625 - ], - "flags": {}, - "order": 8, + "order": 13, "mode": 0, "inputs": [ { - "localized_name": "clip", - "name": "clip", - "type": "CLIP", - "link": 75 + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 120 }, { - "localized_name": "text", - "name": "text", + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 112 + ] + } + ], + "title": "Load CLIP (PE)", + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "models": [ + { + "name": "ernie-image-prompt-enhancer.safetensors", + "url": "https://huggingface.co/Comfy-Org/ERNIE-Image/resolve/main/text_encoders/ernie-image-prompt-enhancer.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "ernie-image-prompt-enhancer.safetensors", + "flux2", + "default" + ] + }, + { + "id": 92, + "type": "PreviewAny", + "pos": [ + -950, + -400 + ], + "size": [ + 400, + 180 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 113 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 116 + ] + } + ], + "title": "Preview as Text (Int to String)", + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 93, + "type": "PreviewAny", + "pos": [ + -950, + -180 + ], + "size": [ + 400, + 180 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "source", + "name": "source", + "type": "*", + "link": 114 + } + ], + "outputs": [ + { + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [ + 118 + ] + } + ], + "title": "Preview as Text (Int to String)", + "properties": { + "Node name for S&R": "PreviewAny", + "cnr_id": "comfy-core", + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 94, + "type": "StringReplace", + "pos": [ + -30, + -640 + ], + "size": [ + 230, + 450 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "string", + "name": "string", "type": "STRING", "widget": { - "name": "text" + "name": "string" + }, + "link": 115 + }, + { + "localized_name": "find", + "name": "find", + "type": "STRING", + "widget": { + "name": "find" }, "link": null + }, + { + "localized_name": "replace", + "name": "replace", + "type": "STRING", + "widget": { + "name": "replace" + }, + "link": 116 } ], "outputs": [ { - "localized_name": "CONDITIONING", - "name": "CONDITIONING", - "type": "CONDITIONING", - "slot_index": 0, + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", "links": [ - 97 + 117 ] } ], - "title": "CLIP Text Encode (Negative Prompt)", "properties": { + "Node name for S&R": "StringReplace", "cnr_id": "comfy-core", - "ver": "0.3.34", - "Node name for S&R": "CLIPTextEncode", + "ver": "0.18.1", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1575,213 +1694,75 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65, - "widget_ue_connectable": {} + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } }, "widgets_values": [ - "过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走,过曝," - ], - "color": "#223", - "bgcolor": "#335" - }, - { - "id": 229, - "type": "ImageFromBatch", - "pos": [ - -510, - 800 - ], - "size": [ - 270, - 82 - ], - "flags": {}, - "order": 25, - "mode": 0, - "inputs": [ - { - "localized_name": "image", - "name": "image", - "type": "IMAGE", - "link": 358 - }, - { - "localized_name": "batch_index", - "name": "batch_index", - "type": "INT", - "widget": { - "name": "batch_index" - }, - "link": null - }, - { - "localized_name": "length", - "name": "length", - "type": "INT", - "widget": { - "name": "length" - }, - "link": null - } - ], - "outputs": [ - { - "localized_name": "IMAGE", - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 359, - 360 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.13.0", - "Node name for S&R": "ImageFromBatch" - }, - "widgets_values": [ - 0, - 81 + "", + "{width}", + "" ] }, { - "id": 49, - "type": "WanVaceToVideo", + "id": 95, + "type": "StringReplace", "pos": [ - 400, - 200 + 220, + -640 ], "size": [ - 315, - 254 + 250, + 450 ], "flags": {}, - "order": 12, + "order": 17, "mode": 0, "inputs": [ { - "localized_name": "positive", - "name": "positive", - "type": "CONDITIONING", - "link": 96 - }, - { - "localized_name": "negative", - "name": "negative", - "type": "CONDITIONING", - "link": 97 - }, - { - "localized_name": "vae", - "name": "vae", - "type": "VAE", - "link": 101 - }, - { - "localized_name": "control_video", - "name": "control_video", - "shape": 7, - "type": "IMAGE", - "link": 344 - }, - { - "localized_name": "control_masks", - "name": "control_masks", - "shape": 7, - "type": "MASK", - "link": 349 - }, - { - "localized_name": "reference_image", - "name": "reference_image", - "shape": 7, - "type": "IMAGE", - "link": 361 - }, - { - "localized_name": "width", - "name": "width", - "type": "INT", + "localized_name": "string", + "name": "string", + "type": "STRING", "widget": { - "name": "width" + "name": "string" }, - "link": 355 + "link": 117 }, { - "localized_name": "height", - "name": "height", - "type": "INT", + "localized_name": "find", + "name": "find", + "type": "STRING", "widget": { - "name": "height" - }, - "link": 356 - }, - { - "localized_name": "length", - "name": "length", - "type": "INT", - "widget": { - "name": "length" + "name": "find" }, "link": null }, { - "localized_name": "batch_size", - "name": "batch_size", - "type": "INT", + "localized_name": "replace", + "name": "replace", + "type": "STRING", "widget": { - "name": "batch_size" + "name": "replace" }, - "link": null - }, - { - "localized_name": "strength", - "name": "strength", - "type": "FLOAT", - "widget": { - "name": "strength" - }, - "link": null + "link": 118 } ], "outputs": [ { - "localized_name": "positive", - "name": "positive", - "type": "CONDITIONING", + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", "links": [ - 98 - ] - }, - { - "localized_name": "negative", - "name": "negative", - "type": "CONDITIONING", - "links": [ - 99 - ] - }, - { - "localized_name": "latent", - "name": "latent", - "type": "LATENT", - "links": [ - 160 - ] - }, - { - "localized_name": "trim_latent", - "name": "trim_latent", - "type": "INT", - "links": [ - 115 + 119 ] } ], "properties": { + "Node name for S&R": "StringReplace", "cnr_id": "comfy-core", - "ver": "0.3.34", - "Node name for S&R": "WanVaceToVideo", + "ver": "0.18.1", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -1789,139 +1770,82 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65, - "widget_ue_connectable": { - "width": true, - "height": true, - "length": true + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} } }, "widgets_values": [ - 720, - 720, - 81, - 1, - 1 + "", + "{height}", + "" ] }, { - "id": 211, - "type": "GetImageSize", + "id": 97, + "type": "PreviewAny", "pos": [ - 70, - 800 + 970, + -650 ], "size": [ - 190, - 66 - ], - "flags": { - "collapsed": false - }, - "order": 20, - "mode": 0, - "inputs": [ - { - "localized_name": "image", - "name": "image", - "type": "IMAGE", - "link": 359 - } - ], - "outputs": [ - { - "localized_name": "width", - "name": "width", - "type": "INT", - "links": null - }, - { - "localized_name": "height", - "name": "height", - "type": "INT", - "links": null - }, - { - "localized_name": "batch_size", - "name": "batch_size", - "type": "INT", - "links": [ - 340, - 346 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.40", - "Node name for S&R": "GetImageSize" - }, - "widgets_values": [] - }, - { - "id": 210, - "type": "GetVideoComponents", - "pos": [ - -510, - 690 - ], - "size": [ - 193.530859375, - 66 + 570, + 790 ], "flags": {}, - "order": 19, + "order": 18, "mode": 0, "inputs": [ { - "localized_name": "video", - "name": "video", - "type": "VIDEO", - "link": 336 + "localized_name": "source", + "name": "source", + "type": "*", + "link": 124 } ], "outputs": [ { - "localized_name": "images", - "name": "images", - "type": "IMAGE", - "links": [ - 358 - ] - }, - { - "localized_name": "audio", - "name": "audio", - "type": "AUDIO", - "links": [ - 362 - ] - }, - { - "localized_name": "fps", - "name": "fps", - "type": "FLOAT", - "links": [ - 353 - ] + "localized_name": "STRING", + "name": "STRING", + "type": "STRING", + "links": [] } ], + "title": "Preview as Text (Int to String)", "properties": { + "Node name for S&R": "PreviewAny", "cnr_id": "comfy-core", - "ver": "0.3.40", - "Node name for S&R": "GetVideoComponents" + "ver": "0.19.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } }, - "widgets_values": [] + "widgets_values": [ + null, + null, + null + ] } ], "groups": [ { - "id": 1, - "title": "Step1 - Load models here", + "id": 6, + "title": "Text to Image", "bounding": [ - -540, - -30, - 430, - 620 + -510, + 200, + 1450, + 1060 ], "color": "#3f789e", "font_size": 24, @@ -1929,50 +1853,11 @@ }, { "id": 2, - "title": "Prompt", + "title": "Image Size", "bounding": [ - -90, - -30, - 450, - 620 - ], - "color": "#3f789e", - "font_size": 24, - "flags": {} - }, - { - "id": 3, - "title": "Sampling & Decoding", - "bounding": [ - 380, - -30, - 720, - 620 - ], - "color": "#3f789e", - "font_size": 24, - "flags": {} - }, - { - "id": 10, - "title": "Repeat Mask Batch", - "bounding": [ - -90, - 910, - 450, - 460 - ], - "color": "#3f789e", - "font_size": 24, - "flags": {} - }, - { - "id": 21, - "title": "Get video info", - "bounding": [ - -540, - 610, - 900, + -480, + 940, + 310, 290 ], "color": "#3f789e", @@ -1980,39 +1865,39 @@ "flags": {} }, { - "id": 22, - "title": "Composite video & masks", + "id": 3, + "title": "Prompt", "bounding": [ - 380, - 610, - 720, - 420 + -160, + 250, + 470, + 670 ], "color": "#3f789e", "font_size": 24, "flags": {} }, { - "id": 23, - "title": "Step4 - Set video size & length", + "id": 4, + "title": "Model", "bounding": [ - 390, - 130, - 360, - 340 + -490, + 250, + 320, + 670 ], - "color": "#A88", + "color": "#3f789e", "font_size": 24, "flags": {} }, { - "id": 25, - "title": "14B", + "id": 5, + "title": "Prompt Enhancement", "bounding": [ - -520, - 10, - 380, - 308.7100524902344 + -510, + -720, + 1450, + 890 ], "color": "#3f789e", "font_size": 24, @@ -2021,367 +1906,285 @@ ], "links": [ { - "id": 116, - "origin_id": 3, + "id": 73, + "origin_id": 70, "origin_slot": 0, - "target_id": 58, + "target_id": 65, "target_slot": 0, "type": "LATENT" }, - { - "id": 115, - "origin_id": 49, - "origin_slot": 3, - "target_id": 58, - "target_slot": 1, - "type": "INT" - }, - { - "id": 117, - "origin_id": 58, - "origin_slot": 0, - "target_id": 8, - "target_slot": 0, - "type": "LATENT" - }, - { - "id": 76, - "origin_id": 39, - "origin_slot": 0, - "target_id": 8, - "target_slot": 1, - "type": "VAE" - }, - { - "id": 279, - "origin_id": 154, - "origin_slot": 0, - "target_id": 48, - "target_slot": 0, - "type": "MODEL" - }, - { - "id": 352, - "origin_id": 219, - "origin_slot": 0, - "target_id": 216, - "target_slot": 0, - "type": "MASK" - }, - { - "id": 340, - "origin_id": 211, - "origin_slot": 2, - "target_id": 213, - "target_slot": 1, - "type": "INT" - }, - { - "id": 96, - "origin_id": 6, - "origin_slot": 0, - "target_id": 49, - "target_slot": 0, - "type": "CONDITIONING" - }, - { - "id": 97, - "origin_id": 7, - "origin_slot": 0, - "target_id": 49, - "target_slot": 1, - "type": "CONDITIONING" - }, - { - "id": 101, - "origin_id": 39, - "origin_slot": 0, - "target_id": 49, - "target_slot": 2, - "type": "VAE" - }, - { - "id": 344, - "origin_id": 208, - "origin_slot": 0, - "target_id": 49, - "target_slot": 3, - "type": "IMAGE" - }, - { - "id": 349, - "origin_id": 130, - "origin_slot": 0, - "target_id": 49, - "target_slot": 4, - "type": "MASK" - }, - { - "id": 139, - "origin_id": 8, - "origin_slot": 0, - "target_id": 68, - "target_slot": 0, - "type": "IMAGE" - }, - { - "id": 353, - "origin_id": 210, - "origin_slot": 2, - "target_id": 68, - "target_slot": 2, - "type": "FLOAT" - }, - { - "id": 333, - "origin_id": 213, - "origin_slot": 0, - "target_id": 208, - "target_slot": 0, - "type": "IMAGE" - }, - { - "id": 334, - "origin_id": 216, - "origin_slot": 0, - "target_id": 208, - "target_slot": 1, - "type": "IMAGE" - }, - { - "id": 341, - "origin_id": 208, - "origin_slot": 0, - "target_id": 214, - "target_slot": 0, - "type": "IMAGE" - }, - { - "id": 201, - "origin_id": 111, - "origin_slot": 0, - "target_id": 129, - "target_slot": 0, - "type": "IMAGE" - }, - { - "id": 346, - "origin_id": 211, - "origin_slot": 2, - "target_id": 129, - "target_slot": 1, - "type": "INT" - }, - { - "id": 202, - "origin_id": 129, - "origin_slot": 0, - "target_id": 130, - "target_slot": 0, - "type": "IMAGE" - }, - { - "id": 280, - "origin_id": 48, - "origin_slot": 0, - "target_id": 3, - "target_slot": 0, - "type": "MODEL" - }, - { - "id": 98, - "origin_id": 49, - "origin_slot": 0, - "target_id": 3, - "target_slot": 1, - "type": "CONDITIONING" - }, - { - "id": 99, - "origin_id": 49, - "origin_slot": 1, - "target_id": 3, - "target_slot": 2, - "type": "CONDITIONING" - }, - { - "id": 160, - "origin_id": 49, - "origin_slot": 2, - "target_id": 3, - "target_slot": 3, - "type": "LATENT" - }, { "id": 74, - "origin_id": 38, + "origin_id": 63, "origin_slot": 0, - "target_id": 6, - "target_slot": 0, - "type": "CLIP" + "target_id": 65, + "target_slot": 1, + "type": "VAE" }, { - "id": 248, - "origin_id": 140, + "id": 85, + "origin_id": 66, "origin_slot": 0, - "target_id": 154, + "target_id": 70, "target_slot": 0, "type": "MODEL" }, { - "id": 75, - "origin_id": 38, + "id": 76, + "origin_id": 67, "origin_slot": 0, - "target_id": 7, + "target_id": 70, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 83, + "origin_id": 72, + "origin_slot": 0, + "target_id": 70, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 80, + "origin_id": 71, + "origin_slot": 0, + "target_id": 70, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 79, + "origin_id": 62, + "origin_slot": 0, + "target_id": 67, "target_slot": 0, "type": "CLIP" }, { - "id": 351, - "origin_id": -10, + "id": 100, + "origin_id": 75, "origin_slot": 0, - "target_id": 219, - "target_slot": 0, - "type": "MASK" + "target_id": 67, + "target_slot": 1, + "type": "STRING" }, { - "id": 335, - "origin_id": -10, + "id": 82, + "origin_id": 62, "origin_slot": 0, - "target_id": 208, + "target_id": 72, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 92, + "origin_id": 78, + "origin_slot": 0, + "target_id": 83, "target_slot": 2, - "type": "MASK" + "type": "STRING" }, { - "id": 345, - "origin_id": -10, + "id": 87, + "origin_id": 78, "origin_slot": 0, - "target_id": 111, + "target_id": 75, "target_slot": 0, - "type": "MASK" + "type": "STRING" }, { - "id": 336, - "origin_id": -10, - "origin_slot": 1, - "target_id": 210, - "target_slot": 0, - "type": "VIDEO" + "id": 89, + "origin_id": 74, + "origin_slot": 0, + "target_id": 75, + "target_slot": 1, + "type": "STRING" }, { - "id": 129, - "origin_id": 68, + "id": 88, + "origin_id": 76, + "origin_slot": 0, + "target_id": 75, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 84, + "origin_id": 65, "origin_slot": 0, "target_id": -20, "target_slot": 0, - "type": "VIDEO" + "type": "IMAGE" }, { - "id": 355, + "id": 102, + "origin_id": -10, + "origin_slot": 0, + "target_id": 78, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 103, + "origin_id": -10, + "origin_slot": 1, + "target_id": 76, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 104, "origin_id": -10, "origin_slot": 2, - "target_id": 49, - "target_slot": 6, + "target_id": 71, + "target_slot": 0, "type": "INT" }, { - "id": 356, + "id": 105, "origin_id": -10, "origin_slot": 3, - "target_id": 49, - "target_slot": 7, + "target_id": 71, + "target_slot": 1, "type": "INT" }, { - "id": 358, - "origin_id": 210, - "origin_slot": 0, - "target_id": 229, - "target_slot": 0, - "type": "IMAGE" - }, - { - "id": 359, - "origin_id": 229, - "origin_slot": 0, - "target_id": 211, - "target_slot": 0, - "type": "IMAGE" - }, - { - "id": 360, - "origin_id": 229, - "origin_slot": 0, - "target_id": 213, - "target_slot": 0, - "type": "IMAGE" - }, - { - "id": 361, + "id": 106, "origin_id": -10, "origin_slot": 4, - "target_id": 49, + "target_id": 70, "target_slot": 5, - "type": "IMAGE" + "type": "INT" }, { - "id": 362, - "origin_id": 210, - "origin_slot": 1, - "target_id": 68, - "target_slot": 1, - "type": "AUDIO" - }, - { - "id": 363, + "id": 107, "origin_id": -10, "origin_slot": 5, - "target_id": 140, - "target_slot": 0, - "type": "COMBO" + "target_id": 70, + "target_slot": 6, + "type": "FLOAT" }, { - "id": 364, + "id": 108, "origin_id": -10, "origin_slot": 6, - "target_id": 154, - "target_slot": 1, - "type": "COMBO" + "target_id": 70, + "target_slot": 4, + "type": "INT" }, { - "id": 365, + "id": 109, "origin_id": -10, "origin_slot": 7, - "target_id": 38, + "target_id": 66, "target_slot": 0, "type": "COMBO" }, { - "id": 366, + "id": 110, "origin_id": -10, "origin_slot": 8, - "target_id": 39, + "target_id": 62, "target_slot": 0, "type": "COMBO" + }, + { + "id": 112, + "origin_id": 91, + "origin_slot": 0, + "target_id": 74, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 113, + "origin_id": -10, + "origin_slot": 2, + "target_id": 92, + "target_slot": 0, + "type": "*" + }, + { + "id": 114, + "origin_id": -10, + "origin_slot": 3, + "target_id": 93, + "target_slot": 0, + "type": "*" + }, + { + "id": 115, + "origin_id": 83, + "origin_slot": 0, + "target_id": 94, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 116, + "origin_id": 92, + "origin_slot": 0, + "target_id": 94, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 117, + "origin_id": 94, + "origin_slot": 0, + "target_id": 95, + "target_slot": 0, + "type": "STRING" + }, + { + "id": 118, + "origin_id": 93, + "origin_slot": 0, + "target_id": 95, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 119, + "origin_id": 95, + "origin_slot": 0, + "target_id": 74, + "target_slot": 2, + "type": "STRING" + }, + { + "id": 120, + "origin_id": -10, + "origin_slot": 9, + "target_id": 91, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 121, + "origin_id": -10, + "origin_slot": 10, + "target_id": 63, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 124, + "origin_id": 75, + "origin_slot": 0, + "target_id": 97, + "target_slot": 0, + "type": "STRING" } ], - "extra": { - "workflowRendererVersion": "LG" - }, - "category": "Video generation and editing/Inpaint video" + "extra": {}, + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Baidu’s open ERNIE Image (~8B DiT): bilingual in-image typography and layouts (posters, infographics, multi-panel compositions) alongside general scenes, with bundled encoders and VAE." } ] }, - "config": {}, "extra": { - "workflowRendererVersion": "LG", - "ds": { - "scale": 0.8183828377358485, - "offset": [ - 1215.8643989712405, - 178.87024992690183 - ] - } - }, - "version": 0.4 + "ue_links": [] + } } diff --git a/blueprints/Text to Image (Flux.1 Dev).json b/blueprints/Text to Image (Flux.1 Dev).json index 04c3cb95a..6d8446e81 100644 --- a/blueprints/Text to Image (Flux.1 Dev).json +++ b/blueprints/Text to Image (Flux.1 Dev).json @@ -1029,7 +1029,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "Generates images from prompts using FLUX.1 [dev]: a 12B rectified-flow MMDiT with dual CLIP plus T5-XXL text encoders and guidance-distilled sampling for sharp prompt following versus classic DDPM diffusion." } ] }, @@ -1043,4 +1044,4 @@ }, "ue_links": [] } -} \ No newline at end of file +} diff --git a/blueprints/Text to Image (Flux.1 Krea Dev).json b/blueprints/Text to Image (Flux.1 Krea Dev).json index fe4db1cfc..0d7fa03c4 100644 --- a/blueprints/Text to Image (Flux.1 Krea Dev).json +++ b/blueprints/Text to Image (Flux.1 Krea Dev).json @@ -1023,7 +1023,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "FLUX.1 Krea [dev] (Black Forest Labs × Krea): open-weight 12B rectified-flow text-to-image drop-in alongside FLUX.1 [dev], tuned away from overcooked saturation toward more natural diversity in people, realism, and style while keeping ecosystem compatibility." } ] }, @@ -1037,4 +1038,4 @@ }, "ue_links": [] } -} \ No newline at end of file +} diff --git a/blueprints/Text to Image (Flux.2 Dev).json b/blueprints/Text to Image (Flux.2 Dev).json new file mode 100644 index 000000000..d5ca3077d --- /dev/null +++ b/blueprints/Text to Image (Flux.2 Dev).json @@ -0,0 +1,1870 @@ +{ + "revision": 0, + "last_node_id": 123, + "last_link_id": 0, + "nodes": [ + { + "id": 123, + "type": "85066daf-feda-4c7b-bbc3-d4797e8ccf0f", + "pos": [ + -800, + 640 + ], + "size": [ + 400, + 0 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "label": "prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": null + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + }, + { + "label": "turbo_lora", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": null + }, + { + "label": "enable_turbo_mode", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": null + }, + { + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "115", + "text" + ], + [ + "113", + "width" + ], + [ + "113", + "height" + ], + [ + "122", + "unet_name" + ], + [ + "111", + "clip_name" + ], + [ + "108", + "vae_name" + ], + [ + "116", + "lora_name" + ], + [ + "121", + "value" + ], + [ + "114", + "noise_seed" + ], + [ + "114", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": { + "value": true, + "lora_name": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Text to Image (Flux.2 Dev)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "85066daf-feda-4c7b-bbc3-d4797e8ccf0f", + "version": 1, + "state": { + "lastGroupId": 6, + "lastNodeId": 123, + "lastLinkId": 232, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Text to Image (Flux.2 Dev)", + "inputNode": { + "id": -10, + "bounding": [ + -1500, + 250, + 151.744140625, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1560, + -20, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "1f4f1091-3f97-41d8-8ed8-e8b02260cf3c", + "name": "text", + "type": "STRING", + "linkIds": [ + 206 + ], + "label": "prompt", + "pos": [ + -1368.255859375, + 270 + ] + }, + { + "id": "b9b59411-4f5f-4482-8f78-369e6d50e71c", + "name": "width", + "type": "INT", + "linkIds": [ + 222, + 231 + ], + "pos": [ + -1368.255859375, + 290 + ] + }, + { + "id": "c6de9a28-3bf6-40d0-be16-f75ec517a766", + "name": "height", + "type": "INT", + "linkIds": [ + 223, + 232 + ], + "pos": [ + -1368.255859375, + 310 + ] + }, + { + "id": "8f1b1c75-e47c-45f5-af57-74abcfe8967c", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 225 + ], + "pos": [ + -1368.255859375, + 330 + ] + }, + { + "id": "6ac27631-1bf0-4161-9670-a662f6180b94", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 226 + ], + "pos": [ + -1368.255859375, + 350 + ] + }, + { + "id": "932e6cbe-f716-4905-ae54-d2b3543497bd", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 227 + ], + "pos": [ + -1368.255859375, + 370 + ] + }, + { + "id": "37400048-5e7b-427b-8b79-ea35841d5306", + "name": "lora_name", + "type": "COMBO", + "linkIds": [ + 228 + ], + "label": "turbo_lora", + "pos": [ + -1368.255859375, + 390 + ] + }, + { + "id": "333212d0-f027-476f-8b97-a921e20e340a", + "name": "value", + "type": "BOOLEAN", + "linkIds": [ + 229 + ], + "label": "enable_turbo_mode", + "pos": [ + -1368.255859375, + 410 + ] + }, + { + "id": "e7e73fad-ce6e-48d5-b719-e2abed685185", + "name": "noise_seed", + "type": "INT", + "linkIds": [ + 230 + ], + "pos": [ + -1368.255859375, + 430 + ] + } + ], + "outputs": [ + { + "id": "ed3c0a0f-a39f-453e-907f-8249c8e3335d", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 9 + ], + "localized_name": "IMAGE", + "pos": [ + 1580, + 0 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 105, + "type": "BasicGuider", + "pos": [ + 570, + 170 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 210 + }, + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 165 + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "slot_index": 0, + "links": [ + 30 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "BasicGuider", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 106, + "type": "FluxGuidance", + "pos": [ + -510, + 470 + ], + "size": [ + 320, + 110 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 41 + }, + { + "localized_name": "guidance", + "name": "guidance", + "type": "FLOAT", + "widget": { + "name": "guidance" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 165 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "FluxGuidance", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 4 + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 107, + "type": "KSamplerSelect", + "pos": [ + 570, + 350 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "SAMPLER", + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 19 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "KSamplerSelect", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "euler" + ] + }, + { + "id": 108, + "type": "VAELoader", + "pos": [ + -1000, + 460 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 227 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "slot_index": 0, + "links": [ + 159 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAELoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "full_encoder_small_decoder.safetensors", + "url": "https://huggingface.co/black-forest-labs/FLUX.2-small-decoder/resolve/main/full_encoder_small_decoder.safetensors", + "directory": "vae" + } + ] + }, + "widgets_values": [ + "full_encoder_small_decoder.safetensors" + ] + }, + { + "id": 109, + "type": "SamplerCustomAdvanced", + "pos": [ + 860, + -20 + ], + "size": [ + 280, + 330 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 37 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 30 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 19 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 132 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 161 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "slot_index": 0, + "links": [ + 24 + ] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "SamplerCustomAdvanced", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 110, + "type": "VAEDecode", + "pos": [ + 1220, + -20 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 24 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 159 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 9 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEDecode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 111, + "type": "CLIPLoader", + "pos": [ + -1000, + 200 + ], + "size": [ + 300, + 150 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 226 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 117 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "mistral_3_small_flux2_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/text_encoders/mistral_3_small_flux2_bf16.safetensors", + "directory": "text_encoders" + } + ] + }, + "widgets_values": [ + "mistral_3_small_flux2_bf16.safetensors", + "flux2", + "default" + ] + }, + { + "id": 112, + "type": "Flux2Scheduler", + "pos": [ + 570, + 550 + ], + "size": [ + 230, + 170 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 213 + }, + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 231 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 232 + } + ], + "outputs": [ + { + "localized_name": "SIGMAS", + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 132 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "Flux2Scheduler", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 20, + 1024, + 1024 + ] + }, + { + "id": 113, + "type": "EmptyFlux2LatentImage", + "pos": [ + -980, + 660 + ], + "size": [ + 270, + 170 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 222 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 223 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 161 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "EmptyFlux2LatentImage", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 114, + "type": "RandomNoise", + "pos": [ + 570, + -20 + ], + "size": [ + 230, + 110 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "noise_seed", + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": 230 + } + ], + "outputs": [ + { + "localized_name": "NOISE", + "name": "NOISE", + "type": "NOISE", + "links": [ + 37 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "RandomNoise", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1027111520328378, + "randomize" + ] + }, + { + "id": 115, + "type": "CLIPTextEncode", + "pos": [ + -630, + -40 + ], + "size": [ + 440, + 450 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 117 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 206 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 41 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 116, + "type": "LoraLoaderModelOnly", + "pos": [ + -150, + 220 + ], + "size": [ + 300, + 140 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 221 + }, + { + "localized_name": "lora_name", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": 228 + }, + { + "localized_name": "strength_model", + "name": "strength_model", + "type": "FLOAT", + "widget": { + "name": "strength_model" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 209 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.7.0", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "LoraLoaderModelOnly", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "Flux_2-Turbo-LoRA_comfyui.safetensors", + "url": "https://huggingface.co/ByteZSzn/Flux.2-Turbo-ComfyUI/resolve/main/Flux_2-Turbo-LoRA_comfyui.safetensors", + "directory": "loras" + } + ] + }, + "widgets_values": [ + "Flux_2-Turbo-LoRA_comfyui.safetensors", + 1 + ] + }, + { + "id": 117, + "type": "ComfySwitchNode", + "pos": [ + 220, + -30 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 208 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 209 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 215 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 210 + ] + } + ], + "title": "Switch(model)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 118, + "type": "PrimitiveInt", + "pos": [ + -140, + -30 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 211 + ] + } + ], + "title": "Steps", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 20, + "fixed" + ] + }, + { + "id": 119, + "type": "PrimitiveInt", + "pos": [ + -150, + 460 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 212 + ] + } + ], + "title": "Steps", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveInt", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 8, + "fixed" + ] + }, + { + "id": 120, + "type": "ComfySwitchNode", + "pos": [ + 220, + 260 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 211 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 212 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 214 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 213 + ] + } + ], + "title": "Switch(steps)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 121, + "type": "PrimitiveBoolean", + "pos": [ + -110, + 690 + ], + "size": [ + 270, + 100 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 229 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 214, + 215 + ] + } + ], + "title": "Enable Turbo LoRA", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.15.1", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "PrimitiveBoolean", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 122, + "type": "UNETLoader", + "pos": [ + -1000, + -30 + ], + "size": [ + 300, + 110 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 225 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 208, + 221 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.71", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "UNETLoader", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "flux2_dev_fp8mixed.safetensors", + "url": "https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/diffusion_models/flux2_dev_fp8mixed.safetensors", + "directory": "diffusion_models" + } + ] + }, + "widgets_values": [ + "flux2_dev_fp8mixed.safetensors", + "default" + ] + } + ], + "groups": [ + { + "id": 1, + "title": "Step 1 - Upload models", + "bounding": [ + -1040, + -110, + 380, + 710 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "Custom sampler", + "bounding": [ + 540, + -110, + 640, + 870 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Step2 - Prompt", + "bounding": [ + -640, + -110, + 460, + 710 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 5, + "title": "Original", + "bounding": [ + -160, + -110, + 320, + 230 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 6, + "title": "8 Steps LoRA", + "bounding": [ + -160, + 140, + 320, + 460 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 165, + "origin_id": 106, + "origin_slot": 0, + "target_id": 105, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 41, + "origin_id": 115, + "origin_slot": 0, + "target_id": 106, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 37, + "origin_id": 114, + "origin_slot": 0, + "target_id": 109, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 30, + "origin_id": 105, + "origin_slot": 0, + "target_id": 109, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 19, + "origin_id": 107, + "origin_slot": 0, + "target_id": 109, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 132, + "origin_id": 112, + "origin_slot": 0, + "target_id": 109, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 161, + "origin_id": 113, + "origin_slot": 0, + "target_id": 109, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 117, + "origin_id": 111, + "origin_slot": 0, + "target_id": 115, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 24, + "origin_id": 109, + "origin_slot": 0, + "target_id": 110, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 159, + "origin_id": 108, + "origin_slot": 0, + "target_id": 110, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 9, + "origin_id": 110, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 206, + "origin_id": -10, + "origin_slot": 0, + "target_id": 115, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 208, + "origin_id": 122, + "origin_slot": 0, + "target_id": 117, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 209, + "origin_id": 116, + "origin_slot": 0, + "target_id": 117, + "target_slot": 1, + "type": "MODEL" + }, + { + "id": 210, + "origin_id": 117, + "origin_slot": 0, + "target_id": 105, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 211, + "origin_id": 118, + "origin_slot": 0, + "target_id": 120, + "target_slot": 0, + "type": "INT" + }, + { + "id": 212, + "origin_id": 119, + "origin_slot": 0, + "target_id": 120, + "target_slot": 1, + "type": "INT" + }, + { + "id": 213, + "origin_id": 120, + "origin_slot": 0, + "target_id": 112, + "target_slot": 0, + "type": "INT" + }, + { + "id": 214, + "origin_id": 121, + "origin_slot": 0, + "target_id": 120, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 215, + "origin_id": 121, + "origin_slot": 0, + "target_id": 117, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 221, + "origin_id": 122, + "origin_slot": 0, + "target_id": 116, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 222, + "origin_id": -10, + "origin_slot": 1, + "target_id": 113, + "target_slot": 0, + "type": "INT" + }, + { + "id": 223, + "origin_id": -10, + "origin_slot": 2, + "target_id": 113, + "target_slot": 1, + "type": "INT" + }, + { + "id": 225, + "origin_id": -10, + "origin_slot": 3, + "target_id": 122, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 226, + "origin_id": -10, + "origin_slot": 4, + "target_id": 111, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 227, + "origin_id": -10, + "origin_slot": 5, + "target_id": 108, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 228, + "origin_id": -10, + "origin_slot": 6, + "target_id": 116, + "target_slot": 1, + "type": "COMBO" + }, + { + "id": 229, + "origin_id": -10, + "origin_slot": 7, + "target_id": 121, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 230, + "origin_id": -10, + "origin_slot": 8, + "target_id": 114, + "target_slot": 0, + "type": "INT" + }, + { + "id": 231, + "origin_id": -10, + "origin_slot": 1, + "target_id": 112, + "target_slot": 1, + "type": "INT" + }, + { + "id": 232, + "origin_id": -10, + "origin_slot": 2, + "target_id": 112, + "target_slot": 2, + "type": "INT" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/Text to image", + "description": "Generates images from prompts using FLUX.2 [dev]: a newer 32B rectified-flow stack with distilled guidance plus a stronger long-context multimodal encoder for complex scenes, sharper typography/UI text, anatomy, lighting, and high-resolution latent decoding." + } + ] + }, + "extra": { + "ue_links": [] + } +} diff --git a/blueprints/Text to Image (NetaYume Lumina).json b/blueprints/Text to Image (NetaYume Lumina).json index 394ad1608..9e11b7a86 100644 --- a/blueprints/Text to Image (NetaYume Lumina).json +++ b/blueprints/Text to Image (NetaYume Lumina).json @@ -1104,7 +1104,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using NetaYume Lumina, fine-tuned from Neta Lumina for anime-style and illustration generation." }, { "id": "a07fdf06-1bda-4dac-bdbd-63ee8ebca1c9", @@ -1458,11 +1459,12 @@ ], "extra": { "workflowRendererVersion": "LG" - } + }, + "description": "Encodes a negative text prompt via CLIP for classifier-free guidance in anime-style generation (NetaYume Lumina)." } ] }, "extra": { "ue_links": [] } -} \ No newline at end of file +} diff --git a/blueprints/Text to Image (Qwen-Image 2512).json b/blueprints/Text to Image (Qwen-Image 2512).json index f52ea2ef2..09612be8b 100644 --- a/blueprints/Text to Image (Qwen-Image 2512).json +++ b/blueprints/Text to Image (Qwen-Image 2512).json @@ -1941,7 +1941,8 @@ "extra": { "workflowRendererVersion": "Vue-corrected" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Qwen-Image-2512, with enhanced human realism and finer natural detail over the base version." } ] }, diff --git a/blueprints/Text to Image (Qwen-Image).json b/blueprints/Text to Image (Qwen-Image).json index 70b4b44b3..e78d5a962 100644 --- a/blueprints/Text to Image (Qwen-Image).json +++ b/blueprints/Text to Image (Qwen-Image).json @@ -1873,7 +1873,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Qwen-Image, Alibaba's 20B MMDiT model with excellent multilingual text rendering." } ] }, diff --git a/blueprints/Text to Image (Z-Image-Base).json b/blueprints/Text to Image (Z-Image-Base).json new file mode 100644 index 000000000..169263712 --- /dev/null +++ b/blueprints/Text to Image (Z-Image-Base).json @@ -0,0 +1,1184 @@ +{ + "revision": 0, + "last_node_id": 126, + "last_link_id": 0, + "nodes": [ + { + "id": 126, + "type": "8a2bb267-5858-4aaf-bdcd-61002711af19", + "pos": [ + -2280, + 2850 + ], + "size": [ + 410, + 560 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "label": "prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": null + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": null + }, + { + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "67", + "text" + ], + [ + "68", + "width" + ], + [ + "68", + "height" + ], + [ + "69", + "steps" + ], + [ + "69", + "cfg" + ], + [ + "69", + "seed" + ], + [ + "66", + "unet_name" + ], + [ + "62", + "clip_name" + ], + [ + "63", + "vae_name" + ], + [ + "69", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.13.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Text to Image (Z-Image-Base)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "8a2bb267-5858-4aaf-bdcd-61002711af19", + "version": 1, + "state": { + "lastGroupId": 16, + "lastNodeId": 126, + "lastLinkId": 229, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Text to Image (Z-Image-Base)", + "description": "Generates images from text prompts using Z-Image base weights with Qwen3 text encoder and bundled VAE.", + "inputNode": { + "id": -10, + "bounding": [ + -220, + 40, + 120, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1840, + -150, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "af36fee5-4f8b-4a8e-bfa8-cb8fe7006cc3", + "name": "text", + "type": "STRING", + "linkIds": [ + 108 + ], + "label": "prompt", + "pos": [ + -120, + 60 + ] + }, + { + "id": "357f0059-e8e6-41f6-a290-c53b0a60c0ed", + "name": "width", + "type": "INT", + "linkIds": [ + 114 + ], + "pos": [ + -120, + 80 + ] + }, + { + "id": "4a442743-a9c2-4aa5-9efd-05d43f3322d3", + "name": "height", + "type": "INT", + "linkIds": [ + 115 + ], + "pos": [ + -120, + 100 + ] + }, + { + "id": "a0fc336b-d349-418e-8415-318653f7b6b3", + "name": "steps", + "type": "INT", + "linkIds": [ + 116 + ], + "pos": [ + -120, + 120 + ] + }, + { + "id": "2f253ace-1e1a-415f-9b95-a10430bd5749", + "name": "cfg", + "type": "FLOAT", + "linkIds": [ + 117 + ], + "pos": [ + -120, + 140 + ] + }, + { + "id": "18a6ad37-23aa-4bf7-a0cd-1d6ca6e2a128", + "name": "seed", + "type": "INT", + "linkIds": [ + 118 + ], + "pos": [ + -120, + 160 + ] + }, + { + "id": "d1fc4937-8505-4ec6-9fc4-a33ef7b45eee", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 119 + ], + "pos": [ + -120, + 180 + ] + }, + { + "id": "db45dd49-d990-4ceb-a849-f96341874cdd", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 120 + ], + "pos": [ + -120, + 200 + ] + }, + { + "id": "37b8eac6-9b1b-452b-81f3-0ba9e34a576a", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 121 + ], + "pos": [ + -120, + 220 + ] + } + ], + "outputs": [ + { + "id": "f2bea309-bfe7-4ccb-9ffe-9475bf1da2ae", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 79 + ], + "localized_name": "IMAGE", + "pos": [ + 1860, + -130 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 67, + "type": "CLIPTextEncode", + "pos": [ + 600, + -90 + ], + "size": [ + 410, + 320 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 78 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 108 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 75 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 68, + "type": "EmptySD3LatentImage", + "pos": [ + 240, + 620 + ], + "size": [ + 260, + 170 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 114 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 115 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 77 + ] + } + ], + "properties": { + "Node name for S&R": "EmptySD3LatentImage", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 63, + "type": "VAELoader", + "pos": [ + 230, + 340 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 121 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 73 + ] + } + ], + "properties": { + "Node name for S&R": "VAELoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "ae.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/vae/ae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "ae.safetensors" + ] + }, + { + "id": 62, + "type": "CLIPLoader", + "pos": [ + 230, + 110 + ], + "size": [ + 270, + 150 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 120 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 78, + 82 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "qwen_3_4b.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "qwen_3_4b.safetensors", + "lumina2", + "default" + ] + }, + { + "id": 65, + "type": "VAEDecode", + "pos": [ + 1450, + -150 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 72 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 73 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 79 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 70, + "type": "ModelSamplingAuraFlow", + "pos": [ + 1100, + -150 + ], + "size": [ + 310, + 110 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 109 + }, + { + "localized_name": "shift", + "name": "shift", + "type": "FLOAT", + "widget": { + "name": "shift" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 74 + ] + } + ], + "properties": { + "Node name for S&R": "ModelSamplingAuraFlow", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 3 + ] + }, + { + "id": 66, + "type": "UNETLoader", + "pos": [ + 230, + -90 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 119 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 109 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "models": [ + { + "name": "z_image_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image/resolve/main/split_files/diffusion_models/z_image_bf16.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "z_image_bf16.safetensors", + "default" + ] + }, + { + "id": 71, + "type": "CLIPTextEncode", + "pos": [ + 600, + 310 + ], + "size": [ + 390, + 140 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 82 + }, + { + "label": "prompt", + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 83 + ] + } + ], + "title": "CLIP Text Encode (Negative Prompt)", + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.73", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 69, + "type": "KSampler", + "pos": [ + 1100, + 10 + ], + "size": [ + 310, + 440 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 74 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 75 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 83 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 77 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 118 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 116 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": 117 + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 72 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + "randomize", + 25, + 4, + "res_multistep", + "simple", + 1 + ] + }, + { + "id": 87, + "type": "MarkdownNote", + "pos": [ + 1110, + -360 + ], + "size": [ + 300, + 120 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "- Steps: 30~50\n- cfg: 3~5" + ], + "color": "#222", + "bgcolor": "#000", + "title": "Original Settings" + } + ], + "groups": [ + { + "id": 2, + "title": "Step2 - Image size", + "bounding": [ + 200, + 530, + 330, + 287.9999544955691 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 3, + "title": "Step3 - Prompt", + "bounding": [ + 570, + -200, + 470, + 700 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 4, + "title": "Step1 - Load models", + "bounding": [ + 200, + -200, + 330, + 700 + ], + "color": "#3f789e", + "flags": {} + } + ], + "links": [ + { + "id": 78, + "origin_id": 62, + "origin_slot": 0, + "target_id": 67, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 74, + "origin_id": 70, + "origin_slot": 0, + "target_id": 69, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 75, + "origin_id": 67, + "origin_slot": 0, + "target_id": 69, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 83, + "origin_id": 71, + "origin_slot": 0, + "target_id": 69, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 77, + "origin_id": 68, + "origin_slot": 0, + "target_id": 69, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 82, + "origin_id": 62, + "origin_slot": 0, + "target_id": 71, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 72, + "origin_id": 69, + "origin_slot": 0, + "target_id": 65, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 73, + "origin_id": 63, + "origin_slot": 0, + "target_id": 65, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 79, + "origin_id": 65, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 108, + "origin_id": -10, + "origin_slot": 0, + "target_id": 67, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 109, + "origin_id": 66, + "origin_slot": 0, + "target_id": 70, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 114, + "origin_id": -10, + "origin_slot": 1, + "target_id": 68, + "target_slot": 0, + "type": "INT" + }, + { + "id": 115, + "origin_id": -10, + "origin_slot": 2, + "target_id": 68, + "target_slot": 1, + "type": "INT" + }, + { + "id": 116, + "origin_id": -10, + "origin_slot": 3, + "target_id": 69, + "target_slot": 5, + "type": "INT" + }, + { + "id": 117, + "origin_id": -10, + "origin_slot": 4, + "target_id": 69, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 118, + "origin_id": -10, + "origin_slot": 5, + "target_id": 69, + "target_slot": 4, + "type": "INT" + }, + { + "id": 119, + "origin_id": -10, + "origin_slot": 6, + "target_id": 66, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 120, + "origin_id": -10, + "origin_slot": 7, + "target_id": 62, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 121, + "origin_id": -10, + "origin_slot": 8, + "target_id": 63, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/Text to image" + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Text to Image (Z-Image-Turbo).json b/blueprints/Text to Image (Z-Image-Turbo).json index 6aa80e327..2501486fa 100644 --- a/blueprints/Text to Image (Z-Image-Turbo).json +++ b/blueprints/Text to Image (Z-Image-Turbo).json @@ -1,22 +1,21 @@ { - "id": "1c3eaa76-5cfa-4dc7-8571-97a570324e01", "revision": 0, - "last_node_id": 34, - "last_link_id": 40, + "last_node_id": 57, + "last_link_id": 0, "nodes": [ { - "id": 5, - "type": "dfe9eb32-97c0-43a5-90d5-4fd37768d91b", + "id": 57, + "type": "f2fdebf6-dfaf-43b6-9eb2-7f70613cfdc1", "pos": [ - -2.5766491043910378e-05, - 1229.999928629805 + 130, + 200 ], "size": [ 400, 470 ], "flags": {}, - "order": 0, + "order": 1, "mode": 0, "inputs": [ { @@ -44,6 +43,22 @@ }, "link": null }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, { "name": "unet_name", "type": "COMBO", @@ -80,15 +95,15 @@ "properties": { "proxyWidgets": [ [ - "-1", + "27", "text" ], [ - "-1", + "13", "width" ], [ - "-1", + "13", "height" ], [ @@ -97,19 +112,23 @@ ], [ "3", - "control_after_generate" + "steps" ], [ - "-1", + "28", "unet_name" ], [ - "-1", + "30", "clip_name" ], [ - "-1", + "29", "vae_name" + ], + [ + "3", + "control_after_generate" ] ], "cnr_id": "comfy-core", @@ -122,48 +141,40 @@ "secondTabOffset": 80, "secondTabWidth": 65 }, - "widgets_values": [ - "", - 1024, - 1024, - null, - null, - "z_image_turbo_bf16.safetensors", - "qwen_3_4b.safetensors", - "ae.safetensors" - ] + "widgets_values": [], + "title": "Text to Image (Z-Image-Turbo)" } ], "links": [], - "groups": [], + "version": 0.4, "definitions": { "subgraphs": [ { - "id": "dfe9eb32-97c0-43a5-90d5-4fd37768d91b", + "id": "f2fdebf6-dfaf-43b6-9eb2-7f70613cfdc1", "version": 1, "state": { "lastGroupId": 4, - "lastNodeId": 34, - "lastLinkId": 40, + "lastNodeId": 61, + "lastLinkId": 75, "lastRerouteId": 0 }, "revision": 0, "config": {}, - "name": "local-Text to Image (Z-Image-Turbo)", + "name": "Text to Image (Z-Image-Turbo)", "inputNode": { "id": -10, "bounding": [ - -80, - 425, + -560, + 480, 120, - 160 + 200 ] }, "outputNode": { "id": -20, "bounding": [ - 1490, - 415, + 1670, + 320, 120, 60 ] @@ -178,8 +189,8 @@ ], "label": "prompt", "pos": [ - 20, - 445 + -460, + 500 ] }, { @@ -190,8 +201,8 @@ 35 ], "pos": [ - 20, - 465 + -460, + 520 ] }, { @@ -202,44 +213,68 @@ 36 ], "pos": [ - 20, - 485 + -460, + 540 ] }, { - "id": "23087d15-8412-4fbd-b71e-9b6d7ef76de1", + "id": "f77677f7-6bf6-4c19-a71f-c4a553d5981e", + "name": "seed", + "type": "INT", + "linkIds": [ + 71 + ], + "pos": [ + -460, + 560 + ] + }, + { + "id": "ef9a9fb1-5983-4bc9-a60b-cf5aec48bff1", + "name": "steps", + "type": "INT", + "linkIds": [ + 72 + ], + "pos": [ + -460, + 580 + ] + }, + { + "id": "a20a1b30-785f-4a04-bb6d-3d61adab9764", "name": "unet_name", "type": "COMBO", "linkIds": [ - 38 + 73 ], "pos": [ - 20, - 505 + -460, + 600 ] }, { - "id": "0677f5c3-2a3f-43d4-98ac-a4c56d5efdc0", + "id": "4af8fc2b-4655-4086-8240-45f8cb38c6f6", "name": "clip_name", "type": "COMBO", "linkIds": [ - 39 + 74 ], "pos": [ - 20, - 525 + -460, + 620 ] }, { - "id": "c85c0445-2641-48b1-bbca-95057edf2fcf", + "id": "4d518693-2807-439c-9cb6-cffd23ccba2c", "name": "vae_name", "type": "COMBO", "linkIds": [ - 40 + 75 ], "pos": [ - 20, - 545 + -460, + 640 ] } ], @@ -253,8 +288,8 @@ ], "localized_name": "IMAGE", "pos": [ - 1510, - 435 + 1690, + 340 ] } ], @@ -264,15 +299,15 @@ "id": 30, "type": "CLIPLoader", "pos": [ - 109.99997264844609, - 329.99999029608756 + 30, + 420 ], "size": [ - 269.9869791666667, - 106 + 270, + 150 ], "flags": {}, - "order": 0, + "order": 7, "mode": 0, "inputs": [ { @@ -282,7 +317,7 @@ "widget": { "name": "clip_name" }, - "link": 39 + "link": 74 }, { "localized_name": "type", @@ -315,9 +350,9 @@ } ], "properties": { + "Node name for S&R": "CLIPLoader", "cnr_id": "comfy-core", "ver": "0.3.73", - "Node name for S&R": "CLIPLoader", "models": [ { "name": "qwen_3_4b.safetensors", @@ -343,15 +378,15 @@ "id": 29, "type": "VAELoader", "pos": [ - 109.99997264844609, - 479.9999847172637 + 30, + 650 ], "size": [ - 269.9869791666667, - 58 + 270, + 110 ], "flags": {}, - "order": 1, + "order": 6, "mode": 0, "inputs": [ { @@ -361,7 +396,7 @@ "widget": { "name": "vae_name" }, - "link": 40 + "link": 75 } ], "outputs": [ @@ -375,9 +410,9 @@ } ], "properties": { + "Node name for S&R": "VAELoader", "cnr_id": "comfy-core", "ver": "0.3.73", - "Node name for S&R": "VAELoader", "models": [ { "name": "ae.safetensors", @@ -401,12 +436,12 @@ "id": 33, "type": "ConditioningZeroOut", "pos": [ - 639.9999103333332, - 620.0000271257795 + 630, + 960 ], "size": [ - 204.134765625, - 26 + 230, + 80 ], "flags": {}, "order": 8, @@ -430,9 +465,9 @@ } ], "properties": { + "Node name for S&R": "ConditioningZeroOut", "cnr_id": "comfy-core", "ver": "0.3.73", - "Node name for S&R": "ConditioningZeroOut", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -440,22 +475,21 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65 - }, - "widgets_values": [] + } }, { "id": 8, "type": "VAEDecode", "pos": [ - 1219.9999088104782, - 160.00009184959066 + 1320, + 230 ], "size": [ - 209.98697916666669, - 46 + 230, + 100 ], "flags": {}, - "order": 5, + "order": 1, "mode": 0, "inputs": [ { @@ -483,9 +517,9 @@ } ], "properties": { + "Node name for S&R": "VAEDecode", "cnr_id": "comfy-core", "ver": "0.3.64", - "Node name for S&R": "VAEDecode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -493,22 +527,21 @@ "secondTabText": "Send Back", "secondTabOffset": 80, "secondTabWidth": 65 - }, - "widgets_values": [] + } }, { "id": 28, "type": "UNETLoader", "pos": [ - 109.99997264844609, - 200.0000502647102 + 30, + 230 ], "size": [ - 269.9869791666667, - 82 + 270, + 110 ], "flags": {}, - "order": 2, + "order": 5, "mode": 0, "inputs": [ { @@ -518,7 +551,7 @@ "widget": { "name": "unet_name" }, - "link": 38 + "link": 73 }, { "localized_name": "weight_dtype", @@ -541,9 +574,9 @@ } ], "properties": { + "Node name for S&R": "UNETLoader", "cnr_id": "comfy-core", "ver": "0.3.73", - "Node name for S&R": "UNETLoader", "models": [ { "name": "z_image_turbo_bf16.safetensors", @@ -568,15 +601,15 @@ "id": 27, "type": "CLIPTextEncode", "pos": [ - 429.99997828947767, - 200.0000502647102 + 400, + 230 ], "size": [ - 409.9869791666667, - 319.9869791666667 + 450, + 650 ], "flags": {}, - "order": 7, + "order": 4, "mode": 0, "inputs": [ { @@ -607,9 +640,9 @@ } ], "properties": { + "Node name for S&R": "CLIPTextEncode", "cnr_id": "comfy-core", "ver": "0.3.73", - "Node name for S&R": "CLIPTextEncode", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -626,15 +659,15 @@ "id": 13, "type": "EmptySD3LatentImage", "pos": [ - 109.99997264844609, - 629.9999791384399 + 40, + 890 ], "size": [ - 259.9869791666667, - 106 + 260, + 170 ], "flags": {}, - "order": 6, + "order": 3, "mode": 0, "inputs": [ { @@ -677,9 +710,9 @@ } ], "properties": { + "Node name for S&R": "EmptySD3LatentImage", "cnr_id": "comfy-core", "ver": "0.3.64", - "Node name for S&R": "EmptySD3LatentImage", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -694,19 +727,77 @@ 1 ] }, + { + "id": 11, + "type": "ModelSamplingAuraFlow", + "pos": [ + 950, + 230 + ], + "size": [ + 310, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 26 + }, + { + "localized_name": "shift", + "name": "shift", + "type": "FLOAT", + "widget": { + "name": "shift" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 13 + ] + } + ], + "properties": { + "Node name for S&R": "ModelSamplingAuraFlow", + "cnr_id": "comfy-core", + "ver": "0.3.64", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 3 + ] + }, { "id": 3, "type": "KSampler", "pos": [ - 879.9999615530063, - 269.9999774911694 + 950, + 400 ], "size": [ - 314.9869791666667, - 262 + 320, + 350 ], "flags": {}, - "order": 4, + "order": 0, "mode": 0, "inputs": [ { @@ -740,7 +831,7 @@ "widget": { "name": "seed" }, - "link": null + "link": 71 }, { "localized_name": "steps", @@ -749,7 +840,7 @@ "widget": { "name": "steps" }, - "link": null + "link": 72 }, { "localized_name": "cfg", @@ -800,9 +891,9 @@ } ], "properties": { + "Node name for S&R": "KSampler", "cnr_id": "comfy-core", "ver": "0.3.64", - "Node name for S&R": "KSampler", "enableTabs": false, "tabWidth": 65, "tabXOffset": 10, @@ -814,81 +905,23 @@ "widgets_values": [ 0, "randomize", - 4, + 8, 1, "res_multistep", "simple", 1 ] - }, - { - "id": 11, - "type": "ModelSamplingAuraFlow", - "pos": [ - 879.9999615530063, - 160.00009184959066 - ], - "size": [ - 309.9869791666667, - 58 - ], - "flags": {}, - "order": 3, - "mode": 0, - "inputs": [ - { - "localized_name": "model", - "name": "model", - "type": "MODEL", - "link": 26 - }, - { - "localized_name": "shift", - "name": "shift", - "type": "FLOAT", - "widget": { - "name": "shift" - }, - "link": null - } - ], - "outputs": [ - { - "localized_name": "MODEL", - "name": "MODEL", - "type": "MODEL", - "slot_index": 0, - "links": [ - 13 - ] - } - ], - "properties": { - "cnr_id": "comfy-core", - "ver": "0.3.64", - "Node name for S&R": "ModelSamplingAuraFlow", - "enableTabs": false, - "tabWidth": 65, - "tabXOffset": 10, - "hasSecondTab": false, - "secondTabText": "Send Back", - "secondTabOffset": 80, - "secondTabWidth": 65 - }, - "widgets_values": [ - 3 - ] } ], "groups": [ { "id": 2, - "title": "Image size", + "title": "Step2 - Image size", "bounding": [ - 100, - 560, - 290, - 200 + 10, + 820, + 320, + 280 ], "color": "#3f789e", "font_size": 24, @@ -896,12 +929,12 @@ }, { "id": 3, - "title": "Prompt", + "title": "Step3 - Prompt", "bounding": [ - 410, + 360, 130, - 450, - 540 + 530, + 970 ], "color": "#3f789e", "font_size": 24, @@ -909,12 +942,12 @@ }, { "id": 4, - "title": "Models", + "title": "Step1 - Load models", "bounding": [ - 100, + 0, 130, - 290, - 413.6 + 330, + 660 ], "color": "#3f789e", "font_size": 24, @@ -1027,25 +1060,41 @@ "type": "INT" }, { - "id": 38, + "id": 71, "origin_id": -10, "origin_slot": 3, + "target_id": 3, + "target_slot": 4, + "type": "INT" + }, + { + "id": 72, + "origin_id": -10, + "origin_slot": 4, + "target_id": 3, + "target_slot": 5, + "type": "INT" + }, + { + "id": 73, + "origin_id": -10, + "origin_slot": 5, "target_id": 28, "target_slot": 0, "type": "COMBO" }, { - "id": 39, + "id": 74, "origin_id": -10, - "origin_slot": 4, + "origin_slot": 6, "target_id": 30, "target_slot": 0, "type": "COMBO" }, { - "id": 40, + "id": 75, "origin_id": -10, - "origin_slot": 5, + "origin_slot": 7, "target_id": 29, "target_slot": 0, "type": "COMBO" @@ -1054,25 +1103,10 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image generation and editing/Text to image" + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Z-Image-Turbo, Alibaba's distilled 6B DiT model." } ] }, - "config": {}, - "extra": { - "frontendVersion": "1.37.10", - "workflowRendererVersion": "LG", - "VHS_latentpreview": false, - "VHS_latentpreviewrate": 0, - "VHS_MetadataImage": true, - "VHS_KeepIntermediate": true, - "ds": { - "scale": 0.8401370345180755, - "offset": [ - 940.0587067393087, - -830.7121087564725 - ] - } - }, - "version": 0.4 -} + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Text to Image.json b/blueprints/Text to Image.json new file mode 100644 index 000000000..ffe3682ff --- /dev/null +++ b/blueprints/Text to Image.json @@ -0,0 +1,1132 @@ +{ + "revision": 0, + "last_node_id": 71, + "last_link_id": 0, + "nodes": [ + { + "id": 71, + "type": "2d5985c9-deef-41ae-9c34-6353d3d7d1ef", + "pos": [ + 90, + 800 + ], + "size": [ + 400, + 80 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "label": "prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": null + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + }, + { + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + } + ], + "title": "Text to Image", + "properties": { + "proxyWidgets": [ + [ + "67", + "text" + ], + [ + "68", + "width" + ], + [ + "68", + "height" + ], + [ + "66", + "unet_name" + ], + [ + "62", + "clip_name" + ], + [ + "63", + "vae_name" + ], + [ + "70", + "steps" + ], + [ + "70", + "control_after_generate" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": { + "text": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "2d5985c9-deef-41ae-9c34-6353d3d7d1ef", + "version": 1, + "state": { + "lastGroupId": 4, + "lastNodeId": 71, + "lastLinkId": 70, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Text to Image", + "inputNode": { + "id": -10, + "bounding": [ + -80, + 425, + 120, + 180 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1490, + 415, + 120, + 60 + ] + }, + "inputs": [ + { + "id": "fb178669-e742-4a53-8a69-7df59834dfd8", + "name": "text", + "type": "STRING", + "linkIds": [ + 34 + ], + "label": "prompt", + "pos": [ + 20, + 445 + ] + }, + { + "id": "dd780b3c-23e9-46ff-8469-156008f42e5a", + "name": "width", + "type": "INT", + "linkIds": [ + 35 + ], + "pos": [ + 20, + 465 + ] + }, + { + "id": "7b08d546-6bb0-4ef9-82e9-ffae5e1ee6bc", + "name": "height", + "type": "INT", + "linkIds": [ + 36 + ], + "pos": [ + 20, + 485 + ] + }, + { + "id": "8ed4eb73-a2bf-4766-8bf4-c5890b560596", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 38 + ], + "pos": [ + 20, + 505 + ] + }, + { + "id": "f362d639-d412-4b5d-8490-1e9995dc5f82", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 39 + ], + "pos": [ + 20, + 525 + ] + }, + { + "id": "ee25ac16-de63-4b74-bbbb-5b29fdc1efcf", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 40 + ], + "pos": [ + 20, + 545 + ] + }, + { + "id": "51cbcd61-9218-4bcb-89ac-ecdfb1ef8892", + "name": "steps", + "type": "INT", + "linkIds": [ + 70 + ], + "pos": [ + 20, + 565 + ] + } + ], + "outputs": [ + { + "id": "1fa72a21-ce00-4952-814e-1f2ffbe87d1d", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 16 + ], + "localized_name": "IMAGE", + "pos": [ + 1510, + 435 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 62, + "type": "CLIPLoader", + "pos": [ + 110, + 330 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 39 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 28 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPLoader", + "models": [ + { + "name": "qwen_3_4b.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "qwen_3_4b.safetensors", + "lumina2", + "default" + ] + }, + { + "id": 63, + "type": "VAELoader", + "pos": [ + 110, + 480 + ], + "size": [ + 270, + 60 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 40 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 27 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAELoader", + "models": [ + { + "name": "ae.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/vae/ae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "ae.safetensors" + ] + }, + { + "id": 64, + "type": "ConditioningZeroOut", + "pos": [ + 640, + 620 + ], + "size": [ + 210, + 30 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "conditioning", + "name": "conditioning", + "type": "CONDITIONING", + "link": 32 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 33 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ConditioningZeroOut", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 65, + "type": "VAEDecode", + "pos": [ + 1220, + 160 + ], + "size": [ + 210, + 50 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 14 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 27 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 16 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "VAEDecode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 66, + "type": "UNETLoader", + "pos": [ + 110, + 200 + ], + "size": [ + 270, + 90 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 38 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 26 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "UNETLoader", + "models": [ + { + "name": "z_image_turbo_bf16.safetensors", + "url": "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/diffusion_models/z_image_turbo_bf16.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "z_image_turbo_bf16.safetensors", + "default" + ] + }, + { + "id": 67, + "type": "CLIPTextEncode", + "pos": [ + 430, + 200 + ], + "size": [ + 410, + 370 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 28 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 34 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 30, + 32 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.73", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "CLIPTextEncode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ] + }, + { + "id": 68, + "type": "EmptySD3LatentImage", + "pos": [ + 110, + 630 + ], + "size": [ + 260, + 110 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 35 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 36 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 17 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "EmptySD3LatentImage", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1024, + 1024, + 1 + ] + }, + { + "id": 69, + "type": "ModelSamplingAuraFlow", + "pos": [ + 880, + 160 + ], + "size": [ + 310, + 60 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 26 + }, + { + "localized_name": "shift", + "name": "shift", + "type": "FLOAT", + "widget": { + "name": "shift" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 13 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "ModelSamplingAuraFlow", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 3 + ] + }, + { + "id": 70, + "type": "KSampler", + "pos": [ + 880, + 270 + ], + "size": [ + 320, + 270 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 13 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 30 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 33 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 17 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 70 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 14 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.64", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + }, + "Node name for S&R": "KSampler", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + "randomize", + 8, + 1, + "res_multistep", + "simple", + 1 + ] + } + ], + "groups": [ + { + "id": 2, + "title": "Step2 - Image size", + "bounding": [ + 100, + 560, + 290, + 200 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "Step3 - Prompt", + "bounding": [ + 410, + 130, + 450, + 540 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Step1 - Load models", + "bounding": [ + 100, + 130, + 290, + 413.6 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "links": [ + { + "id": 32, + "origin_id": 67, + "origin_slot": 0, + "target_id": 64, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 26, + "origin_id": 66, + "origin_slot": 0, + "target_id": 69, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 14, + "origin_id": 70, + "origin_slot": 0, + "target_id": 65, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 27, + "origin_id": 63, + "origin_slot": 0, + "target_id": 65, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 13, + "origin_id": 69, + "origin_slot": 0, + "target_id": 70, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 30, + "origin_id": 67, + "origin_slot": 0, + "target_id": 70, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 33, + "origin_id": 64, + "origin_slot": 0, + "target_id": 70, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 17, + "origin_id": 68, + "origin_slot": 0, + "target_id": 70, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 28, + "origin_id": 62, + "origin_slot": 0, + "target_id": 67, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 16, + "origin_id": 65, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 34, + "origin_id": -10, + "origin_slot": 0, + "target_id": 67, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 35, + "origin_id": -10, + "origin_slot": 1, + "target_id": 68, + "target_slot": 0, + "type": "INT" + }, + { + "id": 36, + "origin_id": -10, + "origin_slot": 2, + "target_id": 68, + "target_slot": 1, + "type": "INT" + }, + { + "id": 38, + "origin_id": -10, + "origin_slot": 3, + "target_id": 66, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 39, + "origin_id": -10, + "origin_slot": 4, + "target_id": 62, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 40, + "origin_id": -10, + "origin_slot": 5, + "target_id": 63, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 70, + "origin_id": -10, + "origin_slot": 6, + "target_id": 70, + "target_slot": 5, + "type": "INT" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Image generation and editing/Text to image", + "description": "Generates images from text prompts using Z-Image-Turbo defaults with Qwen3 text encoder and VAE." + } + ] + }, + "extra": {} +} diff --git a/blueprints/Text to Video (LTX-2.3).json b/blueprints/Text to Video (LTX-2.3).json index ff9bc6ccf..f44a216dd 100644 --- a/blueprints/Text to Video (LTX-2.3).json +++ b/blueprints/Text to Video (LTX-2.3).json @@ -4286,7 +4286,8 @@ "extra": { "workflowRendererVersion": "Vue-corrected" }, - "category": "Video generation and editing/Text to video" + "category": "Video generation and editing/Text to video", + "description": "Generates video from text prompts using LTX-2.3, Lightricks' video diffusion model." } ] }, diff --git a/blueprints/Text to Video (Wan 2.2).json b/blueprints/Text to Video (Wan 2.2).json index 0ce485b67..a264a490d 100644 --- a/blueprints/Text to Video (Wan 2.2).json +++ b/blueprints/Text to Video (Wan 2.2).json @@ -1572,7 +1572,8 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Text to video" + "category": "Video generation and editing/Text to video", + "description": "Generates video from text prompts using Wan2.2, Alibaba's diffusion video model." } ] }, @@ -1586,4 +1587,4 @@ "VHS_KeepIntermediate": true }, "version": 0.4 -} +} \ No newline at end of file diff --git a/blueprints/Unsharp Mask.json b/blueprints/Unsharp Mask.json index 137acaa43..79a4c954f 100644 --- a/blueprints/Unsharp Mask.json +++ b/blueprints/Unsharp Mask.json @@ -434,8 +434,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Image Tools/Sharpen" + "category": "Image Tools/Sharpen", + "description": "Enhances edge contrast via unsharp masking for a sharper image appearance." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Video Captioning (Gemini).json b/blueprints/Video Captioning (Gemini).json index ea6dc8bee..54a7d6e78 100644 --- a/blueprints/Video Captioning (Gemini).json +++ b/blueprints/Video Captioning (Gemini).json @@ -307,8 +307,9 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Text generation/Video Captioning" + "category": "Video Tools", + "description": "Generates descriptive captions for video input using Google's Gemini multimodal LLM." } ] } -} +} \ No newline at end of file diff --git a/blueprints/Video Depth Estimation (MoGe).json b/blueprints/Video Depth Estimation (MoGe).json new file mode 100644 index 000000000..025e20cda --- /dev/null +++ b/blueprints/Video Depth Estimation (MoGe).json @@ -0,0 +1,1226 @@ +{ + "revision": 0, + "last_node_id": 72, + "last_link_id": 0, + "nodes": [ + { + "id": 72, + "type": "7ff83f68-6848-47a8-aa43-9036ca6c46e8", + "pos": [ + -4440, + 4550 + ], + "size": [ + 430, + 330 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "inference_resolution", + "name": "inference_resolution", + "type": "INT", + "widget": { + "name": "inference_resolution" + }, + "link": null + }, + { + "localized_name": "inference_batch_size", + "name": "inference_batch_size", + "type": "INT", + "widget": { + "name": "inference_batch_size" + }, + "link": null + }, + { + "localized_name": "moge_model", + "name": "moge_model", + "type": "COMBO", + "widget": { + "name": "moge_model" + }, + "link": null + }, + { + "label": "auto_resize_input", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": null + }, + { + "name": "video", + "type": "VIDEO", + "link": null + } + ], + "outputs": [ + { + "localized_name": "depth_colored", + "name": "depth_colored", + "type": "IMAGE", + "links": [] + }, + { + "localized_name": "depth", + "name": "depth", + "type": "IMAGE", + "links": [] + }, + { + "name": "MASK", + "type": "MASK", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "13", + "resolution_level" + ], + [ + "13", + "batch_size" + ], + [ + "32", + "model_name" + ], + [ + "53", + "switch" + ] + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.21.1" + }, + "widgets_values": [], + "title": "Video Depth Estimation (MoGe)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "7ff83f68-6848-47a8-aa43-9036ca6c46e8", + "version": 1, + "state": { + "lastGroupId": 1, + "lastNodeId": 72, + "lastLinkId": 96, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Video Depth Estimation (MoGe)", + "inputNode": { + "id": -10, + "bounding": [ + -5320, + 5320, + 167.337890625, + 148 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -3090, + 4966, + 129, + 108 + ] + }, + "inputs": [ + { + "id": "06eefa21-8e60-49f3-9a34-35b081f4ae52", + "name": "inference_resolution", + "type": "INT", + "linkIds": [ + 73 + ], + "localized_name": "inference_resolution", + "pos": [ + -5176.662109375, + 5344 + ] + }, + { + "id": "616638fe-f603-4d10-bae9-fc87c134380f", + "name": "inference_batch_size", + "type": "INT", + "linkIds": [ + 74 + ], + "localized_name": "inference_batch_size", + "pos": [ + -5176.662109375, + 5364 + ] + }, + { + "id": "65694805-186e-4181-a721-df8b5af49d31", + "name": "moge_model", + "type": "COMBO", + "linkIds": [ + 79 + ], + "localized_name": "moge_model", + "pos": [ + -5176.662109375, + 5384 + ] + }, + { + "id": "badf1be1-53c6-4fc1-b5cd-79ad3daf1674", + "name": "switch", + "type": "BOOLEAN", + "linkIds": [ + 83 + ], + "label": "auto_resize_input", + "pos": [ + -5176.662109375, + 5404 + ] + }, + { + "id": "749bad18-d00a-4ec4-a5ff-e45b1d0cf089", + "name": "video", + "type": "VIDEO", + "linkIds": [ + 91 + ], + "pos": [ + -5176.662109375, + 5424 + ] + } + ], + "outputs": [ + { + "id": "59c37b52-074f-49fc-9731-483f899c12c4", + "name": "depth_colored", + "type": "IMAGE", + "linkIds": [ + 36 + ], + "localized_name": "depth_colored", + "pos": [ + -3066, + 4990 + ] + }, + { + "id": "f583e936-da5c-4630-9901-391fa605c1f8", + "name": "depth", + "type": "IMAGE", + "linkIds": [ + 40 + ], + "localized_name": "depth", + "pos": [ + -3066, + 5010 + ] + }, + { + "id": "6845b6a1-1980-454a-9451-314f24495c1d", + "name": "MASK", + "type": "MASK", + "linkIds": [ + 86 + ], + "pos": [ + -3066, + 5030 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 13, + "type": "MoGeInference", + "pos": [ + -3790, + 5180 + ], + "size": [ + 270, + 230 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_model", + "name": "moge_model", + "type": "MOGE_MODEL", + "link": 58 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 81 + }, + { + "localized_name": "resolution_level", + "name": "resolution_level", + "type": "INT", + "widget": { + "name": "resolution_level" + }, + "link": 73 + }, + { + "localized_name": "fov_x_degrees", + "name": "fov_x_degrees", + "type": "FLOAT", + "widget": { + "name": "fov_x_degrees" + }, + "link": null + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": 74 + }, + { + "localized_name": "force_projection", + "name": "force_projection", + "type": "BOOLEAN", + "widget": { + "name": "force_projection" + }, + "link": null + }, + { + "localized_name": "apply_mask", + "name": "apply_mask", + "type": "BOOLEAN", + "widget": { + "name": "apply_mask" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "links": [ + 35, + 39, + 61 + ] + } + ], + "properties": { + "Node name for S&R": "MoGeInference", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.21.1" + }, + "widgets_values": [ + 9, + 0, + 4, + true, + true + ] + }, + { + "id": 23, + "type": "MoGeRender", + "pos": [ + -3430, + 4870 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "link": 35 + }, + { + "localized_name": "output", + "name": "output", + "type": "COMBO", + "widget": { + "name": "output" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 36 + ] + } + ], + "properties": { + "Node name for S&R": "MoGeRender", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.21.1" + }, + "widgets_values": [ + "depth_colored" + ] + }, + { + "id": 25, + "type": "MoGeRender", + "pos": [ + -3430, + 5030 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "link": 39 + }, + { + "localized_name": "output", + "name": "output", + "type": "COMBO", + "widget": { + "name": "output" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 40 + ] + } + ], + "properties": { + "Node name for S&R": "MoGeRender", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.21.1" + }, + "widgets_values": [ + "depth" + ] + }, + { + "id": 32, + "type": "LoadMoGeModel", + "pos": [ + -4180, + 4880 + ], + "size": [ + 270, + 140 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "model_name", + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": 79 + } + ], + "outputs": [ + { + "localized_name": "MOGE_MODEL", + "name": "MOGE_MODEL", + "type": "MOGE_MODEL", + "links": [ + 58 + ] + } + ], + "properties": { + "Node name for S&R": "LoadMoGeModel", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.21.1", + "models": [ + { + "name": "moge_2_vitl_normal_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/MoGe/resolve/main/geometry_estimation/moge_2_vitl_normal_fp16.safetensors", + "directory": "geometry_estimation" + } + ] + }, + "widgets_values": [ + "moge_2_vitl_normal_fp16.safetensors" + ] + }, + { + "id": 36, + "type": "ComfyMathExpression", + "pos": [ + -4720, + 4910 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT,BOOLEAN", + "link": 49 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": null + }, + { + "localized_name": "BOOL", + "name": "BOOL", + "type": "BOOLEAN", + "links": [ + 53 + ] + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.21.1" + }, + "widgets_values": [ + "a > 2048" + ] + }, + { + "id": 37, + "type": "GetImageSize", + "pos": [ + -4980, + 4910 + ], + "size": [ + 230, + 160 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 92 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 49 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": null + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetImageSize", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.21.1" + } + }, + { + "id": 40, + "type": "ResizeImagesByLongerEdge", + "pos": [ + -4650, + 5210 + ], + "size": [ + 310, + 110 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 93 + }, + { + "localized_name": "longer_edge", + "name": "longer_edge", + "type": "INT", + "widget": { + "name": "longer_edge" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 54 + ] + } + ], + "properties": { + "Node name for S&R": "ResizeImagesByLongerEdge", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.21.1" + }, + "widgets_values": [ + 2048 + ] + }, + { + "id": 42, + "type": "ComfySwitchNode", + "pos": [ + -4180, + 5060 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 94 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 54 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 53 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 80 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.21.1" + }, + "widgets_values": [ + false + ] + }, + { + "id": 45, + "type": "MoGeRender", + "pos": [ + -3430, + 5200 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "moge_geometry", + "name": "moge_geometry", + "type": "MOGE_GEOMETRY", + "link": 61 + }, + { + "localized_name": "output", + "name": "output", + "type": "COMBO", + "widget": { + "name": "output" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 85 + ] + } + ], + "properties": { + "Node name for S&R": "MoGeRender", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.21.1" + }, + "widgets_values": [ + "mask" + ] + }, + { + "id": 53, + "type": "ComfySwitchNode", + "pos": [ + -4160, + 5340 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 95 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 80 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 83 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 81 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.21.1" + }, + "widgets_values": [ + true + ] + }, + { + "id": 68, + "type": "ImageToMask", + "pos": [ + -3420, + 5360 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 85 + }, + { + "localized_name": "channel", + "name": "channel", + "type": "COMBO", + "widget": { + "name": "channel" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MASK", + "name": "MASK", + "type": "MASK", + "links": [ + 86 + ] + } + ], + "properties": { + "Node name for S&R": "ImageToMask", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.22.0" + }, + "widgets_values": [ + "red" + ] + }, + { + "id": 70, + "type": "GetVideoComponents", + "pos": [ + -4920, + 5490 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 91 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 92, + 93, + 94, + 95 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": null + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.22.0" + } + } + ], + "groups": [ + { + "id": 1, + "title": "auto_resize_if_width_gt_2048", + "bounding": [ + -5000, + 4840, + 690, + 280 + ], + "color": "#3f789e", + "flags": {} + } + ], + "links": [ + { + "id": 58, + "origin_id": 32, + "origin_slot": 0, + "target_id": 13, + "target_slot": 0, + "type": "MOGE_MODEL" + }, + { + "id": 35, + "origin_id": 13, + "origin_slot": 0, + "target_id": 23, + "target_slot": 0, + "type": "MOGE_GEOMETRY" + }, + { + "id": 39, + "origin_id": 13, + "origin_slot": 0, + "target_id": 25, + "target_slot": 0, + "type": "MOGE_GEOMETRY" + }, + { + "id": 49, + "origin_id": 37, + "origin_slot": 0, + "target_id": 36, + "target_slot": 0, + "type": "INT" + }, + { + "id": 54, + "origin_id": 40, + "origin_slot": 0, + "target_id": 42, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 53, + "origin_id": 36, + "origin_slot": 2, + "target_id": 42, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 61, + "origin_id": 13, + "origin_slot": 0, + "target_id": 45, + "target_slot": 0, + "type": "MOGE_GEOMETRY" + }, + { + "id": 36, + "origin_id": 23, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 40, + "origin_id": 25, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 73, + "origin_id": -10, + "origin_slot": 0, + "target_id": 13, + "target_slot": 2, + "type": "INT" + }, + { + "id": 74, + "origin_id": -10, + "origin_slot": 1, + "target_id": 13, + "target_slot": 4, + "type": "INT" + }, + { + "id": 79, + "origin_id": -10, + "origin_slot": 2, + "target_id": 32, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 80, + "origin_id": 42, + "origin_slot": 0, + "target_id": 53, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 81, + "origin_id": 53, + "origin_slot": 0, + "target_id": 13, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 83, + "origin_id": -10, + "origin_slot": 3, + "target_id": 53, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 85, + "origin_id": 45, + "origin_slot": 0, + "target_id": 68, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 86, + "origin_id": 68, + "origin_slot": 0, + "target_id": -20, + "target_slot": 2, + "type": "MASK" + }, + { + "id": 91, + "origin_id": -10, + "origin_slot": 4, + "target_id": 70, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 92, + "origin_id": 70, + "origin_slot": 0, + "target_id": 37, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 93, + "origin_id": 70, + "origin_slot": 0, + "target_id": 40, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 94, + "origin_id": 70, + "origin_slot": 0, + "target_id": 42, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 95, + "origin_id": 70, + "origin_slot": 0, + "target_id": 53, + "target_slot": 0, + "type": "IMAGE" + } + ], + "extra": {}, + "category": "Conditioning & Preprocessors/Depth", + "description": "Estimates monocular depth from an input video using MoGe, outputting both raw and colorized depth maps plus a mask." + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Video Face Detection (Mediapipe).json b/blueprints/Video Face Detection (Mediapipe).json new file mode 100644 index 000000000..c70352481 --- /dev/null +++ b/blueprints/Video Face Detection (Mediapipe).json @@ -0,0 +1,1109 @@ +{ + "revision": 0, + "last_node_id": 167, + "last_link_id": 0, + "nodes": [ + { + "id": 167, + "type": "ca14b151-8f5e-4386-aab7-d2ec84eaf43c", + "pos": [ + -3410, + 6100 + ], + "size": [ + 420, + 481.3125 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "video", + "type": "VIDEO", + "link": null + }, + { + "label": "trim_audio", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": null + }, + { + "name": "start_time", + "type": "FLOAT", + "widget": { + "name": "start_time" + }, + "link": null + }, + { + "name": "duration", + "type": "FLOAT", + "widget": { + "name": "duration" + }, + "link": null + }, + { + "label": "face_landmarker", + "name": "face_landmarker_1", + "type": "FACE_LANDMARKER", + "link": null + }, + { + "label": "detector_variant", + "name": "detector_variant_1", + "type": "COMBO", + "widget": { + "name": "detector_variant_1" + }, + "link": null + }, + { + "label": "num_faces", + "name": "num_faces_1", + "type": "INT", + "widget": { + "name": "num_faces_1" + }, + "link": null + }, + { + "label": "face_oval", + "name": "regions.face_oval", + "type": "BOOLEAN", + "widget": { + "name": "regions.face_oval" + }, + "link": null + }, + { + "label": "face_lips", + "name": "regions.lips", + "type": "BOOLEAN", + "widget": { + "name": "regions.lips" + }, + "link": null + }, + { + "label": "left_eye", + "name": "regions.left_eye", + "type": "BOOLEAN", + "widget": { + "name": "regions.left_eye" + }, + "link": null + }, + { + "label": "right_eye", + "name": "regions.right_eye_1", + "type": "BOOLEAN", + "widget": { + "name": "regions.right_eye_1" + }, + "link": null + }, + { + "label": "irises", + "name": "regions.irises_1", + "type": "BOOLEAN", + "widget": { + "name": "regions.irises_1" + }, + "link": null + }, + { + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": null + } + ], + "outputs": [ + { + "label": "mask", + "name": "MASK_1", + "type": "MASK", + "links": [] + }, + { + "label": "bboxes", + "name": "bboxes_1", + "type": "BOUNDING_BOX", + "links": null + }, + { + "name": "face_landmarks", + "type": "FACE_LANDMARKS", + "links": null + } + ], + "title": "Video Face Detection (Mediapipe)", + "properties": { + "proxyWidgets": [ + [ + "165", + "switch" + ], + [ + "164", + "start_time" + ], + [ + "164", + "duration" + ], + [ + "11", + "detector_variant" + ], + [ + "11", + "num_faces" + ], + [ + "20", + "regions.face_oval" + ], + [ + "20", + "regions.lips" + ], + [ + "20", + "regions.left_eye" + ], + [ + "20", + "regions.right_eye" + ], + [ + "20", + "regions.irises" + ], + [ + "2", + "model_name" + ] + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.22.0" + }, + "widgets_values": [] + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "ca14b151-8f5e-4386-aab7-d2ec84eaf43c", + "version": 1, + "state": { + "lastGroupId": 2, + "lastNodeId": 167, + "lastLinkId": 168, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Video Face Detection (Mediapipe)", + "description": "Detects facial landmarks from a video using MediaPipe, outputting landmark data, face bounding boxes, and an optional face-region mask.", + "inputNode": { + "id": -10, + "bounding": [ + -1060, + 4350, + 142.587890625, + 308 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 470, + 4460, + 137.677734375, + 108 + ] + }, + "inputs": [ + { + "id": "16e5a20f-22bc-4960-a67b-e32c64409c49", + "name": "video", + "type": "VIDEO", + "linkIds": [ + 150, + 153 + ], + "pos": [ + -941.412109375, + 4374 + ] + }, + { + "id": "cc7fc7d4-24ec-4c00-878e-1af1b6809b4b", + "name": "switch", + "type": "BOOLEAN", + "linkIds": [ + 154 + ], + "label": "trim_audio", + "pos": [ + -941.412109375, + 4394 + ] + }, + { + "id": "efa9ab9f-ca70-449c-be43-5ca60c7f0d59", + "name": "start_time", + "type": "FLOAT", + "linkIds": [ + 155 + ], + "pos": [ + -941.412109375, + 4414 + ] + }, + { + "id": "45050127-4089-4b85-bf81-73b725196c2e", + "name": "duration", + "type": "FLOAT", + "linkIds": [ + 156 + ], + "pos": [ + -941.412109375, + 4434 + ] + }, + { + "id": "239fcd3b-6324-4824-8255-98199ae58914", + "name": "face_landmarker_1", + "type": "FACE_LANDMARKER", + "linkIds": [ + 157 + ], + "label": "face_landmarker", + "pos": [ + -941.412109375, + 4454 + ] + }, + { + "id": "f79f67b9-5bcb-4cab-9101-8b9dee461bca", + "name": "detector_variant_1", + "type": "COMBO", + "linkIds": [ + 158 + ], + "label": "detector_variant", + "pos": [ + -941.412109375, + 4474 + ] + }, + { + "id": "3369790b-e730-41bf-b5b2-dc1f5fafbe11", + "name": "num_faces_1", + "type": "INT", + "linkIds": [ + 159 + ], + "label": "num_faces", + "pos": [ + -941.412109375, + 4494 + ] + }, + { + "id": "964f6b5f-44ac-456e-ba3a-a3039dfe0729", + "name": "regions.face_oval", + "type": "BOOLEAN", + "linkIds": [ + 160 + ], + "label": "face_oval", + "pos": [ + -941.412109375, + 4514 + ] + }, + { + "id": "d6e89b51-65a2-4f37-a561-8cec3a5040fd", + "name": "regions.lips", + "type": "BOOLEAN", + "linkIds": [ + 161 + ], + "label": "face_lips", + "pos": [ + -941.412109375, + 4534 + ] + }, + { + "id": "49f02319-ea4a-4a69-88f8-589d2ef7c97a", + "name": "regions.left_eye", + "type": "BOOLEAN", + "linkIds": [ + 162 + ], + "label": "left_eye", + "pos": [ + -941.412109375, + 4554 + ] + }, + { + "id": "89179a19-aca6-4469-a0b9-2a4bd21bceea", + "name": "regions.right_eye_1", + "type": "BOOLEAN", + "linkIds": [ + 163 + ], + "label": "right_eye", + "pos": [ + -941.412109375, + 4574 + ] + }, + { + "id": "f5667690-24b5-4df9-9210-b8610c68ff5f", + "name": "regions.irises_1", + "type": "BOOLEAN", + "linkIds": [ + 164 + ], + "label": "irises", + "pos": [ + -941.412109375, + 4594 + ] + }, + { + "id": "66c805f6-6ccd-41f9-8a77-fc934b7f4713", + "name": "model_name", + "type": "COMBO", + "linkIds": [ + 165 + ], + "pos": [ + -941.412109375, + 4614 + ] + } + ], + "outputs": [ + { + "id": "f6309e1d-6397-4363-b38f-778a122abc51", + "name": "MASK_1", + "type": "MASK", + "linkIds": [ + 83 + ], + "label": "mask", + "pos": [ + 494, + 4484 + ] + }, + { + "id": "59669f0a-b4b2-49d1-85f8-fc2a88059b1a", + "name": "bboxes_1", + "type": "BOUNDING_BOX", + "linkIds": [ + 166 + ], + "label": "bboxes", + "pos": [ + 494, + 4504 + ] + }, + { + "id": "57f66731-e106-4f8b-a0a0-aed3c620b37b", + "name": "face_landmarks", + "type": "FACE_LANDMARKS", + "linkIds": [ + 167 + ], + "pos": [ + 494, + 4524 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 11, + "type": "MediaPipeFaceLandmarker", + "pos": [ + -60, + 4380 + ], + "size": [ + 350, + 220 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "face_detection_model", + "name": "face_detection_model", + "type": "FACE_DETECTION_MODEL", + "link": 66 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 149 + }, + { + "localized_name": "detector_variant", + "name": "detector_variant", + "type": "COMBO", + "widget": { + "name": "detector_variant" + }, + "link": 158 + }, + { + "localized_name": "num_faces", + "name": "num_faces", + "type": "INT", + "widget": { + "name": "num_faces" + }, + "link": 159 + }, + { + "localized_name": "min_confidence", + "name": "min_confidence", + "type": "FLOAT", + "widget": { + "name": "min_confidence" + }, + "link": null + }, + { + "localized_name": "missing_frame_fallback", + "name": "missing_frame_fallback", + "type": "COMBO", + "widget": { + "name": "missing_frame_fallback" + }, + "link": null + }, + { + "name": "face_landmarker", + "type": "FACE_LANDMARKER", + "link": 157 + } + ], + "outputs": [ + { + "localized_name": "face_landmarks", + "name": "face_landmarks", + "type": "FACE_LANDMARKS", + "links": [ + 46, + 167 + ] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [ + 166 + ] + } + ], + "properties": { + "Node name for S&R": "MediaPipeFaceLandmarker", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.22.0" + }, + "widgets_values": [ + "full", + 0, + 0.5, + "empty" + ] + }, + { + "id": 2, + "type": "LoadMediaPipeFaceLandmarker", + "pos": [ + -70, + 4160 + ], + "size": [ + 350, + 140 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "model_name", + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": 165 + } + ], + "outputs": [ + { + "localized_name": "FACE_DETECTION_MODEL", + "name": "FACE_DETECTION_MODEL", + "type": "FACE_DETECTION_MODEL", + "links": [ + 66 + ] + } + ], + "properties": { + "Node name for S&R": "LoadMediaPipeFaceLandmarker", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.22.0", + "models": [ + { + "name": "mediapipe_face_fp32.safetensors", + "url": "https://huggingface.co/Comfy-Org/mediapipe/resolve/main/detection/mediapipe_face_fp32.safetensors", + "directory": "detection" + } + ] + }, + "widgets_values": [ + "mediapipe_face_fp32.safetensors" + ] + }, + { + "id": 20, + "type": "MediaPipeFaceMask", + "pos": [ + -70, + 4660 + ], + "size": [ + 360, + 180 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "face_landmarks", + "name": "face_landmarks", + "type": "FACE_LANDMARKS", + "link": 46 + }, + { + "localized_name": "regions", + "name": "regions", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "regions" + }, + "link": null + }, + { + "localized_name": "regions.face_oval", + "name": "regions.face_oval", + "type": "BOOLEAN", + "widget": { + "name": "regions.face_oval" + }, + "link": 160 + }, + { + "localized_name": "regions.lips", + "name": "regions.lips", + "type": "BOOLEAN", + "widget": { + "name": "regions.lips" + }, + "link": 161 + }, + { + "localized_name": "regions.left_eye", + "name": "regions.left_eye", + "type": "BOOLEAN", + "widget": { + "name": "regions.left_eye" + }, + "link": 162 + }, + { + "localized_name": "regions.right_eye", + "name": "regions.right_eye", + "type": "BOOLEAN", + "widget": { + "name": "regions.right_eye" + }, + "link": 163 + }, + { + "localized_name": "regions.irises", + "name": "regions.irises", + "type": "BOOLEAN", + "widget": { + "name": "regions.irises" + }, + "link": 164 + } + ], + "outputs": [ + { + "localized_name": "MASK", + "name": "MASK", + "type": "MASK", + "links": [ + 83 + ] + } + ], + "properties": { + "Node name for S&R": "MediaPipeFaceMask", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.22.0" + }, + "widgets_values": [ + "custom", + true, + false, + false, + false, + false + ] + }, + { + "id": 160, + "type": "GetVideoComponents", + "pos": [ + -420, + 4360 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 152 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 149 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": null + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.22.0" + } + }, + { + "id": 164, + "type": "Video Slice", + "pos": [ + -780, + 4330 + ], + "size": [ + 270, + 170 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 150 + }, + { + "localized_name": "start_time", + "name": "start_time", + "type": "FLOAT", + "widget": { + "name": "start_time" + }, + "link": 155 + }, + { + "localized_name": "duration", + "name": "duration", + "type": "FLOAT", + "widget": { + "name": "duration" + }, + "link": 156 + }, + { + "localized_name": "strict_duration", + "name": "strict_duration", + "type": "BOOLEAN", + "widget": { + "name": "strict_duration" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [ + 151 + ] + } + ], + "properties": { + "Node name for S&R": "Video Slice", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.22.0" + }, + "widgets_values": [ + 0, + 0, + false + ] + }, + { + "id": 165, + "type": "ComfySwitchNode", + "pos": [ + -420, + 4590 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 153 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 151 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 154 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 152 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "cnr_id": "comfy-core", + "ver": "0.22.0" + }, + "widgets_values": [ + false + ] + } + ], + "groups": [], + "links": [ + { + "id": 66, + "origin_id": 2, + "origin_slot": 0, + "target_id": 11, + "target_slot": 0, + "type": "FACE_DETECTION_MODEL" + }, + { + "id": 46, + "origin_id": 11, + "origin_slot": 0, + "target_id": 20, + "target_slot": 0, + "type": "FACE_LANDMARKS" + }, + { + "id": 83, + "origin_id": 20, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 149, + "origin_id": 160, + "origin_slot": 0, + "target_id": 11, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 150, + "origin_id": -10, + "origin_slot": 0, + "target_id": 164, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 151, + "origin_id": 164, + "origin_slot": 0, + "target_id": 165, + "target_slot": 1, + "type": "VIDEO" + }, + { + "id": 152, + "origin_id": 165, + "origin_slot": 0, + "target_id": 160, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 153, + "origin_id": -10, + "origin_slot": 0, + "target_id": 165, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 154, + "origin_id": -10, + "origin_slot": 1, + "target_id": 165, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 155, + "origin_id": -10, + "origin_slot": 2, + "target_id": 164, + "target_slot": 1, + "type": "FLOAT" + }, + { + "id": 156, + "origin_id": -10, + "origin_slot": 3, + "target_id": 164, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 157, + "origin_id": -10, + "origin_slot": 4, + "target_id": 11, + "target_slot": 6, + "type": "FACE_LANDMARKER" + }, + { + "id": 158, + "origin_id": -10, + "origin_slot": 5, + "target_id": 11, + "target_slot": 2, + "type": "COMBO" + }, + { + "id": 159, + "origin_id": -10, + "origin_slot": 6, + "target_id": 11, + "target_slot": 3, + "type": "INT" + }, + { + "id": 160, + "origin_id": -10, + "origin_slot": 7, + "target_id": 20, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 161, + "origin_id": -10, + "origin_slot": 8, + "target_id": 20, + "target_slot": 3, + "type": "BOOLEAN" + }, + { + "id": 162, + "origin_id": -10, + "origin_slot": 9, + "target_id": 20, + "target_slot": 4, + "type": "BOOLEAN" + }, + { + "id": 163, + "origin_id": -10, + "origin_slot": 10, + "target_id": 20, + "target_slot": 5, + "type": "BOOLEAN" + }, + { + "id": 164, + "origin_id": -10, + "origin_slot": 11, + "target_id": 20, + "target_slot": 6, + "type": "BOOLEAN" + }, + { + "id": 165, + "origin_id": -10, + "origin_slot": 12, + "target_id": 2, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 166, + "origin_id": 11, + "origin_slot": 1, + "target_id": -20, + "target_slot": 1, + "type": "BOUNDING_BOX" + }, + { + "id": 167, + "origin_id": 11, + "origin_slot": 0, + "target_id": -20, + "target_slot": 2, + "type": "FACE_LANDMARKS" + } + ], + "extra": {}, + "category": "Conditioning & Preprocessors/Face Detection" + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Video Inpaint (VOID).json b/blueprints/Video Inpaint (VOID).json new file mode 100644 index 000000000..a7cc806b5 --- /dev/null +++ b/blueprints/Video Inpaint (VOID).json @@ -0,0 +1,4340 @@ +{ + "revision": 0, + "last_node_id": 167, + "last_link_id": 0, + "nodes": [ + { + "id": 167, + "type": "c3157b75-484a-459e-b8de-57823bef5130", + "pos": [ + -430, + 690 + ], + "size": [ + 590, + 723.9375 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "label": "Source video", + "localized_name": "source_video", + "name": "source_video", + "type": "VIDEO", + "link": null + }, + { + "label": "Positive prompt (inpaint fill)", + "localized_name": "positive_prompt", + "name": "positive_prompt", + "type": "STRING", + "widget": { + "name": "positive_prompt" + }, + "link": null + }, + { + "label": "Negative prompt", + "localized_name": "negative_prompt", + "name": "negative_prompt", + "type": "STRING", + "widget": { + "name": "negative_prompt" + }, + "link": null + }, + { + "label": "SAM3 object mask prompt", + "localized_name": "sam3_text_prompt", + "name": "sam3_text_prompt", + "type": "STRING", + "widget": { + "name": "sam3_text_prompt" + }, + "link": null + }, + { + "label": "Start frame index", + "localized_name": "start_frame_index", + "name": "start_frame_index", + "type": "INT", + "widget": { + "name": "start_frame_index" + }, + "link": null + }, + { + "label": "Clip duration (seconds)", + "localized_name": "duration_seconds", + "name": "duration_seconds", + "type": "INT", + "widget": { + "name": "duration_seconds" + }, + "link": null + }, + { + "label": "Width (pass 2)", + "localized_name": "latent_width", + "name": "latent_width", + "type": "INT", + "widget": { + "name": "latent_width" + }, + "link": null + }, + { + "label": "Height (pass 2)", + "localized_name": "latent_height", + "name": "latent_height", + "type": "INT", + "widget": { + "name": "latent_height" + }, + "link": null + }, + { + "label": "Skip pass 2 (reuse pass 1)", + "localized_name": "skip_pass_2", + "name": "skip_pass_2", + "type": "BOOLEAN", + "widget": { + "name": "skip_pass_2" + }, + "link": null + }, + { + "label": "Noise seed", + "localized_name": "noise_seed", + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": null + }, + { + "label": "SAM3 checkpoint", + "localized_name": "sam3_checkpoint", + "name": "sam3_checkpoint", + "type": "COMBO", + "widget": { + "name": "sam3_checkpoint" + }, + "link": null + }, + { + "label": "VOID UNet — pass 1", + "localized_name": "void_unet_pass1", + "name": "void_unet_pass1", + "type": "COMBO", + "widget": { + "name": "void_unet_pass1" + }, + "link": null + }, + { + "label": "VOID UNet — pass 2", + "localized_name": "void_unet_pass2", + "name": "void_unet_pass2", + "type": "COMBO", + "widget": { + "name": "void_unet_pass2" + }, + "link": null + }, + { + "label": "Optical flow model", + "localized_name": "optical_flow_model", + "name": "optical_flow_model", + "type": "COMBO", + "widget": { + "name": "optical_flow_model" + }, + "link": null + }, + { + "label": "CLIP / T5 weights", + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "label": "VAE weights", + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + } + ], + "outputs": [ + { + "label": "Pass 1 (intermediate)", + "localized_name": "pass_1_video", + "name": "pass_1_video", + "type": "VIDEO", + "links": [] + }, + { + "label": "Pass 2 (final)", + "localized_name": "final_pass_2_video", + "name": "final_pass_2_video", + "type": "VIDEO", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "6", + "text" + ], + [ + "7", + "text" + ], + [ + "149", + "text" + ], + [ + "168", + "value" + ], + [ + "163", + "value" + ], + [ + "147", + "value" + ], + [ + "148", + "value" + ], + [ + "153", + "value" + ], + [ + "141", + "noise_seed" + ], + [ + "149", + "ckpt_name" + ], + [ + "144", + "unet_name" + ], + [ + "143", + "unet_name" + ], + [ + "142", + "model_name" + ], + [ + "2", + "clip_name" + ], + [ + "3", + "vae_name" + ] + ] + }, + "widgets_values": [], + "title": "Video Inpaint (VOID)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "c3157b75-484a-459e-b8de-57823bef5130", + "version": 1, + "state": { + "lastGroupId": 13, + "lastNodeId": 171, + "lastLinkId": 406, + "lastRerouteId": 0 + }, + "revision": 5, + "config": {}, + "name": "Video Inpaint (VOID)", + "inputNode": { + "id": -10, + "bounding": [ + -1530, + 800, + 203.1796875, + 368 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 2030, + 710, + 166.130859375, + 88 + ] + }, + "inputs": [ + { + "id": "1865ea29-14b1-4471-b5e0-d35bba595b9c", + "name": "source_video", + "type": "VIDEO", + "linkIds": [ + 373 + ], + "localized_name": "source_video", + "label": "Source video", + "pos": [ + -1350.8203125, + 824 + ] + }, + { + "id": "f1b2b2c4-bc2e-4e72-b16c-7e560e58d2d6", + "name": "positive_prompt", + "type": "STRING", + "linkIds": [ + 377 + ], + "localized_name": "positive_prompt", + "label": "Positive prompt (inpaint fill)", + "pos": [ + -1350.8203125, + 844 + ] + }, + { + "id": "931ac4dd-3cb6-4555-a1f0-619be81d64f6", + "name": "negative_prompt", + "type": "STRING", + "linkIds": [ + 387 + ], + "localized_name": "negative_prompt", + "label": "Negative prompt", + "pos": [ + -1350.8203125, + 864 + ] + }, + { + "id": "7a0963c3-bf2f-464d-80c2-6a6c90569883", + "name": "sam3_text_prompt", + "type": "STRING", + "linkIds": [ + 388 + ], + "localized_name": "sam3_text_prompt", + "label": "SAM3 object mask prompt", + "pos": [ + -1350.8203125, + 884 + ] + }, + { + "id": "f53f340f-2031-401d-b613-157622ef336f", + "name": "start_frame_index", + "type": "INT", + "linkIds": [ + 389 + ], + "localized_name": "start_frame_index", + "label": "Start frame index", + "pos": [ + -1350.8203125, + 904 + ] + }, + { + "id": "d5b8704b-7c8c-4cf0-87cd-26b293f65f83", + "name": "duration_seconds", + "type": "INT", + "linkIds": [ + 390 + ], + "localized_name": "duration_seconds", + "label": "Clip duration (seconds)", + "pos": [ + -1350.8203125, + 924 + ] + }, + { + "id": "7140209f-5058-4933-ae06-438256f77f23", + "name": "latent_width", + "type": "INT", + "linkIds": [ + 391 + ], + "localized_name": "latent_width", + "label": "Width (pass 2)", + "pos": [ + -1350.8203125, + 944 + ] + }, + { + "id": "084a140a-6fa9-4676-9483-ad30e0b14947", + "name": "latent_height", + "type": "INT", + "linkIds": [ + 392 + ], + "localized_name": "latent_height", + "label": "Height (pass 2)", + "pos": [ + -1350.8203125, + 964 + ] + }, + { + "id": "a8109321-e101-4ed8-b6f3-8ad1c815f35c", + "name": "skip_pass_2", + "type": "BOOLEAN", + "linkIds": [ + 393 + ], + "localized_name": "skip_pass_2", + "label": "Skip pass 2 (reuse pass 1)", + "pos": [ + -1350.8203125, + 984 + ] + }, + { + "id": "6964ab42-0662-47f2-9c2a-96782fdcb883", + "name": "noise_seed", + "type": "INT", + "linkIds": [ + 400 + ], + "localized_name": "noise_seed", + "label": "Noise seed", + "pos": [ + -1350.8203125, + 1004 + ] + }, + { + "id": "dccde360-461d-417e-b3f5-e1a4d6cece39", + "name": "sam3_checkpoint", + "type": "COMBO", + "linkIds": [ + 401 + ], + "localized_name": "sam3_checkpoint", + "label": "SAM3 checkpoint", + "pos": [ + -1350.8203125, + 1024 + ] + }, + { + "id": "5ce0d036-be08-4539-9ec6-e923fcdb8825", + "name": "void_unet_pass1", + "type": "COMBO", + "linkIds": [ + 402 + ], + "localized_name": "void_unet_pass1", + "label": "VOID UNet — pass 1", + "pos": [ + -1350.8203125, + 1044 + ] + }, + { + "id": "c1de695a-a08a-40bc-b9e4-d156fef73cd0", + "name": "void_unet_pass2", + "type": "COMBO", + "linkIds": [ + 403 + ], + "localized_name": "void_unet_pass2", + "label": "VOID UNet — pass 2", + "pos": [ + -1350.8203125, + 1064 + ] + }, + { + "id": "99da50bc-db57-4a21-9831-0f77b3c4fe99", + "name": "optical_flow_model", + "type": "COMBO", + "linkIds": [ + 404 + ], + "localized_name": "optical_flow_model", + "label": "Optical flow model", + "pos": [ + -1350.8203125, + 1084 + ] + }, + { + "id": "c756ce20-cfa6-4fe0-9eb0-543d56781cb7", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 405 + ], + "localized_name": "clip_name", + "label": "CLIP / T5 weights", + "pos": [ + -1350.8203125, + 1104 + ] + }, + { + "id": "d8eb12ad-a805-42d9-86b4-6f2c2cc5a231", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 406 + ], + "localized_name": "vae_name", + "label": "VAE weights", + "pos": [ + -1350.8203125, + 1124 + ] + } + ], + "outputs": [ + { + "id": "a21e83df-8c95-43a3-bd73-feeea67e90cd", + "name": "pass_1_video", + "type": "VIDEO", + "linkIds": [ + 77 + ], + "localized_name": "pass_1_video", + "label": "Pass 1 (intermediate)", + "pos": [ + 2054, + 734 + ] + }, + { + "id": "02c265f3-012f-499f-a4e8-a6d6aaf72885", + "name": "final_pass_2_video", + "type": "VIDEO", + "linkIds": [ + 362 + ], + "localized_name": "final_pass_2_video", + "label": "Pass 2 (final)", + "pos": [ + 2054, + 754 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 2, + "type": "CLIPLoader", + "pos": [ + -710, + 30 + ], + "size": [ + 320, + 150 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 405 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "slot_index": 0, + "links": [ + 2, + 3 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "models": [ + { + "name": "t5xxl_fp16.safetensors", + "url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "t5xxl_fp16.safetensors", + "cogvideox", + "default" + ] + }, + { + "id": 3, + "type": "VAELoader", + "pos": [ + -710, + 220 + ], + "size": [ + 320, + 90 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 406 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "slot_index": 0, + "links": [ + 4, + 45, + 70 + ] + } + ], + "properties": { + "Node name for S&R": "VAELoader", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "models": [ + { + "name": "cogvideox_vae.safetensors", + "url": "https://huggingface.co/Comfy-Org/void-model/resolve/main/vae/cogvideox_vae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "cogvideox_vae.safetensors" + ] + }, + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + -260, + 200 + ], + "size": [ + 590, + 180 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 3 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 387 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 9 + ] + } + ], + "title": "Negative Prompt", + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 136, + "type": "CFGGuider", + "pos": [ + 410, + 1640 + ], + "size": [ + 300, + 130 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 322 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 309 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 310 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "links": [ + 311 + ] + } + ], + "title": "CFGGuider (Pass 2 cfg=6)", + "properties": { + "Node name for S&R": "CFGGuider", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 6 + ] + }, + { + "id": 138, + "type": "BasicScheduler", + "pos": [ + 410, + 160 + ], + "size": [ + 270, + 150 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 324 + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "SIGMAS", + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 315 + ] + } + ], + "properties": { + "Node name for S&R": "BasicScheduler", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "simple", + 30, + 1 + ] + }, + { + "id": 140, + "type": "CFGGuider", + "pos": [ + 410, + -30 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 325 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 317 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 318 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "GUIDER", + "name": "GUIDER", + "type": "GUIDER", + "links": [ + 319 + ] + } + ], + "properties": { + "Node name for S&R": "CFGGuider", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 6 + ] + }, + { + "id": 141, + "type": "RandomNoise", + "pos": [ + 410, + -180 + ], + "size": [ + 270, + 90 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "localized_name": "noise_seed", + "name": "noise_seed", + "type": "INT", + "widget": { + "name": "noise_seed" + }, + "link": 400 + } + ], + "outputs": [ + { + "localized_name": "NOISE", + "name": "NOISE", + "type": "NOISE", + "links": [ + 320 + ] + } + ], + "properties": { + "Node name for S&R": "RandomNoise", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 43, + "fixed" + ] + }, + { + "id": 31, + "type": "VOIDWarpedNoise", + "pos": [ + 410, + 1090 + ], + "size": [ + 300, + 200 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "localized_name": "optical_flow", + "name": "optical_flow", + "type": "OPTICAL_FLOW", + "link": 321 + }, + { + "localized_name": "video", + "name": "video", + "type": "IMAGE", + "link": 72 + }, + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 333 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 335 + }, + { + "localized_name": "length", + "name": "length", + "type": "INT", + "widget": { + "name": "length" + }, + "link": 67 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "warped_noise", + "name": "warped_noise", + "type": "LATENT", + "slot_index": 0, + "links": [ + 53 + ] + } + ], + "title": "Warped Noise (from Pass 1 output)", + "properties": { + "Node name for S&R": "VOIDWarpedNoise", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 672, + 384, + 45, + 1 + ] + }, + { + "id": 35, + "type": "SamplerCustomAdvanced", + "pos": [ + 870, + 1110 + ], + "size": [ + 250, + 170 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 54 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 311 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 305 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 313 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 48 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "slot_index": 0, + "links": [ + 49 + ] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "slot_index": 1, + "links": [] + } + ], + "title": "Pass 2 Sample", + "properties": { + "Node name for S&R": "SamplerCustomAdvanced", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 132, + "type": "MaskPreview", + "pos": [ + 390, + 560 + ], + "size": [ + 790, + 430 + ], + "flags": {}, + "order": 15, + "mode": 4, + "inputs": [ + { + "localized_name": "mask", + "name": "mask", + "type": "MASK", + "link": 340 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "MaskPreview", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 142, + "type": "OpticalFlowLoader", + "pos": [ + -710, + 410 + ], + "size": [ + 320, + 90 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [ + { + "localized_name": "model_name", + "name": "model_name", + "type": "COMBO", + "widget": { + "name": "model_name" + }, + "link": 404 + } + ], + "outputs": [ + { + "localized_name": "OPTICAL_FLOW", + "name": "OPTICAL_FLOW", + "type": "OPTICAL_FLOW", + "links": [ + 321 + ] + } + ], + "properties": { + "Node name for S&R": "OpticalFlowLoader", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "models": [ + { + "name": "raft_large_C_T_SKHT_V2-ff5fadd5.safetensors", + "url": "https://huggingface.co/Comfy-Org/void-model/resolve/main/optical_flow/raft_large_C_T_SKHT_V2-ff5fadd5.safetensors", + "directory": "optical_flow" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "raft_large_C_T_SKHT_V2-ff5fadd5.safetensors" + ] + }, + { + "id": 10, + "type": "VOIDInpaintConditioning", + "pos": [ + -110, + 430 + ], + "size": [ + 300, + 280 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 8 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 9 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 4 + }, + { + "localized_name": "video", + "name": "video", + "type": "IMAGE", + "link": 326 + }, + { + "localized_name": "quadmask", + "name": "quadmask", + "type": "MASK", + "link": 339 + }, + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 332 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 334 + }, + { + "localized_name": "length", + "name": "length", + "type": "INT", + "widget": { + "name": "length" + }, + "link": 63 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 309, + 317 + ] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "slot_index": 1, + "links": [ + 310, + 318 + ] + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "slot_index": 2, + "links": [ + 48, + 82 + ] + } + ], + "properties": { + "Node name for S&R": "VOIDInpaintConditioning", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 672, + 384, + 45, + 1 + ] + }, + { + "id": 32, + "type": "VOIDWarpedNoiseSource", + "pos": [ + 410, + 1350 + ], + "size": [ + 300, + 50 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "warped_noise", + "name": "warped_noise", + "type": "LATENT", + "link": 53 + } + ], + "outputs": [ + { + "localized_name": "NOISE", + "name": "NOISE", + "type": "NOISE", + "slot_index": 0, + "links": [ + 54 + ] + } + ], + "title": "Warped Noise → NOISE", + "properties": { + "Node name for S&R": "VOIDWarpedNoiseSource", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 137, + "type": "BasicScheduler", + "pos": [ + 410, + 1470 + ], + "size": [ + 300, + 150 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 323 + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "SIGMAS", + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 313 + ] + } + ], + "properties": { + "Node name for S&R": "BasicScheduler", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "simple", + 30, + 1 + ] + }, + { + "id": 134, + "type": "VOIDSampler", + "pos": [ + 410, + 1800 + ], + "size": [ + 300, + 50 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "localized_name": "SAMPLER", + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 305 + ] + } + ], + "properties": { + "Node name for S&R": "VOIDSampler", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 143, + "type": "UNETLoader", + "pos": [ + -710, + 550 + ], + "size": [ + 320, + 120 + ], + "flags": {}, + "order": 22, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 403 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 322, + 323 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "models": [ + { + "name": "void_pass2.safetensors", + "url": "https://huggingface.co/Comfy-Org/void-model/resolve/main/diffusion_models/void_pass2.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "void_pass2.safetensors", + "default" + ] + }, + { + "id": 144, + "type": "UNETLoader", + "pos": [ + -720, + -150 + ], + "size": [ + 320, + 120 + ], + "flags": {}, + "order": 23, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 402 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 324, + 325 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "models": [ + { + "name": "void_pass1.safetensors", + "url": "https://huggingface.co/Comfy-Org/void-model/resolve/main/diffusion_models/void_pass1.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "void_pass1.safetensors", + "default" + ] + }, + { + "id": 46, + "type": "CreateVideo", + "pos": [ + 1230, + -20 + ], + "size": [ + 240, + 110 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 73 + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 355 + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 368 + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [ + 77 + ] + } + ], + "properties": { + "Node name for S&R": "CreateVideo", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 30 + ] + }, + { + "id": 133, + "type": "VOIDSampler", + "pos": [ + 410, + 370 + ], + "size": [ + 280, + 50 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "localized_name": "SAMPLER", + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 304 + ] + } + ], + "properties": { + "Node name for S&R": "VOIDSampler", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 49, + "type": "SamplerCustomAdvanced", + "pos": [ + 880, + -180 + ], + "size": [ + 250, + 270 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "noise", + "name": "noise", + "type": "NOISE", + "link": 320 + }, + { + "localized_name": "guider", + "name": "guider", + "type": "GUIDER", + "link": 319 + }, + { + "localized_name": "sampler", + "name": "sampler", + "type": "SAMPLER", + "link": 304 + }, + { + "localized_name": "sigmas", + "name": "sigmas", + "type": "SIGMAS", + "link": 315 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 82 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "LATENT", + "links": [ + 83 + ] + }, + { + "localized_name": "denoised_output", + "name": "denoised_output", + "type": "LATENT", + "links": null + } + ], + "properties": { + "Node name for S&R": "SamplerCustomAdvanced", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 45, + "type": "VAEDecode", + "pos": [ + 1230, + -180 + ], + "size": [ + 230, + 80 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 83 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 70 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 72, + 73, + 342 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + -260, + -180 + ], + "size": [ + 580, + 310 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 2 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 377 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 8 + ] + } + ], + "title": "Positive Prompt", + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 145, + "type": "ImageFromBatch", + "pos": [ + -410, + 850 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 24, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 366 + }, + { + "localized_name": "batch_index", + "name": "batch_index", + "type": "INT", + "widget": { + "name": "batch_index" + }, + "link": 384 + }, + { + "localized_name": "length", + "name": "length", + "type": "INT", + "widget": { + "name": "length" + }, + "link": 361 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 326, + 327, + 336 + ] + } + ], + "properties": { + "Node name for S&R": "ImageFromBatch", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + 197 + ] + }, + { + "id": 36, + "type": "VAEDecode", + "pos": [ + 1220, + 1110 + ], + "size": [ + 230, + 80 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 49 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 45 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 341 + ] + } + ], + "title": "Pass 2 VAE Decode", + "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 149, + "type": "c3e0d783-9aa3-4e75-a94d-19937968ef86", + "pos": [ + -20, + 840 + ], + "size": [ + 290, + 370 + ], + "flags": {}, + "order": 27, + "mode": 0, + "inputs": [ + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 336 + }, + { + "label": "object", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 388 + }, + { + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": null + }, + { + "name": "positive_coords", + "shape": 7, + "type": "STRING", + "link": null + }, + { + "name": "negative_coords", + "shape": 7, + "type": "STRING", + "link": null + }, + { + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": null + }, + { + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": null + }, + { + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": null + }, + { + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 401 + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [ + 339, + 340 + ] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "78", + "text" + ], + [ + "75", + "threshold" + ], + [ + "75", + "refine_iterations" + ], + [ + "75", + "individual_masks" + ], + [ + "77", + "ckpt_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": { + "text": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [] + }, + { + "id": 43, + "type": "GetImageSize", + "pos": [ + -410, + 1140 + ], + "size": [ + 230, + 160 + ], + "flags": { + "collapsed": false + }, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 327 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": null + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": null + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": [ + 63, + 67 + ] + } + ], + "properties": { + "Node name for S&R": "GetImageSize", + "cnr_id": "comfy-core", + "ver": "0.20.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 147, + "type": "PrimitiveInt", + "pos": [ + -570, + 1660 + ], + "size": [ + 270, + 90 + ], + "flags": {}, + "order": 25, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 391 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 332, + 333 + ] + } + ], + "title": "Int (Width)", + "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 672, + "fixed" + ] + }, + { + "id": 148, + "type": "PrimitiveInt", + "pos": [ + -570, + 1790 + ], + "size": [ + 270, + 90 + ], + "flags": {}, + "order": 26, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 392 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 334, + 335 + ] + } + ], + "title": "Int (Height)", + "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 384, + "fixed" + ] + }, + { + "id": 150, + "type": "ComfySwitchNode", + "pos": [ + 1510, + 1080 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 28, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 342 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 341 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 346 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 363 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 153, + "type": "PrimitiveBoolean", + "pos": [ + -580, + 1440 + ], + "size": [ + 270, + 80 + ], + "flags": {}, + "order": 29, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 393 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 346 + ] + } + ], + "title": "Boolean (Skip Pass 2?)", + "properties": { + "Node name for S&R": "PrimitiveBoolean", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 158, + "type": "TrimAudioDuration", + "pos": [ + -10, + 1580 + ], + "size": [ + 270, + 120 + ], + "flags": {}, + "order": 30, + "mode": 0, + "inputs": [ + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "link": 367 + }, + { + "localized_name": "start_index", + "name": "start_index", + "type": "FLOAT", + "widget": { + "name": "start_index" + }, + "link": 386 + }, + { + "localized_name": "duration", + "name": "duration", + "type": "FLOAT", + "widget": { + "name": "duration" + }, + "link": 385 + } + ], + "outputs": [ + { + "localized_name": "AUDIO", + "name": "AUDIO", + "type": "AUDIO", + "links": [ + 355, + 364 + ] + } + ], + "properties": { + "Node name for S&R": "TrimAudioDuration", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + 60 + ] + }, + { + "id": 163, + "type": "PrimitiveInt", + "pos": [ + -740, + 1170 + ], + "size": [ + 230, + 90 + ], + "flags": {}, + "order": 31, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 390 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 360 + ] + } + ], + "title": "Int (Video duration)", + "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 5, + "fixed" + ] + }, + { + "id": 164, + "type": "ComfyMathExpression", + "pos": [ + -740, + 1300 + ], + "size": [ + 230, + 100 + ], + "flags": { + "collapsed": true + }, + "order": 32, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT,BOOLEAN", + "link": 360 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": 371 + }, + { + "label": "c", + "localized_name": "values.c", + "name": "values.c", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 385 + ] + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 361 + ] + }, + { + "localized_name": "BOOL", + "name": "BOOL", + "type": "BOOLEAN", + "links": null + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "a * b" + ] + }, + { + "id": 165, + "type": "CreateVideo", + "pos": [ + 1510, + 1270 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 33, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 363 + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 364 + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 372 + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [ + 362 + ] + } + ], + "properties": { + "Node name for S&R": "CreateVideo" + }, + "widgets_values": [ + 24 + ] + }, + { + "id": 166, + "type": "GetVideoComponents", + "pos": [ + -740, + 840 + ], + "size": [ + 230, + 100 + ], + "flags": {}, + "order": 34, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 373 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 366 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": [ + 367 + ] + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": [ + 368, + 371, + 372, + 383 + ] + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents" + } + }, + { + "id": 168, + "type": "PrimitiveInt", + "pos": [ + -740, + 980 + ], + "size": [ + 230, + 90 + ], + "flags": {}, + "order": 35, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 389 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 382 + ] + } + ], + "title": "Int (Index)", + "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.21.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + "fixed" + ] + }, + { + "id": 169, + "type": "ComfyMathExpression", + "pos": [ + -740, + 1110 + ], + "size": [ + 230, + 100 + ], + "flags": { + "collapsed": true + }, + "order": 36, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT,BOOLEAN", + "link": 382 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": 383 + }, + { + "label": "c", + "localized_name": "values.c", + "name": "values.c", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 386 + ] + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 384 + ] + }, + { + "localized_name": "BOOL", + "name": "BOOL", + "type": "BOOLEAN", + "links": null + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "a * b" + ] + } + ], + "groups": [ + { + "id": 1, + "title": "Models", + "bounding": [ + -790, + -260, + 470, + 990 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 2, + "title": "Input videos (place files in ComfyUI/input/)", + "bounding": [ + -790, + 760, + 660, + 560 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 3, + "title": "Shared: Text & Mask Conditioning", + "bounding": [ + -290, + -260, + 640, + 990 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 4, + "title": "Pass 1: Sample (Random Noise → DDIM)", + "bounding": [ + 380, + -260, + 810, + 750 + ], + "color": "#8A8", + "flags": {} + }, + { + "id": 6, + "title": "Pass 2: Sample (Warped Noise → DDIM)", + "bounding": [ + 380, + 1020, + 810, + 880 + ], + "color": "#8A8", + "flags": {} + }, + { + "id": 8, + "title": "Create Mask", + "bounding": [ + -100, + 760, + 450, + 560 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 9, + "title": "Pass 1", + "bounding": [ + -730, + -220, + 360, + 210 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 10, + "title": "Pass 2", + "bounding": [ + -720, + 340, + 340, + 340 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 11, + "title": "Output Video Size", + "bounding": [ + -790, + 1580, + 660, + 320 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 12, + "title": "Skip Pass 2", + "bounding": [ + -790, + 1350, + 660, + 200 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 13, + "title": "Trim Audio", + "bounding": [ + -100, + 1350, + 450, + 550 + ], + "color": "#3f789e", + "flags": {} + } + ], + "links": [ + { + "id": 3, + "origin_id": 2, + "origin_slot": 0, + "target_id": 7, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 322, + "origin_id": 143, + "origin_slot": 0, + "target_id": 136, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 309, + "origin_id": 10, + "origin_slot": 0, + "target_id": 136, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 310, + "origin_id": 10, + "origin_slot": 1, + "target_id": 136, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 324, + "origin_id": 144, + "origin_slot": 0, + "target_id": 138, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 325, + "origin_id": 144, + "origin_slot": 0, + "target_id": 140, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 317, + "origin_id": 10, + "origin_slot": 0, + "target_id": 140, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 318, + "origin_id": 10, + "origin_slot": 1, + "target_id": 140, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 321, + "origin_id": 142, + "origin_slot": 0, + "target_id": 31, + "target_slot": 0, + "type": "OPTICAL_FLOW" + }, + { + "id": 72, + "origin_id": 45, + "origin_slot": 0, + "target_id": 31, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 333, + "origin_id": 147, + "origin_slot": 0, + "target_id": 31, + "target_slot": 2, + "type": "INT" + }, + { + "id": 335, + "origin_id": 148, + "origin_slot": 0, + "target_id": 31, + "target_slot": 3, + "type": "INT" + }, + { + "id": 67, + "origin_id": 43, + "origin_slot": 2, + "target_id": 31, + "target_slot": 4, + "type": "INT" + }, + { + "id": 54, + "origin_id": 32, + "origin_slot": 0, + "target_id": 35, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 311, + "origin_id": 136, + "origin_slot": 0, + "target_id": 35, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 305, + "origin_id": 134, + "origin_slot": 0, + "target_id": 35, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 313, + "origin_id": 137, + "origin_slot": 0, + "target_id": 35, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 48, + "origin_id": 10, + "origin_slot": 2, + "target_id": 35, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 340, + "origin_id": 149, + "origin_slot": 0, + "target_id": 132, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 8, + "origin_id": 6, + "origin_slot": 0, + "target_id": 10, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 9, + "origin_id": 7, + "origin_slot": 0, + "target_id": 10, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 4, + "origin_id": 3, + "origin_slot": 0, + "target_id": 10, + "target_slot": 2, + "type": "VAE" + }, + { + "id": 326, + "origin_id": 145, + "origin_slot": 0, + "target_id": 10, + "target_slot": 3, + "type": "IMAGE" + }, + { + "id": 339, + "origin_id": 149, + "origin_slot": 0, + "target_id": 10, + "target_slot": 4, + "type": "MASK" + }, + { + "id": 332, + "origin_id": 147, + "origin_slot": 0, + "target_id": 10, + "target_slot": 5, + "type": "INT" + }, + { + "id": 334, + "origin_id": 148, + "origin_slot": 0, + "target_id": 10, + "target_slot": 6, + "type": "INT" + }, + { + "id": 63, + "origin_id": 43, + "origin_slot": 2, + "target_id": 10, + "target_slot": 7, + "type": "INT" + }, + { + "id": 53, + "origin_id": 31, + "origin_slot": 0, + "target_id": 32, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 323, + "origin_id": 143, + "origin_slot": 0, + "target_id": 137, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 73, + "origin_id": 45, + "origin_slot": 0, + "target_id": 46, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 355, + "origin_id": 158, + "origin_slot": 0, + "target_id": 46, + "target_slot": 1, + "type": "AUDIO" + }, + { + "id": 368, + "origin_id": 166, + "origin_slot": 2, + "target_id": 46, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 320, + "origin_id": 141, + "origin_slot": 0, + "target_id": 49, + "target_slot": 0, + "type": "NOISE" + }, + { + "id": 319, + "origin_id": 140, + "origin_slot": 0, + "target_id": 49, + "target_slot": 1, + "type": "GUIDER" + }, + { + "id": 304, + "origin_id": 133, + "origin_slot": 0, + "target_id": 49, + "target_slot": 2, + "type": "SAMPLER" + }, + { + "id": 315, + "origin_id": 138, + "origin_slot": 0, + "target_id": 49, + "target_slot": 3, + "type": "SIGMAS" + }, + { + "id": 82, + "origin_id": 10, + "origin_slot": 2, + "target_id": 49, + "target_slot": 4, + "type": "LATENT" + }, + { + "id": 83, + "origin_id": 49, + "origin_slot": 0, + "target_id": 45, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 70, + "origin_id": 3, + "origin_slot": 0, + "target_id": 45, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 2, + "origin_id": 2, + "origin_slot": 0, + "target_id": 6, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 366, + "origin_id": 166, + "origin_slot": 0, + "target_id": 145, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 361, + "origin_id": 164, + "origin_slot": 1, + "target_id": 145, + "target_slot": 2, + "type": "INT" + }, + { + "id": 49, + "origin_id": 35, + "origin_slot": 0, + "target_id": 36, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 45, + "origin_id": 3, + "origin_slot": 0, + "target_id": 36, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 336, + "origin_id": 145, + "origin_slot": 0, + "target_id": 149, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 327, + "origin_id": 145, + "origin_slot": 0, + "target_id": 43, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 342, + "origin_id": 45, + "origin_slot": 0, + "target_id": 150, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 341, + "origin_id": 36, + "origin_slot": 0, + "target_id": 150, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 346, + "origin_id": 153, + "origin_slot": 0, + "target_id": 150, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 367, + "origin_id": 166, + "origin_slot": 1, + "target_id": 158, + "target_slot": 0, + "type": "AUDIO" + }, + { + "id": 360, + "origin_id": 163, + "origin_slot": 0, + "target_id": 164, + "target_slot": 0, + "type": "INT" + }, + { + "id": 371, + "origin_id": 166, + "origin_slot": 2, + "target_id": 164, + "target_slot": 1, + "type": "FLOAT" + }, + { + "id": 363, + "origin_id": 150, + "origin_slot": 0, + "target_id": 165, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 364, + "origin_id": 158, + "origin_slot": 0, + "target_id": 165, + "target_slot": 1, + "type": "AUDIO" + }, + { + "id": 372, + "origin_id": 166, + "origin_slot": 2, + "target_id": 165, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 373, + "origin_id": -10, + "origin_slot": 0, + "target_id": 166, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 77, + "origin_id": 46, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 362, + "origin_id": 165, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "VIDEO" + }, + { + "id": 377, + "origin_id": -10, + "origin_slot": 1, + "target_id": 6, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 382, + "origin_id": 168, + "origin_slot": 0, + "target_id": 169, + "target_slot": 0, + "type": "INT" + }, + { + "id": 383, + "origin_id": 166, + "origin_slot": 2, + "target_id": 169, + "target_slot": 1, + "type": "FLOAT" + }, + { + "id": 384, + "origin_id": 169, + "origin_slot": 1, + "target_id": 145, + "target_slot": 1, + "type": "INT" + }, + { + "id": 385, + "origin_id": 164, + "origin_slot": 0, + "target_id": 158, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 386, + "origin_id": 169, + "origin_slot": 0, + "target_id": 158, + "target_slot": 1, + "type": "FLOAT" + }, + { + "id": 387, + "origin_id": -10, + "origin_slot": 2, + "target_id": 7, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 388, + "origin_id": -10, + "origin_slot": 3, + "target_id": 149, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 389, + "origin_id": -10, + "origin_slot": 4, + "target_id": 168, + "target_slot": 0, + "type": "INT" + }, + { + "id": 390, + "origin_id": -10, + "origin_slot": 5, + "target_id": 163, + "target_slot": 0, + "type": "INT" + }, + { + "id": 391, + "origin_id": -10, + "origin_slot": 6, + "target_id": 147, + "target_slot": 0, + "type": "INT" + }, + { + "id": 392, + "origin_id": -10, + "origin_slot": 7, + "target_id": 148, + "target_slot": 0, + "type": "INT" + }, + { + "id": 393, + "origin_id": -10, + "origin_slot": 8, + "target_id": 153, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 400, + "origin_id": -10, + "origin_slot": 9, + "target_id": 141, + "target_slot": 0, + "type": "INT" + }, + { + "id": 401, + "origin_id": -10, + "origin_slot": 10, + "target_id": 149, + "target_slot": 8, + "type": "COMBO" + }, + { + "id": 402, + "origin_id": -10, + "origin_slot": 11, + "target_id": 144, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 403, + "origin_id": -10, + "origin_slot": 12, + "target_id": 143, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 404, + "origin_id": -10, + "origin_slot": 13, + "target_id": 142, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 405, + "origin_id": -10, + "origin_slot": 14, + "target_id": 2, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 406, + "origin_id": -10, + "origin_slot": 15, + "target_id": 3, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": {}, + "category": "Video generation and editing/Inpaint video", + "description": "Removes objects from video by inpainting masked regions using VOID (CogVideoX), with SAM3 text-guided segmentation and optional two-pass optical-flow refinement." + }, + { + "id": "c3e0d783-9aa3-4e75-a94d-19937968ef86", + "version": 1, + "state": { + "lastGroupId": 13, + "lastNodeId": 171, + "lastLinkId": 406, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image Segmentation (SAM3)", + "description": "Segments images into masks using Meta SAM3 from text prompts, points, or boxes.", + "inputNode": { + "id": -10, + "bounding": [ + -2260, + -3450, + 144.369140625, + 228 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -1130, + -3305, + 128, + 88 + ] + }, + "inputs": [ + { + "id": "a6e75fa2-162a-4af0-a2fd-1e9c899a5ab6", + "name": "image", + "type": "IMAGE", + "linkIds": [ + 264 + ], + "localized_name": "image", + "label": "image", + "pos": [ + -2139.630859375, + -3426 + ] + }, + { + "id": "3cefd304-7631-4ff6-a5a0-5a0ffb120745", + "name": "text", + "type": "STRING", + "linkIds": [ + 265 + ], + "label": "object", + "pos": [ + -2139.630859375, + -3406 + ] + }, + { + "id": "1aec91c5-d8d2-441c-928c-49c14e7e80ed", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 266 + ], + "pos": [ + -2139.630859375, + -3386 + ] + }, + { + "id": "1ec7ce1a-8257-4719-8a81-60ebc8a98899", + "name": "positive_coords", + "type": "STRING", + "linkIds": [ + 267 + ], + "pos": [ + -2139.630859375, + -3366 + ] + }, + { + "id": "c65f8b87-9bd7-48be-9fc2-823431e95019", + "name": "negative_coords", + "type": "STRING", + "linkIds": [ + 268 + ], + "pos": [ + -2139.630859375, + -3346 + ] + }, + { + "id": "bb4ba35a-ccfe-4c37-98e5-d9b0d69585fb", + "name": "threshold", + "type": "FLOAT", + "linkIds": [ + 269 + ], + "pos": [ + -2139.630859375, + -3326 + ] + }, + { + "id": "b1439668-b050-490b-a5dc-fc4052c55666", + "name": "refine_iterations", + "type": "INT", + "linkIds": [ + 270 + ], + "pos": [ + -2139.630859375, + -3306 + ] + }, + { + "id": "86e239e5-c098-4302-b54d-d42a38bc0f89", + "name": "individual_masks", + "type": "BOOLEAN", + "linkIds": [ + 271 + ], + "pos": [ + -2139.630859375, + -3286 + ] + }, + { + "id": "f9e0b9d4-b2f1-4907-a4a5-305656576706", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 272 + ], + "pos": [ + -2139.630859375, + -3266 + ] + } + ], + "outputs": [ + { + "id": "ff50da09-1e59-4a58-9b7f-be1a00aa5913", + "name": "masks", + "type": "MASK", + "linkIds": [ + 231 + ], + "localized_name": "masks", + "pos": [ + -1106, + -3281 + ] + }, + { + "id": "8f622e40-8528-4078-b7d3-147e9f872194", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 232 + ], + "localized_name": "bboxes", + "pos": [ + -1106, + -3261 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 75, + "type": "SAM3_Detect", + "pos": [ + -1470, + -3460 + ], + "size": [ + 270, + 260 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "label": "model", + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 237 + }, + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 264 + }, + { + "label": "conditioning", + "localized_name": "conditioning", + "name": "conditioning", + "shape": 7, + "type": "CONDITIONING", + "link": 200 + }, + { + "label": "bboxes", + "localized_name": "bboxes", + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": 266 + }, + { + "label": "positive_coords", + "localized_name": "positive_coords", + "name": "positive_coords", + "shape": 7, + "type": "STRING", + "link": 267 + }, + { + "label": "negative_coords", + "localized_name": "negative_coords", + "name": "negative_coords", + "shape": 7, + "type": "STRING", + "link": 268 + }, + { + "localized_name": "threshold", + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": 269 + }, + { + "localized_name": "refine_iterations", + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": 270 + }, + { + "localized_name": "individual_masks", + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": 271 + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [ + 231 + ] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [ + 232 + ] + } + ], + "properties": { + "Node name for S&R": "SAM3_Detect", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 0.5, + 2, + false + ] + }, + { + "id": 77, + "type": "CheckpointLoaderSimple", + "pos": [ + -1970, + -3200 + ], + "size": [ + 330, + 140 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 272 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 237 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 240 + ] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": null + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "models": [ + { + "name": "sam3.1_multiplex_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors", + "directory": "checkpoints" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sam3.1_multiplex_fp16.safetensors" + ] + }, + { + "id": 78, + "type": "CLIPTextEncode", + "pos": [ + -2000, + -3000 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 240 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 265 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 200 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ] + } + ], + "groups": [], + "links": [ + { + "id": 237, + "origin_id": 77, + "origin_slot": 0, + "target_id": 75, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 200, + "origin_id": 78, + "origin_slot": 0, + "target_id": 75, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 240, + "origin_id": 77, + "origin_slot": 1, + "target_id": 78, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 231, + "origin_id": 75, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 232, + "origin_id": 75, + "origin_slot": 1, + "target_id": -20, + "target_slot": 1, + "type": "BOUNDING_BOX" + }, + { + "id": 264, + "origin_id": -10, + "origin_slot": 0, + "target_id": 75, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 265, + "origin_id": -10, + "origin_slot": 1, + "target_id": 78, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 266, + "origin_id": -10, + "origin_slot": 2, + "target_id": 75, + "target_slot": 3, + "type": "BOUNDING_BOX" + }, + { + "id": 267, + "origin_id": -10, + "origin_slot": 3, + "target_id": 75, + "target_slot": 4, + "type": "STRING" + }, + { + "id": 268, + "origin_id": -10, + "origin_slot": 4, + "target_id": 75, + "target_slot": 5, + "type": "STRING" + }, + { + "id": 269, + "origin_id": -10, + "origin_slot": 5, + "target_id": 75, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 270, + "origin_id": -10, + "origin_slot": 6, + "target_id": 75, + "target_slot": 7, + "type": "INT" + }, + { + "id": 271, + "origin_id": -10, + "origin_slot": 7, + "target_id": 75, + "target_slot": 8, + "type": "BOOLEAN" + }, + { + "id": 272, + "origin_id": -10, + "origin_slot": 8, + "target_id": 77, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": { + "ue_links": [] + } + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Video Inpainting (Wan2.1 VACE).json b/blueprints/Video Inpainting (Wan2.1 VACE).json new file mode 100644 index 000000000..7460f3d44 --- /dev/null +++ b/blueprints/Video Inpainting (Wan2.1 VACE).json @@ -0,0 +1,4196 @@ +{ + "revision": 0, + "last_node_id": 306, + "last_link_id": 0, + "nodes": [ + { + "id": 306, + "type": "bd7f73a0-ec67-4f46-8671-17088d8e31b7", + "pos": [ + -2950, + -410 + ], + "size": [ + 440, + 650 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "label": "source_video", + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": null + }, + { + "label": "reference_image", + "name": "reference_image_1", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "label": "prompt", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "label": "width", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + }, + { + "label": "height", + "name": "value_1", + "type": "INT", + "widget": { + "name": "value_1" + }, + "link": null + }, + { + "label": "frame_counts", + "name": "length", + "type": "INT", + "widget": { + "name": "length" + }, + "link": null + }, + { + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": null + }, + { + "label": "wan_vace_model", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "label": "clip_model", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": null + }, + { + "label": "vae_model", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": null + }, + { + "label": "enable_turbo_mode", + "name": "value_2", + "type": "BOOLEAN", + "widget": { + "name": "value_2" + }, + "link": null + }, + { + "label": "lightning_lora", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": null + }, + { + "label": "sam3_mask_object", + "name": "text_1", + "type": "STRING", + "widget": { + "name": "text_1" + }, + "link": null + }, + { + "label": "mask_expand", + "name": "expand", + "type": "INT", + "widget": { + "name": "expand" + }, + "link": null + }, + { + "label": "sam3_model", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "280", + "text" + ], + [ + "297", + "value" + ], + [ + "290", + "value" + ], + [ + "289", + "length" + ], + [ + "288", + "seed" + ], + [ + "299", + "unet_name" + ], + [ + "277", + "clip_name" + ], + [ + "278", + "vae_name" + ], + [ + "300", + "value" + ], + [ + "272", + "lora_name" + ], + [ + "268", + "text" + ], + [ + "269", + "expand" + ], + [ + "268", + "ckpt_name" + ], + [ + "312", + "$$canvas-image-preview" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Video Inpainting (Wan2.1 VACE)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "bd7f73a0-ec67-4f46-8671-17088d8e31b7", + "version": 1, + "state": { + "lastGroupId": 31, + "lastNodeId": 315, + "lastLinkId": 499, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Video Inpainting (Wan2.1 VACE)", + "inputNode": { + "id": -10, + "bounding": [ + -3450, + 3170, + 159.744140625, + 348 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 900, + 2840, + 128, + 68 + ] + }, + "inputs": [ + { + "id": "a636746e-5b9f-4b91-96f0-7f2657415b93", + "name": "video", + "type": "VIDEO", + "linkIds": [ + 473 + ], + "localized_name": "video", + "label": "source_video", + "pos": [ + -3314.255859375, + 3194 + ] + }, + { + "id": "46275350-98b8-4d7c-8ca4-c452dc40a6bd", + "name": "reference_image_1", + "type": "IMAGE", + "linkIds": [ + 478 + ], + "label": "reference_image", + "pos": [ + -3314.255859375, + 3214 + ] + }, + { + "id": "0f5bee71-3485-4e10-81a7-2b9f85851353", + "name": "text", + "type": "STRING", + "linkIds": [ + 479 + ], + "label": "prompt", + "pos": [ + -3314.255859375, + 3234 + ] + }, + { + "id": "16675512-c229-43ed-944e-190a7f61b571", + "name": "value", + "type": "INT", + "linkIds": [ + 480 + ], + "label": "width", + "pos": [ + -3314.255859375, + 3254 + ] + }, + { + "id": "84330129-a0c7-44cd-91fe-c033946749db", + "name": "value_1", + "type": "INT", + "linkIds": [ + 481 + ], + "label": "height", + "pos": [ + -3314.255859375, + 3274 + ] + }, + { + "id": "3bd895e6-cba9-477b-bf6e-8c77dd56bb4a", + "name": "length", + "type": "INT", + "linkIds": [ + 494 + ], + "label": "frame_counts", + "pos": [ + -3314.255859375, + 3294 + ] + }, + { + "id": "dbc2e9c5-f86a-48ba-874a-2991c75d1ae7", + "name": "seed", + "type": "INT", + "linkIds": [ + 483 + ], + "pos": [ + -3314.255859375, + 3314 + ] + }, + { + "id": "572db94d-e64d-464f-bf3c-23a23aeb79f1", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 485 + ], + "label": "wan_vace_model", + "pos": [ + -3314.255859375, + 3334 + ] + }, + { + "id": "32185180-f627-47c2-971b-6ef3007e9455", + "name": "clip_name", + "type": "COMBO", + "linkIds": [ + 486 + ], + "label": "clip_model", + "pos": [ + -3314.255859375, + 3354 + ] + }, + { + "id": "2af354d3-108a-42a9-acfc-7bad158715aa", + "name": "vae_name", + "type": "COMBO", + "linkIds": [ + 487 + ], + "label": "vae_model", + "pos": [ + -3314.255859375, + 3374 + ] + }, + { + "id": "c9777a8c-267f-4c5e-b4d5-e9727d822e50", + "name": "value_2", + "type": "BOOLEAN", + "linkIds": [ + 489 + ], + "label": "enable_turbo_mode", + "pos": [ + -3314.255859375, + 3394 + ] + }, + { + "id": "84a258a3-4f25-4edb-9f50-6fcd8411394e", + "name": "lora_name", + "type": "COMBO", + "linkIds": [ + 490 + ], + "label": "lightning_lora", + "pos": [ + -3314.255859375, + 3414 + ] + }, + { + "id": "9c5fb6f8-407b-4a13-94d8-cbbba546a082", + "name": "text_1", + "type": "STRING", + "linkIds": [ + 491 + ], + "label": "sam3_mask_object", + "pos": [ + -3314.255859375, + 3434 + ] + }, + { + "id": "598323c9-2256-44bd-9745-492a74628300", + "name": "expand", + "type": "INT", + "linkIds": [ + 496 + ], + "label": "mask_expand", + "pos": [ + -3314.255859375, + 3454 + ] + }, + { + "id": "856c1937-8caa-4d85-9d8a-6a900234d6d6", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 497 + ], + "label": "sam3_model", + "pos": [ + -3314.255859375, + 3474 + ] + } + ], + "outputs": [ + { + "id": "be46c9d5-ced7-445b-996f-fff59d9b684d", + "name": "VIDEO", + "type": "VIDEO", + "linkIds": [ + 474 + ], + "localized_name": "VIDEO", + "pos": [ + 924, + 2864 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 266, + "type": "ModelSamplingSD3", + "pos": [ + -560, + 1940 + ], + "size": [ + 320, + 110 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 422 + }, + { + "localized_name": "shift", + "name": "shift", + "type": "FLOAT", + "widget": { + "name": "shift" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 454 + ] + } + ], + "properties": { + "Node name for S&R": "ModelSamplingSD3", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + 5 + ] + }, + { + "id": 267, + "type": "CreateVideo", + "pos": [ + 530, + 2590 + ], + "size": [ + 310, + 130 + ], + "flags": { + "collapsed": false + }, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 423 + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 424 + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 425 + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [ + 474 + ] + } + ], + "properties": { + "Node name for S&R": "CreateVideo", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + 16 + ] + }, + { + "id": 268, + "type": "17df2eeb-d89e-46ee-9480-a4ca2494b207", + "pos": [ + -1960, + 3220 + ], + "size": [ + 290, + 370 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 426 + }, + { + "label": "object", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 491 + }, + { + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": null + }, + { + "name": "positive_coords", + "shape": 7, + "type": "STRING", + "link": null + }, + { + "name": "negative_coords", + "shape": 7, + "type": "STRING", + "link": null + }, + { + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": null + }, + { + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": null + }, + { + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": null + }, + { + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 497 + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [ + 427 + ] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "237", + "text" + ], + [ + "75", + "threshold" + ], + [ + "75", + "refine_iterations" + ], + [ + "75", + "individual_masks" + ], + [ + "236", + "ckpt_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": { + "text": true + }, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [] + }, + { + "id": 269, + "type": "GrowMask", + "pos": [ + -1530, + 3220 + ], + "size": [ + 270, + 140 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "localized_name": "mask", + "name": "mask", + "type": "MASK", + "link": 427 + }, + { + "localized_name": "expand", + "name": "expand", + "type": "INT", + "widget": { + "name": "expand" + }, + "link": 496 + }, + { + "localized_name": "tapered_corners", + "name": "tapered_corners", + "type": "BOOLEAN", + "widget": { + "name": "tapered_corners" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MASK", + "name": "MASK", + "type": "MASK", + "links": [ + 441, + 445, + 449, + 498 + ] + } + ], + "properties": { + "Node name for S&R": "GrowMask", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 20, + true + ] + }, + { + "id": 270, + "type": "PrimitiveInt", + "pos": [ + -1350, + 1980 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 466 + ] + } + ], + "title": "Int (Steps)", + "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 20, + "fixed" + ] + }, + { + "id": 271, + "type": "PrimitiveFloat", + "pos": [ + -1340, + 2140 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "FLOAT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 432 + ] + } + ], + "title": "Float (CFG)", + "properties": { + "Node name for S&R": "PrimitiveFloat", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 6 + ] + }, + { + "id": 272, + "type": "LoraLoaderModelOnly", + "pos": [ + -1380, + 2390 + ], + "size": [ + 350, + 140 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 428 + }, + { + "localized_name": "lora_name", + "name": "lora_name", + "type": "COMBO", + "widget": { + "name": "lora_name" + }, + "link": 490 + }, + { + "localized_name": "strength_model", + "name": "strength_model", + "type": "FLOAT", + "widget": { + "name": "strength_model" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 430 + ] + } + ], + "properties": { + "Node name for S&R": "LoraLoaderModelOnly", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "models": [ + { + "name": "Wan21_CausVid_14B_T2V_lora_rank32.safetensors", + "url": "https://huggingface.co/Kijai/WanVideo_comfy/resolve/main/Wan21_CausVid_14B_T2V_lora_rank32.safetensors", + "directory": "loras" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "Wan21_CausVid_14B_T2V_lora_rank32.safetensors", + 0.30000000000000004 + ] + }, + { + "id": 273, + "type": "PrimitiveInt", + "pos": [ + -1340, + 2600 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 467 + ] + } + ], + "title": "Int (Steps)", + "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 6, + "fixed" + ] + }, + { + "id": 274, + "type": "PrimitiveFloat", + "pos": [ + -1340, + 2760 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "FLOAT", + "widget": { + "name": "value" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [ + 433 + ] + } + ], + "title": "Float (CFG)", + "properties": { + "Node name for S&R": "PrimitiveFloat", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 275, + "type": "ComfySwitchNode", + "pos": [ + -960, + 2530 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 429 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 430 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 431 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 422 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 276, + "type": "ComfySwitchNode", + "pos": [ + -960, + 2340 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 432 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 433 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 434 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 459 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 277, + "type": "CLIPLoader", + "pos": [ + -2710, + 2210 + ], + "size": [ + 360, + 170 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "localized_name": "clip_name", + "name": "clip_name", + "type": "COMBO", + "widget": { + "name": "clip_name" + }, + "link": 486 + }, + { + "localized_name": "type", + "name": "type", + "type": "COMBO", + "widget": { + "name": "type" + }, + "link": null + }, + { + "localized_name": "device", + "name": "device", + "shape": 7, + "type": "COMBO", + "widget": { + "name": "device" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "slot_index": 0, + "links": [ + 435, + 436 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPLoader", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "models": [ + { + "name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", + "url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors?download=true", + "directory": "text_encoders" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "umt5_xxl_fp8_e4m3fn_scaled.safetensors", + "wan", + "default" + ] + }, + { + "id": 278, + "type": "VAELoader", + "pos": [ + -2700, + 2500 + ], + "size": [ + 360, + 110 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "localized_name": "vae_name", + "name": "vae_name", + "type": "COMBO", + "widget": { + "name": "vae_name" + }, + "link": 487 + } + ], + "outputs": [ + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "slot_index": 0, + "links": [ + 439, + 471 + ] + } + ], + "properties": { + "Node name for S&R": "VAELoader", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "models": [ + { + "name": "wan_2.1_vae.safetensors", + "url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/vae/wan_2.1_vae.safetensors", + "directory": "vae" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "wan_2.1_vae.safetensors" + ] + }, + { + "id": 279, + "type": "CLIPTextEncode", + "pos": [ + -2280, + 2410 + ], + "size": [ + 430, + 190 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 435 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 438 + ] + } + ], + "title": "CLIP Text Encode (Negative Prompt)", + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走,过曝," + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 280, + "type": "CLIPTextEncode", + "pos": [ + -2270, + 1940 + ], + "size": [ + 420, + 420 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 436 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 479 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 437 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 281, + "type": "WanVaceToVideo", + "pos": [ + -1780, + 1940 + ], + "size": [ + 320, + 360 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 437 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 438 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 439 + }, + { + "localized_name": "control_video", + "name": "control_video", + "shape": 7, + "type": "IMAGE", + "link": 440 + }, + { + "localized_name": "control_masks", + "name": "control_masks", + "shape": 7, + "type": "MASK", + "link": 441 + }, + { + "localized_name": "reference_image", + "name": "reference_image", + "shape": 7, + "type": "IMAGE", + "link": 478 + }, + { + "localized_name": "width", + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 442 + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 443 + }, + { + "localized_name": "length", + "name": "length", + "type": "INT", + "widget": { + "name": "length" + }, + "link": 444 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + }, + { + "localized_name": "strength", + "name": "strength", + "type": "FLOAT", + "widget": { + "name": "strength" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "links": [ + 455 + ] + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "links": [ + 456 + ] + }, + { + "localized_name": "latent", + "name": "latent", + "type": "LATENT", + "links": [ + 457 + ] + }, + { + "localized_name": "trim_latent", + "name": "trim_latent", + "type": "INT", + "links": [ + 453 + ] + } + ], + "properties": { + "Node name for S&R": "WanVaceToVideo", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": { + "width": true, + "height": true, + "length": true + } + }, + "widgets_values": [ + 720, + 720, + 81, + 1, + 1 + ] + }, + { + "id": 282, + "type": "InvertMask", + "pos": [ + -1510, + 3410 + ], + "size": [ + 230, + 80 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "localized_name": "mask", + "name": "mask", + "type": "MASK", + "link": 445 + } + ], + "outputs": [ + { + "localized_name": "MASK", + "name": "MASK", + "type": "MASK", + "links": [ + 446 + ] + } + ], + "properties": { + "Node name for S&R": "InvertMask", + "cnr_id": "comfy-core", + "ver": "0.3.40", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 283, + "type": "MaskToImage", + "pos": [ + -1510, + 3550 + ], + "size": [ + 230, + 80 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "localized_name": "mask", + "name": "mask", + "type": "MASK", + "link": 446 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 448 + ] + } + ], + "properties": { + "Node name for S&R": "MaskToImage", + "cnr_id": "comfy-core", + "ver": "0.3.40", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 284, + "type": "ImageCompositeMasked", + "pos": [ + -1210, + 3210 + ], + "size": [ + 230, + 220 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "localized_name": "destination", + "name": "destination", + "type": "IMAGE", + "link": 447 + }, + { + "localized_name": "source", + "name": "source", + "type": "IMAGE", + "link": 448 + }, + { + "localized_name": "mask", + "name": "mask", + "shape": 7, + "type": "MASK", + "link": 449 + }, + { + "localized_name": "x", + "name": "x", + "type": "INT", + "widget": { + "name": "x" + }, + "link": null + }, + { + "localized_name": "y", + "name": "y", + "type": "INT", + "widget": { + "name": "y" + }, + "link": null + }, + { + "localized_name": "resize_source", + "name": "resize_source", + "type": "BOOLEAN", + "widget": { + "name": "resize_source" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 440, + 499 + ] + } + ], + "properties": { + "Node name for S&R": "ImageCompositeMasked", + "cnr_id": "comfy-core", + "ver": "0.3.40", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + 0, + true + ] + }, + { + "id": 287, + "type": "TrimVideoLatent", + "pos": [ + -220, + 1950 + ], + "size": [ + 320, + 110 + ], + "flags": { + "collapsed": false + }, + "order": 20, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 452 + }, + { + "localized_name": "trim_amount", + "name": "trim_amount", + "type": "INT", + "widget": { + "name": "trim_amount" + }, + "link": 453 + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 470 + ] + } + ], + "properties": { + "Node name for S&R": "TrimVideoLatent", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": { + "trim_amount": true + } + }, + "widgets_values": [ + 0 + ] + }, + { + "id": 288, + "type": "KSampler", + "pos": [ + -560, + 2120 + ], + "size": [ + 320, + 350 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 454 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 455 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 456 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 457 + }, + { + "localized_name": "seed", + "name": "seed", + "type": "INT", + "widget": { + "name": "seed" + }, + "link": 483 + }, + { + "localized_name": "steps", + "name": "steps", + "type": "INT", + "widget": { + "name": "steps" + }, + "link": 458 + }, + { + "localized_name": "cfg", + "name": "cfg", + "type": "FLOAT", + "widget": { + "name": "cfg" + }, + "link": 459 + }, + { + "localized_name": "sampler_name", + "name": "sampler_name", + "type": "COMBO", + "widget": { + "name": "sampler_name" + }, + "link": null + }, + { + "localized_name": "scheduler", + "name": "scheduler", + "type": "COMBO", + "widget": { + "name": "scheduler" + }, + "link": null + }, + { + "localized_name": "denoise", + "name": "denoise", + "type": "FLOAT", + "widget": { + "name": "denoise" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 452 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + 832378512055965, + "fixed", + 4, + 1, + "uni_pc", + "simple", + 1 + ] + }, + { + "id": 289, + "type": "ImageFromBatch", + "pos": [ + -2360, + 3410 + ], + "size": [ + 270, + 140 + ], + "flags": {}, + "order": 22, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 460 + }, + { + "localized_name": "batch_index", + "name": "batch_index", + "type": "INT", + "widget": { + "name": "batch_index" + }, + "link": null + }, + { + "localized_name": "length", + "name": "length", + "type": "INT", + "widget": { + "name": "length" + }, + "link": 494 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 463 + ] + } + ], + "properties": { + "Node name for S&R": "ImageFromBatch", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + 81 + ] + }, + { + "id": 290, + "type": "PrimitiveInt", + "pos": [ + -2690, + 3540 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 23, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 481 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 461 + ] + } + ], + "title": "Int (Height)", + "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 720, + "fixed" + ] + }, + { + "id": 291, + "type": "ComfyMathExpression", + "pos": [ + -2650, + 3700 + ], + "size": [ + 230, + 80 + ], + "flags": { + "collapsed": true + }, + "order": 24, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT,BOOLEAN", + "link": 461 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [] + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 465 + ] + }, + { + "localized_name": "BOOL", + "name": "BOOL", + "type": "BOOLEAN", + "links": [] + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "floor(a/16)*16" + ] + }, + { + "id": 292, + "type": "ComfyMathExpression", + "pos": [ + -2650, + 3500 + ], + "size": [ + 230, + 80 + ], + "flags": { + "collapsed": true + }, + "order": 25, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT,BOOLEAN", + "link": 462 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT,BOOLEAN", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": [] + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 464 + ] + }, + { + "localized_name": "BOOL", + "name": "BOOL", + "type": "BOOLEAN", + "links": [] + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "floor(a/16)*16" + ] + }, + { + "id": 293, + "type": "ResizeImageMaskNode", + "pos": [ + -2360, + 3590 + ], + "size": [ + 280, + 160 + ], + "flags": {}, + "order": 26, + "mode": 0, + "inputs": [ + { + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": 463 + }, + { + "localized_name": "resize_type", + "name": "resize_type", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "resize_type" + }, + "link": null + }, + { + "localized_name": "width", + "name": "resize_type.width", + "type": "INT", + "widget": { + "name": "resize_type.width" + }, + "link": 464 + }, + { + "localized_name": "height", + "name": "resize_type.height", + "type": "INT", + "widget": { + "name": "resize_type.height" + }, + "link": 465 + }, + { + "localized_name": "crop", + "name": "resize_type.crop", + "type": "COMBO", + "widget": { + "name": "resize_type.crop" + }, + "link": null + }, + { + "localized_name": "scale_method", + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "resized", + "name": "resized", + "type": "*", + "links": [ + 426, + 447, + 469 + ] + } + ], + "properties": { + "Node name for S&R": "ResizeImageMaskNode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "scale dimensions", + 512, + 512, + "center", + "area" + ] + }, + { + "id": 294, + "type": "ComfySwitchNode", + "pos": [ + -960, + 2150 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 27, + "mode": 0, + "inputs": [ + { + "localized_name": "on_false", + "name": "on_false", + "type": "*", + "link": 466 + }, + { + "localized_name": "on_true", + "name": "on_true", + "type": "*", + "link": 467 + }, + { + "localized_name": "switch", + "name": "switch", + "type": "BOOLEAN", + "widget": { + "name": "switch" + }, + "link": 468 + } + ], + "outputs": [ + { + "localized_name": "output", + "name": "output", + "type": "*", + "links": [ + 458 + ] + } + ], + "properties": { + "Node name for S&R": "ComfySwitchNode", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + false + ] + }, + { + "id": 295, + "type": "GetImageSize", + "pos": [ + -2010, + 2920 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 28, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 469 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 442 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": [ + 443 + ] + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": [ + 444 + ] + } + ], + "properties": { + "Node name for S&R": "GetImageSize", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 296, + "type": "VAEDecode", + "pos": [ + 520, + 2450 + ], + "size": [ + 320, + 100 + ], + "flags": { + "collapsed": false + }, + "order": 29, + "mode": 0, + "inputs": [ + { + "localized_name": "samples", + "name": "samples", + "type": "LATENT", + "link": 470 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 471 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 423 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecode", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + } + }, + { + "id": 297, + "type": "PrimitiveInt", + "pos": [ + -2690, + 3350 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 30, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "INT", + "widget": { + "name": "value" + }, + "link": 480 + } + ], + "outputs": [ + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 462 + ] + } + ], + "title": "Int (Width)", + "properties": { + "Node name for S&R": "PrimitiveInt", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 720, + "fixed" + ] + }, + { + "id": 298, + "type": "GetVideoComponents", + "pos": [ + -2330, + 3210 + ], + "size": [ + 230, + 120 + ], + "flags": { + "collapsed": false + }, + "order": 31, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 473 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 460 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": [ + 424 + ] + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": [ + 425 + ] + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents", + "cnr_id": "comfy-core", + "ver": "0.3.40", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 299, + "type": "UNETLoader", + "pos": [ + -2720, + 1980 + ], + "size": [ + 370, + 140 + ], + "flags": {}, + "order": 32, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 485 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 428, + 429 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.3.34", + "models": [ + { + "name": "wan2.1_vace_14B_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_vace_14B_fp16.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "widget_ue_connectable": {} + }, + "widgets_values": [ + "wan2.1_vace_14B_fp16.safetensors", + "fp8_e4m3fn_fast" + ] + }, + { + "id": 300, + "type": "PrimitiveBoolean", + "pos": [ + -1390, + 2980 + ], + "size": [ + 270, + 100 + ], + "flags": {}, + "order": 33, + "mode": 0, + "inputs": [ + { + "localized_name": "value", + "name": "value", + "type": "BOOLEAN", + "widget": { + "name": "value" + }, + "link": 489 + } + ], + "outputs": [ + { + "localized_name": "BOOLEAN", + "name": "BOOLEAN", + "type": "BOOLEAN", + "links": [ + 431, + 434, + 468 + ] + } + ], + "title": "Boolean (Enable Lightning LoRA)", + "properties": { + "Node name for S&R": "PrimitiveBoolean", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + true + ] + }, + { + "id": 308, + "type": "ImageFromBatch", + "pos": [ + -2360, + 3410 + ], + "size": [ + 270, + 140 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": null + }, + { + "localized_name": "batch_index", + "name": "batch_index", + "type": "INT", + "widget": { + "name": "batch_index" + }, + "link": null + }, + { + "localized_name": "length", + "name": "length", + "type": "INT", + "widget": { + "name": "length" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "Node name for S&R": "ImageFromBatch", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0, + 1 + ] + }, + { + "id": 310, + "type": "MaskPreview", + "pos": [ + -900, + 3230 + ], + "size": [ + 230, + 80 + ], + "flags": {}, + "order": 34, + "mode": 4, + "inputs": [ + { + "localized_name": "mask", + "name": "mask", + "type": "MASK", + "link": 498 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "MaskPreview", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 312, + "type": "PreviewImage", + "pos": [ + -520, + 3230 + ], + "size": [ + 230, + 80 + ], + "flags": {}, + "order": 35, + "mode": 4, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 499 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PreviewImage", + "cnr_id": "comfy-core", + "ver": "0.21.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + } + ], + "groups": [ + { + "id": 1, + "title": "Models", + "bounding": [ + -2750, + 1860, + 430, + 770 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 2, + "title": "Prompt", + "bounding": [ + -2290, + 1860, + 460, + 770 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 3, + "title": "Sampling", + "bounding": [ + -590, + 1860, + 700, + 620 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 20, + "title": "Create Video Mask", + "bounding": [ + -2030, + 3110, + 440, + 550 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 23, + "title": "Conditioning", + "bounding": [ + -1800, + 1860, + 370, + 450 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 26, + "title": "Apply Mask to Video", + "bounding": [ + -1560, + 3110, + 1320, + 550 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 29, + "title": "Swtich Logic", + "bounding": [ + -1400, + 1860, + 780, + 1060 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 27, + "title": "Lightning LoRA", + "bounding": [ + -1390, + 2290, + 370, + 620 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 28, + "title": "Original", + "bounding": [ + -1390, + 1900, + 370, + 370 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 31, + "title": "Video Size Preprocessing", + "bounding": [ + -2740, + 3110, + 680, + 770 + ], + "color": "#3f789e", + "flags": {} + }, + { + "id": 30, + "title": "Size", + "bounding": [ + -2710, + 3270, + 330, + 470 + ], + "color": "#3f789e", + "flags": {} + } + ], + "links": [ + { + "id": 422, + "origin_id": 275, + "origin_slot": 0, + "target_id": 266, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 423, + "origin_id": 296, + "origin_slot": 0, + "target_id": 267, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 424, + "origin_id": 298, + "origin_slot": 1, + "target_id": 267, + "target_slot": 1, + "type": "AUDIO" + }, + { + "id": 425, + "origin_id": 298, + "origin_slot": 2, + "target_id": 267, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 426, + "origin_id": 293, + "origin_slot": 0, + "target_id": 268, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 427, + "origin_id": 268, + "origin_slot": 0, + "target_id": 269, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 428, + "origin_id": 299, + "origin_slot": 0, + "target_id": 272, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 429, + "origin_id": 299, + "origin_slot": 0, + "target_id": 275, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 430, + "origin_id": 272, + "origin_slot": 0, + "target_id": 275, + "target_slot": 1, + "type": "MODEL" + }, + { + "id": 431, + "origin_id": 300, + "origin_slot": 0, + "target_id": 275, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 432, + "origin_id": 271, + "origin_slot": 0, + "target_id": 276, + "target_slot": 0, + "type": "FLOAT" + }, + { + "id": 433, + "origin_id": 274, + "origin_slot": 0, + "target_id": 276, + "target_slot": 1, + "type": "FLOAT" + }, + { + "id": 434, + "origin_id": 300, + "origin_slot": 0, + "target_id": 276, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 435, + "origin_id": 277, + "origin_slot": 0, + "target_id": 279, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 436, + "origin_id": 277, + "origin_slot": 0, + "target_id": 280, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 437, + "origin_id": 280, + "origin_slot": 0, + "target_id": 281, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 438, + "origin_id": 279, + "origin_slot": 0, + "target_id": 281, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 439, + "origin_id": 278, + "origin_slot": 0, + "target_id": 281, + "target_slot": 2, + "type": "VAE" + }, + { + "id": 440, + "origin_id": 284, + "origin_slot": 0, + "target_id": 281, + "target_slot": 3, + "type": "IMAGE" + }, + { + "id": 441, + "origin_id": 269, + "origin_slot": 0, + "target_id": 281, + "target_slot": 4, + "type": "MASK" + }, + { + "id": 442, + "origin_id": 295, + "origin_slot": 0, + "target_id": 281, + "target_slot": 6, + "type": "INT" + }, + { + "id": 443, + "origin_id": 295, + "origin_slot": 1, + "target_id": 281, + "target_slot": 7, + "type": "INT" + }, + { + "id": 444, + "origin_id": 295, + "origin_slot": 2, + "target_id": 281, + "target_slot": 8, + "type": "INT" + }, + { + "id": 445, + "origin_id": 269, + "origin_slot": 0, + "target_id": 282, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 446, + "origin_id": 282, + "origin_slot": 0, + "target_id": 283, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 447, + "origin_id": 293, + "origin_slot": 0, + "target_id": 284, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 448, + "origin_id": 283, + "origin_slot": 0, + "target_id": 284, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 449, + "origin_id": 269, + "origin_slot": 0, + "target_id": 284, + "target_slot": 2, + "type": "MASK" + }, + { + "id": 452, + "origin_id": 288, + "origin_slot": 0, + "target_id": 287, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 453, + "origin_id": 281, + "origin_slot": 3, + "target_id": 287, + "target_slot": 1, + "type": "INT" + }, + { + "id": 454, + "origin_id": 266, + "origin_slot": 0, + "target_id": 288, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 455, + "origin_id": 281, + "origin_slot": 0, + "target_id": 288, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 456, + "origin_id": 281, + "origin_slot": 1, + "target_id": 288, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 457, + "origin_id": 281, + "origin_slot": 2, + "target_id": 288, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 458, + "origin_id": 294, + "origin_slot": 0, + "target_id": 288, + "target_slot": 5, + "type": "INT" + }, + { + "id": 459, + "origin_id": 276, + "origin_slot": 0, + "target_id": 288, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 460, + "origin_id": 298, + "origin_slot": 0, + "target_id": 289, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 461, + "origin_id": 290, + "origin_slot": 0, + "target_id": 291, + "target_slot": 0, + "type": "INT" + }, + { + "id": 462, + "origin_id": 297, + "origin_slot": 0, + "target_id": 292, + "target_slot": 0, + "type": "INT" + }, + { + "id": 463, + "origin_id": 289, + "origin_slot": 0, + "target_id": 293, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 464, + "origin_id": 292, + "origin_slot": 1, + "target_id": 293, + "target_slot": 2, + "type": "INT" + }, + { + "id": 465, + "origin_id": 291, + "origin_slot": 1, + "target_id": 293, + "target_slot": 3, + "type": "INT" + }, + { + "id": 466, + "origin_id": 270, + "origin_slot": 0, + "target_id": 294, + "target_slot": 0, + "type": "INT" + }, + { + "id": 467, + "origin_id": 273, + "origin_slot": 0, + "target_id": 294, + "target_slot": 1, + "type": "INT" + }, + { + "id": 468, + "origin_id": 300, + "origin_slot": 0, + "target_id": 294, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 469, + "origin_id": 293, + "origin_slot": 0, + "target_id": 295, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 470, + "origin_id": 287, + "origin_slot": 0, + "target_id": 296, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 471, + "origin_id": 278, + "origin_slot": 0, + "target_id": 296, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 473, + "origin_id": -10, + "origin_slot": 0, + "target_id": 298, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 474, + "origin_id": 267, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 478, + "origin_id": -10, + "origin_slot": 1, + "target_id": 281, + "target_slot": 5, + "type": "IMAGE" + }, + { + "id": 479, + "origin_id": -10, + "origin_slot": 2, + "target_id": 280, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 480, + "origin_id": -10, + "origin_slot": 3, + "target_id": 297, + "target_slot": 0, + "type": "INT" + }, + { + "id": 481, + "origin_id": -10, + "origin_slot": 4, + "target_id": 290, + "target_slot": 0, + "type": "INT" + }, + { + "id": 494, + "origin_id": -10, + "origin_slot": 5, + "target_id": 289, + "target_slot": 2, + "type": "INT" + }, + { + "id": 483, + "origin_id": -10, + "origin_slot": 6, + "target_id": 288, + "target_slot": 4, + "type": "INT" + }, + { + "id": 485, + "origin_id": -10, + "origin_slot": 7, + "target_id": 299, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 486, + "origin_id": -10, + "origin_slot": 8, + "target_id": 277, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 487, + "origin_id": -10, + "origin_slot": 9, + "target_id": 278, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 489, + "origin_id": -10, + "origin_slot": 10, + "target_id": 300, + "target_slot": 0, + "type": "BOOLEAN" + }, + { + "id": 490, + "origin_id": -10, + "origin_slot": 11, + "target_id": 272, + "target_slot": 1, + "type": "COMBO" + }, + { + "id": 491, + "origin_id": -10, + "origin_slot": 12, + "target_id": 268, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 496, + "origin_id": -10, + "origin_slot": 13, + "target_id": 269, + "target_slot": 1, + "type": "INT" + }, + { + "id": 497, + "origin_id": -10, + "origin_slot": 14, + "target_id": 268, + "target_slot": 8, + "type": "COMBO" + }, + { + "id": 498, + "origin_id": 269, + "origin_slot": 0, + "target_id": 310, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 499, + "origin_id": 284, + "origin_slot": 0, + "target_id": 312, + "target_slot": 0, + "type": "IMAGE" + } + ], + "extra": {}, + "category": "Video generation and editing/Inpaint video", + "description": "Removes objects from video by inpainting masked regions using Wan 2.1 VACE, with SAM3 text-guided segmentation and optional Lightning LoRA turbo mode." + }, + { + "id": "17df2eeb-d89e-46ee-9480-a4ca2494b207", + "version": 1, + "state": { + "lastGroupId": 31, + "lastNodeId": 315, + "lastLinkId": 499, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Image Segmentation (SAM3)", + "description": "Segments images into masks using Meta SAM3 from text prompts, points, or boxes.", + "inputNode": { + "id": -10, + "bounding": [ + -2260, + -3450, + 136.369140625, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -1130, + -3305, + 120, + 80 + ] + }, + "inputs": [ + { + "id": "a6e75fa2-162a-4af0-a2fd-1e9c899a5ab6", + "name": "image", + "type": "IMAGE", + "linkIds": [ + 264 + ], + "localized_name": "image", + "label": "image", + "pos": [ + -2143.630859375, + -3430 + ] + }, + { + "id": "3cefd304-7631-4ff6-a5a0-5a0ffb120745", + "name": "text", + "type": "STRING", + "linkIds": [ + 265 + ], + "label": "object", + "pos": [ + -2143.630859375, + -3410 + ] + }, + { + "id": "1aec91c5-d8d2-441c-928c-49c14e7e80ed", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 266 + ], + "pos": [ + -2143.630859375, + -3390 + ] + }, + { + "id": "1ec7ce1a-8257-4719-8a81-60ebc8a98899", + "name": "positive_coords", + "type": "STRING", + "linkIds": [ + 267 + ], + "pos": [ + -2143.630859375, + -3370 + ] + }, + { + "id": "c65f8b87-9bd7-48be-9fc2-823431e95019", + "name": "negative_coords", + "type": "STRING", + "linkIds": [ + 268 + ], + "pos": [ + -2143.630859375, + -3350 + ] + }, + { + "id": "bb4ba35a-ccfe-4c37-98e5-d9b0d69585fb", + "name": "threshold", + "type": "FLOAT", + "linkIds": [ + 269 + ], + "pos": [ + -2143.630859375, + -3330 + ] + }, + { + "id": "b1439668-b050-490b-a5dc-fc4052c55666", + "name": "refine_iterations", + "type": "INT", + "linkIds": [ + 270 + ], + "pos": [ + -2143.630859375, + -3310 + ] + }, + { + "id": "86e239e5-c098-4302-b54d-d42a38bc0f89", + "name": "individual_masks", + "type": "BOOLEAN", + "linkIds": [ + 271 + ], + "pos": [ + -2143.630859375, + -3290 + ] + }, + { + "id": "f9e0b9d4-b2f1-4907-a4a5-305656576706", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 272 + ], + "pos": [ + -2143.630859375, + -3270 + ] + } + ], + "outputs": [ + { + "id": "ff50da09-1e59-4a58-9b7f-be1a00aa5913", + "name": "masks", + "type": "MASK", + "linkIds": [ + 231 + ], + "localized_name": "masks", + "pos": [ + -1110, + -3285 + ] + }, + { + "id": "8f622e40-8528-4078-b7d3-147e9f872194", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 232 + ], + "localized_name": "bboxes", + "pos": [ + -1110, + -3265 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 75, + "type": "SAM3_Detect", + "pos": [ + -1470, + -3460 + ], + "size": [ + 270, + 260 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "label": "model", + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 237 + }, + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 264 + }, + { + "label": "conditioning", + "localized_name": "conditioning", + "name": "conditioning", + "shape": 7, + "type": "CONDITIONING", + "link": 200 + }, + { + "label": "bboxes", + "localized_name": "bboxes", + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": 266 + }, + { + "label": "positive_coords", + "localized_name": "positive_coords", + "name": "positive_coords", + "shape": 7, + "type": "STRING", + "link": 267 + }, + { + "label": "negative_coords", + "localized_name": "negative_coords", + "name": "negative_coords", + "shape": 7, + "type": "STRING", + "link": 268 + }, + { + "localized_name": "threshold", + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": 269 + }, + { + "localized_name": "refine_iterations", + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": 270 + }, + { + "localized_name": "individual_masks", + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": 271 + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [ + 231 + ] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [ + 232 + ] + } + ], + "properties": { + "Node name for S&R": "SAM3_Detect", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + 0.5, + 2, + false + ] + }, + { + "id": 236, + "type": "CheckpointLoaderSimple", + "pos": [ + -1970, + -3200 + ], + "size": [ + 330, + 140 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 272 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 237 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 240 + ] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": null + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "models": [ + { + "name": "sam3.1_multiplex_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors", + "directory": "checkpoints" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "sam3.1_multiplex_fp16.safetensors" + ] + }, + { + "id": 237, + "type": "CLIPTextEncode", + "pos": [ + -2000, + -3000 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 240 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 265 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 200 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.7", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "" + ] + } + ], + "groups": [], + "links": [ + { + "id": 237, + "origin_id": 236, + "origin_slot": 0, + "target_id": 75, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 200, + "origin_id": 237, + "origin_slot": 0, + "target_id": 75, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 240, + "origin_id": 236, + "origin_slot": 1, + "target_id": 237, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 231, + "origin_id": 75, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 232, + "origin_id": 75, + "origin_slot": 1, + "target_id": -20, + "target_slot": 1, + "type": "BOUNDING_BOX" + }, + { + "id": 264, + "origin_id": -10, + "origin_slot": 0, + "target_id": 75, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 265, + "origin_id": -10, + "origin_slot": 1, + "target_id": 237, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 266, + "origin_id": -10, + "origin_slot": 2, + "target_id": 75, + "target_slot": 3, + "type": "BOUNDING_BOX" + }, + { + "id": 267, + "origin_id": -10, + "origin_slot": 3, + "target_id": 75, + "target_slot": 4, + "type": "STRING" + }, + { + "id": 268, + "origin_id": -10, + "origin_slot": 4, + "target_id": 75, + "target_slot": 5, + "type": "STRING" + }, + { + "id": 269, + "origin_id": -10, + "origin_slot": 5, + "target_id": 75, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 270, + "origin_id": -10, + "origin_slot": 6, + "target_id": 75, + "target_slot": 7, + "type": "INT" + }, + { + "id": 271, + "origin_id": -10, + "origin_slot": 7, + "target_id": 75, + "target_slot": 8, + "type": "BOOLEAN" + }, + { + "id": 272, + "origin_id": -10, + "origin_slot": 8, + "target_id": 236, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": { + "ue_links": [] + } + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Video Segmentation (SAM3).json b/blueprints/Video Segmentation (SAM3).json new file mode 100644 index 000000000..4c7253869 --- /dev/null +++ b/blueprints/Video Segmentation (SAM3).json @@ -0,0 +1,827 @@ +{ + "revision": 0, + "last_node_id": 130, + "last_link_id": 0, + "nodes": [ + { + "id": 130, + "type": "7937cf78-b52b-40a3-93b2-b4e2e5f98df1", + "pos": [ + -1210, + -2780 + ], + "size": [ + 300, + 370 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "video", + "type": "VIDEO", + "link": null + }, + { + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": null + }, + { + "name": "bboxes", + "type": "BOUNDING_BOX", + "link": null + }, + { + "name": "positive_coords", + "type": "STRING", + "link": null + }, + { + "name": "negative_coords", + "type": "STRING", + "link": null + }, + { + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": null + }, + { + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": null + }, + { + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": null + }, + { + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [] + }, + { + "name": "audio", + "type": "AUDIO", + "links": null + }, + { + "name": "fps", + "type": "FLOAT", + "links": null + } + ], + "properties": { + "proxyWidgets": [ + [ + "125", + "text" + ], + [ + "126", + "threshold" + ], + [ + "126", + "refine_iterations" + ], + [ + "126", + "individual_masks" + ], + [ + "127", + "ckpt_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Video Segmentation (SAM3)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "7937cf78-b52b-40a3-93b2-b4e2e5f98df1", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 130, + "lastLinkId": 299, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Video Segmentation (SAM3)", + "inputNode": { + "id": -10, + "bounding": [ + -2260, + -3450, + 136.369140625, + 220 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -1050, + -3510, + 120, + 120 + ] + }, + "inputs": [ + { + "id": "680ffd88-32fe-48be-88d6-91ea44d5eaee", + "name": "video", + "type": "VIDEO", + "linkIds": [ + 252 + ], + "pos": [ + -2143.630859375, + -3430 + ] + }, + { + "id": "ceaf249c-32d7-4624-8bf6-e590e347ed90", + "name": "text", + "type": "STRING", + "linkIds": [ + 254 + ], + "pos": [ + -2143.630859375, + -3410 + ] + }, + { + "id": "1ffbff36-da0c-4854-8cb4-88ad31e64f99", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 255 + ], + "pos": [ + -2143.630859375, + -3390 + ] + }, + { + "id": "67b7f4c7-cec0-4e00-b154-23cc1abf880e", + "name": "positive_coords", + "type": "STRING", + "linkIds": [ + 256 + ], + "pos": [ + -2143.630859375, + -3370 + ] + }, + { + "id": "b090a498-2bde-46b9-9554-18501401d687", + "name": "negative_coords", + "type": "STRING", + "linkIds": [ + 257 + ], + "pos": [ + -2143.630859375, + -3350 + ] + }, + { + "id": "1a76dfcf-ce95-46af-bba5-c42160c683dd", + "name": "threshold", + "type": "FLOAT", + "linkIds": [ + 261 + ], + "pos": [ + -2143.630859375, + -3330 + ] + }, + { + "id": "999523fa-c476-4c53-80c3-0a2f554d18ab", + "name": "refine_iterations", + "type": "INT", + "linkIds": [ + 262 + ], + "pos": [ + -2143.630859375, + -3310 + ] + }, + { + "id": "d2371011-7fe5-4a39-b0c1-df2e0bbd6ece", + "name": "individual_masks", + "type": "BOOLEAN", + "linkIds": [ + 263 + ], + "pos": [ + -2143.630859375, + -3290 + ] + }, + { + "id": "675a8b37-17db-48d1-853c-2fe5d6a74582", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 273 + ], + "pos": [ + -2143.630859375, + -3270 + ] + } + ], + "outputs": [ + { + "id": "ff50da09-1e59-4a58-9b7f-be1a00aa5913", + "name": "masks", + "type": "MASK", + "linkIds": [ + 231 + ], + "localized_name": "masks", + "pos": [ + -1030, + -3490 + ] + }, + { + "id": "8f622e40-8528-4078-b7d3-147e9f872194", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 232 + ], + "localized_name": "bboxes", + "pos": [ + -1030, + -3470 + ] + }, + { + "id": "6c9924ec-f0fa-4509-83ea-8f97f5889bcc", + "name": "audio", + "type": "AUDIO", + "linkIds": [ + 259 + ], + "pos": [ + -1030, + -3450 + ] + }, + { + "id": "82c1cddc-ab11-44eb-9e2f-1a5c7ea5645b", + "name": "fps", + "type": "FLOAT", + "linkIds": [ + 260 + ], + "pos": [ + -1030, + -3430 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 125, + "type": "CLIPTextEncode", + "pos": [ + -2010, + -3040 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 240 + }, + { + "localized_name": "text", + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 254 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 200 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "" + ] + }, + { + "id": 126, + "type": "SAM3_Detect", + "pos": [ + -1520, + -3520 + ], + "size": [ + 270, + 290 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "label": "model", + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 237 + }, + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 253 + }, + { + "label": "conditioning", + "localized_name": "conditioning", + "name": "conditioning", + "shape": 7, + "type": "CONDITIONING", + "link": 200 + }, + { + "label": "bboxes", + "localized_name": "bboxes", + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": 255 + }, + { + "label": "positive_coords", + "localized_name": "positive_coords", + "name": "positive_coords", + "shape": 7, + "type": "STRING", + "link": 256 + }, + { + "label": "negative_coords", + "localized_name": "negative_coords", + "name": "negative_coords", + "shape": 7, + "type": "STRING", + "link": 257 + }, + { + "localized_name": "threshold", + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": 261 + }, + { + "localized_name": "refine_iterations", + "name": "refine_iterations", + "type": "INT", + "widget": { + "name": "refine_iterations" + }, + "link": 262 + }, + { + "localized_name": "individual_masks", + "name": "individual_masks", + "type": "BOOLEAN", + "widget": { + "name": "individual_masks" + }, + "link": 263 + } + ], + "outputs": [ + { + "localized_name": "masks", + "name": "masks", + "type": "MASK", + "links": [ + 231 + ] + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [ + 232 + ] + } + ], + "properties": { + "Node name for S&R": "SAM3_Detect", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0.5, + 2, + false + ] + }, + { + "id": 127, + "type": "CheckpointLoaderSimple", + "pos": [ + -1970, + -3310 + ], + "size": [ + 330, + 160 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 273 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 237 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 240 + ] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": null + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65, + "models": [ + { + "name": "sam3.1_multiplex_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors", + "directory": "checkpoints" + } + ] + }, + "widgets_values": [ + "sam3.1_multiplex_fp16.safetensors" + ] + }, + { + "id": 128, + "type": "GetVideoComponents", + "pos": [ + -1910, + -3540 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 252 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 253 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": [ + 259 + ] + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": [ + 260 + ] + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents", + "cnr_id": "comfy-core", + "ver": "0.19.3", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + }, + { + "id": 129, + "type": "Note", + "pos": [ + -1980, + -2790 + ], + "size": [ + 370, + 250 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [], + "title": "Note: Prompt format", + "properties": {}, + "widgets_values": [ + "Max tokens for this model is only 32, to separately prompt multiple subjects you can separate prompts with comma, and set the max amount of objects detected for each prompt with :N\n\nFor example above test prompt finds 2 cakes, one apron, 4 window panels" + ], + "color": "#432", + "bgcolor": "#653" + } + ], + "groups": [], + "links": [ + { + "id": 237, + "origin_id": 127, + "origin_slot": 0, + "target_id": 126, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 200, + "origin_id": 125, + "origin_slot": 0, + "target_id": 126, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 240, + "origin_id": 127, + "origin_slot": 1, + "target_id": 125, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 231, + "origin_id": 126, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "MASK" + }, + { + "id": 232, + "origin_id": 126, + "origin_slot": 1, + "target_id": -20, + "target_slot": 1, + "type": "BOUNDING_BOX" + }, + { + "id": 252, + "origin_id": -10, + "origin_slot": 0, + "target_id": 128, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 253, + "origin_id": 128, + "origin_slot": 0, + "target_id": 126, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 254, + "origin_id": -10, + "origin_slot": 1, + "target_id": 125, + "target_slot": 1, + "type": "STRING" + }, + { + "id": 255, + "origin_id": -10, + "origin_slot": 2, + "target_id": 126, + "target_slot": 3, + "type": "BOUNDING_BOX" + }, + { + "id": 256, + "origin_id": -10, + "origin_slot": 3, + "target_id": 126, + "target_slot": 4, + "type": "STRING" + }, + { + "id": 257, + "origin_id": -10, + "origin_slot": 4, + "target_id": 126, + "target_slot": 5, + "type": "STRING" + }, + { + "id": 259, + "origin_id": 128, + "origin_slot": 1, + "target_id": -20, + "target_slot": 2, + "type": "AUDIO" + }, + { + "id": 260, + "origin_id": 128, + "origin_slot": 2, + "target_id": -20, + "target_slot": 3, + "type": "FLOAT" + }, + { + "id": 261, + "origin_id": -10, + "origin_slot": 5, + "target_id": 126, + "target_slot": 6, + "type": "FLOAT" + }, + { + "id": 262, + "origin_id": -10, + "origin_slot": 6, + "target_id": 126, + "target_slot": 7, + "type": "INT" + }, + { + "id": 263, + "origin_id": -10, + "origin_slot": 7, + "target_id": 126, + "target_slot": 8, + "type": "BOOLEAN" + }, + { + "id": 273, + "origin_id": -10, + "origin_slot": 8, + "target_id": 127, + "target_slot": 0, + "type": "COMBO" + } + ], + "extra": {}, + "category": "Conditioning & Preprocessors/Segmentation & Mask", + "description": "Segments video into temporally consistent masks using Meta SAM3 from text or interactive prompts." + } + ] + }, + "extra": {} +} diff --git a/blueprints/Video Stitch.json b/blueprints/Video Stitch.json index 020896d78..2ac78b328 100644 --- a/blueprints/Video Stitch.json +++ b/blueprints/Video Stitch.json @@ -1,21 +1,21 @@ { "revision": 0, - "last_node_id": 84, + "last_node_id": 85, "last_link_id": 0, "nodes": [ { - "id": 84, - "type": "8e8aa94a-647e-436d-8440-8ee4691864de", + "id": 85, + "type": "637913e7-0206-46ba-8ded-70ae3a7c2e19", "pos": [ - -6100, - 2620 + -880, + -2260 ], "size": [ 290, 160 ], "flags": {}, - "order": 0, + "order": 2, "mode": 0, "inputs": [ { @@ -76,31 +76,26 @@ "properties": { "proxyWidgets": [ [ - "-1", + "79", "direction" ], [ - "-1", + "79", "match_image_size" ], [ - "-1", + "79", "spacing_width" ], [ - "-1", + "79", "spacing_color" ] ], "cnr_id": "comfy-core", "ver": "0.13.0" }, - "widgets_values": [ - "right", - true, - 0, - "white" - ], + "widgets_values": [], "title": "Video Stitch" } ], @@ -109,12 +104,12 @@ "definitions": { "subgraphs": [ { - "id": "8e8aa94a-647e-436d-8440-8ee4691864de", + "id": "637913e7-0206-46ba-8ded-70ae3a7c2e19", "version": 1, "state": { "lastGroupId": 1, - "lastNodeId": 84, - "lastLinkId": 262, + "lastNodeId": 97, + "lastLinkId": 282, "lastRerouteId": 0 }, "revision": 0, @@ -123,8 +118,8 @@ "inputNode": { "id": -10, "bounding": [ - -6580, - 2649, + -6810, + 2580, 143.55859375, 160 ] @@ -132,8 +127,8 @@ "outputNode": { "id": -20, "bounding": [ - -5720, - 2659, + -4770, + 2600, 120, 60 ] @@ -149,8 +144,8 @@ "localized_name": "video", "label": "Before Video", "pos": [ - -6456.44140625, - 2669 + -6686.44140625, + 2600 ] }, { @@ -163,8 +158,8 @@ "localized_name": "video_1", "label": "After Video", "pos": [ - -6456.44140625, - 2689 + -6686.44140625, + 2620 ] }, { @@ -175,8 +170,8 @@ 259 ], "pos": [ - -6456.44140625, - 2709 + -6686.44140625, + 2640 ] }, { @@ -187,8 +182,8 @@ 260 ], "pos": [ - -6456.44140625, - 2729 + -6686.44140625, + 2660 ] }, { @@ -199,8 +194,8 @@ 261 ], "pos": [ - -6456.44140625, - 2749 + -6686.44140625, + 2680 ] }, { @@ -211,8 +206,8 @@ 262 ], "pos": [ - -6456.44140625, - 2769 + -6686.44140625, + 2700 ] } ], @@ -226,8 +221,8 @@ ], "localized_name": "VIDEO", "pos": [ - -5700, - 2679 + -4750, + 2620 ] } ], @@ -238,11 +233,11 @@ "type": "GetVideoComponents", "pos": [ -6390, - 2560 + 2600 ], "size": [ - 193.530859375, - 66 + 230, + 120 ], "flags": {}, "order": 1, @@ -278,9 +273,9 @@ } ], "properties": { + "Node name for S&R": "GetVideoComponents", "cnr_id": "comfy-core", - "ver": "0.13.0", - "Node name for S&R": "GetVideoComponents" + "ver": "0.13.0" } }, { @@ -291,8 +286,8 @@ 2420 ], "size": [ - 193.530859375, - 66 + 230, + 120 ], "flags": {}, "order": 0, @@ -332,21 +327,254 @@ } ], "properties": { + "Node name for S&R": "GetVideoComponents", "cnr_id": "comfy-core", - "ver": "0.13.0", - "Node name for S&R": "GetVideoComponents" + "ver": "0.13.0" } }, + { + "id": 90, + "type": "GetImageSize", + "pos": [ + -6390, + 3030 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 266 + } + ], + "outputs": [ + { + "localized_name": "width", + "name": "width", + "type": "INT", + "links": [ + 274 + ] + }, + { + "localized_name": "height", + "name": "height", + "type": "INT", + "links": [ + 276 + ] + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "links": null + } + ], + "properties": { + "Node name for S&R": "GetImageSize" + } + }, + { + "id": 80, + "type": "CreateVideo", + "pos": [ + -5190, + 2420 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "link": 282 + }, + { + "localized_name": "audio", + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 251 + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 252 + } + ], + "outputs": [ + { + "localized_name": "VIDEO", + "name": "VIDEO", + "type": "VIDEO", + "links": [ + 255 + ] + } + ], + "properties": { + "Node name for S&R": "CreateVideo", + "cnr_id": "comfy-core", + "ver": "0.13.0" + }, + "widgets_values": [ + 30 + ] + }, + { + "id": 95, + "type": "ComfyMathExpression", + "pos": [ + -6040, + 3020 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 274 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 279 + ] + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "a & ~1" + ] + }, + { + "id": 96, + "type": "ComfyMathExpression", + "pos": [ + -6040, + 3290 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "label": "a", + "localized_name": "values.a", + "name": "values.a", + "type": "FLOAT,INT", + "link": 276 + }, + { + "label": "b", + "localized_name": "values.b", + "name": "values.b", + "shape": 7, + "type": "FLOAT,INT", + "link": null + }, + { + "localized_name": "expression", + "name": "expression", + "type": "STRING", + "widget": { + "name": "expression" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "FLOAT", + "name": "FLOAT", + "type": "FLOAT", + "links": null + }, + { + "localized_name": "INT", + "name": "INT", + "type": "INT", + "links": [ + 280 + ] + } + ], + "properties": { + "Node name for S&R": "ComfyMathExpression" + }, + "widgets_values": [ + "a & ~1" + ] + }, { "id": 79, "type": "ImageStitch", "pos": [ -6390, - 2700 + 2780 ], "size": [ 270, - 150 + 160 ], "flags": {}, "order": 2, @@ -408,14 +636,15 @@ "name": "IMAGE", "type": "IMAGE", "links": [ - 250 + 266, + 281 ] } ], "properties": { + "Node name for S&R": "ImageStitch", "cnr_id": "comfy-core", - "ver": "0.13.0", - "Node name for S&R": "ImageStitch" + "ver": "0.13.0" }, "widgets_values": [ "right", @@ -425,60 +654,91 @@ ] }, { - "id": 80, - "type": "CreateVideo", + "id": 97, + "type": "ResizeImageMaskNode", "pos": [ - -6040, - 2610 + -5560, + 2790 ], "size": [ 270, - 78 + 160 ], "flags": {}, - "order": 3, + "order": 7, "mode": 0, "inputs": [ { - "localized_name": "images", - "name": "images", - "type": "IMAGE", - "link": 250 + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": 281 }, { - "localized_name": "audio", - "name": "audio", - "shape": 7, - "type": "AUDIO", - "link": 251 - }, - { - "localized_name": "fps", - "name": "fps", - "type": "FLOAT", + "localized_name": "resize_type", + "name": "resize_type", + "type": "COMFY_DYNAMICCOMBO_V3", "widget": { - "name": "fps" + "name": "resize_type" }, - "link": 252 + "link": null + }, + { + "localized_name": "width", + "name": "resize_type.width", + "type": "INT", + "widget": { + "name": "resize_type.width" + }, + "link": 279 + }, + { + "localized_name": "height", + "name": "resize_type.height", + "type": "INT", + "widget": { + "name": "resize_type.height" + }, + "link": 280 + }, + { + "localized_name": "crop", + "name": "resize_type.crop", + "type": "COMBO", + "widget": { + "name": "resize_type.crop" + }, + "link": null + }, + { + "localized_name": "scale_method", + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": null } ], "outputs": [ { - "localized_name": "VIDEO", - "name": "VIDEO", - "type": "VIDEO", + "localized_name": "resized", + "name": "resized", + "type": "*", "links": [ - 255 + 282 ] } ], "properties": { - "cnr_id": "comfy-core", - "ver": "0.13.0", - "Node name for S&R": "CreateVideo" + "Node name for S&R": "ResizeImageMaskNode" }, "widgets_values": [ - 30 + "scale dimensions", + 512, + 512, + "center", + "area" ] } ], @@ -500,14 +760,6 @@ "target_slot": 1, "type": "IMAGE" }, - { - "id": 250, - "origin_id": 79, - "origin_slot": 0, - "target_id": 80, - "target_slot": 0, - "type": "IMAGE" - }, { "id": 251, "origin_id": 77, @@ -579,13 +831,71 @@ "target_id": 79, "target_slot": 5, "type": "COMBO" + }, + { + "id": 266, + "origin_id": 79, + "origin_slot": 0, + "target_id": 90, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 274, + "origin_id": 90, + "origin_slot": 0, + "target_id": 95, + "target_slot": 0, + "type": "INT" + }, + { + "id": 276, + "origin_id": 90, + "origin_slot": 1, + "target_id": 96, + "target_slot": 0, + "type": "INT" + }, + { + "id": 279, + "origin_id": 95, + "origin_slot": 1, + "target_id": 97, + "target_slot": 2, + "type": "INT" + }, + { + "id": 280, + "origin_id": 96, + "origin_slot": 1, + "target_id": 97, + "target_slot": 3, + "type": "INT" + }, + { + "id": 281, + "origin_id": 79, + "origin_slot": 0, + "target_id": 97, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 282, + "origin_id": 97, + "origin_slot": 0, + "target_id": 80, + "target_slot": 0, + "type": "IMAGE" } ], "extra": { "workflowRendererVersion": "LG" }, - "category": "Video Tools/Stitch videos" + "category": "Video Tools/Stitch videos", + "description": "Stitches multiple video clips into a single sequential video file." } ] - } -} + }, + "extra": {} +} \ No newline at end of file diff --git a/blueprints/Video Upscale(GAN x4).json b/blueprints/Video Upscale(GAN x4).json index b61dc88d7..fc291ac41 100644 --- a/blueprints/Video Upscale(GAN x4).json +++ b/blueprints/Video Upscale(GAN x4).json @@ -412,9 +412,10 @@ "extra": { "workflowRendererVersion": "LG" }, - "category": "Video generation and editing/Enhance video" + "category": "Video generation and editing/Upscale", + "description": "Upscales video to 4× resolution using a GAN-based upscaling model." } ] }, "extra": {} -} +} \ No newline at end of file diff --git a/blueprints/Video to Pose Map (SDPose Multi-Person).json b/blueprints/Video to Pose Map (SDPose Multi-Person).json new file mode 100644 index 000000000..64ef6e524 --- /dev/null +++ b/blueprints/Video to Pose Map (SDPose Multi-Person).json @@ -0,0 +1,1323 @@ +{ + "revision": 0, + "last_node_id": 675, + "last_link_id": 0, + "nodes": [ + { + "id": 675, + "type": "01b6a731-fb78-4070-9a38-c87146da9604", + "pos": [ + -2480, + 3400 + ], + "size": [ + 370, + 638.625 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "label": "resize_target_longer_size", + "name": "resize_type.longer_size", + "type": "INT", + "widget": { + "name": "resize_type.longer_size" + }, + "link": null + }, + { + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": null + }, + { + "name": "draw_body", + "type": "BOOLEAN", + "widget": { + "name": "draw_body" + }, + "link": null + }, + { + "name": "draw_hands", + "type": "BOOLEAN", + "widget": { + "name": "draw_hands" + }, + "link": null + }, + { + "name": "draw_face", + "type": "BOOLEAN", + "widget": { + "name": "draw_face" + }, + "link": null + }, + { + "name": "draw_feet", + "type": "BOOLEAN", + "widget": { + "name": "draw_feet" + }, + "link": null + }, + { + "name": "stick_width", + "type": "INT", + "widget": { + "name": "stick_width" + }, + "link": null + }, + { + "name": "face_point_size", + "type": "INT", + "widget": { + "name": "face_point_size" + }, + "link": null + }, + { + "name": "score_threshold", + "type": "FLOAT", + "widget": { + "name": "score_threshold" + }, + "link": null + }, + { + "label": "detect_threshold", + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": null + }, + { + "label": "detect_class", + "name": "class_name", + "type": "COMBO", + "widget": { + "name": "class_name" + }, + "link": null + }, + { + "name": "max_detections", + "type": "INT", + "widget": { + "name": "max_detections" + }, + "link": null + }, + { + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": null + }, + { + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": null + }, + { + "name": "video", + "type": "VIDEO", + "link": null + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [] + }, + { + "name": "keypoints", + "type": "POSE_KEYPOINT", + "links": null + }, + { + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [] + }, + { + "name": "audio", + "type": "AUDIO", + "links": [] + }, + { + "name": "fps", + "type": "FLOAT", + "links": [] + } + ], + "properties": { + "proxyWidgets": [ + [ + "674", + "resize_type.longer_size" + ], + [ + "674", + "scale_method" + ], + [ + "672", + "draw_body" + ], + [ + "672", + "draw_hands" + ], + [ + "672", + "draw_face" + ], + [ + "672", + "draw_feet" + ], + [ + "672", + "stick_width" + ], + [ + "672", + "face_point_size" + ], + [ + "672", + "score_threshold" + ], + [ + "678", + "threshold" + ], + [ + "678", + "class_name" + ], + [ + "678", + "max_detections" + ], + [ + "673", + "ckpt_name" + ], + [ + "677", + "unet_name" + ] + ], + "cnr_id": "comfy-core", + "ver": "0.15.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [], + "title": "Video to Pose Map (SDPose Multi-Person)" + } + ], + "links": [], + "version": 0.4, + "definitions": { + "subgraphs": [ + { + "id": "01b6a731-fb78-4070-9a38-c87146da9604", + "version": 1, + "state": { + "lastGroupId": 2, + "lastNodeId": 699, + "lastLinkId": 1754, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "Video to Pose Map (SDPose Multi-Person)", + "inputNode": { + "id": -10, + "bounding": [ + -3570, + 3300, + 182.8984375, + 340 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -1890, + 3730, + 120, + 140 + ] + }, + "inputs": [ + { + "id": "088eefc1-cd8a-4573-993f-9e4da008a12d", + "name": "resize_type.longer_size", + "type": "INT", + "linkIds": [ + 1704 + ], + "label": "resize_target_longer_size", + "pos": [ + -3407.1015625, + 3320 + ] + }, + { + "id": "b6449bd3-73d4-41c8-b81f-cf8d33f76a2e", + "name": "scale_method", + "type": "COMBO", + "linkIds": [ + 1705 + ], + "pos": [ + -3407.1015625, + 3340 + ] + }, + { + "id": "4cff52ad-ed07-4c97-8803-fcbd89554fd0", + "name": "draw_body", + "type": "BOOLEAN", + "linkIds": [ + 1706 + ], + "pos": [ + -3407.1015625, + 3360 + ] + }, + { + "id": "7af63dce-f7df-4d7e-8215-d7c7f60bf81c", + "name": "draw_hands", + "type": "BOOLEAN", + "linkIds": [ + 1707 + ], + "pos": [ + -3407.1015625, + 3380 + ] + }, + { + "id": "af3a9bce-61f9-4aca-b530-9f65e028b35e", + "name": "draw_face", + "type": "BOOLEAN", + "linkIds": [ + 1708 + ], + "pos": [ + -3407.1015625, + 3400 + ] + }, + { + "id": "4620f6a3-2c85-4b79-ad8f-35d0326b568f", + "name": "draw_feet", + "type": "BOOLEAN", + "linkIds": [ + 1709 + ], + "pos": [ + -3407.1015625, + 3420 + ] + }, + { + "id": "fee5d0c9-8d4b-4934-81d8-ba2206dc56cb", + "name": "stick_width", + "type": "INT", + "linkIds": [ + 1710 + ], + "pos": [ + -3407.1015625, + 3440 + ] + }, + { + "id": "aafdd060-ba81-4324-a9cc-b656e1ebc133", + "name": "face_point_size", + "type": "INT", + "linkIds": [ + 1711 + ], + "pos": [ + -3407.1015625, + 3460 + ] + }, + { + "id": "514c5503-f9e6-4d23-b1ae-1d3291acb2a3", + "name": "score_threshold", + "type": "FLOAT", + "linkIds": [ + 1712 + ], + "pos": [ + -3407.1015625, + 3480 + ] + }, + { + "id": "4eb3e4ea-7a36-4511-8483-0d12aadd32f7", + "name": "threshold", + "type": "FLOAT", + "linkIds": [ + 1718 + ], + "label": "detect_threshold", + "pos": [ + -3407.1015625, + 3500 + ] + }, + { + "id": "c76a7a05-81e6-4b17-a9e0-85f47a5844f2", + "name": "class_name", + "type": "COMBO", + "linkIds": [ + 1719 + ], + "label": "detect_class", + "pos": [ + -3407.1015625, + 3520 + ] + }, + { + "id": "4417e988-6e80-4236-be31-4c179037f5a2", + "name": "max_detections", + "type": "INT", + "linkIds": [ + 1720 + ], + "pos": [ + -3407.1015625, + 3540 + ] + }, + { + "id": "7d7c4a0b-0d1b-4c98-942b-f90548d2a492", + "name": "ckpt_name", + "type": "COMBO", + "linkIds": [ + 1721 + ], + "pos": [ + -3407.1015625, + 3560 + ] + }, + { + "id": "4d75122c-2c14-452a-98fe-d1545d3e012a", + "name": "unet_name", + "type": "COMBO", + "linkIds": [ + 1722 + ], + "pos": [ + -3407.1015625, + 3580 + ] + }, + { + "id": "6c46c988-4dd1-41a2-957e-03caf60d7657", + "name": "video", + "type": "VIDEO", + "linkIds": [ + 1741 + ], + "pos": [ + -3407.1015625, + 3600 + ] + } + ], + "outputs": [ + { + "id": "f05ed8cc-9403-4f14-8085-4364b06f8a48", + "name": "IMAGE", + "type": "IMAGE", + "linkIds": [ + 1701 + ], + "localized_name": "IMAGE", + "pos": [ + -1870, + 3750 + ] + }, + { + "id": "4b64118e-3cef-4eeb-9dad-4cd09cfd63a2", + "name": "keypoints", + "type": "POSE_KEYPOINT", + "linkIds": [ + 1725 + ], + "pos": [ + -1870, + 3770 + ] + }, + { + "id": "a27f7e34-dcbc-4fb0-a4e1-2c5fc423ca5f", + "name": "bboxes", + "type": "BOUNDING_BOX", + "linkIds": [ + 1726 + ], + "pos": [ + -1870, + 3790 + ] + }, + { + "id": "b7fe351d-2b38-41ea-9f4d-3be1a0aad275", + "name": "audio", + "type": "AUDIO", + "linkIds": [ + 1743 + ], + "pos": [ + -1870, + 3810 + ] + }, + { + "id": "ae187b6f-c9ca-4487-b5c1-3ad775fe945e", + "name": "fps", + "type": "FLOAT", + "linkIds": [ + 1744 + ], + "pos": [ + -1870, + 3830 + ] + } + ], + "widgets": [], + "nodes": [ + { + "id": 671, + "type": "SDPoseKeypointExtractor", + "pos": [ + -2550, + 3080 + ], + "size": [ + 270, + 180 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 1696 + }, + { + "localized_name": "vae", + "name": "vae", + "type": "VAE", + "link": 1697 + }, + { + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 1698 + }, + { + "localized_name": "bboxes", + "name": "bboxes", + "shape": 7, + "type": "BOUNDING_BOX", + "link": 1717 + }, + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "keypoints", + "name": "keypoints", + "type": "POSE_KEYPOINT", + "links": [ + 1699, + 1725 + ] + } + ], + "properties": { + "Node name for S&R": "SDPoseKeypointExtractor", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 16 + ] + }, + { + "id": 674, + "type": "ResizeImageMaskNode", + "pos": [ + -3010, + 3880 + ], + "size": [ + 270, + 110 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "localized_name": "input", + "name": "input", + "type": "IMAGE,MASK", + "link": 1742 + }, + { + "localized_name": "resize_type", + "name": "resize_type", + "type": "COMFY_DYNAMICCOMBO_V3", + "widget": { + "name": "resize_type" + }, + "link": null + }, + { + "localized_name": "resize_type.longer_size", + "name": "resize_type.longer_size", + "type": "INT", + "widget": { + "name": "resize_type.longer_size" + }, + "link": 1704 + }, + { + "localized_name": "scale_method", + "name": "scale_method", + "type": "COMBO", + "widget": { + "name": "scale_method" + }, + "link": 1705 + } + ], + "outputs": [ + { + "localized_name": "resized", + "name": "resized", + "type": "*", + "links": [ + 1698, + 1716 + ] + } + ], + "properties": { + "Node name for S&R": "ResizeImageMaskNode", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "scale longer dimension", + 1024, + "lanczos" + ] + }, + { + "id": 672, + "type": "SDPoseDrawKeypoints", + "pos": [ + -2540, + 3590 + ], + "size": [ + 270, + 280 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "localized_name": "keypoints", + "name": "keypoints", + "type": "POSE_KEYPOINT", + "link": 1699 + }, + { + "localized_name": "draw_body", + "name": "draw_body", + "type": "BOOLEAN", + "widget": { + "name": "draw_body" + }, + "link": 1706 + }, + { + "localized_name": "draw_hands", + "name": "draw_hands", + "type": "BOOLEAN", + "widget": { + "name": "draw_hands" + }, + "link": 1707 + }, + { + "localized_name": "draw_face", + "name": "draw_face", + "type": "BOOLEAN", + "widget": { + "name": "draw_face" + }, + "link": 1708 + }, + { + "localized_name": "draw_feet", + "name": "draw_feet", + "type": "BOOLEAN", + "widget": { + "name": "draw_feet" + }, + "link": 1709 + }, + { + "localized_name": "stick_width", + "name": "stick_width", + "type": "INT", + "widget": { + "name": "stick_width" + }, + "link": 1710 + }, + { + "localized_name": "face_point_size", + "name": "face_point_size", + "type": "INT", + "widget": { + "name": "face_point_size" + }, + "link": 1711 + }, + { + "localized_name": "score_threshold", + "name": "score_threshold", + "type": "FLOAT", + "widget": { + "name": "score_threshold" + }, + "link": 1712 + } + ], + "outputs": [ + { + "localized_name": "IMAGE", + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 1701 + ] + } + ], + "properties": { + "Node name for S&R": "SDPoseDrawKeypoints", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + true, + true, + true, + true, + 4, + 2, + 0.5 + ] + }, + { + "id": 673, + "type": "CheckpointLoaderSimple", + "pos": [ + -3040, + 3080 + ], + "size": [ + 390, + 160 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "ckpt_name", + "name": "ckpt_name", + "type": "COMBO", + "widget": { + "name": "ckpt_name" + }, + "link": 1721 + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 1696 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 1697 + ] + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple", + "cnr_id": "comfy-core", + "ver": "0.15.0", + "models": [ + { + "name": "sdpose_wholebody_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/SDPose/resolve/main/checkpoints/sdpose_wholebody_fp16.safetensors", + "directory": "checkpoints" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "sdpose_wholebody_fp16.safetensors" + ] + }, + { + "id": 677, + "type": "UNETLoader", + "pos": [ + -3030, + 3300 + ], + "size": [ + 370, + 110 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "localized_name": "unet_name", + "name": "unet_name", + "type": "COMBO", + "widget": { + "name": "unet_name" + }, + "link": 1722 + }, + { + "localized_name": "weight_dtype", + "name": "weight_dtype", + "type": "COMBO", + "widget": { + "name": "weight_dtype" + }, + "link": null + } + ], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 1715 + ] + } + ], + "properties": { + "Node name for S&R": "UNETLoader", + "cnr_id": "comfy-core", + "ver": "0.14.1", + "models": [ + { + "name": "rt_detr_v4-x-hgnet_fp16.safetensors", + "url": "https://huggingface.co/Comfy-Org/SDPose/resolve/main/diffusion_models/rt_detr_v4-x-hgnet_fp16.safetensors", + "directory": "diffusion_models" + } + ], + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + "rt_detr_v4-x-hgnet_fp16.safetensors", + "default" + ] + }, + { + "id": 678, + "type": "RTDETR_detect", + "pos": [ + -2540, + 3320 + ], + "size": [ + 270, + 200 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "label": "model", + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 1715 + }, + { + "label": "image", + "localized_name": "image", + "name": "image", + "type": "IMAGE", + "link": 1716 + }, + { + "localized_name": "threshold", + "name": "threshold", + "type": "FLOAT", + "widget": { + "name": "threshold" + }, + "link": 1718 + }, + { + "localized_name": "class_name", + "name": "class_name", + "type": "COMBO", + "widget": { + "name": "class_name" + }, + "link": 1719 + }, + { + "localized_name": "max_detections", + "name": "max_detections", + "type": "INT", + "widget": { + "name": "max_detections" + }, + "link": 1720 + } + ], + "outputs": [ + { + "localized_name": "bboxes", + "name": "bboxes", + "type": "BOUNDING_BOX", + "links": [ + 1717, + 1726 + ] + } + ], + "properties": { + "Node name for S&R": "RTDETR_detect", + "cnr_id": "comfy-core", + "ver": "0.15.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + }, + "widgets_values": [ + 0.5, + "person", + 2 + ] + }, + { + "id": 692, + "type": "GetVideoComponents", + "pos": [ + -3010, + 4100 + ], + "size": [ + 230, + 120 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "localized_name": "video", + "name": "video", + "type": "VIDEO", + "link": 1741 + } + ], + "outputs": [ + { + "localized_name": "images", + "name": "images", + "type": "IMAGE", + "links": [ + 1742 + ] + }, + { + "localized_name": "audio", + "name": "audio", + "type": "AUDIO", + "links": [ + 1743 + ] + }, + { + "localized_name": "fps", + "name": "fps", + "type": "FLOAT", + "links": [ + 1744 + ] + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents", + "cnr_id": "comfy-core", + "ver": "0.18.1", + "enableTabs": false, + "tabWidth": 65, + "tabXOffset": 10, + "hasSecondTab": false, + "secondTabText": "Send Back", + "secondTabOffset": 80, + "secondTabWidth": 65 + } + } + ], + "groups": [], + "links": [ + { + "id": 1696, + "origin_id": 673, + "origin_slot": 0, + "target_id": 671, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 1697, + "origin_id": 673, + "origin_slot": 2, + "target_id": 671, + "target_slot": 1, + "type": "VAE" + }, + { + "id": 1698, + "origin_id": 674, + "origin_slot": 0, + "target_id": 671, + "target_slot": 2, + "type": "IMAGE" + }, + { + "id": 1699, + "origin_id": 671, + "origin_slot": 0, + "target_id": 672, + "target_slot": 0, + "type": "POSE_KEYPOINT" + }, + { + "id": 1701, + "origin_id": 672, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 1704, + "origin_id": -10, + "origin_slot": 0, + "target_id": 674, + "target_slot": 2, + "type": "INT" + }, + { + "id": 1705, + "origin_id": -10, + "origin_slot": 1, + "target_id": 674, + "target_slot": 3, + "type": "COMBO" + }, + { + "id": 1706, + "origin_id": -10, + "origin_slot": 2, + "target_id": 672, + "target_slot": 1, + "type": "BOOLEAN" + }, + { + "id": 1707, + "origin_id": -10, + "origin_slot": 3, + "target_id": 672, + "target_slot": 2, + "type": "BOOLEAN" + }, + { + "id": 1708, + "origin_id": -10, + "origin_slot": 4, + "target_id": 672, + "target_slot": 3, + "type": "BOOLEAN" + }, + { + "id": 1709, + "origin_id": -10, + "origin_slot": 5, + "target_id": 672, + "target_slot": 4, + "type": "BOOLEAN" + }, + { + "id": 1710, + "origin_id": -10, + "origin_slot": 6, + "target_id": 672, + "target_slot": 5, + "type": "INT" + }, + { + "id": 1711, + "origin_id": -10, + "origin_slot": 7, + "target_id": 672, + "target_slot": 6, + "type": "INT" + }, + { + "id": 1712, + "origin_id": -10, + "origin_slot": 8, + "target_id": 672, + "target_slot": 7, + "type": "FLOAT" + }, + { + "id": 1715, + "origin_id": 677, + "origin_slot": 0, + "target_id": 678, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 1716, + "origin_id": 674, + "origin_slot": 0, + "target_id": 678, + "target_slot": 1, + "type": "IMAGE" + }, + { + "id": 1717, + "origin_id": 678, + "origin_slot": 0, + "target_id": 671, + "target_slot": 3, + "type": "BOUNDING_BOX" + }, + { + "id": 1718, + "origin_id": -10, + "origin_slot": 9, + "target_id": 678, + "target_slot": 2, + "type": "FLOAT" + }, + { + "id": 1719, + "origin_id": -10, + "origin_slot": 10, + "target_id": 678, + "target_slot": 3, + "type": "COMBO" + }, + { + "id": 1720, + "origin_id": -10, + "origin_slot": 11, + "target_id": 678, + "target_slot": 4, + "type": "INT" + }, + { + "id": 1721, + "origin_id": -10, + "origin_slot": 12, + "target_id": 673, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 1722, + "origin_id": -10, + "origin_slot": 13, + "target_id": 677, + "target_slot": 0, + "type": "COMBO" + }, + { + "id": 1725, + "origin_id": 671, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "POSE_KEYPOINT" + }, + { + "id": 1726, + "origin_id": 678, + "origin_slot": 0, + "target_id": -20, + "target_slot": 2, + "type": "BOUNDING_BOX" + }, + { + "id": 1741, + "origin_id": -10, + "origin_slot": 14, + "target_id": 692, + "target_slot": 0, + "type": "VIDEO" + }, + { + "id": 1742, + "origin_id": 692, + "origin_slot": 0, + "target_id": 674, + "target_slot": 0, + "type": "IMAGE" + }, + { + "id": 1743, + "origin_id": 692, + "origin_slot": 1, + "target_id": -20, + "target_slot": 3, + "type": "AUDIO" + }, + { + "id": 1744, + "origin_id": 692, + "origin_slot": 2, + "target_id": -20, + "target_slot": 4, + "type": "FLOAT" + } + ], + "extra": { + "workflowRendererVersion": "LG" + }, + "category": "Conditioning & Preprocessors/Pose", + "description": "Extracts multi-person pose keypoints and skeleton frame sequences from video using SDPose with built-in person detection." + } + ] + }, + "extra": {} +} \ No newline at end of file diff --git a/comfy/background_removal/birefnet.json b/comfy/background_removal/birefnet.json new file mode 100644 index 000000000..f0960af39 --- /dev/null +++ b/comfy/background_removal/birefnet.json @@ -0,0 +1,7 @@ +{ + "model_type": "birefnet", + "image_std": [1.0, 1.0, 1.0], + "image_mean": [0.0, 0.0, 0.0], + "image_size": 1024, + "resize_to_original": true +} diff --git a/comfy/background_removal/birefnet.py b/comfy/background_removal/birefnet.py new file mode 100644 index 000000000..df54b2b90 --- /dev/null +++ b/comfy/background_removal/birefnet.py @@ -0,0 +1,689 @@ +import torch +import comfy.ops +import numpy as np +import torch.nn as nn +from functools import partial +import torch.nn.functional as F +from torchvision.ops import deform_conv2d +from comfy.ldm.modules.attention import optimized_attention_for_device + +CXT = [3072, 1536, 768, 384][1:][::-1][-3:] + +class Attention(nn.Module): + def __init__(self, dim, num_heads=8, qkv_bias=False, qk_scale=None, device=None, dtype=None, operations=None): + super().__init__() + + self.dim = dim + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = qk_scale or head_dim ** -0.5 + + self.q = operations.Linear(dim, dim, bias=qkv_bias, device=device, dtype=dtype) + self.kv = operations.Linear(dim, dim * 2, bias=qkv_bias, device=device, dtype=dtype) + self.proj = operations.Linear(dim, dim, device=device, dtype=dtype) + + def forward(self, x): + B, N, C = x.shape + optimized_attention = optimized_attention_for_device(x.device, mask=False, small_input=True) + q = self.q(x).reshape(B, N, self.num_heads, C // self.num_heads).permute(0, 2, 1, 3) + kv = self.kv(x).reshape(B, -1, 2, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) + k, v = kv[0], kv[1] + + x = optimized_attention( + q, k, v, heads=self.num_heads, skip_output_reshape=True, skip_reshape=True + ).transpose(1, 2).reshape(B, N, C) + x = self.proj(x) + + return x + +class Mlp(nn.Module): + def __init__(self, in_features, hidden_features=None, out_features=None, device=None, dtype=None, operations=None): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = operations.Linear(in_features, hidden_features, device=device, dtype=dtype) + self.act = nn.GELU() + self.fc2 = operations.Linear(hidden_features, out_features, device=device, dtype=dtype) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.fc2(x) + return x + + +def window_partition(x, window_size): + B, H, W, C = x.shape + x = x.view(B, H // window_size, window_size, W // window_size, window_size, C) + windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C) + return windows + + +def window_reverse(windows, window_size, H, W): + B = int(windows.shape[0] / (H * W / window_size / window_size)) + x = windows.view(B, H // window_size, W // window_size, window_size, window_size, -1) + x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1) + return x + + +class WindowAttention(nn.Module): + def __init__(self, dim, window_size, num_heads, qkv_bias=True, qk_scale=None, device=None, dtype=None, operations=None): + + super().__init__() + self.dim = dim + self.window_size = window_size # Wh, Ww + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = qk_scale or head_dim ** -0.5 + + self.relative_position_bias_table = nn.Parameter( + torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads, device=device, dtype=dtype)) + + coords_h = torch.arange(self.window_size[0]) + coords_w = torch.arange(self.window_size[1]) + coords = torch.stack(torch.meshgrid([coords_h, coords_w], indexing='ij')) # 2, Wh, Ww + coords_flatten = torch.flatten(coords, 1) # 2, Wh*Ww + relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :] # 2, Wh*Ww, Wh*Ww + relative_coords = relative_coords.permute(1, 2, 0).contiguous() # Wh*Ww, Wh*Ww, 2 + relative_coords[:, :, 0] += self.window_size[0] - 1 + relative_coords[:, :, 1] += self.window_size[1] - 1 + relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1 + relative_position_index = relative_coords.sum(-1) # Wh*Ww, Wh*Ww + self.register_buffer("relative_position_index", relative_position_index) + + self.qkv = operations.Linear(dim, dim * 3, bias=qkv_bias, device=device, dtype=dtype) + self.proj = operations.Linear(dim, dim, device=device, dtype=dtype) + self.softmax = nn.Softmax(dim=-1) + + def forward(self, x, mask=None): + B_, N, C = x.shape + qkv = self.qkv(x).reshape(B_, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) + q, k, v = qkv[0], qkv[1], qkv[2] + + q = q * self.scale + attn = (q @ k.transpose(-2, -1)) + + relative_position_bias = self.relative_position_bias_table[self.relative_position_index.long().view(-1)].view( + self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1) # Wh*Ww,Wh*Ww,nH + relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous() # nH, Wh*Ww, Wh*Ww + attn = attn + relative_position_bias.unsqueeze(0) + + if mask is not None: + nW = mask.shape[0] + attn = attn.view(B_ // nW, nW, self.num_heads, N, N) + mask.unsqueeze(1).unsqueeze(0) + attn = attn.view(-1, self.num_heads, N, N) + attn = self.softmax(attn) + else: + attn = self.softmax(attn) + + x = (attn @ v).transpose(1, 2).reshape(B_, N, C) + x = self.proj(x) + return x + + +class SwinTransformerBlock(nn.Module): + def __init__(self, dim, num_heads, window_size=7, shift_size=0, + mlp_ratio=4., qkv_bias=True, qk_scale=None, + norm_layer=nn.LayerNorm, device=None, dtype=None, operations=None): + super().__init__() + self.dim = dim + self.num_heads = num_heads + self.window_size = window_size + self.shift_size = shift_size + self.mlp_ratio = mlp_ratio + + self.norm1 = norm_layer(dim, device=device, dtype=dtype) + self.attn = WindowAttention( + dim, window_size=(self.window_size, self.window_size), num_heads=num_heads, + qkv_bias=qkv_bias, qk_scale=qk_scale, device=device, dtype=dtype, operations=operations) + + self.norm2 = norm_layer(dim, device=device, dtype=dtype) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, device=device, dtype=dtype, operations=operations) + + self.H = None + self.W = None + + def forward(self, x, mask_matrix): + B, L, C = x.shape + H, W = self.H, self.W + + shortcut = x + x = self.norm1(x) + x = x.view(B, H, W, C) + + pad_l = pad_t = 0 + pad_r = (self.window_size - W % self.window_size) % self.window_size + pad_b = (self.window_size - H % self.window_size) % self.window_size + x = F.pad(x, (0, 0, pad_l, pad_r, pad_t, pad_b)) + _, Hp, Wp, _ = x.shape + + if self.shift_size > 0: + shifted_x = torch.roll(x, shifts=(-self.shift_size, -self.shift_size), dims=(1, 2)) + attn_mask = mask_matrix + else: + shifted_x = x + attn_mask = None + + x_windows = window_partition(shifted_x, self.window_size) + x_windows = x_windows.view(-1, self.window_size * self.window_size, C) + + attn_windows = self.attn(x_windows, mask=attn_mask) + + attn_windows = attn_windows.view(-1, self.window_size, self.window_size, C) + shifted_x = window_reverse(attn_windows, self.window_size, Hp, Wp) # B H' W' C + + if self.shift_size > 0: + x = torch.roll(shifted_x, shifts=(self.shift_size, self.shift_size), dims=(1, 2)) + else: + x = shifted_x + + if pad_r > 0 or pad_b > 0: + x = x[:, :H, :W, :].contiguous() + + x = x.view(B, H * W, C) + + x = shortcut + x + x = x + self.mlp(self.norm2(x)) + + return x + + +class PatchMerging(nn.Module): + def __init__(self, dim, device=None, dtype=None, operations=None): + super().__init__() + self.dim = dim + self.reduction = operations.Linear(4 * dim, 2 * dim, bias=False, device=device, dtype=dtype) + self.norm = operations.LayerNorm(4 * dim, device=device, dtype=dtype) + + def forward(self, x, H, W): + B, L, C = x.shape + x = x.view(B, H, W, C) + + # padding + pad_input = (H % 2 == 1) or (W % 2 == 1) + if pad_input: + x = F.pad(x, (0, 0, 0, W % 2, 0, H % 2)) + + x0 = x[:, 0::2, 0::2, :] # B H/2 W/2 C + x1 = x[:, 1::2, 0::2, :] # B H/2 W/2 C + x2 = x[:, 0::2, 1::2, :] # B H/2 W/2 C + x3 = x[:, 1::2, 1::2, :] # B H/2 W/2 C + x = torch.cat([x0, x1, x2, x3], -1) # B H/2 W/2 4*C + x = x.view(B, -1, 4 * C) # B H/2*W/2 4*C + + x = self.norm(x) + x = self.reduction(x) + + return x + + +class BasicLayer(nn.Module): + def __init__(self, + dim, + depth, + num_heads, + window_size=7, + mlp_ratio=4., + qkv_bias=True, + qk_scale=None, + norm_layer=nn.LayerNorm, + downsample=None, + device=None, dtype=None, operations=None): + super().__init__() + self.window_size = window_size + self.shift_size = window_size // 2 + self.depth = depth + + # build blocks + self.blocks = nn.ModuleList([ + SwinTransformerBlock( + dim=dim, + num_heads=num_heads, + window_size=window_size, + shift_size=0 if (i % 2 == 0) else window_size // 2, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + norm_layer=norm_layer, + device=device, dtype=dtype, operations=operations) + for i in range(depth)]) + + # patch merging layer + if downsample is not None: + self.downsample = downsample(dim=dim, device=device, dtype=dtype, operations=operations) + else: + self.downsample = None + + def forward(self, x, H, W): + Hp = int(np.ceil(H / self.window_size)) * self.window_size + Wp = int(np.ceil(W / self.window_size)) * self.window_size + img_mask = torch.zeros((1, Hp, Wp, 1), device=x.device) # 1 Hp Wp 1 + h_slices = (slice(0, -self.window_size), + slice(-self.window_size, -self.shift_size), + slice(-self.shift_size, None)) + w_slices = (slice(0, -self.window_size), + slice(-self.window_size, -self.shift_size), + slice(-self.shift_size, None)) + cnt = 0 + for h in h_slices: + for w in w_slices: + img_mask[:, h, w, :] = cnt + cnt += 1 + + mask_windows = window_partition(img_mask, self.window_size) + mask_windows = mask_windows.view(-1, self.window_size * self.window_size) + attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2) + attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0)) + + for blk in self.blocks: + blk.H, blk.W = H, W + x = blk(x, attn_mask) + if self.downsample is not None: + x_down = self.downsample(x, H, W) + Wh, Ww = (H + 1) // 2, (W + 1) // 2 + return x, H, W, x_down, Wh, Ww + else: + return x, H, W, x, H, W + + +class PatchEmbed(nn.Module): + def __init__(self, patch_size=4, in_channels=3, embed_dim=96, norm_layer=None, device=None, dtype=None, operations=None): + super().__init__() + patch_size = (patch_size, patch_size) + self.patch_size = patch_size + + self.in_channels = in_channels + self.embed_dim = embed_dim + + self.proj = operations.Conv2d(in_channels, embed_dim, kernel_size=patch_size, stride=patch_size, device=device, dtype=dtype) + if norm_layer is not None: + self.norm = norm_layer(embed_dim, device=device, dtype=dtype) + else: + self.norm = None + + def forward(self, x): + _, _, H, W = x.size() + if W % self.patch_size[1] != 0: + x = F.pad(x, (0, self.patch_size[1] - W % self.patch_size[1])) + if H % self.patch_size[0] != 0: + x = F.pad(x, (0, 0, 0, self.patch_size[0] - H % self.patch_size[0])) + + x = self.proj(x) # B C Wh Ww + if self.norm is not None: + Wh, Ww = x.size(2), x.size(3) + x = x.flatten(2).transpose(1, 2) + x = self.norm(x) + x = x.transpose(1, 2).view(-1, self.embed_dim, Wh, Ww) + + return x + + +class SwinTransformer(nn.Module): + def __init__(self, + pretrain_img_size=224, + patch_size=4, + in_channels=3, + embed_dim=96, + depths=[2, 2, 6, 2], + num_heads=[3, 6, 12, 24], + window_size=7, + mlp_ratio=4., + qkv_bias=True, + qk_scale=None, + patch_norm=True, + out_indices=(0, 1, 2, 3), + frozen_stages=-1, + device=None, dtype=None, operations=None): + super().__init__() + + norm_layer = partial(operations.LayerNorm, device=device, dtype=dtype) + self.pretrain_img_size = pretrain_img_size + self.num_layers = len(depths) + self.embed_dim = embed_dim + self.patch_norm = patch_norm + self.out_indices = out_indices + self.frozen_stages = frozen_stages + + self.patch_embed = PatchEmbed( + patch_size=patch_size, in_channels=in_channels, embed_dim=embed_dim, + device=device, dtype=dtype, operations=operations, + norm_layer=norm_layer if self.patch_norm else None) + + self.layers = nn.ModuleList() + for i_layer in range(self.num_layers): + layer = BasicLayer( + dim=int(embed_dim * 2 ** i_layer), + depth=depths[i_layer], + num_heads=num_heads[i_layer], + window_size=window_size, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + norm_layer=norm_layer, + downsample=PatchMerging if (i_layer < self.num_layers - 1) else None, + device=device, dtype=dtype, operations=operations) + self.layers.append(layer) + + num_features = [int(embed_dim * 2 ** i) for i in range(self.num_layers)] + self.num_features = num_features + + for i_layer in out_indices: + layer = norm_layer(num_features[i_layer]) + layer_name = f'norm{i_layer}' + self.add_module(layer_name, layer) + + + def forward(self, x): + x = self.patch_embed(x) + + Wh, Ww = x.size(2), x.size(3) + + outs = [] + x = x.flatten(2).transpose(1, 2) + for i in range(self.num_layers): + layer = self.layers[i] + x_out, H, W, x, Wh, Ww = layer(x, Wh, Ww) + + if i in self.out_indices: + norm_layer = getattr(self, f'norm{i}') + x_out = norm_layer(x_out) + + out = x_out.view(-1, H, W, self.num_features[i]).permute(0, 3, 1, 2).contiguous() + outs.append(out) + + return tuple(outs) + +class DeformableConv2d(nn.Module): + def __init__(self, + in_channels, + out_channels, + kernel_size=3, + stride=1, + padding=1, + bias=False, device=None, dtype=None, operations=None): + + super(DeformableConv2d, self).__init__() + + kernel_size = kernel_size if type(kernel_size) is tuple else (kernel_size, kernel_size) + self.stride = stride if type(stride) is tuple else (stride, stride) + self.padding = padding + + self.offset_conv = operations.Conv2d(in_channels, + 2 * kernel_size[0] * kernel_size[1], + kernel_size=kernel_size, + stride=stride, + padding=self.padding, + bias=True, device=device, dtype=dtype) + + self.modulator_conv = operations.Conv2d(in_channels, + 1 * kernel_size[0] * kernel_size[1], + kernel_size=kernel_size, + stride=stride, + padding=self.padding, + bias=True, device=device, dtype=dtype) + + self.regular_conv = operations.Conv2d(in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=self.padding, + bias=bias, device=device, dtype=dtype) + + def forward(self, x): + offset = self.offset_conv(x) + modulator = 2. * torch.sigmoid(self.modulator_conv(x)) + weight, bias, offload_info = comfy.ops.cast_bias_weight(self.regular_conv, x, offloadable=True) + + x = deform_conv2d( + input=x, + offset=offset, + weight=weight, + bias=None, + padding=self.padding, + mask=modulator, + stride=self.stride, + ) + comfy.ops.uncast_bias_weight(self.regular_conv, weight, bias, offload_info) + return x + +class BasicDecBlk(nn.Module): + def __init__(self, in_channels=64, out_channels=64, inter_channels=64, device=None, dtype=None, operations=None): + super(BasicDecBlk, self).__init__() + inter_channels = 64 + self.conv_in = operations.Conv2d(in_channels, inter_channels, 3, 1, padding=1, device=device, dtype=dtype) + self.relu_in = nn.ReLU(inplace=True) + self.dec_att = ASPPDeformable(in_channels=inter_channels, device=device, dtype=dtype, operations=operations) + self.conv_out = operations.Conv2d(inter_channels, out_channels, 3, 1, padding=1, device=device, dtype=dtype) + self.bn_in = operations.BatchNorm2d(inter_channels, device=device, dtype=dtype) + self.bn_out = operations.BatchNorm2d(out_channels, device=device, dtype=dtype) + + def forward(self, x): + x = self.conv_in(x) + x = self.bn_in(x) + x = self.relu_in(x) + x = self.dec_att(x) + x = self.conv_out(x) + x = self.bn_out(x) + return x + + +class BasicLatBlk(nn.Module): + def __init__(self, in_channels=64, out_channels=64, device=None, dtype=None, operations=None): + super(BasicLatBlk, self).__init__() + self.conv = operations.Conv2d(in_channels, out_channels, 1, 1, 0, device=device, dtype=dtype) + + def forward(self, x): + x = self.conv(x) + return x + + +class _ASPPModuleDeformable(nn.Module): + def __init__(self, in_channels, planes, kernel_size, padding, device, dtype, operations): + super(_ASPPModuleDeformable, self).__init__() + self.atrous_conv = DeformableConv2d(in_channels, planes, kernel_size=kernel_size, + stride=1, padding=padding, bias=False, device=device, dtype=dtype, operations=operations) + self.bn = operations.BatchNorm2d(planes, device=device, dtype=dtype) + self.relu = nn.ReLU(inplace=True) + + def forward(self, x): + x = self.atrous_conv(x) + x = self.bn(x) + + return self.relu(x) + + +class ASPPDeformable(nn.Module): + def __init__(self, in_channels, out_channels=None, parallel_block_sizes=[1, 3, 7], device=None, dtype=None, operations=None): + super(ASPPDeformable, self).__init__() + self.down_scale = 1 + if out_channels is None: + out_channels = in_channels + self.in_channelster = 256 // self.down_scale + + self.aspp1 = _ASPPModuleDeformable(in_channels, self.in_channelster, 1, padding=0, device=device, dtype=dtype, operations=operations) + self.aspp_deforms = nn.ModuleList([ + _ASPPModuleDeformable(in_channels, self.in_channelster, conv_size, padding=int(conv_size//2), device=device, dtype=dtype, operations=operations) + for conv_size in parallel_block_sizes + ]) + + self.global_avg_pool = nn.Sequential(nn.AdaptiveAvgPool2d((1, 1)), + operations.Conv2d(in_channels, self.in_channelster, 1, stride=1, bias=False, device=device, dtype=dtype), + operations.BatchNorm2d(self.in_channelster, device=device, dtype=dtype), + nn.ReLU(inplace=True)) + self.conv1 = operations.Conv2d(self.in_channelster * (2 + len(self.aspp_deforms)), out_channels, 1, bias=False, device=device, dtype=dtype) + self.bn1 = operations.BatchNorm2d(out_channels, device=device, dtype=dtype) + self.relu = nn.ReLU(inplace=True) + + def forward(self, x): + x1 = self.aspp1(x) + x_aspp_deforms = [aspp_deform(x) for aspp_deform in self.aspp_deforms] + x5 = self.global_avg_pool(x) + x5 = F.interpolate(x5, size=x1.size()[2:], mode='bilinear', align_corners=True) + x = torch.cat((x1, *x_aspp_deforms, x5), dim=1) + + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + + return x + +class BiRefNet(nn.Module): + def __init__(self, config=None, dtype=None, device=None, operations=None): + super(BiRefNet, self).__init__() + self.bb = SwinTransformer(embed_dim=192, depths=[2, 2, 18, 2], num_heads=[6, 12, 24, 48], window_size=12, device=device, dtype=dtype, operations=operations) + + channels = [1536, 768, 384, 192] + channels = [c * 2 for c in channels] + self.cxt = channels[1:][::-1][-3:] + self.squeeze_module = nn.Sequential(*[ + BasicDecBlk(channels[0]+sum(self.cxt), channels[0], device=device, dtype=dtype, operations=operations) + for _ in range(1) + ]) + + self.decoder = Decoder(channels, device=device, dtype=dtype, operations=operations) + + def forward_enc(self, x): + x1, x2, x3, x4 = self.bb(x) + B, C, H, W = x.shape + x1_, x2_, x3_, x4_ = self.bb(F.interpolate(x, size=(H//2, W//2), mode='bilinear', align_corners=True)) + x1 = torch.cat([x1, F.interpolate(x1_, size=x1.shape[2:], mode='bilinear', align_corners=True)], dim=1) + x2 = torch.cat([x2, F.interpolate(x2_, size=x2.shape[2:], mode='bilinear', align_corners=True)], dim=1) + x3 = torch.cat([x3, F.interpolate(x3_, size=x3.shape[2:], mode='bilinear', align_corners=True)], dim=1) + x4 = torch.cat([x4, F.interpolate(x4_, size=x4.shape[2:], mode='bilinear', align_corners=True)], dim=1) + x4 = torch.cat( + ( + *[ + F.interpolate(x1, size=x4.shape[2:], mode='bilinear', align_corners=True), + F.interpolate(x2, size=x4.shape[2:], mode='bilinear', align_corners=True), + F.interpolate(x3, size=x4.shape[2:], mode='bilinear', align_corners=True), + ][-len(CXT):], + x4 + ), + dim=1 + ) + return (x1, x2, x3, x4) + + def forward_ori(self, x): + (x1, x2, x3, x4) = self.forward_enc(x) + x4 = self.squeeze_module(x4) + features = [x, x1, x2, x3, x4] + scaled_preds = self.decoder(features) + return scaled_preds + + def forward(self, pixel_values, intermediate_output=None): + scaled_preds = self.forward_ori(pixel_values) + return scaled_preds + + +class Decoder(nn.Module): + def __init__(self, channels, device, dtype, operations): + super(Decoder, self).__init__() + # factory kwargs + fk = {"device":device, "dtype":dtype, "operations":operations} + DecoderBlock = partial(BasicDecBlk, **fk) + LateralBlock = partial(BasicLatBlk, **fk) + DBlock = partial(SimpleConvs, **fk) + + self.split = True + N_dec_ipt = 64 + ic = 64 + ipt_cha_opt = 1 + self.ipt_blk5 = DBlock(2**10*3 if self.split else 3, [N_dec_ipt, channels[0]//8][ipt_cha_opt], inter_channels=ic) + self.ipt_blk4 = DBlock(2**8*3 if self.split else 3, [N_dec_ipt, channels[0]//8][ipt_cha_opt], inter_channels=ic) + self.ipt_blk3 = DBlock(2**6*3 if self.split else 3, [N_dec_ipt, channels[1]//8][ipt_cha_opt], inter_channels=ic) + self.ipt_blk2 = DBlock(2**4*3 if self.split else 3, [N_dec_ipt, channels[2]//8][ipt_cha_opt], inter_channels=ic) + self.ipt_blk1 = DBlock(2**0*3 if self.split else 3, [N_dec_ipt, channels[3]//8][ipt_cha_opt], inter_channels=ic) + + self.decoder_block4 = DecoderBlock(channels[0]+([N_dec_ipt, channels[0]//8][ipt_cha_opt]), channels[1]) + self.decoder_block3 = DecoderBlock(channels[1]+([N_dec_ipt, channels[0]//8][ipt_cha_opt]), channels[2]) + self.decoder_block2 = DecoderBlock(channels[2]+([N_dec_ipt, channels[1]//8][ipt_cha_opt]), channels[3]) + self.decoder_block1 = DecoderBlock(channels[3]+([N_dec_ipt, channels[2]//8][ipt_cha_opt]), channels[3]//2) + + fk = {"device":device, "dtype":dtype} + + self.conv_out1 = nn.Sequential(operations.Conv2d(channels[3]//2+([N_dec_ipt, channels[3]//8][ipt_cha_opt]), 1, 1, 1, 0, **fk)) + + self.lateral_block4 = LateralBlock(channels[1], channels[1]) + self.lateral_block3 = LateralBlock(channels[2], channels[2]) + self.lateral_block2 = LateralBlock(channels[3], channels[3]) + + self.conv_ms_spvn_4 = operations.Conv2d(channels[1], 1, 1, 1, 0, **fk) + self.conv_ms_spvn_3 = operations.Conv2d(channels[2], 1, 1, 1, 0, **fk) + self.conv_ms_spvn_2 = operations.Conv2d(channels[3], 1, 1, 1, 0, **fk) + + _N = 16 + + self.gdt_convs_4 = nn.Sequential(operations.Conv2d(channels[0] // 2, _N, 3, 1, 1, **fk), operations.BatchNorm2d(_N, **fk), nn.ReLU(inplace=True)) + self.gdt_convs_3 = nn.Sequential(operations.Conv2d(channels[1] // 2, _N, 3, 1, 1, **fk), operations.BatchNorm2d(_N, **fk), nn.ReLU(inplace=True)) + self.gdt_convs_2 = nn.Sequential(operations.Conv2d(channels[2] // 2, _N, 3, 1, 1, **fk), operations.BatchNorm2d(_N, **fk), nn.ReLU(inplace=True)) + + [setattr(self, f"gdt_convs_pred_{i}", nn.Sequential(operations.Conv2d(_N, 1, 1, 1, 0, **fk))) for i in range(2, 5)] + [setattr(self, f"gdt_convs_attn_{i}", nn.Sequential(operations.Conv2d(_N, 1, 1, 1, 0, **fk))) for i in range(2, 5)] + + def get_patches_batch(self, x, p): + _size_h, _size_w = p.shape[2:] + patches_batch = [] + for idx in range(x.shape[0]): + columns_x = torch.split(x[idx], split_size_or_sections=_size_w, dim=-1) + patches_x = [] + for column_x in columns_x: + patches_x += [p.unsqueeze(0) for p in torch.split(column_x, split_size_or_sections=_size_h, dim=-2)] + patch_sample = torch.cat(patches_x, dim=1) + patches_batch.append(patch_sample) + return torch.cat(patches_batch, dim=0) + + def forward(self, features): + x, x1, x2, x3, x4 = features + + patches_batch = self.get_patches_batch(x, x4) if self.split else x + x4 = torch.cat((x4, self.ipt_blk5(F.interpolate(patches_batch, size=x4.shape[2:], mode='bilinear', align_corners=True))), 1) + p4 = self.decoder_block4(x4) + p4_gdt = self.gdt_convs_4(p4) + gdt_attn_4 = self.gdt_convs_attn_4(p4_gdt).sigmoid() + p4 = p4 * gdt_attn_4 + _p4 = F.interpolate(p4, size=x3.shape[2:], mode='bilinear', align_corners=True) + _p3 = _p4 + self.lateral_block4(x3) + + patches_batch = self.get_patches_batch(x, _p3) if self.split else x + _p3 = torch.cat((_p3, self.ipt_blk4(F.interpolate(patches_batch, size=x3.shape[2:], mode='bilinear', align_corners=True))), 1) + p3 = self.decoder_block3(_p3) + + p3_gdt = self.gdt_convs_3(p3) + gdt_attn_3 = self.gdt_convs_attn_3(p3_gdt).sigmoid() + p3 = p3 * gdt_attn_3 + _p3 = F.interpolate(p3, size=x2.shape[2:], mode='bilinear', align_corners=True) + _p2 = _p3 + self.lateral_block3(x2) + + patches_batch = self.get_patches_batch(x, _p2) if self.split else x + _p2 = torch.cat((_p2, self.ipt_blk3(F.interpolate(patches_batch, size=x2.shape[2:], mode='bilinear', align_corners=True))), 1) + p2 = self.decoder_block2(_p2) + + p2_gdt = self.gdt_convs_2(p2) + gdt_attn_2 = self.gdt_convs_attn_2(p2_gdt).sigmoid() + p2 = p2 * gdt_attn_2 + + _p2 = F.interpolate(p2, size=x1.shape[2:], mode='bilinear', align_corners=True) + _p1 = _p2 + self.lateral_block2(x1) + + patches_batch = self.get_patches_batch(x, _p1) if self.split else x + _p1 = torch.cat((_p1, self.ipt_blk2(F.interpolate(patches_batch, size=x1.shape[2:], mode='bilinear', align_corners=True))), 1) + _p1 = self.decoder_block1(_p1) + _p1 = F.interpolate(_p1, size=x.shape[2:], mode='bilinear', align_corners=True) + + patches_batch = self.get_patches_batch(x, _p1) if self.split else x + _p1 = torch.cat((_p1, self.ipt_blk1(F.interpolate(patches_batch, size=x.shape[2:], mode='bilinear', align_corners=True))), 1) + p1_out = self.conv_out1(_p1) + return p1_out + + +class SimpleConvs(nn.Module): + def __init__( + self, in_channels: int, out_channels: int, inter_channels=64, device=None, dtype=None, operations=None + ) -> None: + super().__init__() + self.conv1 = operations.Conv2d(in_channels, inter_channels, 3, 1, 1, device=device, dtype=dtype) + self.conv_out = operations.Conv2d(inter_channels, out_channels, 3, 1, 1, device=device, dtype=dtype) + + def forward(self, x): + return self.conv_out(self.conv1(x)) diff --git a/comfy/bg_removal_model.py b/comfy/bg_removal_model.py new file mode 100644 index 000000000..6dec65e63 --- /dev/null +++ b/comfy/bg_removal_model.py @@ -0,0 +1,85 @@ +from .utils import load_torch_file +import os +import json +import torch +import logging + +import comfy.ops +import comfy.model_patcher +import comfy.model_management +import comfy.clip_model +import comfy.background_removal.birefnet + +BG_REMOVAL_MODELS = { + "birefnet": comfy.background_removal.birefnet.BiRefNet +} + +class BackgroundRemovalModel(): + def __init__(self, json_config): + with open(json_config) as f: + config = json.load(f) + + self.image_size = config.get("image_size", 1024) + self.image_mean = config.get("image_mean", [0.0, 0.0, 0.0]) + self.image_std = config.get("image_std", [1.0, 1.0, 1.0]) + self.model_type = config.get("model_type", "birefnet") + self.config = config.copy() + model_class = BG_REMOVAL_MODELS.get(self.model_type) + + self.load_device = comfy.model_management.text_encoder_device() + offload_device = comfy.model_management.text_encoder_offload_device() + self.dtype = comfy.model_management.text_encoder_dtype(self.load_device) + self.model = model_class(config, self.dtype, offload_device, comfy.ops.manual_cast) + self.model.eval() + + self.patcher = comfy.model_patcher.CoreModelPatcher(self.model, load_device=self.load_device, offload_device=offload_device) + + def load_sd(self, sd): + return self.model.load_state_dict(sd, strict=False, assign=self.patcher.is_dynamic()) + + def get_sd(self): + return self.model.state_dict() + + def encode_image(self, image): + comfy.model_management.load_model_gpu(self.patcher) + H, W = image.shape[1], image.shape[2] + pixel_values = comfy.clip_model.clip_preprocess(image.to(self.load_device), size=self.image_size, mean=self.image_mean, std=self.image_std, crop=False) + + if pixel_values.shape[0] > 1: + out = torch.cat([ + self.model(pixel_values=pixel_values[i:i+1]) + for i in range(pixel_values.shape[0]) + ], dim=0) + else: + out = self.model(pixel_values=pixel_values) + out = torch.nn.functional.interpolate(out, size=(H, W), mode="bicubic", antialias=False) + + mask = out.sigmoid().to(device=comfy.model_management.intermediate_device(), dtype=comfy.model_management.intermediate_dtype()) + if mask.ndim == 3: + mask = mask.unsqueeze(0) + if mask.shape[1] != 1: + mask = mask.movedim(-1, 1) + + return mask + + +def load_background_removal_model(sd): + if "bb.layers.1.blocks.0.attn.relative_position_index" in sd: + json_config = os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "background_removal"), "birefnet.json") + else: + return None + + bg_model = BackgroundRemovalModel(json_config) + m, u = bg_model.load_sd(sd) + if len(m) > 0: + logging.warning("missing background removal: {}".format(m)) + u = set(u) + keys = list(sd.keys()) + for k in keys: + if k not in u: + sd.pop(k) + return bg_model + +def load(ckpt_path): + sd = load_torch_file(ckpt_path) + return load_background_removal_model(sd) diff --git a/comfy/cli_args.py b/comfy/cli_args.py index 9dadb0093..9bda414d1 100644 --- a/comfy/cli_args.py +++ b/comfy/cli_args.py @@ -49,7 +49,7 @@ parser.add_argument("--temp-directory", type=str, default=None, help="Set the Co parser.add_argument("--input-directory", type=str, default=None, help="Set the ComfyUI input directory. Overrides --base-directory.") parser.add_argument("--auto-launch", action="store_true", help="Automatically launch ComfyUI in the default browser.") parser.add_argument("--disable-auto-launch", action="store_true", help="Disable auto launching the browser.") -parser.add_argument("--cuda-device", type=int, default=None, metavar="DEVICE_ID", help="Set the id of the cuda device this instance will use. All other devices will not be visible.") +parser.add_argument("--cuda-device", type=str, default=None, metavar="DEVICE_ID", help="Set the ids of cuda devices this instance will use, as a comma-separated list (e.g. '0' or '0,1'). All other devices will not be visible.") parser.add_argument("--default-device", type=int, default=None, metavar="DEFAULT_DEVICE_ID", help="Set the id of the default device, all other devices will stay visible.") cm_group = parser.add_mutually_exclusive_group() cm_group.add_argument("--cuda-malloc", action="store_true", help="Enable cudaMallocAsync (enabled by default for torch 2.0 and up).") @@ -110,13 +110,11 @@ parser.add_argument("--preview-method", type=LatentPreviewMethod, default=Latent parser.add_argument("--preview-size", type=int, default=512, help="Sets the maximum preview size for sampler nodes.") -CACHE_RAM_AUTO_GB = -1.0 - cache_group = parser.add_mutually_exclusive_group() +cache_group.add_argument("--cache-ram", nargs='*', type=float, default=[], metavar="GB", help="Use RAM pressure caching with the specified headroom thresholds. This is the default caching mode. The first value sets the active-cache threshold; the optional second value sets the inactive-cache/pin threshold. Defaults when no values are provided: active 10%% of system RAM (min 2GB, max 10GB), inactive 100%% of system RAM (max 96GB).") cache_group.add_argument("--cache-classic", action="store_true", help="Use the old style (aggressive) caching.") cache_group.add_argument("--cache-lru", type=int, default=0, help="Use LRU caching with a maximum of N node results cached. May use more RAM/VRAM.") cache_group.add_argument("--cache-none", action="store_true", help="Reduced RAM/VRAM usage at the expense of executing every node for each run.") -cache_group.add_argument("--cache-ram", nargs='?', const=CACHE_RAM_AUTO_GB, type=float, default=0, help="Use RAM pressure caching with the specified headroom threshold. If available RAM drops below the threshold the cache removes large items to free RAM. Default (when no value is provided): 25%% of system RAM (min 4GB, max 32GB).") attn_group = parser.add_mutually_exclusive_group() attn_group.add_argument("--use-split-cross-attention", action="store_true", help="Use the split cross attention optimization. Ignored when xformers is used.") @@ -141,8 +139,7 @@ manager_group.add_argument("--enable-manager-legacy-ui", action="store_true", he vram_group = parser.add_mutually_exclusive_group() vram_group.add_argument("--gpu-only", action="store_true", help="Store and run everything (text encoders/CLIP models, etc... on the GPU).") vram_group.add_argument("--highvram", action="store_true", help="By default models will be unloaded to CPU memory after being used. This option keeps them in GPU memory.") -vram_group.add_argument("--normalvram", action="store_true", help="Used to force normal vram use if lowvram gets automatically enabled.") -vram_group.add_argument("--lowvram", action="store_true", help="Split the unet in parts to use less vram.") +vram_group.add_argument("--lowvram", action="store_true", help="Doesn't do anything if dynamic vram is enabled. If dynamic vram isn't being used this option makes the text encoders run on the CPU.") vram_group.add_argument("--novram", action="store_true", help="When lowvram isn't enough.") vram_group.add_argument("--cpu", action="store_true", help="To use the CPU for everything (slow).") @@ -246,6 +243,9 @@ if comfy.options.args_parsing: else: args = parser.parse_args([]) +if args.cache_ram is not None and len(args.cache_ram) > 2: + parser.error("--cache-ram accepts at most two values: active GB and inactive GB") + if args.windows_standalone_build: args.auto_launch = True diff --git a/comfy/comfy_types/node_typing.py b/comfy/comfy_types/node_typing.py index 57126fa4a..bb21eb1d1 100644 --- a/comfy/comfy_types/node_typing.py +++ b/comfy/comfy_types/node_typing.py @@ -1,6 +1,5 @@ """Comfy-specific type hinting""" -from __future__ import annotations from typing import Literal, TypedDict, Optional from typing_extensions import NotRequired from abc import ABC, abstractmethod diff --git a/comfy/controlnet.py b/comfy/controlnet.py index ba670b16d..6dbbaa959 100644 --- a/comfy/controlnet.py +++ b/comfy/controlnet.py @@ -15,13 +15,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ - +from __future__ import annotations import torch from enum import Enum import math import os import logging +import copy import comfy.utils import comfy.model_management import comfy.model_detection @@ -38,7 +39,7 @@ import comfy.ldm.hydit.controlnet import comfy.ldm.flux.controlnet import comfy.ldm.qwen_image.controlnet import comfy.cldm.dit_embedder -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: from comfy.hooks import HookGroup @@ -64,6 +65,18 @@ class StrengthType(Enum): CONSTANT = 1 LINEAR_UP = 2 +class ControlIsolation: + '''Temporarily set a ControlBase object's previous_controlnet to None to prevent cascading calls.''' + def __init__(self, control: ControlBase): + self.control = control + self.orig_previous_controlnet = control.previous_controlnet + + def __enter__(self): + self.control.previous_controlnet = None + + def __exit__(self, *args): + self.control.previous_controlnet = self.orig_previous_controlnet + class ControlBase: def __init__(self): self.cond_hint_original = None @@ -77,7 +90,7 @@ class ControlBase: self.compression_ratio = 8 self.upscale_algorithm = 'nearest-exact' self.extra_args = {} - self.previous_controlnet = None + self.previous_controlnet: Union[ControlBase, None] = None self.extra_conds = [] self.strength_type = StrengthType.CONSTANT self.concat_mask = False @@ -85,6 +98,7 @@ class ControlBase: self.extra_concat = None self.extra_hooks: HookGroup = None self.preprocess_image = lambda a: a + self.multigpu_clones: dict[torch.device, ControlBase] = {} def set_cond_hint(self, cond_hint, strength=1.0, timestep_percent_range=(0.0, 1.0), vae=None, extra_concat=[]): self.cond_hint_original = cond_hint @@ -111,17 +125,38 @@ class ControlBase: def cleanup(self): if self.previous_controlnet is not None: self.previous_controlnet.cleanup() - + for device_cnet in self.multigpu_clones.values(): + with ControlIsolation(device_cnet): + device_cnet.cleanup() self.cond_hint = None self.extra_concat = None self.timestep_range = None def get_models(self): out = [] + for device_cnet in self.multigpu_clones.values(): + out += device_cnet.get_models_only_self() if self.previous_controlnet is not None: out += self.previous_controlnet.get_models() return out + def get_models_only_self(self): + 'Calls get_models, but temporarily sets previous_controlnet to None.' + with ControlIsolation(self): + return self.get_models() + + def get_instance_for_device(self, device): + 'Returns instance of this Control object intended for selected device.' + return self.multigpu_clones.get(device, self) + + def deepclone_multigpu(self, load_device, autoregister=False): + ''' + Create deep clone of Control object where model(s) is set to other devices. + + When autoregister is set to True, the deep clone is also added to multigpu_clones dict. + ''' + raise NotImplementedError("Classes inheriting from ControlBase should define their own deepclone_multigpu funtion.") + def get_extra_hooks(self): out = [] if self.extra_hooks is not None: @@ -130,7 +165,7 @@ class ControlBase: out += self.previous_controlnet.get_extra_hooks() return out - def copy_to(self, c): + def copy_to(self, c: ControlBase): c.cond_hint_original = self.cond_hint_original c.strength = self.strength c.timestep_percent_range = self.timestep_percent_range @@ -284,6 +319,14 @@ class ControlNet(ControlBase): self.copy_to(c) return c + def deepclone_multigpu(self, load_device, autoregister=False): + c = self.copy() + c.control_model = copy.deepcopy(c.control_model) + c.control_model_wrapped = comfy.model_patcher.ModelPatcher(c.control_model, load_device=load_device, offload_device=comfy.model_management.unet_offload_device()) + if autoregister: + self.multigpu_clones[load_device] = c + return c + def get_models(self): out = super().get_models() out.append(self.control_model_wrapped) @@ -314,6 +357,10 @@ class QwenFunControlNet(ControlNet): super().pre_run(model, percent_to_timestep_function) self.set_extra_arg("base_model", model.diffusion_model) + def cleanup(self): + self.extra_args.pop("base_model", None) + super().cleanup() + def copy(self): c = QwenFunControlNet(None, global_average_pooling=self.global_average_pooling, load_device=self.load_device, manual_cast_dtype=self.manual_cast_dtype) c.control_model = self.control_model @@ -906,6 +953,14 @@ class T2IAdapter(ControlBase): self.copy_to(c) return c + def deepclone_multigpu(self, load_device, autoregister=False): + c = self.copy() + c.t2i_model = copy.deepcopy(c.t2i_model) + c.device = load_device + if autoregister: + self.multigpu_clones[load_device] = c + return c + def load_t2i_adapter(t2i_data, model_options={}): #TODO: model_options compression_ratio = 8 upscale_algorithm = 'nearest-exact' diff --git a/comfy/hooks.py b/comfy/hooks.py index 1a76c7ba4..5458fc3d8 100644 --- a/comfy/hooks.py +++ b/comfy/hooks.py @@ -93,7 +93,7 @@ class Hook: self.hook_scope = hook_scope '''Scope of where this hook should apply in terms of the conds used in sampling run.''' self.custom_should_register = default_should_register - '''Can be overriden with a compatible function to decide if this hook should be registered without the need to override .should_register''' + '''Can be overridden with a compatible function to decide if this hook should be registered without the need to override .should_register''' @property def strength(self): diff --git a/comfy/image_encoders/dino2.py b/comfy/image_encoders/dino2.py index 9b6dace9d..ee86f8309 100644 --- a/comfy/image_encoders/dino2.py +++ b/comfy/image_encoders/dino2.py @@ -106,6 +106,7 @@ class Dino2Encoder(torch.nn.Module): class Dino2PatchEmbeddings(torch.nn.Module): def __init__(self, dim, num_channels=3, patch_size=14, image_size=518, dtype=None, device=None, operations=None): super().__init__() + self.patch_size = patch_size self.projection = operations.Conv2d( in_channels=num_channels, out_channels=dim, @@ -125,17 +126,37 @@ class Dino2Embeddings(torch.nn.Module): super().__init__() patch_size = 14 image_size = 518 + self.patch_size = patch_size self.patch_embeddings = Dino2PatchEmbeddings(dim, patch_size=patch_size, image_size=image_size, dtype=dtype, device=device, operations=operations) self.position_embeddings = torch.nn.Parameter(torch.empty(1, (image_size // patch_size) ** 2 + 1, dim, dtype=dtype, device=device)) - self.cls_token = torch.nn.Parameter(torch.empty(1, 1, dim, dtype=dtype, device=device)) + self.cls_token = torch.nn.Parameter(torch.empty(1, 1, dim, dtype=dtype, device=device)) # mask_token is a pre-training param, kept only so strict loading accepts the key. self.mask_token = torch.nn.Parameter(torch.empty(1, dim, dtype=dtype, device=device)) + def interpolate_pos_encoding(self, x, h_pixels, w_pixels): + pos_embed = comfy.model_management.cast_to_device(self.position_embeddings, x.device, torch.float32) + + class_pos = pos_embed[:, 0:1] + patch_pos = pos_embed[:, 1:] + N = patch_pos.shape[1] + M = int(N ** 0.5) + h0 = h_pixels // self.patch_size + w0 = w_pixels // self.patch_size + scale_factor = ((h0 + 0.1) / M, (w0 + 0.1) / M) # +0.1 matches upstream DINOv2's FP-rounding workaround so the interpolate output size lands on (h0, w0). + + patch_pos = patch_pos.reshape(1, M, M, -1).permute(0, 3, 1, 2) + patch_pos = torch.nn.functional.interpolate(patch_pos, scale_factor=scale_factor, mode="bicubic", antialias=False) + patch_pos = patch_pos.permute(0, 2, 3, 1).flatten(1, 2) + return torch.cat((class_pos, patch_pos), dim=1).to(x.dtype) + def forward(self, pixel_values): x = self.patch_embeddings(pixel_values) - # TODO: mask_token? x = torch.cat((self.cls_token.to(device=x.device, dtype=x.dtype).expand(x.shape[0], -1, -1), x), dim=1) - x = x + comfy.model_management.cast_to_device(self.position_embeddings, x.device, x.dtype) + if x.shape[1] - 1 == self.position_embeddings.shape[1] - 1: + x = x + comfy.model_management.cast_to_device(self.position_embeddings, x.device, x.dtype) + else: + h, w = pixel_values.shape[-2:] + x = x + self.interpolate_pos_encoding(x, h, w) return x @@ -158,3 +179,21 @@ class Dinov2Model(torch.nn.Module): x = self.layernorm(x) pooled_output = x[:, 0, :] return x, i, pooled_output, None + + def get_intermediate_layers(self, pixel_values, indices, apply_norm=True): + x = self.embeddings(pixel_values) + optimized_attention = optimized_attention_for_device(x.device, False, small_input=True) + n_layers = len(self.encoder.layer) + resolved = [(i if i >= 0 else n_layers + i) for i in indices] + target = set(resolved) + max_idx = max(resolved) + n_skip = 1 # skip cls token + cache = {} + for i, layer in enumerate(self.encoder.layer): + x = layer(x, optimized_attention) + if i in target: + normed = self.layernorm(x) if apply_norm else x + cache[i] = (normed[:, n_skip:], normed[:, 0]) + if i >= max_idx: + break + return [cache[i] for i in resolved] diff --git a/comfy/k_diffusion/sampling.py b/comfy/k_diffusion/sampling.py index d33bc7199..11db46d94 100644 --- a/comfy/k_diffusion/sampling.py +++ b/comfy/k_diffusion/sampling.py @@ -242,6 +242,7 @@ def sample_euler_ancestral_RF(model, x, sigmas, extra_args=None, callback=None, extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) s_in = x.new_ones([x.shape[0]]) for i in trange(len(sigmas) - 1, disable=disable): denoised = model(x, sigmas[i] * s_in, **extra_args) @@ -373,6 +374,7 @@ def sample_dpm_2_ancestral_RF(model, x, sigmas, extra_args=None, callback=None, extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) s_in = x.new_ones([x.shape[0]]) for i in trange(len(sigmas) - 1, disable=disable): denoised = model(x, sigmas[i] * s_in, **extra_args) @@ -686,6 +688,7 @@ def sample_dpmpp_2s_ancestral_RF(model, x, sigmas, extra_args=None, callback=Non extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) s_in = x.new_ones([x.shape[0]]) sigma_fn = lambda lbda: (lbda.exp() + 1) ** -1 lambda_fn = lambda sigma: ((1-sigma)/sigma).log() @@ -747,6 +750,7 @@ def sample_dpmpp_sde(model, x, sigmas, extra_args=None, callback=None, disable=N sigma_fn = partial(half_log_snr_to_sigma, model_sampling=model_sampling) lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) for i in trange(len(sigmas) - 1, disable=disable): denoised = model(x, sigmas[i] * s_in, **extra_args) @@ -832,6 +836,7 @@ def sample_dpmpp_2m_sde(model, x, sigmas, extra_args=None, callback=None, disabl model_sampling = model.inner_model.model_patcher.get_model_object('model_sampling') lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) old_denoised = None h, h_last = None, None @@ -889,6 +894,7 @@ def sample_dpmpp_3m_sde(model, x, sigmas, extra_args=None, callback=None, disabl model_sampling = model.inner_model.model_patcher.get_model_object('model_sampling') lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) denoised_1, denoised_2 = None, None h, h_1, h_2 = None, None, None @@ -1006,23 +1012,39 @@ def sample_ddpm(model, x, sigmas, extra_args=None, callback=None, disable=None, return generic_step_sampler(model, x, sigmas, extra_args, callback, disable, noise_sampler, DDPMSampler_step) @torch.no_grad() -def sample_lcm(model, x, sigmas, extra_args=None, callback=None, disable=None, noise_sampler=None): +def sample_lcm(model, x, sigmas, extra_args=None, callback=None, disable=None, noise_sampler=None, s_noise=1.0, s_noise_end=None, noise_clip_std=0.0): + + # s_noise / s_noise_end: per-step noise multiplier, linearly interpolated across steps + # noise_clip_std: clamp injected noise to +/- N stddevs (0 disables). + extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler s_in = x.new_ones([x.shape[0]]) - for i in trange(len(sigmas) - 1, disable=disable): + n_steps = max(1, len(sigmas) - 1) + model_sampling = model.inner_model.model_patcher.get_model_object('model_sampling') + + s_start = float(s_noise) + s_end = s_start if s_noise_end is None else float(s_noise_end) + for i in trange(n_steps, disable=disable): denoised = model(x, sigmas[i] * s_in, **extra_args) if callback is not None: callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised}) x = denoised if sigmas[i + 1] > 0: - x = model.inner_model.inner_model.model_sampling.noise_scaling(sigmas[i + 1], noise_sampler(sigmas[i], sigmas[i + 1]), x) + noise = noise_sampler(sigmas[i], sigmas[i + 1]) + if noise_clip_std > 0: + clip_val = noise_clip_std * noise.std() + noise = noise.clamp(min=-clip_val, max=clip_val) + t = (i / (n_steps - 1)) if n_steps > 1 else 0.0 + s_noise_i = s_start + (s_end - s_start) * t + if s_noise_i != 1.0: + noise = noise * s_noise_i + x = model_sampling.noise_scaling(sigmas[i + 1], noise, x) return x - @torch.no_grad() def sample_heunpp2(model, x, sigmas, extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.): # From MIT licensed: https://github.com/Carzit/sd-webui-samplers-scheduler/ @@ -1249,6 +1271,7 @@ def sample_euler_ancestral_cfg_pp(model, x, sigmas, extra_args=None, callback=No model_sampling = model.inner_model.model_patcher.get_model_object("model_sampling") lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) uncond_denoised = None @@ -1296,6 +1319,7 @@ def sample_dpmpp_2s_ancestral_cfg_pp(model, x, sigmas, extra_args=None, callback extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) temp = [0] def post_cfg_function(args): @@ -1371,6 +1395,7 @@ def res_multistep(model, x, sigmas, extra_args=None, callback=None, disable=None extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) s_in = x.new_ones([x.shape[0]]) sigma_fn = lambda t: t.neg().exp() t_fn = lambda sigma: sigma.log().neg() @@ -1504,6 +1529,7 @@ def sample_er_sde(model, x, sigmas, extra_args=None, callback=None, disable=None extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_noise = s_noise * getattr(model.inner_model.model_patcher.get_model_object('model_sampling'), "noise_scale", 1.0) s_in = x.new_ones([x.shape[0]]) def default_er_sde_noise_scaler(x): @@ -1574,9 +1600,10 @@ def sample_seeds_2(model, x, sigmas, extra_args=None, callback=None, disable=Non seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler s_in = x.new_ones([x.shape[0]]) - inject_noise = eta > 0 and s_noise > 0 model_sampling = model.inner_model.model_patcher.get_model_object('model_sampling') + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) + inject_noise = eta > 0 and s_noise > 0 sigma_fn = partial(half_log_snr_to_sigma, model_sampling=model_sampling) lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) @@ -1645,9 +1672,10 @@ def sample_seeds_3(model, x, sigmas, extra_args=None, callback=None, disable=Non seed = extra_args.get("seed", None) noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler s_in = x.new_ones([x.shape[0]]) - inject_noise = eta > 0 and s_noise > 0 model_sampling = model.inner_model.model_patcher.get_model_object('model_sampling') + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) + inject_noise = eta > 0 and s_noise > 0 sigma_fn = partial(half_log_snr_to_sigma, model_sampling=model_sampling) lambda_fn = partial(sigma_to_half_log_snr, model_sampling=model_sampling) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) @@ -1713,6 +1741,7 @@ def sample_sa_solver(model, x, sigmas, extra_args=None, callback=None, disable=F s_in = x.new_ones([x.shape[0]]) model_sampling = model.inner_model.model_patcher.get_model_object("model_sampling") + s_noise = s_noise * getattr(model_sampling, "noise_scale", 1.0) sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) lambdas = sigma_to_half_log_snr(sigmas, model_sampling=model_sampling) @@ -1859,6 +1888,23 @@ def sample_ar_video(model, x, sigmas, extra_args=None, callback=None, disable=No output = torch.zeros_like(x) s_in = x.new_ones([x.shape[0]]) current_start_frame = 0 + + # I2V: seed KV cache with the initial image latent before the denoising loop + initial_latent = transformer_options.get("ar_config", {}).get("initial_latent", None) + if initial_latent is not None: + initial_latent = inner_model.process_latent_in(initial_latent).to(device=device, dtype=model_dtype) + n_init = initial_latent.shape[2] + output[:, :, :n_init] = initial_latent + + ar_state = {"start_frame": 0, "kv_caches": kv_caches, "crossattn_caches": crossattn_caches} + transformer_options["ar_state"] = ar_state + zero_sigma = sigmas.new_zeros([1]) + _ = model(initial_latent, zero_sigma * s_in, **extra_args) + + current_start_frame = n_init + remaining = lat_t - n_init + num_blocks = -(-remaining // num_frame_per_block) + num_sigma_steps = len(sigmas) - 1 total_real_steps = num_blocks * num_sigma_steps step_count = 0 diff --git a/comfy/latent_formats.py b/comfy/latent_formats.py index 60c0dfd7e..75d459b59 100644 --- a/comfy/latent_formats.py +++ b/comfy/latent_formats.py @@ -150,6 +150,12 @@ class SD3(LatentFormat): class StableAudio1(LatentFormat): latent_channels = 64 latent_dimensions = 1 + temporal_downscale_ratio = 2048 + +class StableAudio3(LatentFormat): + latent_channels = 256 + latent_dimensions = 1 + temporal_downscale_ratio = 4096 class Flux(SD3): latent_channels = 16 @@ -766,6 +772,7 @@ class ACEAudio(LatentFormat): class ACEAudio15(LatentFormat): latent_channels = 64 latent_dimensions = 1 + temporal_downscale_ratio = 1764 class ChromaRadiance(LatentFormat): latent_channels = 3 @@ -792,10 +799,35 @@ class ZImagePixelSpace(ChromaRadiance): """ pass + +class HiDreamO1Pixel(ChromaRadiance): + """Pixel-space latent format for HiDream-O1. + No VAE — model patches/unpatches raw RGB internally with patch_size=32. + """ + pass + class CogVideoX(LatentFormat): + """Latent format for CogVideoX-2b (THUDM/CogVideoX-2b). + + scale_factor matches the vae/config.json scaling_factor for the 2b variant. + The 5b-class checkpoints (CogVideoX-5b, CogVideoX-1.5-5B, CogVideoX-Fun-V1.5-*) + use a different value; see CogVideoX1_5 below. + """ latent_channels = 16 latent_dimensions = 3 temporal_downscale_ratio = 4 def __init__(self): self.scale_factor = 1.15258426 + + +class CogVideoX1_5(CogVideoX): + """Latent format for 5b-class CogVideoX checkpoints. + + Covers THUDM/CogVideoX-5b, THUDM/CogVideoX-1.5-5B, and the CogVideoX-Fun + V1.5-5b family (including VOID inpainting). All of these have + scaling_factor=0.7 in their vae/config.json. Auto-selected in + supported_models.CogVideoX_T2V based on transformer hidden dim. + """ + def __init__(self): + self.scale_factor = 0.7 diff --git a/comfy/ldm/audio/dit.py b/comfy/ldm/audio/dit.py index ca865189e..a6258b755 100644 --- a/comfy/ldm/audio/dit.py +++ b/comfy/ldm/audio/dit.py @@ -10,6 +10,17 @@ from torch import nn from torch.nn import functional as F import math import comfy.ops +from .embedders import ExpoFourierFeatures + + +def _left_pad_to_match(emb, target_len): + emb_len = emb.shape[-2] + if emb_len < target_len: + return F.pad(emb, (0, 0, target_len - emb_len, 0), value=0.) + elif emb_len > target_len: + return emb[:, -target_len:, :] + return emb + class FourierFeatures(nn.Module): def __init__(self, in_features, out_features, std=1., dtype=None, device=None): @@ -22,6 +33,7 @@ class FourierFeatures(nn.Module): f = 2 * math.pi * input @ comfy.ops.cast_to_input(self.weight.T, input) return torch.cat([f.cos(), f.sin()], dim=-1) + # norms class LayerNorm(nn.Module): def __init__(self, dim, bias=False, fix_scale=False, dtype=None, device=None): @@ -43,6 +55,16 @@ class LayerNorm(nn.Module): beta = comfy.ops.cast_to_input(beta, x) return F.layer_norm(x, x.shape[-1:], weight=comfy.ops.cast_to_input(self.gamma, x), bias=beta) + +class RMSNorm(nn.Module): + def __init__(self, dim, dtype=None, device=None): + super().__init__() + self.gamma = nn.Parameter(torch.empty(dim, dtype=dtype, device=device)) + + def forward(self, x): + return F.rms_norm(x, x.shape[-1:], weight=comfy.ops.cast_to_input(self.gamma, x)) + + class GLU(nn.Module): def __init__( self, @@ -236,13 +258,6 @@ class FeedForward(nn.Module): linear_out = operations.Linear(inner_dim, dim_out, bias = not no_bias, dtype=dtype, device=device) if not use_conv else operations.Conv1d(inner_dim, dim_out, conv_kernel_size, padding = (conv_kernel_size // 2), bias = not no_bias, dtype=dtype, device=device) - # # init last linear layer to 0 - # if zero_init_output: - # nn.init.zeros_(linear_out.weight) - # if not no_bias: - # nn.init.zeros_(linear_out.bias) - - self.ff = nn.Sequential( linear_in, rearrange('b d n -> b n d') if use_conv else nn.Identity(), @@ -261,8 +276,10 @@ class Attention(nn.Module): dim_context = None, causal = False, zero_init_output=True, - qk_norm = False, + qk_norm = "none", + differential = False, natten_kernel_size = None, + feat_scale = False, dtype=None, device=None, operations=None, @@ -271,6 +288,7 @@ class Attention(nn.Module): self.dim = dim self.dim_heads = dim_heads self.causal = causal + self.differential = differential dim_kv = dim_context if dim_context is not None else dim @@ -278,18 +296,37 @@ class Attention(nn.Module): self.kv_heads = dim_kv // dim_heads if dim_context is not None: - self.to_q = operations.Linear(dim, dim, bias=False, dtype=dtype, device=device) - self.to_kv = operations.Linear(dim_kv, dim_kv * 2, bias=False, dtype=dtype, device=device) + if differential: + self.to_q = operations.Linear(dim, dim * 2, bias=False, dtype=dtype, device=device) + self.to_kv = operations.Linear(dim_kv, dim_kv * 3, bias=False, dtype=dtype, device=device) + else: + self.to_q = operations.Linear(dim, dim, bias=False, dtype=dtype, device=device) + self.to_kv = operations.Linear(dim_kv, dim_kv * 2, bias=False, dtype=dtype, device=device) else: - self.to_qkv = operations.Linear(dim, dim * 3, bias=False, dtype=dtype, device=device) + if differential: + self.to_qkv = operations.Linear(dim, dim * 5, bias=False, dtype=dtype, device=device) + else: + self.to_qkv = operations.Linear(dim, dim * 3, bias=False, dtype=dtype, device=device) self.to_out = operations.Linear(dim, dim, bias=False, dtype=dtype, device=device) - # if zero_init_output: - # nn.init.zeros_(self.to_out.weight) - + # Accept bool for backward compat + if isinstance(qk_norm, bool): + qk_norm = "l2" if qk_norm else "none" self.qk_norm = qk_norm + if self.qk_norm == "ln": + self.q_norm = operations.LayerNorm(dim_heads, elementwise_affine=True, eps=1.0e-6, dtype=dtype, device=device) + self.k_norm = operations.LayerNorm(dim_heads, elementwise_affine=True, eps=1.0e-6, dtype=dtype, device=device) + elif self.qk_norm == "rms": + self.q_norm = RMSNorm(dim_heads, dtype=dtype, device=device) + self.k_norm = RMSNorm(dim_heads, dtype=dtype, device=device) + + self.feat_scale = feat_scale + + if self.feat_scale: + self.lambda_dc = nn.Parameter(torch.empty(dim, dtype=dtype, device=device)) + self.lambda_hf = nn.Parameter(torch.empty(dim, dtype=dtype, device=device)) def forward( self, @@ -306,22 +343,51 @@ class Attention(nn.Module): kv_input = context if has_context else x if hasattr(self, 'to_q'): - # Use separate linear projections for q and k/v - q = self.to_q(x) - q = rearrange(q, 'b n (h d) -> b h n d', h = h) + if self.differential: + # cross-attention differential: to_q → (q, q_diff), to_kv → (k, k_diff, v) + q, q_diff = self.to_q(x).chunk(2, dim=-1) + q = rearrange(q, 'b n (h d) -> b h n d', h=h) + q_diff = rearrange(q_diff, 'b n (h d) -> b h n d', h=h) + q = torch.stack([q, q_diff], dim=1) # (B, 2, H, N, D) + k, k_diff, v = self.to_kv(kv_input).chunk(3, dim=-1) + k = rearrange(k, 'b n (h d) -> b h n d', h=kv_h) + k_diff = rearrange(k_diff, 'b n (h d) -> b h n d', h=kv_h) + v = rearrange(v, 'b n (h d) -> b h n d', h=kv_h) + k = torch.stack([k, k_diff], dim=1) # (B, 2, H, M, D) + else: + # Use separate linear projections for q and k/v + q = self.to_q(x) + q = rearrange(q, 'b n (h d) -> b h n d', h = h) - k, v = self.to_kv(kv_input).chunk(2, dim=-1) + k, v = self.to_kv(kv_input).chunk(2, dim=-1) - k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = kv_h), (k, v)) + k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = kv_h), (k, v)) else: - # Use fused linear projection - q, k, v = self.to_qkv(x).chunk(3, dim=-1) - q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = h), (q, k, v)) + if self.differential: + # self-attention differential: to_qkv → (q, k, v, q_diff, k_diff) + q, k, v, q_diff, k_diff = self.to_qkv(x).chunk(5, dim=-1) + q, k, v, q_diff, k_diff = map( + lambda t: rearrange(t, 'b n (h d) -> b h n d', h=h), + (q, k, v, q_diff, k_diff) + ) + q = torch.stack([q, q_diff], dim=1) # (B, 2, H, N, D) + k = torch.stack([k, k_diff], dim=1) + else: + # Use fused linear projection + q, k, v = self.to_qkv(x).chunk(3, dim=-1) + q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = h), (q, k, v)) # Normalize q and k for cosine sim attention - if self.qk_norm: + if self.qk_norm == "l2": q = F.normalize(q, dim=-1) k = F.normalize(k, dim=-1) + elif self.qk_norm == "rms": + q_type, k_type = q.dtype, k.dtype + q = self.q_norm(q).to(q_type) + k = self.k_norm(k).to(k_type) + elif self.qk_norm != 'none': + q = self.q_norm(q) + k = self.k_norm(k) if rotary_pos_emb is not None and not has_context: freqs, _ = rotary_pos_emb @@ -364,9 +430,24 @@ class Attention(nn.Module): heads_per_kv_head = h // kv_h k, v = map(lambda t: t.repeat_interleave(heads_per_kv_head, dim = 1), (k, v)) - out = optimized_attention(q, k, v, h, skip_reshape=True, transformer_options=transformer_options) + if self.differential: + q, q_diff = q.unbind(dim=1) + k, k_diff = k.unbind(dim=1) + out = optimized_attention(q, k, v, h, skip_reshape=True, transformer_options=transformer_options) + out_diff = optimized_attention(q_diff, k_diff, v, h, skip_reshape=True, transformer_options=transformer_options) + out = out - out_diff + else: + out = optimized_attention(q, k, v, h, skip_reshape=True, transformer_options=transformer_options) + out = self.to_out(out) + if self.feat_scale: + out_dc = out.mean(dim=-2, keepdim=True) + out_hf = out - out_dc + + # Selectively modulate DC and high frequency components + out = out + comfy.ops.cast_to_input(self.lambda_dc, out) * out_dc + comfy.ops.cast_to_input(self.lambda_hf, out) * out_hf + if mask is not None: mask = rearrange(mask, 'b n -> b n 1') out = out.masked_fill(~mask, 0.) @@ -417,11 +498,14 @@ class TransformerBlock(nn.Module): cross_attend = False, dim_context = None, global_cond_dim = None, + global_cond_shared_embed = False, + local_add_cond_dim = None, causal = False, zero_init_branch_outputs = True, conformer = False, layer_ix = -1, remove_norms = False, + norm_type = "layer_norm", attn_kwargs = {}, ff_kwargs = {}, norm_kwargs = {}, @@ -436,8 +520,20 @@ class TransformerBlock(nn.Module): self.cross_attend = cross_attend self.dim_context = dim_context self.causal = causal + self.global_cond_shared_embed = global_cond_shared_embed - self.pre_norm = LayerNorm(dim, dtype=dtype, device=device, **norm_kwargs) if not remove_norms else nn.Identity() + norm_layer_map = { + "layer_norm": LayerNorm, + "rms_norm": RMSNorm, + } + norm_cls = norm_layer_map.get(norm_type, LayerNorm) + + def make_norm(): + if remove_norms: + return nn.Identity() + return norm_cls(dim, dtype=dtype, device=device, **norm_kwargs) + + self.pre_norm = make_norm() self.self_attn = Attention( dim, @@ -451,7 +547,7 @@ class TransformerBlock(nn.Module): ) if cross_attend: - self.cross_attend_norm = LayerNorm(dim, dtype=dtype, device=device, **norm_kwargs) if not remove_norms else nn.Identity() + self.cross_attend_norm = make_norm() self.cross_attn = Attention( dim, dim_heads = dim_heads, @@ -464,37 +560,56 @@ class TransformerBlock(nn.Module): **attn_kwargs ) - self.ff_norm = LayerNorm(dim, dtype=dtype, device=device, **norm_kwargs) if not remove_norms else nn.Identity() - self.ff = FeedForward(dim, zero_init_output=zero_init_branch_outputs, dtype=dtype, device=device, operations=operations,**ff_kwargs) + self.ff_norm = make_norm() + self.ff = FeedForward(dim, zero_init_output=zero_init_branch_outputs, dtype=dtype, device=device, operations=operations, **ff_kwargs) self.layer_ix = layer_ix self.conformer = ConformerModule(dim, norm_kwargs=norm_kwargs) if conformer else None - self.global_cond_dim = global_cond_dim + # Global conditioning + self.has_global_cond = (global_cond_dim is not None) or global_cond_shared_embed - if global_cond_dim is not None: + if global_cond_shared_embed: + # SA3 style: learnable per-block additive bias; global_cond is pre-projected to (B, dim*6) + self.to_scale_shift_gate = nn.Parameter(torch.empty(dim * 6, device=device, dtype=dtype)) + elif global_cond_dim is not None: + # SA1 style: per-block MLP projects global_cond → (B, dim*6) self.to_scale_shift_gate = nn.Sequential( nn.SiLU(), - nn.Linear(global_cond_dim, dim * 6, bias=False) + operations.Linear(global_cond_dim, dim * 6, bias=False, device=device, dtype=dtype) ) - nn.init.zeros_(self.to_scale_shift_gate[1].weight) - #nn.init.zeros_(self.to_scale_shift_gate_self[1].bias) + # Local additive conditioning (e.g. inpaint mask + masked latent) + self.local_add_cond_dim = local_add_cond_dim + if local_add_cond_dim is not None: + self.to_local_embed = nn.Sequential( + operations.Linear(local_add_cond_dim, dim, bias=True, dtype=dtype, device=device), + nn.SiLU(), + operations.Linear(dim, dim, bias=True, dtype=dtype, device=device), + ) + else: + self.to_local_embed = None def forward( self, x, context = None, global_cond=None, + local_add_cond=None, mask = None, context_mask = None, rotary_pos_emb = None, transformer_options={} ): - if self.global_cond_dim is not None and self.global_cond_dim > 0 and global_cond is not None: + if self.has_global_cond and global_cond is not None: + if self.global_cond_shared_embed: + # global_cond already has shape (B, dim*6) + ssg = (comfy.ops.cast_to_input(self.to_scale_shift_gate, global_cond) + global_cond).unsqueeze(1) + else: + ssg = self.to_scale_shift_gate(global_cond).unsqueeze(1) - scale_self, shift_self, gate_self, scale_ff, shift_ff, gate_ff = self.to_scale_shift_gate(global_cond).unsqueeze(1).chunk(6, dim = -1) + scale_self, shift_self, gate_self, scale_ff, shift_ff, gate_ff = ssg.chunk(6, dim = -1) # self-attention with adaLN residual = x @@ -510,6 +625,9 @@ class TransformerBlock(nn.Module): if self.conformer is not None: x = x + self.conformer(x) + if local_add_cond is not None and self.to_local_embed is not None: + x = x + _left_pad_to_match(self.to_local_embed(local_add_cond), x.shape[-2]) + # feedforward with adaLN residual = x x = self.ff_norm(x) @@ -527,6 +645,9 @@ class TransformerBlock(nn.Module): if self.conformer is not None: x = x + self.conformer(x) + if local_add_cond is not None and self.to_local_embed is not None: + x = x + _left_pad_to_match(self.to_local_embed(local_add_cond), x.shape[-2]) + x = x + self.ff(self.ff_norm(x)) return x @@ -543,6 +664,8 @@ class ContinuousTransformer(nn.Module): cross_attend=False, cond_token_dim=None, global_cond_dim=None, + global_cond_shared_embed=False, + local_add_cond_dim=None, causal=False, rotary_pos_emb=True, zero_init_branch_outputs=True, @@ -550,6 +673,7 @@ class ContinuousTransformer(nn.Module): use_sinusoidal_emb=False, use_abs_pos_emb=False, abs_pos_emb_max_length=10000, + num_memory_tokens=0, dtype=None, device=None, operations=None, @@ -562,6 +686,8 @@ class ContinuousTransformer(nn.Module): self.depth = depth self.causal = causal self.layers = nn.ModuleList([]) + self.num_memory_tokens = num_memory_tokens + self.global_cond_shared_embed = global_cond_shared_embed self.project_in = operations.Linear(dim_in, dim, bias=False, dtype=dtype, device=device) if dim_in is not None else nn.Identity() self.project_out = operations.Linear(dim, dim_out, bias=False, dtype=dtype, device=device) if dim_out is not None else nn.Identity() @@ -577,7 +703,22 @@ class ContinuousTransformer(nn.Module): self.use_abs_pos_emb = use_abs_pos_emb if use_abs_pos_emb: - self.pos_emb = AbsolutePositionalEmbedding(dim, abs_pos_emb_max_length) + self.pos_emb = AbsolutePositionalEmbedding(dim, abs_pos_emb_max_length + num_memory_tokens) + + if num_memory_tokens > 0: + self.memory_tokens = nn.Parameter(torch.empty(num_memory_tokens, dim, device=device, dtype=dtype)) + + # Shared global-cond embedder (SA3 style): projects (B, global_cond_dim) → (B, dim*6) + self.global_cond_embedder = None + if global_cond_shared_embed and global_cond_dim is not None: + self.global_cond_embedder = nn.Sequential( + operations.Linear(global_cond_dim, dim, bias=True, dtype=dtype, device=device), + nn.SiLU(), + operations.Linear(dim, dim * 6, bias=True, dtype=dtype, device=device), + ) + + # When using shared embed, TransformerBlocks use per-block Parameter (not per-block MLP) + block_global_cond_dim = None if global_cond_shared_embed else global_cond_dim for i in range(depth): self.layers.append( @@ -586,7 +727,9 @@ class ContinuousTransformer(nn.Module): dim_heads = dim_heads, cross_attend = cross_attend, dim_context = cond_token_dim, - global_cond_dim = global_cond_dim, + global_cond_dim = block_global_cond_dim, + global_cond_shared_embed = global_cond_shared_embed, + local_add_cond_dim = local_add_cond_dim, causal = causal, zero_init_branch_outputs = zero_init_branch_outputs, conformer=conformer, @@ -605,6 +748,7 @@ class ContinuousTransformer(nn.Module): prepend_embeds = None, prepend_mask = None, global_cond = None, + local_add_cond = None, return_info = False, **kwargs ): @@ -632,7 +776,9 @@ class ContinuousTransformer(nn.Module): mask = torch.cat((prepend_mask, mask), dim = -1) - # Attention layers + if self.num_memory_tokens > 0: + memory_tokens = comfy.ops.cast_to_input(self.memory_tokens, x).expand(batch, -1, -1) + x = torch.cat((memory_tokens, x), dim=1) if self.rotary_pos_emb is not None: rotary_pos_emb = self.rotary_pos_emb.forward_from_seq_len(x.shape[1], dtype=torch.float, device=x.device) @@ -642,6 +788,10 @@ class ContinuousTransformer(nn.Module): if self.use_sinusoidal_emb or self.use_abs_pos_emb: x = x + self.pos_emb(x) + # Project global_cond once (SA3 shared-embed path) + if global_cond is not None and self.global_cond_embedder is not None: + global_cond = self.global_cond_embedder(global_cond) + blocks_replace = patches_replace.get("dit", {}) # Iterate over the transformer layers for i, layer in enumerate(self.layers): @@ -654,12 +804,17 @@ class ContinuousTransformer(nn.Module): out = blocks_replace[("double_block", i)]({"img": x, "txt": context, "vec": global_cond, "pe": rotary_pos_emb, "transformer_options": transformer_options}, {"original_block": block_wrap}) x = out["img"] else: - x = layer(x, rotary_pos_emb = rotary_pos_emb, global_cond=global_cond, context=context, transformer_options=transformer_options) - # x = checkpoint(layer, x, rotary_pos_emb = rotary_pos_emb, global_cond=global_cond, **kwargs) + x = layer(x, rotary_pos_emb=rotary_pos_emb, global_cond=global_cond, + local_add_cond=local_add_cond, context=context, + transformer_options=transformer_options) if return_info: info["hidden_states"].append(x) + # Strip memory tokens before projecting out + if self.num_memory_tokens > 0: + x = x[:, self.num_memory_tokens:, :] + x = self.project_out(x) if return_info: @@ -682,6 +837,7 @@ class AudioDiffusionTransformer(nn.Module): num_heads=24, transformer_type: tp.Literal["continuous_transformer"] = "continuous_transformer", global_cond_type: tp.Literal["prepend", "adaLN"] = "prepend", + timestep_features_type: str = "learned", audio_model="", dtype=None, device=None, @@ -696,7 +852,10 @@ class AudioDiffusionTransformer(nn.Module): # Timestep embeddings timestep_features_dim = 256 - self.timestep_features = FourierFeatures(1, timestep_features_dim, dtype=dtype, device=device) + if timestep_features_type == "expo": + self.timestep_features = ExpoFourierFeatures(timestep_features_dim, 0.5, 10000.0) + else: + self.timestep_features = FourierFeatures(1, timestep_features_dim, dtype=dtype, device=device) self.to_timestep_embed = nn.Sequential( operations.Linear(timestep_features_dim, embed_dim, bias=True, dtype=dtype, device=device), @@ -781,6 +940,7 @@ class AudioDiffusionTransformer(nn.Module): cross_attn_cond=None, cross_attn_cond_mask=None, input_concat_cond=None, + local_add_cond=None, global_embed=None, prepend_cond=None, prepend_cond_mask=None, @@ -802,9 +962,13 @@ class AudioDiffusionTransformer(nn.Module): prepend_cond = self.to_prepend_embed(prepend_cond) prepend_inputs = prepend_cond + prepend_length = prepend_cond.shape[1] if prepend_cond_mask is not None: prepend_mask = prepend_cond_mask + if local_add_cond is not None and local_add_cond.dim() == 3: + local_add_cond = local_add_cond.permute(0, 2, 1) + if input_concat_cond is not None: # Interpolate input_concat_cond to the same length as x @@ -850,7 +1014,7 @@ class AudioDiffusionTransformer(nn.Module): if self.transformer_type == "x-transformers": output = self.transformer(x, prepend_embeds=prepend_inputs, context=cross_attn_cond, context_mask=cross_attn_cond_mask, mask=mask, prepend_mask=prepend_mask, **extra_args, **kwargs) elif self.transformer_type == "continuous_transformer": - output = self.transformer(x, prepend_embeds=prepend_inputs, context=cross_attn_cond, context_mask=cross_attn_cond_mask, mask=mask, prepend_mask=prepend_mask, return_info=return_info, **extra_args, **kwargs) + output = self.transformer(x, prepend_embeds=prepend_inputs, context=cross_attn_cond, context_mask=cross_attn_cond_mask, mask=mask, prepend_mask=prepend_mask, return_info=return_info, local_add_cond=local_add_cond, **extra_args, **kwargs) if return_info: output, info = output @@ -876,6 +1040,7 @@ class AudioDiffusionTransformer(nn.Module): context=None, context_mask=None, input_concat_cond=None, + local_add_cond=None, global_embed=None, negative_global_embed=None, prepend_cond=None, @@ -890,6 +1055,7 @@ class AudioDiffusionTransformer(nn.Module): cross_attn_cond=context, cross_attn_cond_mask=context_mask, input_concat_cond=input_concat_cond, + local_add_cond=local_add_cond, global_embed=global_embed, prepend_cond=prepend_cond, prepend_cond_mask=prepend_cond_mask, diff --git a/comfy/ldm/audio/embedders.py b/comfy/ldm/audio/embedders.py index 20edb365a..ba9a62837 100644 --- a/comfy/ldm/audio/embedders.py +++ b/comfy/ldm/audio/embedders.py @@ -31,15 +31,39 @@ def TimePositionalEmbedding(dim: int, out_features: int) -> nn.Module: ) +class ExpoFourierFeatures(nn.Module): + """Exponentially-spaced Fourier features (no learnable parameters).""" + def __init__(self, dim, min_freq=0.5, max_freq=10000.0): + super().__init__() + self.dim = dim + self.min_freq = min_freq + self.max_freq = max_freq + + def forward(self, t): + in_dtype = t.dtype + t = t.float() + if t.dim() == 1: + t = t.unsqueeze(-1) + half_dim = self.dim // 2 + ramp = torch.linspace(0, 1, half_dim, device=t.device, dtype=torch.float32) + freqs = torch.exp(ramp * (math.log(self.max_freq) - math.log(self.min_freq)) + math.log(self.min_freq)) + args = t * freqs * 2 * math.pi + return torch.cat([args.cos(), args.sin()], dim=-1).to(in_dtype) + + class NumberEmbedder(nn.Module): def __init__( self, features: int, dim: int = 256, + fourier_features_type="learned", ): super().__init__() self.features = features - self.embedding = TimePositionalEmbedding(dim=dim, out_features=features) + if fourier_features_type == "expo": + self.embedding = nn.Sequential(ExpoFourierFeatures(dim=dim), comfy.ops.manual_cast.Linear(in_features=dim, out_features=features)) + else: + self.embedding = TimePositionalEmbedding(dim=dim, out_features=features) def forward(self, x: Union[List[float], Tensor]) -> Tensor: if not torch.is_tensor(x): @@ -77,14 +101,15 @@ class NumberConditioner(Conditioner): def __init__(self, output_dim: int, min_val: float=0, - max_val: float=1 + max_val: float=1, + fourier_features_type: str = "learned", ): super().__init__(output_dim, output_dim) self.min_val = min_val self.max_val = max_val - self.embedder = NumberEmbedder(features=output_dim) + self.embedder = NumberEmbedder(features=output_dim, fourier_features_type=fourier_features_type) def forward(self, floats, device=None): # Cast the inputs to floats diff --git a/comfy/ldm/audio/vae_sa3.py b/comfy/ldm/audio/vae_sa3.py new file mode 100644 index 000000000..276846444 --- /dev/null +++ b/comfy/ldm/audio/vae_sa3.py @@ -0,0 +1,533 @@ +import torch +import torch.nn as nn + +import comfy.ops +import comfy.model_management +from comfy.ldm.modules.attention import optimized_attention +from comfy.ldm.audio.autoencoder import WNConv1d + +ops = comfy.ops.disable_weight_init + +class Transpose(nn.Module): + def forward(self, x, **kwargs): + return x.transpose(-2, -1) + + +def _zero_pad_modulo_sequence(x, size, dim=-2): + input_len = x.shape[dim] + pad_len = (size - input_len % size) % size + if pad_len > 0: + pad_shape = list(x.shape) + pad_shape[dim] = pad_len + x = torch.cat([x, torch.zeros(pad_shape, device=x.device, dtype=x.dtype)], dim=dim) + return x + + +def _sliding_window_mask(seq_len, window, device, dtype): + """Additive attention mask enforcing a ±window local window (matches flash_attn window_size).""" + i = torch.arange(seq_len, device=device).unsqueeze(1) + j = torch.arange(seq_len, device=device).unsqueeze(0) + out_of_window = (j - i).abs() > window + return torch.where( + out_of_window, + torch.full((1,), torch.finfo(dtype).min / 4, device=device, dtype=dtype), + torch.zeros(1, device=device, dtype=dtype), + ) + + +class DynamicTanh(nn.Module): + def __init__(self, dim, init_alpha=4.0, dtype=None, device=None, **kwargs): + super().__init__() + self.alpha = nn.Parameter(torch.empty(1, dtype=dtype, device=device)) + self.gamma = nn.Parameter(torch.empty(dim, dtype=dtype, device=device)) + self.beta = nn.Parameter(torch.empty(dim, dtype=dtype, device=device)) + + def forward(self, x): + alpha = comfy.ops.cast_to_input(self.alpha, x) + gamma = comfy.ops.cast_to_input(self.gamma, x) + beta = comfy.ops.cast_to_input(self.beta, x) + return gamma * torch.tanh(alpha * x) + beta + + +class RotaryEmbedding(nn.Module): + def __init__(self, dim, base=10000, base_rescale_factor=1., dtype=None, device=None): + super().__init__() + base = base * base_rescale_factor ** (dim / (dim - 2)) + self.register_buffer("inv_freq", torch.empty(dim // 2, dtype=dtype, device=device)) + + def forward_from_seq_len(self, seq_len, device, dtype=None): + t = torch.arange(seq_len, device=device, dtype=torch.float32) + return self.forward(t) + + def forward(self, t): + freqs = torch.outer(t.float(), comfy.model_management.cast_to(self.inv_freq, dtype=torch.float32, device=t.device)) + freqs = torch.cat((freqs, freqs), dim=-1) + return freqs, 1. + + +def _rotate_half(x): + d = x.shape[-1] // 2 + return torch.cat((-x[..., d:], x[..., :d]), dim=-1) + + +def _apply_rotary_pos_emb(t, freqs): + out_dtype = t.dtype + rot_dim = freqs.shape[-1] + seq_len = t.shape[-2] + freqs = freqs[-seq_len:] + t_rot, t_pass = t[..., :rot_dim], t[..., rot_dim:] + t_rot = t_rot * freqs.cos() + _rotate_half(t_rot) * freqs.sin() + return torch.cat((t_rot.to(out_dtype), t_pass.to(out_dtype)), dim=-1) + + +class Attention(nn.Module): + def __init__(self, dim, dim_heads=64, qk_norm="none", qk_norm_eps=1e-6, + differential=False, zero_init_output=True, + dtype=None, device=None, operations=None, **kwargs): + super().__init__() + self.num_heads = dim // dim_heads + self.differential = differential + self.qk_norm = qk_norm + + self.to_qkv = operations.Linear( + dim, dim * (5 if differential else 3), bias=False, dtype=dtype, device=device) + self.to_out = operations.Linear(dim, dim, bias=False, dtype=dtype, device=device) + + if qk_norm == "dyt": + self.q_norm = DynamicTanh(dim_heads, dtype=dtype, device=device) + self.k_norm = DynamicTanh(dim_heads, dtype=dtype, device=device) + elif qk_norm == "rms": + self.q_norm = operations.RMSNorm(dim_heads, eps=qk_norm_eps, dtype=dtype, device=device) + self.k_norm = operations.RMSNorm(dim_heads, eps=qk_norm_eps, dtype=dtype, device=device) + + def forward(self, x, rotary_pos_emb=None, mask=None, **kwargs): + B, N, _ = x.shape + h = self.num_heads + + qkv = self.to_qkv(x) + if self.differential: + q, k, v, q_diff, k_diff = qkv.chunk(5, dim=-1) + del qkv + q = q.view(B, N, h, -1).transpose(1, 2) + k = k.view(B, N, h, -1).transpose(1, 2) + v = v.view(B, N, h, -1).transpose(1, 2) + q_diff = q_diff.view(B, N, h, -1).transpose(1, 2) + k_diff = k_diff.view(B, N, h, -1).transpose(1, 2) + else: + q, k, v = qkv.chunk(3, dim=-1) + del qkv + q = q.view(B, N, h, -1).transpose(1, 2) + k = k.view(B, N, h, -1).transpose(1, 2) + v = v.view(B, N, h, -1).transpose(1, 2) + + if self.qk_norm != "none": + q_dtype, k_dtype = q.dtype, k.dtype + q = self.q_norm(q).to(q_dtype) + k = self.k_norm(k).to(k_dtype) + if self.differential: + q_diff = self.q_norm(q_diff).to(q_dtype) + k_diff = self.k_norm(k_diff).to(k_dtype) + + if rotary_pos_emb is not None: + freqs, _ = rotary_pos_emb + q_dtype, k_dtype = q.dtype, k.dtype + q = _apply_rotary_pos_emb(q.float(), freqs).to(q_dtype) + k = _apply_rotary_pos_emb(k.float(), freqs).to(k_dtype) + if self.differential: + q_diff = _apply_rotary_pos_emb(q_diff.float(), freqs).to(q_dtype) + k_diff = _apply_rotary_pos_emb(k_diff.float(), freqs).to(k_dtype) + + if self.differential: + out = (optimized_attention(q, k, v, h, mask=mask, skip_reshape=True) + - optimized_attention(q_diff, k_diff, v, h, mask=mask, skip_reshape=True)) + del q, k, v, q_diff, k_diff + else: + out = optimized_attention(q, k, v, h, mask=mask, skip_reshape=True) + del q, k, v + + return self.to_out(out) + + +class _Sin(nn.Module): + def forward(self, x): + return torch.sin(3.14159265359 * x) + + +class _GLU(nn.Module): + def __init__(self, dim_in, dim_out, activation, dtype=None, device=None, operations=None): + super().__init__() + self.act = activation + self.proj = operations.Linear(dim_in, dim_out * 2, dtype=dtype, device=device) + + def forward(self, x): + x = self.proj(x) + x, gate = x.chunk(2, dim=-1) + return x * self.act(gate) + + +class FeedForward(nn.Module): + def __init__(self, dim, mult=4, no_bias=False, zero_init_output=True, + sinusoidal=False, dtype=None, device=None, operations=None, **kwargs): + super().__init__() + inner_dim = int(dim * mult) + act = _Sin() if sinusoidal else nn.SiLU() + self.ff = nn.Sequential( + _GLU(dim, inner_dim, act, dtype=dtype, device=device, operations=operations), + nn.Identity(), + operations.Linear(inner_dim, dim, bias=not no_bias, dtype=dtype, device=device), + nn.Identity(), + ) + + def forward(self, x, **kwargs): + return self.ff(x) + + +class TransformerBlock(nn.Module): + def __init__(self, dim, dim_heads=64, causal=False, zero_init_branch_outputs=True, + norm_type="dyt", add_rope=False, attn_kwargs=None, ff_kwargs=None, + norm_kwargs=None, dtype=None, device=None, operations=None, **kwargs): + super().__init__() + if attn_kwargs is None: + attn_kwargs = {} + if ff_kwargs is None: + ff_kwargs = {} + if norm_kwargs is None: + norm_kwargs = {} + dim_heads = min(dim_heads, dim) + + Norm = DynamicTanh if norm_type == "dyt" else operations.RMSNorm + norm_kw = {**norm_kwargs, "dtype": dtype, "device": device} + + self.pre_norm = Norm(dim, **norm_kw) + self.self_attn = Attention(dim, dim_heads=dim_heads, + zero_init_output=zero_init_branch_outputs, + dtype=dtype, device=device, operations=operations, + **attn_kwargs) + self.ff_norm = Norm(dim, **norm_kw) + self.ff = FeedForward(dim, zero_init_output=zero_init_branch_outputs, + dtype=dtype, device=device, operations=operations, **ff_kwargs) + self.rope = RotaryEmbedding(dim_heads // 2, dtype=dtype, device=device) if add_rope else None + + def forward(self, x, mask=None, **kwargs): + rope = self.rope.forward_from_seq_len(x.shape[-2], device=x.device) \ + if self.rope is not None else None + x = x + self.self_attn(self.pre_norm(x), rotary_pos_emb=rope, mask=mask) + x = x + self.ff(self.ff_norm(x)) + return x + + +class TransformerResamplingBlock(nn.Module): + def __init__(self, in_channels, out_channels, stride, type="encoder", + transformer_depth=3, dim_heads=128, differential=True, + sliding_window=None, chunk_size=128, chunk_midpoint_shift=False, + dyt=True, ff_mult=3, mapping_bias=True, variable_stride=False, + sinusoidal_blocks=0, conv_mapping=False, dtype=None, device=None, operations=None, **kwargs): + super().__init__() + if type not in ("encoder", "decoder"): + raise ValueError(f"type must be 'encoder' or 'decoder', got {type!r}") + + self.type = type + self.stride = stride + self.chunk_size = chunk_size + self.chunk_midpoint_shift = chunk_midpoint_shift + self.variable_stride = variable_stride + self.transformer_depth = transformer_depth + + transformer_dim = out_channels if type == "encoder" else in_channels + + self.mapping = (WNConv1d(in_channels, out_channels, 3 if conv_mapping else 1, padding="same", bias=mapping_bias) + if in_channels != out_channels else nn.Identity()) + + self.sliding_window_latents = sliding_window + self.sliding_window_seq = self._get_sliding_window_size(sliding_window, stride) + self.input_seg_size, self.output_seg_size, self.sub_chunk_size = self._get_seg_sizes(stride) + + token_seq = 1 if variable_stride else self.output_seg_size + self.new_tokens = nn.Parameter(torch.empty(1, token_seq, transformer_dim, dtype=dtype, device=device)) + + norm_type = "dyt" if dyt else "rms_norm" + attn_kwargs = {"qk_norm": "dyt" if dyt else "rms", "qk_norm_eps": 1e-3, + "differential": differential} + norm_kwargs = {"eps": 1e-3} + transformers = [] + for i in range(transformer_depth): + sinusoidal = (transformer_depth - i) < sinusoidal_blocks + transformers.append(TransformerBlock( + transformer_dim, + dim_heads=dim_heads, + causal=False, + zero_init_branch_outputs=True, + norm_type=norm_type, + add_rope=True, + attn_kwargs=attn_kwargs, + ff_kwargs={"mult": ff_mult, "no_bias": False, "sinusoidal": sinusoidal}, + norm_kwargs=norm_kwargs, + dtype=dtype, device=device, operations=operations, + )) + self.transformers = nn.ModuleList(transformers) + + def _get_sliding_window_size(self, window, stride, prepend_cond_length=0): + if window is None: + return None + return [w * (stride + 1 + prepend_cond_length) for w in window] + + def _get_seg_sizes(self, stride, prepend_cond_length=0): + sub_chunk_size = stride + 1 + prepend_cond_length + input_seg_size = stride if self.type == "encoder" else 1 + output_seg_size = 1 if self.type == "encoder" else stride + return input_seg_size, output_seg_size, sub_chunk_size + + def forward(self, x, stride=None, **kwargs): + B = x.shape[0] + + if stride is None: + input_seg = self.input_seg_size + output_seg = self.output_seg_size + sub_chunk = self.sub_chunk_size + sliding_window = self.sliding_window_seq + else: + input_seg, output_seg, sub_chunk = self._get_seg_sizes(stride) + sliding_window = self._get_sliding_window_size(self.sliding_window_latents, stride) + + if self.type == "encoder": + if self.transformer_depth > 0: + pad_mod = self.chunk_size if sliding_window is None else input_seg + x = _zero_pad_modulo_sequence(x, pad_mod, dim=-1) + x = self.mapping(x) + + if self.transformer_depth > 0: + x = x.permute(0, 2, 1) + + if self.type != "encoder": + pad_mod = 1 if sliding_window is not None else ( + self.chunk_size // (stride if stride is not None else self.stride)) + x = _zero_pad_modulo_sequence(x, pad_mod) + + C = x.shape[2] + x = x.reshape(-1, input_seg, C) + + new_tokens = self.new_tokens.expand(x.shape[0], output_seg, -1) + x = torch.cat([x, comfy.ops.cast_to_input(new_tokens, x)], dim=-2) + del new_tokens + + x = x.reshape(B, -1, C) + + if sliding_window is None: + eff_chunk = self.chunk_size + self.chunk_size // (stride if stride is not None else self.stride) + + if sliding_window is None and self.chunk_midpoint_shift: + split = self.transformer_depth // 2 + shift = eff_chunk // 2 + + x = x.reshape(-1, eff_chunk, C) + for layer in self.transformers[:split]: + x = layer(x) + x = x.reshape(B, -1, C) + + shifted = torch.cat([x[:, :shift, :], x, x[:, -shift:, :]], dim=1) + del x + x = shifted.reshape(-1, eff_chunk, C) + del shifted + for layer in self.transformers[split:]: + x = layer(x) + x = x.reshape(B, -1, C) + x = x[:, shift:-shift, :] + elif sliding_window is None: + x = x.reshape(-1, eff_chunk, C) + for layer in self.transformers: + x = layer(x) + x = x.reshape(B, -1, C) + else: + attn_mask = _sliding_window_mask(x.shape[1], sliding_window[0], x.device, x.dtype) + for layer in self.transformers: + x = layer(x, mask=attn_mask) + + x = x.reshape(-1, sub_chunk, C) + x = x[:, -output_seg:, :] + x = x.reshape(B, -1, C).transpose(1, 2) + + if self.type == "decoder": + x = self.mapping(x) + + return x + + +class SAMEEncoder(nn.Module): + def __init__(self, in_channels=2, channels=128, latent_dim=32, + c_mults=(1, 2, 4, 8), strides=(2, 4, 8, 8), + transformer_depths=(3, 3, 3, 3), + dtype=None, device=None, operations=None, **kwargs): + super().__init__() + channel_dims = [in_channels] + [channels * c for c in c_mults] + layers = [] + for i in range(len(c_mults)): + layers.append(TransformerResamplingBlock( + in_channels=channel_dims[i], out_channels=channel_dims[i + 1], + stride=strides[i], type="encoder", + transformer_depth=transformer_depths[i], + dtype=dtype, device=device, operations=operations, **kwargs)) + layers += [ + Transpose(), + operations.Linear(channel_dims[-1], latent_dim, dtype=dtype, device=device), + Transpose(), + ] + self.layers = nn.ModuleList(layers) + + def forward(self, x, **kwargs): + for layer in self.layers: + x = layer(x) + return x + + +class SAMEDecoder(nn.Module): + def __init__(self, out_channels=2, channels=128, latent_dim=32, + c_mults=(1, 2, 4, 8), strides=(2, 4, 8, 8), + transformer_depths=(3, 3, 3, 3), sinusoidal_blocks=None, + dtype=None, device=None, operations=None, **kwargs): + super().__init__() + if sinusoidal_blocks is None: + sinusoidal_blocks = [0] * len(c_mults) + channel_dims = [out_channels] + [channels * c for c in c_mults] + layers = [ + Transpose(), + operations.Linear(latent_dim, channel_dims[-1], dtype=dtype, device=device), + Transpose(), + ] + for i in range(len(c_mults) - 1, -1, -1): + layers.append(TransformerResamplingBlock( + in_channels=channel_dims[i + 1], out_channels=channel_dims[i], + stride=strides[i], type="decoder", + transformer_depth=transformer_depths[i], + sinusoidal_blocks=sinusoidal_blocks[i], + dtype=dtype, device=device, operations=operations, **kwargs)) + self.layers = nn.ModuleList(layers) + + def forward(self, x, **kwargs): + for layer in self.layers: + x = layer(x) + return x + + +class SoftNormBottleneck(nn.Module): + def __init__(self, dim=32, noise_augment_dim=0, noise_regularize=False, + auto_scale=False, freeze=False, dtype=None, device=None, **kwargs): + super().__init__() + self.noise_augment_dim = noise_augment_dim + self.noise_regularize = noise_regularize + self.scaling_factor = nn.Parameter(torch.empty(1, dim, 1, dtype=dtype, device=device)) + self.bias = nn.Parameter(torch.empty(1, dim, 1, dtype=dtype, device=device)) + self.noise_scaling_factor = nn.Parameter(torch.empty(1, noise_augment_dim, 1, dtype=dtype, device=device)) + if auto_scale: + self.register_parameter("running_std", nn.Parameter( + torch.empty(1, dtype=dtype, device=device), requires_grad=False)) + if freeze: + for p in self.parameters(): + p.requires_grad = False + + def encode(self, x, return_info=False, **kwargs): + x = x * comfy.ops.cast_to_input(self.scaling_factor, x) \ + + comfy.ops.cast_to_input(self.bias, x) + if hasattr(self, "running_std"): + x = x / comfy.ops.cast_to_input(self.running_std, x) + if return_info: + return x, {} + return x + + def decode(self, x, **kwargs): + if hasattr(self, "running_std"): + x = x * comfy.ops.cast_to_input(self.running_std, x) + if self.noise_regularize: + scaling = self.running_std if hasattr(self, "running_std") \ + else x.std(dim=-1, keepdim=True) + noise = torch.randn_like(x) * comfy.ops.cast_to_input(scaling, x) * 1e-3 + x = x + noise + if self.noise_augment_dim > 0: + noise = comfy.ops.cast_to_input(self.noise_scaling_factor, x) * torch.randn( + x.shape[0], self.noise_augment_dim, x.shape[-1], device=x.device, dtype=x.dtype) + x = torch.cat([x, noise], dim=1) + return x + + +class PatchedPretransform(nn.Module): + def __init__(self, channels, patch_size, **kwargs): + super().__init__() + self.channels = channels + self.patch_size = patch_size + self.enable_grad = False + + def _pad(self, x): + pad_len = (self.patch_size - x.shape[-1] % self.patch_size) % self.patch_size + if pad_len > 0: + x = torch.cat([x, torch.zeros_like(x[:, :, :pad_len])], dim=-1) + return x + + def encode(self, x): + x = self._pad(x) + B, C, T = x.shape + h = self.patch_size + L = T // h + # b c (l h) -> b (c h) l + return x.reshape(B, C, L, h).permute(0, 1, 3, 2).reshape(B, C * h, L) + + def decode(self, x): + B, Ch, L = x.shape + h = self.patch_size + C = Ch // h + # b (c h) l -> b c (l h) + return x.reshape(B, C, h, L).permute(0, 1, 3, 2).reshape(B, C, L * h) + + +class SA3AudioVAE(nn.Module): + """SA3 VAE. State dict keys match checkpoint after stripping 'pretransform.model.'""" + + def __init__(self, channels=256, transformer_depths=12, sinusoidal_blocks=8, + sliding_window=None, decoder_conv_mapping=False, + chunk_size=128, chunk_midpoint_shift=False, + dtype=None, device=None, operations=None): + super().__init__() + if operations is None: + operations = ops + + self.pretransform = PatchedPretransform(channels=2, patch_size=256) + + common_kwargs = dict( + differential=True, dyt=True, dim_heads=64, + sliding_window=sliding_window, variable_stride=True, + chunk_size=chunk_size, chunk_midpoint_shift=chunk_midpoint_shift, + dtype=dtype, device=device, operations=operations, + ) + self.encoder = SAMEEncoder( + in_channels=512, channels=channels, c_mults=[6], strides=[16], + latent_dim=256, transformer_depths=[transformer_depths], + conv_mapping=False, **common_kwargs, + ) + self.decoder = SAMEDecoder( + out_channels=512, channels=channels, c_mults=[6], strides=[16], + latent_dim=256, transformer_depths=[transformer_depths], sinusoidal_blocks=[sinusoidal_blocks], + conv_mapping=decoder_conv_mapping, **common_kwargs, + ) + self.bottleneck = SoftNormBottleneck( + dim=256, noise_augment_dim=0, noise_regularize=True, + auto_scale=True, freeze=True, + dtype=dtype, device=device, + ) + + @torch.no_grad() + def _pretransform_encode(self, x): + return self.pretransform.encode(x) + + @torch.no_grad() + def _pretransform_decode(self, x): + return self.pretransform.decode(x) + + def encode(self, x): + x = self._pretransform_encode(x) + x = self.encoder(x) + x = self.bottleneck.encode(x) + return x + + def decode(self, x): + x = self.bottleneck.decode(x) + x = self.decoder(x) + x = self._pretransform_decode(x) + return x diff --git a/comfy/ldm/hidream_o1/attention.py b/comfy/ldm/hidream_o1/attention.py new file mode 100644 index 000000000..1b68f1771 --- /dev/null +++ b/comfy/ldm/hidream_o1/attention.py @@ -0,0 +1,41 @@ +"""HiDream-O1 two-pass attention: tokens [0, ar_len) are causal, [ar_len, T) +attend full K/V. Splitting Q at the boundary avoids the (B, 1, T, T) additive +mask the general-purpose path would build (~500 MB at T~16K) and lets the +gen half hit the user's preferred backend via optimized_attention. +""" + +import torch + +import comfy.ops +from comfy.ldm.modules.attention import optimized_attention + + +def make_two_pass_attention(ar_len: int, transformer_options=None): + """Build a two-pass attention callable. AR pass uses SDPA-causal directly, gen pass routes through optimized_attention. + The AR pass goes through SDPA directand bypasses wrappers, it is only ~1% of T at typical edit sizes. + """ + + def two_pass_attention(q, k, v, heads, **kwargs): + B, H, T, D = q.shape + + if T < k.shape[2]: # KV-cache hot path: Q is shorter than K/V (cached AR prefix is in K/V only), all fresh Q positions are in the gen region, single full-attention call + out = optimized_attention(q, k, v, heads, mask=None, skip_reshape=True, skip_output_reshape=True, transformer_options=transformer_options) + elif ar_len >= T: + out = comfy.ops.scaled_dot_product_attention(q, k, v, attn_mask=None, dropout_p=0.0, is_causal=True) + elif ar_len <= 0: + out = optimized_attention(q, k, v, heads, mask=None, skip_reshape=True, skip_output_reshape=True, transformer_options=transformer_options) + else: + out_ar = comfy.ops.scaled_dot_product_attention( + q[:, :, :ar_len], k[:, :, :ar_len], v[:, :, :ar_len], + attn_mask=None, dropout_p=0.0, is_causal=True, + ) + out_gen = optimized_attention( + q[:, :, ar_len:], k, v, heads, + mask=None, skip_reshape=True, skip_output_reshape=True, + transformer_options=transformer_options, + ) + out = torch.cat([out_ar, out_gen], dim=2) + + return out.transpose(1, 2).reshape(B, T, H * D) + + return two_pass_attention diff --git a/comfy/ldm/hidream_o1/conditioning.py b/comfy/ldm/hidream_o1/conditioning.py new file mode 100644 index 000000000..7496f0035 --- /dev/null +++ b/comfy/ldm/hidream_o1/conditioning.py @@ -0,0 +1,230 @@ +"""HiDream-O1 conditioning prep — ref-image dual path + extra_conds assembly. + +Each ref image goes through two paths: a 32x32 patchified stream concatenated +to the noised target, and a Qwen3-VL ViT path producing tokens that scatter +into input_ids at <|image_pad|> positions. +""" + +from typing import List + +import torch + +import comfy.utils +from comfy.text_encoders.qwen_vl import process_qwen2vl_images + +from .utils import (PATCH_SIZE, calculate_dimensions, cond_image_size, ref_max_size, resize_tensor) + +# Qwen3-VL ViT preprocessing constants (preprocessor_config.json). +VIT_PATCH = 16 +VIT_MERGE = 2 +VIT_IMAGE_MEAN = [0.5, 0.5, 0.5] +VIT_IMAGE_STD = [0.5, 0.5, 0.5] + + +def prepare_ref_images( + ref_images: List[torch.Tensor], + target_h: int, + target_w: int, + device: torch.device, + dtype: torch.dtype, +): + """Build the dual-path tensors for K reference images at (target_h, target_w). + + Returns None for K=0, else a dict with ref_patches, ref_pixel_values, + ref_image_grid_thw, per_ref_vit_tokens, per_ref_patch_grids. + """ + K = len(ref_images) + if K == 0: + return None + max_size = ref_max_size(max(target_h, target_w), K) + cis = cond_image_size(K) + + refs_t = [img[0].clamp(0, 1).permute(2, 0, 1).unsqueeze(0).contiguous().float() for img in ref_images] + refs_t = [resize_tensor(t, max_size, PATCH_SIZE) for t in refs_t] + + # 32-patch path. + ref_patches_per = [] + per_ref_patch_grids = [] + for t in refs_t: + t_norm = (t.squeeze(0) - 0.5) / 0.5 # (3, H, W) in [-1, 1] + h_p, w_p = t_norm.shape[-2] // PATCH_SIZE, t_norm.shape[-1] // PATCH_SIZE + per_ref_patch_grids.append((h_p, w_p)) + patches = ( + t_norm.reshape(3, h_p, PATCH_SIZE, w_p, PATCH_SIZE) + .permute(1, 3, 0, 2, 4) + .reshape(h_p * w_p, 3 * PATCH_SIZE * PATCH_SIZE) + ) + ref_patches_per.append(patches) + ref_patches = torch.cat(ref_patches_per, dim=0).unsqueeze(0).to(device=device, dtype=dtype) + + # ViT path. + refs_vlm_t = [] + for t in refs_t: + _, _, h, w = t.shape + cond_w, cond_h = calculate_dimensions(cis, w / h) + cond_w = max(cond_w, VIT_PATCH * VIT_MERGE) + cond_h = max(cond_h, VIT_PATCH * VIT_MERGE) + refs_vlm_t.append(comfy.utils.common_upscale(t, cond_w, cond_h, "lanczos", "disabled")) + + pv_list, grid_list, per_ref_vit_tokens = [], [], [] + for t_v in refs_vlm_t: + pv, grid_thw = process_qwen2vl_images( + t_v.permute(0, 2, 3, 1), + min_pixels=0, max_pixels=10**12, + patch_size=VIT_PATCH, merge_size=VIT_MERGE, + image_mean=VIT_IMAGE_MEAN, image_std=VIT_IMAGE_STD, + ) + grid_thw = grid_thw[0] + pv_list.append(pv.to(device=device, dtype=dtype)) + grid_list.append(grid_thw.to(device=device)) + # Post-merge token count = number of <|image_pad|> tokens this image expands to in input_ids. + gh, gw = int(grid_thw[1].item()), int(grid_thw[2].item()) + per_ref_vit_tokens.append((gh // VIT_MERGE) * (gw // VIT_MERGE)) + + return { + "ref_patches": ref_patches, + "ref_pixel_values": torch.cat(pv_list, dim=0), + "ref_image_grid_thw": torch.stack(grid_list, dim=0), + "per_ref_vit_tokens": per_ref_vit_tokens, + "per_ref_patch_grids": per_ref_patch_grids, + } + + +def build_ref_input_ids( + text_input_ids: torch.Tensor, + per_ref_vit_tokens: List[int], + image_token_id: int, + vision_start_id: int, + vision_end_id: int, +): + """Splice [vision_start, image_pad*N, vision_end] blocks into input_ids + after the [im_start, user, \\n] prefix (matches original chat template). + """ + ids = text_input_ids[0].tolist() + inserted = [] + for n_pad in per_ref_vit_tokens: + inserted.extend([vision_start_id] + [image_token_id] * n_pad + [vision_end_id]) + new_ids = ids[:3] + inserted + ids[3:] # 3 = len([im_start, user, \n]) + return torch.tensor([new_ids], dtype=text_input_ids.dtype, device=text_input_ids.device) + + +def build_extra_conds( + text_input_ids: torch.Tensor, + noise: torch.Tensor, + ref_images: List[torch.Tensor] = None, + target_patch_size: int = 32, +): + """Assemble all conditioning tensors for HiDreamO1Transformer.forward: + input_ids (with ref-vision tokens spliced in for the edit/IP path), + position_ids (MRoPE), token_types, vinput_mask, plus the ref + dual-path tensors when refs are provided. + """ + from .utils import get_rope_index_fix_point + from comfy.text_encoders.hidream_o1 import ( + IMAGE_TOKEN_ID, VISION_START_ID, VISION_END_ID, + ) + + if text_input_ids.dim() == 1: + text_input_ids = text_input_ids.unsqueeze(0) + text_input_ids = text_input_ids.long().to(noise.device) + B = noise.shape[0] + if text_input_ids.shape[0] == 1 and B > 1: + text_input_ids = text_input_ids.expand(B, -1) + + H, W = noise.shape[-2], noise.shape[-1] + h_p, w_p = H // target_patch_size, W // target_patch_size + image_len = h_p * w_p + image_grid_thw_tgt = torch.tensor( + [[1, h_p, w_p]], dtype=torch.long, device=text_input_ids.device, + ) + + out = {} + if ref_images: + ref = prepare_ref_images(ref_images, H, W, device=noise.device, dtype=noise.dtype) + text_input_ids = build_ref_input_ids( + text_input_ids, ref["per_ref_vit_tokens"], + IMAGE_TOKEN_ID, VISION_START_ID, VISION_END_ID, + ) + new_txt_len = text_input_ids.shape[1] + + # Each ref's patchified stream gets a [vision_start, image_pad*N-1] + # block in the position-id stream after the noised target. + ref_grid_lengths = [hp * wp for (hp, wp) in ref["per_ref_patch_grids"]] + tgt_vision = torch.full((1, image_len), IMAGE_TOKEN_ID, + dtype=text_input_ids.dtype, device=text_input_ids.device) + tgt_vision[:, 0] = VISION_START_ID + ref_vision_blocks = [] + for rl in ref_grid_lengths: + blk = torch.full((1, rl), IMAGE_TOKEN_ID, + dtype=text_input_ids.dtype, device=text_input_ids.device) + blk[:, 0] = VISION_START_ID + ref_vision_blocks.append(blk) + ref_vision_cat = torch.cat([tgt_vision] + ref_vision_blocks, dim=1) + input_ids_pad = torch.cat([text_input_ids, ref_vision_cat], dim=-1) + total_ref_patches_len = sum(ref_grid_lengths) + total_len = new_txt_len + image_len + total_ref_patches_len + + # K (ViT, post-merge) + 1 (target) + K (ref-patches) image grids. + K = len(ref_images) + igthw_cond = ref["ref_image_grid_thw"].clone() + igthw_cond[:, 1] //= 2 + igthw_cond[:, 2] //= 2 + image_grid_thw_ref = torch.tensor( + [[1, hp, wp] for (hp, wp) in ref["per_ref_patch_grids"]], + dtype=torch.long, device=text_input_ids.device, + ) + igthw_all = torch.cat([ + igthw_cond.to(text_input_ids.device), + image_grid_thw_tgt, + image_grid_thw_ref, + ], dim=0) + position_ids, _ = get_rope_index_fix_point( + spatial_merge_size=1, + image_token_id=IMAGE_TOKEN_ID, + vision_start_token_id=VISION_START_ID, + input_ids=input_ids_pad, image_grid_thw=igthw_all, + attention_mask=None, + skip_vision_start_token=[0] * K + [1] + [1] * K, + fix_point=4096, + ) + + # tms + target_image + ref_patches are all gen. + tms_pos = new_txt_len - 1 + ar_len = tms_pos + token_types = torch.zeros(B, total_len, dtype=torch.long, device=noise.device) + token_types[:, tms_pos:] = 1 + vinput_mask = torch.zeros(B, total_len, dtype=torch.bool, device=noise.device) + vinput_mask[:, new_txt_len:] = True + + # Leading batch dim sidesteps CONDRegular.process_cond's repeat_to_batch_size truncation + out["ref_pixel_values"] = ref["ref_pixel_values"].unsqueeze(0) + out["ref_image_grid_thw"] = ref["ref_image_grid_thw"].unsqueeze(0) + out["ref_patches"] = ref["ref_patches"] + else: + # T2I: text + noised target only, vision_start replaces the first image token + txt_len = text_input_ids.shape[1] + total_len = txt_len + image_len + vision_tokens = torch.full((B, image_len), IMAGE_TOKEN_ID, + dtype=text_input_ids.dtype, device=text_input_ids.device) + vision_tokens[:, 0] = VISION_START_ID + input_ids_pad = torch.cat([text_input_ids, vision_tokens], dim=-1) + position_ids, _ = get_rope_index_fix_point( + spatial_merge_size=1, + image_token_id=IMAGE_TOKEN_ID, + vision_start_token_id=VISION_START_ID, + input_ids=input_ids_pad, image_grid_thw=image_grid_thw_tgt, + attention_mask=None, + skip_vision_start_token=[1], + ) + ar_len = txt_len - 1 + token_types = torch.zeros(B, total_len, dtype=torch.long, device=noise.device) + token_types[:, ar_len:] = 1 + vinput_mask = torch.zeros(B, total_len, dtype=torch.bool, device=noise.device) + vinput_mask[:, txt_len:] = True + + out["input_ids"] = text_input_ids + out["position_ids"] = position_ids[:, 0].unsqueeze(0) # Collapse position_ids batch and add a leading dim so CONDRegular's batch-resize doesn't truncate the 3-axis MRoPE dim + out["token_types"] = token_types + out["vinput_mask"] = vinput_mask + out["ar_len"] = ar_len + return out diff --git a/comfy/ldm/hidream_o1/model.py b/comfy/ldm/hidream_o1/model.py new file mode 100644 index 000000000..a223e706f --- /dev/null +++ b/comfy/ldm/hidream_o1/model.py @@ -0,0 +1,306 @@ +"""HiDream-O1-Image transformer. + +Pixel-space DiT built on Qwen3-VL: the vision tower (Qwen35VisionModel) +encodes ref images, the Qwen3-VL-8B decoder (Llama2_ with interleaved MRoPE) +processes a unified text+image sequence, and 32x32 patch embed/unembed +shims map raw RGB in and out of LLM hidden space. The Qwen3-VL deepstack +mergers go unused — their weights are dropped at load. +""" + +from dataclasses import dataclass, field +from typing import List, Optional + +import einops +import torch +import torch.nn as nn + +import comfy.patcher_extension +from comfy.ldm.modules.diffusionmodules.mmdit import TimestepEmbedder +from comfy.text_encoders.llama import Llama2_ +from comfy.text_encoders.qwen35 import Qwen35VisionModel + +from .attention import make_two_pass_attention + + +IMAGE_TOKEN_ID = 151655 # Qwen3-VL <|image_pad|> +TMS_TOKEN_ID = 151673 # HiDream-O1 <|tms_token|> +PATCH_SIZE = 32 + + +@dataclass +class HiDreamO1TextConfig: + """Qwen3-VL-8B text-decoder dims (matches public Qwen3-VL-8B-Instruct).""" + vocab_size: int = 151936 + hidden_size: int = 4096 + intermediate_size: int = 12288 + num_hidden_layers: int = 36 + num_attention_heads: int = 32 + num_key_value_heads: int = 8 + head_dim: int = 128 + max_position_embeddings: int = 128000 + rms_norm_eps: float = 1e-6 + rope_theta: float = 5000000.0 + rope_scale: Optional[float] = None + rope_dims: List[int] = field(default_factory=lambda: [24, 20, 20]) + interleaved_mrope: bool = True + transformer_type: str = "llama" + rms_norm_add: bool = False + mlp_activation: str = "silu" + qkv_bias: bool = False + q_norm: str = "gemma3" + k_norm: str = "gemma3" + final_norm: bool = True + lm_head: bool = False + stop_tokens: List[int] = field(default_factory=lambda: [151643, 151645]) + + +QWEN3VL_VISION_DEFAULTS = dict( + hidden_size=1152, + num_heads=16, + intermediate_size=4304, + depth=27, + patch_size=16, + temporal_patch_size=2, + in_channels=3, + spatial_merge_size=2, + num_position_embeddings=2304, + deepstack_visual_indexes=(8, 16, 24), + out_hidden_size=4096, # final merger projects directly into LLM hidden +) + + +class BottleneckPatchEmbed(nn.Module): + # 3072 -> 1024 -> 4096 (raw 32x32 RGB patch -> bottleneck -> LLM hidden). + def __init__(self, patch_size=32, in_chans=3, pca_dim=1024, embed_dim=4096, bias=True, device=None, dtype=None, ops=None): + super().__init__() + self.proj1 = ops.Linear(patch_size * patch_size * in_chans, pca_dim, bias=False, device=device, dtype=dtype) + self.proj2 = ops.Linear(pca_dim, embed_dim, bias=bias, device=device, dtype=dtype) + + def forward(self, x): + return self.proj2(self.proj1(x)) + + +class FinalLayer(nn.Module): + # 4096 -> 3072 (LLM hidden -> flat pixel patch). + def __init__(self, hidden_size, patch_size=32, out_channels=3, device=None, dtype=None, ops=None): + super().__init__() + self.linear = ops.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True, device=device, dtype=dtype) + + def forward(self, x): + return self.linear(x) + + +class HiDreamO1Transformer(nn.Module): + """HiDream-O1 unified pixel-level transformer.""" + + def __init__(self, image_model=None, dtype=None, device=None, operations=None, + text_config_overrides=None, vision_config_overrides=None, **kwargs): + super().__init__() + self.dtype = dtype + + text_cfg = HiDreamO1TextConfig(**(text_config_overrides or {})) + vision_cfg = dict(QWEN3VL_VISION_DEFAULTS) + if vision_config_overrides: + vision_cfg.update(vision_config_overrides) + vision_cfg["out_hidden_size"] = text_cfg.hidden_size + + self.text_config = text_cfg + self.vision_config = vision_cfg + self.hidden_size = text_cfg.hidden_size + self.patch_size = PATCH_SIZE + self.in_channels = 3 + self.tms_token_id = TMS_TOKEN_ID + + self.visual = Qwen35VisionModel(vision_cfg, device=device, dtype=dtype, ops=operations) + self.language_model = Llama2_(text_cfg, device=device, dtype=dtype, ops=operations) + self.t_embedder1 = TimestepEmbedder( + text_cfg.hidden_size, device=device, dtype=dtype, operations=operations, + ) + self.x_embedder = BottleneckPatchEmbed( + patch_size=self.patch_size, in_chans=self.in_channels, + pca_dim=text_cfg.hidden_size // 4, embed_dim=text_cfg.hidden_size, + bias=True, device=device, dtype=dtype, ops=operations, + ) + self.final_layer2 = FinalLayer( + text_cfg.hidden_size, patch_size=self.patch_size, + out_channels=self.in_channels, device=device, dtype=dtype, ops=operations, + ) + + self._visual_cache = None + self._kv_cache_entries = [] + + def clear_kv_cache(self): + self._kv_cache_entries = [] + self._visual_cache = None + + def forward(self, x, timesteps, context=None, transformer_options={}, **kwargs): + return comfy.patcher_extension.WrapperExecutor.new_class_executor( + self._forward, + self, + comfy.patcher_extension.get_all_wrappers(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, transformer_options) + ).execute(x, timesteps, context, transformer_options, **kwargs) + + def _forward(self, x, timesteps, context=None, transformer_options={}, input_ids=None, attention_mask=None, position_ids=None, + vinput_mask=None, ar_len=None, ref_pixel_values=None, ref_image_grid_thw=None, ref_patches=None, **kwargs): + """Returns flow-match velocity (x - x_pred) / sigma""" + + if input_ids is None or position_ids is None: + raise ValueError("HiDreamO1Transformer requires input_ids and position_ids in conditioning") + + B, _, H, W = x.shape + h_p, w_p = H // self.patch_size, W // self.patch_size + tgt_image_len = h_p * w_p + + z = einops.rearrange( + x, 'B C (H p1) (W p2) -> B (H W) (C p1 p2)', + p1=self.patch_size, p2=self.patch_size, + ) + vinputs = torch.cat([z, ref_patches.to(z.dtype)], dim=1) if ref_patches is not None else z + + inputs_embeds = self.language_model.embed_tokens(input_ids).to(x.dtype) + + if ref_pixel_values is not None and ref_image_grid_thw is not None: + # ViT output is constant across sampling steps within a generation + # identity-key by the input tensor so refs don't recompute every step. + cached = self._visual_cache + if cached is not None and cached[0] is ref_pixel_values: + image_embeds = cached[1] + else: + ref_pv = ref_pixel_values.to(inputs_embeds.device) + ref_grid = ref_image_grid_thw.to(inputs_embeds.device).long() + # extra_conds wraps with a leading batch dim; refs are model-level so [0] always recovers them. + if ref_pv.dim() == 3: + ref_pv = ref_pv[0] + if ref_grid.dim() == 3: + ref_grid = ref_grid[0] + image_embeds = self.visual(ref_pv, ref_grid).to(inputs_embeds.dtype) + self._visual_cache = (ref_pixel_values, image_embeds) + # image_pad positions identical across batch (input_ids shared cond/uncond). + image_idx = (input_ids[0] == IMAGE_TOKEN_ID).nonzero(as_tuple=True)[0] + if image_idx.shape[0] != image_embeds.shape[0]: + raise ValueError( + f"Image-token count {image_idx.shape[0]} != ViT output count " + f"{image_embeds.shape[0]}; check tokenizer/processor alignment." + ) + inputs_embeds[:, image_idx] = image_embeds.unsqueeze(0).expand(B, -1, -1) + + sigma = timesteps.float() / 1000.0 + t_pixeldit = 1.0 - sigma + t_emb = self.t_embedder1(t_pixeldit * 1000, inputs_embeds.dtype) + tms_mask_3d = (input_ids == self.tms_token_id).unsqueeze(-1).expand_as(inputs_embeds) + inputs_embeds = torch.where(tms_mask_3d, t_emb.unsqueeze(1).expand_as(inputs_embeds), inputs_embeds) + + vinputs_embedded = self.x_embedder(vinputs.to(inputs_embeds.dtype)) + inputs_embeds = torch.cat([inputs_embeds, vinputs_embedded], dim=1) + + # extra_conds stores position_ids as (1, 3, T); process_cond repeats dim 0 to B. Take row 0. + freqs_cis = self.language_model.compute_freqs_cis(position_ids[0].to(x.device), x.device) + freqs_cis = tuple(t.to(x.dtype) for t in freqs_cis) + + two_pass_attn = make_two_pass_attention(ar_len, transformer_options=transformer_options) + patches_replace = transformer_options.get("patches_replace", {}) + blocks_replace = patches_replace.get("dit", {}) + transformer_options["total_blocks"] = len(self.language_model.layers) + transformer_options["block_type"] = "double" + + # Cache prefix K/V across steps. Key includes input_ids (prompt), ref_id + # (refs scatter into inputs_embeds), and position_ids (RoPE baked into cached K). + can_cache = not blocks_replace and ar_len > 0 + cache_len = ar_len if can_cache else 0 + ref_id = id(ref_pixel_values) if ref_pixel_values is not None else None + pos_ids_key = position_ids[..., :cache_len] if can_cache else position_ids + cache_entries = self._kv_cache_entries + # Drop stale entries from a previous device (model was unloaded and reloaded). + if cache_entries and cache_entries[0]["input_ids"].device != input_ids.device: + cache_entries = [] + self._kv_cache_entries = [] + kv_cache = None + if can_cache: + for entry in cache_entries: + ck = entry["input_ids"] + ep = entry["position_ids"] + if (entry["cache_len"] == cache_len + and ck.shape == input_ids.shape and torch.equal(ck, input_ids) + and entry["ref_id"] == ref_id + and ep.shape == pos_ids_key.shape and torch.equal(ep, pos_ids_key)): + kv_cache = entry + break + + if kv_cache is not None: + # Hot path: project Q/K/V only for fresh positions; past_key_value prepends cached AR K/V. + hidden_states = inputs_embeds[:, cache_len:] + sliced_freqs = tuple(t[..., cache_len:, :] for t in freqs_cis) + for i, layer in enumerate(self.language_model.layers): + transformer_options["block_index"] = i + K_i, V_i = kv_cache["kv"][i] + hidden_states, _ = layer( + x=hidden_states, attention_mask=None, freqs_cis=sliced_freqs, optimized_attention=two_pass_attn, + past_key_value=(K_i, V_i, cache_len), + ) + else: + # Cold path: run full sequence; if cacheable, snapshot K/V at AR positions. + snapshots = [] if can_cache else None + past_kv_cold = () if can_cache else None + hidden_states = inputs_embeds + for i, layer in enumerate(self.language_model.layers): + transformer_options["block_index"] = i + if ("double_block", i) in blocks_replace: + def block_wrap(args, _layer=layer): + out = {} + out["x"], _ = _layer( + x=args["x"], attention_mask=args.get("attention_mask"), + freqs_cis=args["freqs_cis"], optimized_attention=args["optimized_attention"], + past_key_value=None, + ) + return out + out = blocks_replace[("double_block", i)]( + {"x": hidden_states, "attention_mask": None, + "freqs_cis": freqs_cis, "optimized_attention": two_pass_attn, + "transformer_options": transformer_options}, + {"original_block": block_wrap}, + ) + hidden_states = out["x"] + else: + hidden_states, present_kv = layer( + x=hidden_states, attention_mask=None, + freqs_cis=freqs_cis, optimized_attention=two_pass_attn, + past_key_value=past_kv_cold, + ) + if snapshots is not None: + K, V, _ = present_kv + snapshots.append((K[:, :, :cache_len].contiguous(), + V[:, :, :cache_len].contiguous())) + if snapshots is not None: + # Cap at 2 entries (cond + uncond). Multi-cond workflows LRU-evict. + new_entry = { + "input_ids": input_ids.clone(), + "cache_len": cache_len, + "kv": snapshots, + "ref_id": ref_id, + "position_ids": pos_ids_key.clone(), + } + self._kv_cache_entries = (cache_entries + [new_entry])[-2:] + + if self.language_model.norm is not None: + hidden_states = self.language_model.norm(hidden_states) + + # Slice target-image positions before the final projection so the Linear only runs on tgt_image_len tokens. + # In the hot path hidden_states starts at original position cache_len, so masks/indices shift by cache_len. + sliced_offset = cache_len if kv_cache is not None else 0 + if vinput_mask is not None: + vmask = vinput_mask.to(x.device).bool() + if sliced_offset > 0: + vmask = vmask[:, sliced_offset:] + target_hidden = hidden_states[vmask].view(B, -1, hidden_states.shape[-1])[:, :tgt_image_len] + else: + txt_seq_len = input_ids.shape[1] + start = txt_seq_len - sliced_offset + target_hidden = hidden_states[:, start:start + tgt_image_len] + x_pred_tgt = self.final_layer2(target_hidden) + + # fp32 final subtraction, bf16 here noticeably degrades samples. + x_pred_img = einops.rearrange( + x_pred_tgt, 'B (H W) (C p1 p2) -> B C (H p1) (W p2)', + H=h_p, W=w_p, p1=self.patch_size, p2=self.patch_size, + ) + return (x.float() - x_pred_img.float()) / sigma.view(B, 1, 1, 1).clamp_min(1e-3) diff --git a/comfy/ldm/hidream_o1/utils.py b/comfy/ldm/hidream_o1/utils.py new file mode 100644 index 000000000..5a1249c72 --- /dev/null +++ b/comfy/ldm/hidream_o1/utils.py @@ -0,0 +1,173 @@ +"""HiDream-O1 input-prep helpers: image/resolution math and unified-sequence +RoPE position-id assembly. The fix_point offset in get_rope_index_fix_point +lets the target image and patchified ref images share spatial RoPE positions +despite living at different sequence indices — same 2D image plane. +""" + +import math +from typing import Optional + +import torch + + +PATCH_SIZE = 32 +CONDITION_IMAGE_SIZE = 384 # ViT-side base size for ref images + + +def resize_tensor(img_t, image_size, patch_size=16): + """img_t: (1, 3, H, W) float [0, 1]. Fit to image_size**2 area, patch-aligned, center-cropped.""" + + while min(img_t.shape[-2], img_t.shape[-1]) >= 2 * image_size: # Pre-halves with 2x2 box averaging while the image is still very large + img_t = torch.nn.functional.avg_pool2d(img_t, kernel_size=2, stride=2) + + _, _, height, width = img_t.shape + m = patch_size + s_max = image_size * image_size + scale = math.sqrt(s_max / (width * height)) + + candidates = [ + (round(width * scale) // m * m, round(height * scale) // m * m), + (round(width * scale) // m * m, math.floor(height * scale) // m * m), + (math.floor(width * scale) // m * m, round(height * scale) // m * m), + (math.floor(width * scale) // m * m, math.floor(height * scale) // m * m), + ] + candidates = sorted(candidates, key=lambda x: x[0] * x[1], reverse=True) + new_size = candidates[-1] + for c in candidates: + if c[0] * c[1] <= s_max: + new_size = c + break + + new_w, new_h = new_size + s1 = width / new_w + s2 = height / new_h + if s1 < s2: + resize_w, resize_h = new_w, round(height / s1) + else: + resize_w, resize_h = round(width / s2), new_h + img_t = torch.nn.functional.interpolate(img_t, size=(resize_h, resize_w), mode="bicubic") + top = (resize_h - new_h) // 2 + left = (resize_w - new_w) // 2 + return img_t[..., top:top + new_h, left:left + new_w] + + +def calculate_dimensions(max_size, ratio): + """(W, H) for an aspect ratio fitting in max_size**2 area, 32-aligned.""" + width = math.sqrt(max_size * max_size * ratio) + height = width / ratio + width = int(width / 32) * 32 + height = int(height / 32) * 32 + return width, height + + +def ref_max_size(target_max_dim, k): + """K-dependent ref-image max dim before patchifying.""" + if k == 1: + return target_max_dim + if k == 2: + return target_max_dim * 48 // 64 + if k <= 4: + return target_max_dim // 2 + if k <= 8: + return target_max_dim * 24 // 64 + return target_max_dim // 4 + + +def cond_image_size(k): + """K-dependent ViT-side image size.""" + if k <= 4: + return CONDITION_IMAGE_SIZE + if k <= 8: + return CONDITION_IMAGE_SIZE * 48 // 64 + return CONDITION_IMAGE_SIZE // 2 + + +def get_rope_index_fix_point( + spatial_merge_size: int, + image_token_id: int, + vision_start_token_id: int, + input_ids: Optional[torch.LongTensor] = None, + image_grid_thw: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + skip_vision_start_token=None, + fix_point: int = 4096, +): + mrope_position_deltas = [] + if input_ids is not None and image_grid_thw is not None: + total_input_ids = input_ids + if attention_mask is None: + attention_mask = torch.ones_like(total_input_ids) + position_ids = torch.ones( + 3, input_ids.shape[0], input_ids.shape[1], + dtype=input_ids.dtype, device=input_ids.device, + ) + attention_mask = attention_mask.to(total_input_ids.device) + for i, input_ids_b in enumerate(total_input_ids): + fp = fix_point + image_index = 0 + input_ids_b = input_ids_b[attention_mask[i] == 1] + vision_start_indices = torch.argwhere(input_ids_b == vision_start_token_id).squeeze(1) + vision_tokens = input_ids_b[vision_start_indices + 1] + image_nums = (vision_tokens == image_token_id).sum() + input_tokens = input_ids_b.tolist() + llm_pos_ids_list = [] + st = 0 + remain_images = image_nums + for _ in range(image_nums): + if image_token_id in input_tokens and remain_images > 0: + ed = input_tokens.index(image_token_id, st) + else: + ed = len(input_tokens) + 1 + t = image_grid_thw[image_index][0] + h = image_grid_thw[image_index][1] + w = image_grid_thw[image_index][2] + image_index += 1 + remain_images -= 1 + llm_grid_t = t.item() + llm_grid_h = h.item() // spatial_merge_size + llm_grid_w = w.item() // spatial_merge_size + text_len = ed - st + text_len -= skip_vision_start_token[image_index - 1] + text_len = max(0, text_len) + st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 + llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx) + + t_index = torch.arange(llm_grid_t).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w).flatten() + h_index = torch.arange(llm_grid_h).view(1, -1, 1).expand(llm_grid_t, -1, llm_grid_w).flatten() + w_index = torch.arange(llm_grid_w).view(1, 1, -1).expand(llm_grid_t, llm_grid_h, -1).flatten() + + if skip_vision_start_token[image_index - 1]: + if fp > 0: + fp = fp - st_idx + llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + fp + st_idx) + fp = 0 + else: + llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + text_len + st_idx) + st = ed + llm_grid_t * llm_grid_h * llm_grid_w + + if st < len(input_tokens): + st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 + text_len = len(input_tokens) - st + llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx) + + llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1) + position_ids[..., i, attention_mask[i] == 1] = llm_positions.to(position_ids.device) + mrope_position_deltas.append(llm_positions.max() + 1 - len(total_input_ids[i])) + mrope_position_deltas = torch.tensor(mrope_position_deltas, device=input_ids.device).unsqueeze(1) + return position_ids, mrope_position_deltas + + if attention_mask is not None: + position_ids = attention_mask.long().cumsum(-1) - 1 + position_ids.masked_fill_(attention_mask == 0, 1) + position_ids = position_ids.unsqueeze(0).expand(3, -1, -1).to(attention_mask.device) + max_position_ids = position_ids.max(0, keepdim=False)[0].max(-1, keepdim=True)[0] + mrope_position_deltas = max_position_ids + 1 - attention_mask.shape[-1] + else: + position_ids = ( + torch.arange(input_ids.shape[1], device=input_ids.device) + .view(1, 1, -1).expand(3, input_ids.shape[0], -1) + ) + mrope_position_deltas = torch.zeros( + [input_ids.shape[0], 1], device=input_ids.device, dtype=input_ids.dtype, + ) + return position_ids, mrope_position_deltas diff --git a/comfy/ldm/hunyuan3dv2_1/hunyuandit.py b/comfy/ldm/hunyuan3dv2_1/hunyuandit.py index f67ba84e9..4e4819fe3 100644 --- a/comfy/ldm/hunyuan3dv2_1/hunyuandit.py +++ b/comfy/ldm/hunyuan3dv2_1/hunyuandit.py @@ -328,7 +328,7 @@ class CrossAttention(nn.Module): kv = torch.cat((k, v), dim=-1) split_size = kv.shape[-1] // self.num_heads // 2 - kv = kv.view(1, -1, self.num_heads, split_size * 2) + kv = kv.view(b, -1, self.num_heads, split_size * 2) k, v = torch.split(kv, split_size, dim=-1) q = q.view(b, s1, self.num_heads, self.head_dim) @@ -398,7 +398,7 @@ class Attention(nn.Module): qkv_combined = torch.cat((query, key, value), dim=-1) split_size = qkv_combined.shape[-1] // self.num_heads // 3 - qkv = qkv_combined.view(1, -1, self.num_heads, split_size * 3) + qkv = qkv_combined.view(B, -1, self.num_heads, split_size * 3) query, key, value = torch.split(qkv, split_size, dim=-1) query = query.reshape(B, N, self.num_heads, self.head_dim) @@ -607,9 +607,13 @@ class HunYuanDiTPlain(nn.Module): def forward(self, x, t, context, transformer_options = {}, **kwargs): x = x.movedim(-1, -2) - uncond_emb, cond_emb = context.chunk(2, dim = 0) - context = torch.cat([cond_emb, uncond_emb], dim = 0) + swap_cfg_halves = context.shape[0] >= 2 + + if swap_cfg_halves: + first_half, second_half = context.chunk(2, dim = 0) + context = torch.cat([second_half, first_half], dim = 0) + main_condition = context t = 1.0 - t @@ -657,5 +661,8 @@ class HunYuanDiTPlain(nn.Module): output = self.final_layer(combined) output = output.movedim(-2, -1) * (-1.0) - cond_emb, uncond_emb = output.chunk(2, dim = 0) - return torch.cat([uncond_emb, cond_emb]) + if swap_cfg_halves: + first_half, second_half = output.chunk(2, dim = 0) + output = torch.cat([second_half, first_half], dim = 0) + + return output diff --git a/comfy/ldm/lens/model.py b/comfy/ldm/lens/model.py new file mode 100644 index 000000000..cd5015ddc --- /dev/null +++ b/comfy/ldm/lens/model.py @@ -0,0 +1,510 @@ +"""Lens denoising transformer (DiT)""" + +from __future__ import annotations + +from typing import Any, Dict, Optional, Tuple + +import torch +import torch.nn as nn +import torch.nn.functional as F + +import comfy.ldm.flux.layers +import comfy.patcher_extension +from comfy.ldm.flux.layers import EmbedND +from comfy.ldm.flux.math import apply_rope +from comfy.ldm.modules.attention import optimized_attention + + +def _lens_time_proj(t: torch.Tensor, dim: int = 256) -> torch.Tensor: + return comfy.ldm.flux.layers.timestep_embedding(t, dim) + + +def _lens_position_ids( + frame: int, height: int, width: int, text_seq_len: int, + scale_rope: bool = True, device=None, +) -> torch.Tensor: + """Lens axial (frame, h, w) position ids for joint image + text sequence. + + With ``scale_rope=True`` h/w are centered around 0 (negative + positive + halves) and text starts at ``max(h//2, w//2)``. Result shape ``[seq, 3]``; + caller adds a batch dim for ``EmbedND``. + """ + if scale_rope: + h_pos = torch.cat([torch.arange(-(height - height // 2), 0, device=device), + torch.arange(0, height // 2, device=device)]) + w_pos = torch.cat([torch.arange(-(width - width // 2), 0, device=device), + torch.arange(0, width // 2, device=device)]) + text_start = max(height // 2, width // 2) + else: + h_pos = torch.arange(height, device=device) + w_pos = torch.arange(width, device=device) + text_start = max(height, width) + + f_pos = torch.arange(frame, device=device) + img_ids = torch.zeros(frame, height, width, 3, device=device) + img_ids[..., 0] = f_pos[:, None, None] + img_ids[..., 1] = h_pos[None, :, None] + img_ids[..., 2] = w_pos[None, None, :] + img_ids = img_ids.reshape(-1, 3) + + # Text positions replicate across all 3 axes (matches original packing). + txt_pos = torch.arange(text_start, text_start + text_seq_len, device=device).float() + txt_ids = txt_pos[:, None].expand(text_seq_len, 3) + + return torch.cat([img_ids, txt_ids], dim=0) + + +class _TimestepEmbedder(nn.Module): + def __init__(self, in_channels: int, time_embed_dim: int, dtype=None, device=None, operations=None) -> None: + super().__init__() + self.linear_1 = operations.Linear(in_channels, time_embed_dim, dtype=dtype, device=device) + self.linear_2 = operations.Linear(time_embed_dim, time_embed_dim, dtype=dtype, device=device) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.linear_1(x) + x = F.silu(x) + return self.linear_2(x) + + +class LensTimestepProjEmbeddings(nn.Module): + def __init__(self, embedding_dim: int, dtype=None, device=None, operations=None) -> None: + super().__init__() + self.timestep_embedder = _TimestepEmbedder(256, embedding_dim, dtype=dtype, device=device, operations=operations) + + def forward(self, timestep: torch.Tensor, hidden_states: torch.Tensor) -> torch.Tensor: + proj = _lens_time_proj(timestep, 256) + return self.timestep_embedder(proj.to(dtype=hidden_states.dtype)) + + +class GateMLP(nn.Module): + """SwiGLU MLP.""" + + def __init__(self, dim: int, hidden_dim: int, dtype=None, device=None, operations=None) -> None: + super().__init__() + self.w1 = operations.Linear(dim, hidden_dim, bias=False, dtype=dtype, device=device) + self.w2 = operations.Linear(hidden_dim, dim, bias=False, dtype=dtype, device=device) + self.w3 = operations.Linear(dim, hidden_dim, bias=False, dtype=dtype, device=device) + + def forward(self, x): + return self.w2(F.silu(self.w1(x), inplace=True).mul_(self.w3(x))) + + +class LensJointAttention(nn.Module): + """Joint image+text attention with fused QKV per stream.""" + + def __init__( + self, + query_dim: int, + added_kv_proj_dim: int, + dim_head: int = 64, + heads: int = 8, + out_dim: Optional[int] = None, + eps: float = 1e-5, + dtype=None, + device=None, + operations=None, + ) -> None: + super().__init__() + self.inner_dim = out_dim if out_dim is not None else dim_head * heads + self.heads = self.inner_dim // dim_head + self.dim_head = dim_head + self.out_dim = out_dim if out_dim is not None else query_dim + + self.norm_q = operations.RMSNorm(dim_head, eps=eps, dtype=dtype, device=device) + self.norm_k = operations.RMSNorm(dim_head, eps=eps, dtype=dtype, device=device) + self.norm_added_q = operations.RMSNorm(dim_head, eps=eps, dtype=dtype, device=device) + self.norm_added_k = operations.RMSNorm(dim_head, eps=eps, dtype=dtype, device=device) + + self.img_qkv = operations.Linear(query_dim, 3 * self.inner_dim, bias=True, dtype=dtype, device=device) + self.txt_qkv = operations.Linear(added_kv_proj_dim, 3 * self.inner_dim, bias=True, dtype=dtype, device=device) + + # ModuleList([Linear, Identity]) for state-dict key compatibility. + self.to_out = nn.ModuleList([ + operations.Linear(self.inner_dim, self.out_dim, bias=True, dtype=dtype, device=device), + nn.Identity(), + ]) + self.to_add_out = operations.Linear(self.inner_dim, query_dim, bias=True, dtype=dtype, device=device) + + def forward( + self, + hidden_states: torch.Tensor, + encoder_hidden_states: torch.Tensor, + freqs_cis: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + transformer_options: Optional[Dict[str, Any]] = None, + ) -> Tuple[torch.Tensor, torch.Tensor]: + bsz, seq_img, _ = hidden_states.shape + seq_txt = encoder_hidden_states.shape[1] + + # image stream + img_qkv = self.img_qkv(hidden_states).view(bsz, seq_img, 3, self.heads, self.dim_head) + img_q, img_k, img_v = img_qkv.unbind(dim=2) + img_q = self.norm_q(img_q) + img_k = self.norm_k(img_k) + del img_qkv + + # text stream + txt_qkv = self.txt_qkv(encoder_hidden_states).view(bsz, seq_txt, 3, self.heads, self.dim_head) + txt_q, txt_k, txt_v = txt_qkv.unbind(dim=2) + txt_q = self.norm_added_q(txt_q) + txt_k = self.norm_added_k(txt_k) + + # [B, S, H, D] → [B, H, S, D] for attention, dels to avoid VRAM peaks + q = torch.cat([img_q, txt_q], dim=1).transpose(1, 2) + del img_q, txt_q + k = torch.cat([img_k, txt_k], dim=1).transpose(1, 2) + del img_k, txt_k + v = torch.cat([img_v, txt_v], dim=1).transpose(1, 2) + del img_v, txt_v + + q, k = apply_rope(q, k, freqs_cis) + + if attention_mask is not None: + expected = (bsz, 1, 1, seq_img + seq_txt) + if attention_mask.shape != expected: + raise ValueError( + f"attention_mask must be {expected}, got {tuple(attention_mask.shape)}" + ) + attention_mask = attention_mask.to(q.dtype) + + out = optimized_attention( + q, k, v, self.heads, mask=attention_mask, skip_reshape=True, + transformer_options=transformer_options, + ) + + img_out = self.to_out[1](self.to_out[0](out[:, :seq_img, :])) + txt_out = self.to_add_out(out[:, seq_img:, :]) + return img_out, txt_out + + +class LensTransformerBlock(nn.Module): + def __init__( + self, + dim: int, + num_attention_heads: int, + attention_head_dim: int, + eps: float = 1e-6, + rms_norm: bool = True, + dtype=None, + device=None, + operations=None, + ) -> None: + super().__init__() + + self.attn = LensJointAttention( + query_dim=dim, + added_kv_proj_dim=dim, + dim_head=attention_head_dim, + heads=num_attention_heads, + out_dim=dim, + eps=1e-5, + dtype=dtype, + device=device, + operations=operations, + ) + + if rms_norm: + NormCls = operations.RMSNorm + norm_kwargs = {} + else: + NormCls = operations.LayerNorm + norm_kwargs = {"elementwise_affine": False} + + mlp_hidden = int(dim / 3 * 8) + + # Sequential(SiLU, Linear) so state-dict lands at img_mod.1.{weight,bias}. + self.img_mod = nn.Sequential( + nn.SiLU(), + operations.Linear(dim, 6 * dim, bias=True, dtype=dtype, device=device), + ) + self.img_norm1 = NormCls(dim, eps=eps, dtype=dtype, device=device, **norm_kwargs) + self.img_norm2 = NormCls(dim, eps=eps, dtype=dtype, device=device, **norm_kwargs) + self.img_mlp = GateMLP(dim, mlp_hidden, dtype=dtype, device=device, operations=operations) + + self.txt_mod = nn.Sequential( + nn.SiLU(), + operations.Linear(dim, 6 * dim, bias=True, dtype=dtype, device=device), + ) + self.txt_norm1 = NormCls(dim, eps=eps, dtype=dtype, device=device, **norm_kwargs) + self.txt_norm2 = NormCls(dim, eps=eps, dtype=dtype, device=device, **norm_kwargs) + self.txt_mlp = GateMLP(dim, mlp_hidden, dtype=dtype, device=device, operations=operations) + + @staticmethod + def _modulate(x: torch.Tensor, mod_params: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + shift, scale, gate = mod_params.chunk(3, dim=-1) + return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1), gate.unsqueeze(1) + + def forward( + self, + hidden_states: torch.Tensor, + encoder_hidden_states: torch.Tensor, + temb: torch.Tensor, + freqs_cis: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + transformer_options: Optional[Dict[str, Any]] = None, + ) -> Tuple[torch.Tensor, torch.Tensor]: + img_mod1, img_mod2 = self.img_mod(temb).chunk(2, dim=-1) + txt_mod1, txt_mod2 = self.txt_mod(temb).chunk(2, dim=-1) + + img_modulated, img_gate1 = self._modulate(self.img_norm1(hidden_states), img_mod1) + txt_modulated, txt_gate1 = self._modulate(self.txt_norm1(encoder_hidden_states), txt_mod1) + + img_attn, txt_attn = self.attn( + hidden_states=img_modulated, + encoder_hidden_states=txt_modulated, + freqs_cis=freqs_cis, + attention_mask=attention_mask, + transformer_options=transformer_options, + ) + + hidden_states = hidden_states + img_gate1 * img_attn + encoder_hidden_states = encoder_hidden_states + txt_gate1 * txt_attn + + img_modulated2, img_gate2 = self._modulate(self.img_norm2(hidden_states), img_mod2) + hidden_states = hidden_states + img_gate2 * self.img_mlp(img_modulated2) + + txt_modulated2, txt_gate2 = self._modulate(self.txt_norm2(encoder_hidden_states), txt_mod2) + encoder_hidden_states = encoder_hidden_states + txt_gate2 * self.txt_mlp(txt_modulated2) + + return encoder_hidden_states, hidden_states + + +class _AdaLayerNormContinuousNoAffine(nn.Module): + """AdaLayerNormContinuous(elementwise_affine=False). + + The reference uses ``scale, shift = chunk(2)`` (scale first) — opposite + to Flux's ``LastLayer``. + """ + + def __init__(self, embedding_dim: int, conditioning_embedding_dim: int, eps: float = 1e-6, + dtype=None, device=None, operations=None) -> None: + super().__init__() + self.linear = operations.Linear( + conditioning_embedding_dim, embedding_dim * 2, bias=True, dtype=dtype, device=device + ) + self.eps = eps + self.embedding_dim = embedding_dim + + def forward(self, x: torch.Tensor, conditioning: torch.Tensor) -> torch.Tensor: + emb = self.linear(F.silu(conditioning)) + scale, shift = torch.chunk(emb, 2, dim=-1) + x = F.layer_norm(x, (self.embedding_dim,), None, None, self.eps) + return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1) + + +class LensTransformer2DModel(nn.Module): + """Lens dual-stream MMDiT (48 blocks, inner_dim=1536, multi-layer text).""" + + def __init__( + self, + patch_size: int = 2, + in_channels: int = 128, + out_channels: Optional[int] = 32, + num_layers: int = 48, + attention_head_dim: int = 64, + num_attention_heads: int = 24, + enc_hidden_dim: int = 2880, + axes_dims_rope: Tuple[int, int, int] = (8, 28, 28), + rms_norm: bool = True, + multi_layer_encoder_feature: bool = True, + selected_layer_index: Tuple[int, ...] = (5, 11, 17, 23), + image_model=None, # unused; accepted for detection-side configs. + dtype=None, + device=None, + operations=None, + ) -> None: + super().__init__() + self.patch_size = patch_size + self.in_channels = in_channels + self.out_channels = out_channels if out_channels is not None else in_channels + self.inner_dim = num_attention_heads * attention_head_dim + self.multi_layer_encoder_feature = multi_layer_encoder_feature + self.selected_layer_index = list(selected_layer_index) + self.dtype = dtype + + self.pos_embed = EmbedND(dim=attention_head_dim, theta=10000, axes_dim=list(axes_dims_rope)) + self.time_text_embed = LensTimestepProjEmbeddings( + embedding_dim=self.inner_dim, dtype=dtype, device=device, operations=operations + ) + + if self.multi_layer_encoder_feature: + self.txt_norm = nn.ModuleList( + [operations.RMSNorm(enc_hidden_dim, eps=1e-5, dtype=dtype, device=device) + for _ in self.selected_layer_index] + ) + self.txt_in = operations.Linear( + enc_hidden_dim * len(self.selected_layer_index), + self.inner_dim, bias=True, dtype=dtype, device=device, + ) + else: + self.txt_norm = operations.RMSNorm(enc_hidden_dim, eps=1e-5, dtype=dtype, device=device) + self.txt_in = operations.Linear(enc_hidden_dim, self.inner_dim, bias=True, dtype=dtype, device=device) + + self.img_in = operations.Linear(in_channels, self.inner_dim, bias=True, dtype=dtype, device=device) + + self.transformer_blocks = nn.ModuleList([ + LensTransformerBlock( + dim=self.inner_dim, + num_attention_heads=num_attention_heads, + attention_head_dim=attention_head_dim, + eps=1e-6, + rms_norm=rms_norm, + dtype=dtype, device=device, operations=operations, + ) + for _ in range(num_layers) + ]) + + self.norm_out = _AdaLayerNormContinuousNoAffine( + self.inner_dim, self.inner_dim, eps=1e-6, + dtype=dtype, device=device, operations=operations, + ) + self.proj_out = operations.Linear( + self.inner_dim, patch_size * patch_size * self.out_channels, bias=True, + dtype=dtype, device=device, + ) + + def forward(self, x: torch.Tensor, timestep: torch.Tensor, context: torch.Tensor, attention_mask: Optional[torch.Tensor] = None, + transformer_options: Optional[Dict[str, Any]] = None, **kwargs) -> torch.Tensor: + if transformer_options is None: + transformer_options = {} + return comfy.patcher_extension.WrapperExecutor.new_class_executor( + self._forward, self, + comfy.patcher_extension.get_all_wrappers(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, transformer_options), + ).execute(x, timestep, context, attention_mask, transformer_options, **kwargs) + + def _forward( + self, + x: torch.Tensor, + timestep: torch.Tensor, + context: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + transformer_options: Optional[Dict[str, Any]] = None, + control: Optional[Dict[str, Any]] = None, + **kwargs, + ) -> torch.Tensor: + """ComfyUI bridge: ``(x[B,128,h,w], t[B], context[B,S,L*H], mask[B,S])``.""" + if transformer_options is None: + transformer_options = {} + transformer_options = transformer_options.copy() + patches = transformer_options.get("patches", {}) + patches_replace = transformer_options.get("patches_replace", {}) + blocks_replace = patches_replace.get("dit", {}) + + B, C, h, w = x.shape + hidden_states = x.permute(0, 2, 3, 1).reshape(B, h * w, C) + + if self.multi_layer_encoder_feature: + L = len(self.selected_layer_index) + enc_dim = context.shape[-1] // L + encoder_hidden_states = list( + context.reshape(B, -1, L, enc_dim).unbind(dim=2) + ) + text_seq_len = encoder_hidden_states[0].shape[1] + else: + encoder_hidden_states = context + text_seq_len = context.shape[1] + + if attention_mask is None: + attention_mask = torch.ones( + (B, text_seq_len), dtype=torch.bool, device=x.device + ) + + img_len = h * w + joint_mask = self._build_joint_attention_mask(attention_mask, img_len) + + hidden_states = self.img_in(hidden_states) + timestep = timestep.to(hidden_states.dtype) + + if self.multi_layer_encoder_feature: + normed = [self.txt_norm[i](encoder_hidden_states[i]) for i in range(L)] + encoder_hidden_states = torch.cat(normed, dim=-1) + else: + encoder_hidden_states = self.txt_norm(encoder_hidden_states) + encoder_hidden_states = self.txt_in(encoder_hidden_states) + + if "post_input" in patches: + for p in patches["post_input"]: + out = p({ + "img": hidden_states, + "txt": encoder_hidden_states, + "transformer_options": transformer_options, + }) + hidden_states = out["img"] + encoder_hidden_states = out["txt"] + + temb = self.time_text_embed(timestep, hidden_states) + ids = _lens_position_ids(1, h, w, text_seq_len, device=hidden_states.device).unsqueeze(0) + freqs_cis = self.pos_embed(ids) + + transformer_options["total_blocks"] = len(self.transformer_blocks) + transformer_options["block_type"] = "double" + for i, block in enumerate(self.transformer_blocks): + transformer_options["block_index"] = i + if ("double_block", i) in blocks_replace: + def block_wrap(args): + out = {} + out["txt"], out["img"] = block( + hidden_states=args["img"], + encoder_hidden_states=args["txt"], + temb=args["vec"], + freqs_cis=args["pe"], + attention_mask=args.get("attn_mask"), + transformer_options=args.get("transformer_options"), + ) + return out + out = blocks_replace[("double_block", i)]( + { + "img": hidden_states, + "txt": encoder_hidden_states, + "vec": temb, + "pe": freqs_cis, + "attn_mask": joint_mask, + "transformer_options": transformer_options, + }, + {"original_block": block_wrap}, + ) + encoder_hidden_states = out["txt"] + hidden_states = out["img"] + else: + encoder_hidden_states, hidden_states = block( + hidden_states=hidden_states, + encoder_hidden_states=encoder_hidden_states, + temb=temb, + freqs_cis=freqs_cis, + attention_mask=joint_mask, + transformer_options=transformer_options, + ) + + if "double_block" in patches: + for p in patches["double_block"]: + out = p({ + "img": hidden_states, + "txt": encoder_hidden_states, + "x": x, + "block_index": i, + "transformer_options": transformer_options, + }) + hidden_states = out["img"] + encoder_hidden_states = out["txt"] + + if control is not None: + control_i = control.get("input") + if control_i is not None and i < len(control_i): + add = control_i[i] + if add is not None: + hidden_states[:, :add.shape[1]] += add + + hidden_states = self.norm_out(hidden_states, temb) + out = self.proj_out(hidden_states) + return out.reshape(B, h, w, C).permute(0, 3, 1, 2).contiguous() + + @staticmethod + def _build_joint_attention_mask(text_mask: torch.Tensor, img_len: int) -> torch.Tensor: + if text_mask.dtype != torch.bool: + text_mask = text_mask.bool() + bsz = text_mask.shape[0] + img_ones = torch.ones((bsz, img_len), dtype=torch.bool, device=text_mask.device) + joint = torch.cat([img_ones, text_mask], dim=1) + additive = torch.zeros_like(joint, dtype=torch.float32) + additive.masked_fill_(~joint, torch.finfo(torch.float32).min) + return additive[:, None, None, :] diff --git a/comfy/ldm/lightricks/av_model.py b/comfy/ldm/lightricks/av_model.py index 3fb87b4a3..ef9938465 100644 --- a/comfy/ldm/lightricks/av_model.py +++ b/comfy/ldm/lightricks/av_model.py @@ -22,26 +22,25 @@ class CompressedTimestep: """Store video timestep embeddings in compressed form using per-frame indexing.""" __slots__ = ('data', 'batch_size', 'num_frames', 'patches_per_frame', 'feature_dim') - def __init__(self, tensor: torch.Tensor, patches_per_frame: int): + def __init__(self, tensor: torch.Tensor, patches_per_frame: int, per_frame: bool = False): """ - tensor: [batch_size, num_tokens, feature_dim] tensor where num_tokens = num_frames * patches_per_frame - patches_per_frame: Number of spatial patches per frame (height * width in latent space), or None to disable compression + tensor: [batch, num_tokens, feature_dim] (per-token, default) or + [batch, num_frames, feature_dim] (per_frame=True, already compressed). + patches_per_frame: spatial patches per frame; pass None to disable compression. """ - self.batch_size, num_tokens, self.feature_dim = tensor.shape - - # Check if compression is valid (num_tokens must be divisible by patches_per_frame) - if patches_per_frame is not None and num_tokens % patches_per_frame == 0 and num_tokens >= patches_per_frame: + self.batch_size, n, self.feature_dim = tensor.shape + if per_frame: self.patches_per_frame = patches_per_frame - self.num_frames = num_tokens // patches_per_frame - - # Reshape to [batch, frames, patches_per_frame, feature_dim] and store one value per frame - # All patches in a frame are identical, so we only keep the first one - reshaped = tensor.view(self.batch_size, self.num_frames, patches_per_frame, self.feature_dim) - self.data = reshaped[:, :, 0, :].contiguous() # [batch, frames, feature_dim] + self.num_frames = n + self.data = tensor + elif patches_per_frame is not None and n >= patches_per_frame and n % patches_per_frame == 0: + self.patches_per_frame = patches_per_frame + self.num_frames = n // patches_per_frame + # All patches in a frame are identical — keep only the first. + self.data = tensor.view(self.batch_size, self.num_frames, patches_per_frame, self.feature_dim)[:, :, 0, :].contiguous() else: - # Not divisible or too small - store directly without compression self.patches_per_frame = 1 - self.num_frames = num_tokens + self.num_frames = n self.data = tensor def expand(self): @@ -716,32 +715,35 @@ class LTXAVModel(LTXVModel): def _prepare_timestep(self, timestep, batch_size, hidden_dtype, **kwargs): """Prepare timestep embeddings.""" - # TODO: some code reuse is needed here. grid_mask = kwargs.get("grid_mask", None) - if grid_mask is not None: - timestep = timestep[:, grid_mask] - - timestep_scaled = timestep * self.timestep_scale_multiplier - - v_timestep, v_embedded_timestep = self.adaln_single( - timestep_scaled.flatten(), - {"resolution": None, "aspect_ratio": None}, - batch_size=batch_size, - hidden_dtype=hidden_dtype, - ) - - # Calculate patches_per_frame from orig_shape: [batch, channels, frames, height, width] - # Video tokens are arranged as (frames * height * width), so patches_per_frame = height * width orig_shape = kwargs.get("orig_shape") has_spatial_mask = kwargs.get("has_spatial_mask", None) v_patches_per_frame = None if not has_spatial_mask and orig_shape is not None and len(orig_shape) == 5: - # orig_shape[3] = height, orig_shape[4] = width (in latent space) v_patches_per_frame = orig_shape[3] * orig_shape[4] - # Reshape to [batch_size, num_tokens, dim] and compress for storage - v_timestep = CompressedTimestep(v_timestep.view(batch_size, -1, v_timestep.shape[-1]), v_patches_per_frame) - v_embedded_timestep = CompressedTimestep(v_embedded_timestep.view(batch_size, -1, v_embedded_timestep.shape[-1]), v_patches_per_frame) + # Used by compute_prompt_timestep and the audio cross-attention paths. + timestep_scaled = (timestep[:, grid_mask] if grid_mask is not None else timestep) * self.timestep_scale_multiplier + + # When patches in a frame share a timestep (no spatial mask), project one row per frame instead of one per token + per_frame_path = v_patches_per_frame is not None and (timestep.numel() // batch_size) % v_patches_per_frame == 0 + if per_frame_path: + per_frame = timestep.reshape(batch_size, -1, v_patches_per_frame)[:, :, 0] + if grid_mask is not None: + # All-or-nothing per frame when has_spatial_mask=False. + per_frame = per_frame[:, grid_mask[::v_patches_per_frame]] + ts_input = per_frame * self.timestep_scale_multiplier + else: + ts_input = timestep_scaled + + v_timestep, v_embedded_timestep = self.adaln_single( + ts_input.flatten(), + {"resolution": None, "aspect_ratio": None}, + batch_size=batch_size, + hidden_dtype=hidden_dtype, + ) + v_timestep = CompressedTimestep(v_timestep.view(batch_size, -1, v_timestep.shape[-1]), v_patches_per_frame, per_frame=per_frame_path) + v_embedded_timestep = CompressedTimestep(v_embedded_timestep.view(batch_size, -1, v_embedded_timestep.shape[-1]), v_patches_per_frame, per_frame=per_frame_path) v_prompt_timestep = compute_prompt_timestep( self.prompt_adaln_single, timestep_scaled, batch_size, hidden_dtype @@ -765,25 +767,25 @@ class LTXAVModel(LTXVModel): # Cross-attention timesteps - compress these too av_ca_audio_scale_shift_timestep, _ = self.av_ca_audio_scale_shift_adaln_single( - timestep.max().expand_as(a_timestep_flat), + a_timestep_flat, {"resolution": None, "aspect_ratio": None}, batch_size=batch_size, hidden_dtype=hidden_dtype, ) av_ca_video_scale_shift_timestep, _ = self.av_ca_video_scale_shift_adaln_single( - a_timestep.max().expand_as(timestep_flat), + timestep_flat, {"resolution": None, "aspect_ratio": None}, batch_size=batch_size, hidden_dtype=hidden_dtype, ) av_ca_a2v_gate_noise_timestep, _ = self.av_ca_a2v_gate_adaln_single( - a_timestep.max().expand_as(timestep_flat) * av_ca_factor, + a_timestep_scaled.max().expand_as(timestep_flat) * av_ca_factor, {"resolution": None, "aspect_ratio": None}, batch_size=batch_size, hidden_dtype=hidden_dtype, ) av_ca_v2a_gate_noise_timestep, _ = self.av_ca_v2a_gate_adaln_single( - timestep.max().expand_as(a_timestep_flat) * av_ca_factor, + timestep_scaled.max().expand_as(a_timestep_flat) * av_ca_factor, {"resolution": None, "aspect_ratio": None}, batch_size=batch_size, hidden_dtype=hidden_dtype, diff --git a/comfy/ldm/lightricks/model.py b/comfy/ldm/lightricks/model.py index 74395beae..9953b6679 100644 --- a/comfy/ldm/lightricks/model.py +++ b/comfy/ldm/lightricks/model.py @@ -358,6 +358,61 @@ def apply_split_rotary_emb(input_tensor, cos, sin): return output.swapaxes(1, 2).reshape(B, T, -1) if needs_reshape else output +class GuideAttentionMask: + """Holds the two per-group masks for LTXV guide self-attention. + _attention_with_guide_mask splits queries into noisy and tracked-guide + groups, so the largest mask is (1, 1, tracked_count, T). + """ + __slots__ = ("guide_start", "tracked_count", "noisy_mask", "tracked_mask") + + def __init__(self, total_tokens, guide_start, tracked_count, tracked_weights): + device = tracked_weights.device + dtype = tracked_weights.dtype + finfo = torch.finfo(dtype) + + pos = tracked_weights > 0 + log_w = torch.full_like(tracked_weights, finfo.min) + log_w[pos] = torch.log(tracked_weights[pos].clamp(min=finfo.tiny)) + + self.guide_start = guide_start + self.tracked_count = tracked_count + + self.noisy_mask = torch.zeros((1, 1, 1, total_tokens), device=device, dtype=dtype) + self.noisy_mask[:, :, :, guide_start:guide_start + tracked_count] = log_w.view(1, 1, 1, -1) + + self.tracked_mask = torch.zeros((1, 1, tracked_count, total_tokens), device=device, dtype=dtype) + self.tracked_mask[:, :, :, :guide_start] = log_w.view(1, 1, -1, 1) + + +def _attention_with_guide_mask(q, k, v, heads, guide_mask, attn_precision, transformer_options): + """Apply the guide mask by partitioning Q into noisy and tracked-guide + groups, so each group needs only its own sub-mask. Avoids materializing + the (1,1,T,T) dense mask. + """ + guide_start = guide_mask.guide_start + tracked_end = guide_start + guide_mask.tracked_count + + out = torch.empty_like(q) + + if guide_start > 0: # In practice currently guides are always after noise, guard for safety if this changes. + out[:, :guide_start, :] = comfy.ldm.modules.attention.optimized_attention( + q[:, :guide_start, :], k, v, heads, mask=guide_mask.noisy_mask, + attn_precision=attn_precision, transformer_options=transformer_options, + low_precision_attention=False, # sageattn mask support is unreliable + ) + out[:, guide_start:tracked_end, :] = comfy.ldm.modules.attention.optimized_attention( + q[:, guide_start:tracked_end, :], k, v, heads, mask=guide_mask.tracked_mask, + attn_precision=attn_precision, transformer_options=transformer_options, + low_precision_attention=False, + ) + if tracked_end < q.shape[1]: # Every guide token is tracked, and nothing comes after them, guard for safety if this changes. + out[:, tracked_end:, :] = comfy.ldm.modules.attention.optimized_attention( + q[:, tracked_end:, :], k, v, heads, + attn_precision=attn_precision, transformer_options=transformer_options, + ) + return out + + class CrossAttention(nn.Module): def __init__( self, @@ -412,8 +467,10 @@ class CrossAttention(nn.Module): if mask is None: out = comfy.ldm.modules.attention.optimized_attention(q, k, v, self.heads, attn_precision=self.attn_precision, transformer_options=transformer_options) + elif isinstance(mask, GuideAttentionMask): + out = _attention_with_guide_mask(q, k, v, self.heads, mask, attn_precision=self.attn_precision, transformer_options=transformer_options) else: - out = comfy.ldm.modules.attention.optimized_attention_masked(q, k, v, self.heads, mask, attn_precision=self.attn_precision, transformer_options=transformer_options) + out = comfy.ldm.modules.attention.optimized_attention(q, k, v, self.heads, mask=mask, attn_precision=self.attn_precision, transformer_options=transformer_options) # Apply per-head gating if enabled if self.to_gate_logits is not None: @@ -1063,7 +1120,9 @@ class LTXVModel(LTXBaseModel): additional_args["resolved_guide_entries"] = resolved_entries keyframe_idxs = keyframe_idxs[..., kf_grid_mask, :] - pixel_coords[:, :, -keyframe_idxs.shape[2]:, :] = keyframe_idxs + + if keyframe_idxs.shape[2] > 0: # Guard for the case of no keyframes surviving + pixel_coords[:, :, -keyframe_idxs.shape[2]:, :] = keyframe_idxs # Total surviving guide tokens (all guides) additional_args["num_guide_tokens"] = keyframe_idxs.shape[2] @@ -1099,12 +1158,12 @@ class LTXVModel(LTXBaseModel): if not resolved_entries: return None - # Check if any attenuation is actually needed - needs_attenuation = any( - e["strength"] < 1.0 or e.get("pixel_mask") is not None + # strength != 1.0 means we want to either attenuate (< 1) or amplify (> 1) guide attention. + needs_mask = any( + e["strength"] != 1.0 or e.get("pixel_mask") is not None for e in resolved_entries ) - if not needs_attenuation: + if not needs_mask: return None # Build per-guide-token weights for all tracked guide tokens. @@ -1159,16 +1218,11 @@ class LTXVModel(LTXBaseModel): # Concatenate per-token weights for all tracked guides tracked_weights = torch.cat(all_weights, dim=1) # (1, total_tracked) - # Check if any weight is actually < 1.0 (otherwise no attenuation needed) - if (tracked_weights >= 1.0).all(): + # Skip when every weight is exactly 1.0 (additive bias would be 0). + if (tracked_weights == 1.0).all(): return None - # Build the mask: guide tokens are at the end of the sequence. - # Tracked guides come first (in order), untracked follow. - return self._build_self_attention_mask( - total_tokens, num_guide_tokens, total_tracked, - tracked_weights, guide_start, device, dtype, - ) + return GuideAttentionMask(total_tokens, guide_start, total_tracked, tracked_weights) @staticmethod def _downsample_mask_to_latent(mask, f_lat, h_lat, w_lat): @@ -1234,45 +1288,6 @@ class LTXVModel(LTXBaseModel): return rearrange(latent_mask, "b 1 f h w -> b (f h w)") - @staticmethod - def _build_self_attention_mask(total_tokens, num_guide_tokens, tracked_count, - tracked_weights, guide_start, device, dtype): - """Build a log-space additive self-attention bias mask. - - Attenuates attention between noisy tokens and tracked guide tokens. - Untracked guide tokens (at the end of the guide portion) keep full attention. - - Args: - total_tokens: Total sequence length. - num_guide_tokens: Total guide tokens (all guides) at end of sequence. - tracked_count: Number of tracked guide tokens (first in the guide portion). - tracked_weights: (1, tracked_count) tensor, values in [0, 1]. - guide_start: Index where guide tokens begin in the sequence. - device: Target device. - dtype: Target dtype. - - Returns: - (1, 1, total_tokens, total_tokens) additive bias mask. - 0.0 = full attention, negative = attenuated, finfo.min = effectively fully masked. - """ - finfo = torch.finfo(dtype) - mask = torch.zeros((1, 1, total_tokens, total_tokens), device=device, dtype=dtype) - tracked_end = guide_start + tracked_count - - # Convert weights to log-space bias - w = tracked_weights.to(device=device, dtype=dtype) # (1, tracked_count) - log_w = torch.full_like(w, finfo.min) - positive_mask = w > 0 - if positive_mask.any(): - log_w[positive_mask] = torch.log(w[positive_mask].clamp(min=finfo.tiny)) - - # noisy → tracked guides: each noisy row gets the same per-guide weight - mask[:, :, :guide_start, guide_start:tracked_end] = log_w.view(1, 1, 1, -1) - # tracked guides → noisy: each guide row broadcasts its weight across noisy cols - mask[:, :, guide_start:tracked_end, :guide_start] = log_w.view(1, 1, -1, 1) - - return mask - def _process_transformer_blocks(self, x, context, attention_mask, timestep, pe, transformer_options={}, self_attention_mask=None, **kwargs): """Process transformer blocks for LTXV.""" patches_replace = transformer_options.get("patches_replace", {}) diff --git a/comfy/ldm/lightricks/vae/causal_audio_autoencoder.py b/comfy/ldm/lightricks/vae/causal_audio_autoencoder.py index b556b128f..58b67d45a 100644 --- a/comfy/ldm/lightricks/vae/causal_audio_autoencoder.py +++ b/comfy/ldm/lightricks/vae/causal_audio_autoencoder.py @@ -1,4 +1,3 @@ -from __future__ import annotations import torch from torch import nn from torch.nn import functional as F diff --git a/comfy/ldm/lightricks/vae/causal_video_autoencoder.py b/comfy/ldm/lightricks/vae/causal_video_autoencoder.py index 998122c85..5975015e2 100644 --- a/comfy/ldm/lightricks/vae/causal_video_autoencoder.py +++ b/comfy/ldm/lightricks/vae/causal_video_autoencoder.py @@ -1,4 +1,3 @@ -from __future__ import annotations import threading import torch from torch import nn diff --git a/comfy/ldm/lumina/model.py b/comfy/ldm/lumina/model.py index 9e432d5c0..d0ee97d33 100644 --- a/comfy/ldm/lumina/model.py +++ b/comfy/ldm/lumina/model.py @@ -1,5 +1,4 @@ # Code from: https://github.com/Alpha-VLLM/Lumina-Image-2.0/blob/main/models/model.py -from __future__ import annotations from typing import List, Optional, Tuple diff --git a/comfy/ldm/modules/attention.py b/comfy/ldm/modules/attention.py index a68cb8439..55360535a 100644 --- a/comfy/ldm/modules/attention.py +++ b/comfy/ldm/modules/attention.py @@ -741,12 +741,12 @@ optimized_attention = attention_basic if model_management.sage_attention_enabled(): logging.info("Using sage attention") optimized_attention = attention_sage -elif model_management.xformers_enabled(): - logging.info("Using xformers attention") - optimized_attention = attention_xformers elif model_management.flash_attention_enabled(): logging.info("Using Flash Attention") optimized_attention = attention_flash +elif model_management.xformers_enabled(): + logging.info("Using xformers attention") + optimized_attention = attention_xformers elif model_management.pytorch_attention_enabled(): logging.info("Using pytorch attention") optimized_attention = attention_pytorch diff --git a/comfy/ldm/modules/diffusionmodules/util.py b/comfy/ldm/modules/diffusionmodules/util.py index 233011dc9..aed5c149c 100644 --- a/comfy/ldm/modules/diffusionmodules/util.py +++ b/comfy/ldm/modules/diffusionmodules/util.py @@ -140,7 +140,7 @@ def make_ddim_sampling_parameters(alphacums, ddim_timesteps, eta, verbose=True): alphas = alphacums[ddim_timesteps] alphas_prev = np.asarray([alphacums[0]] + alphacums[ddim_timesteps[:-1]].tolist()) - # according the the formula provided in https://arxiv.org/abs/2010.02502 + # according to the formula provided in https://arxiv.org/abs/2010.02502 sigmas = eta * np.sqrt((1 - alphas_prev) / (1 - alphas) * (1 - alphas / alphas_prev)) if verbose: logging.info(f'Selected alphas for ddim sampler: a_t: {alphas}; a_(t-1): {alphas_prev}') diff --git a/comfy/ldm/moge/geometry.py b/comfy/ldm/moge/geometry.py new file mode 100644 index 000000000..d1a1e445f --- /dev/null +++ b/comfy/ldm/moge/geometry.py @@ -0,0 +1,188 @@ +"""Pure-torch + scipy geometry helpers for MoGe inference and mesh export.""" + + +from typing import Optional, Tuple + +import numpy as np +import torch +import torch.nn.functional as F + +from scipy.optimize import least_squares + +def normalized_view_plane_uv(width: int, height: int, aspect_ratio: Optional[float] = None, + dtype: Optional[torch.dtype] = None, device: Optional[torch.device] = None) -> torch.Tensor: + """Normalized view-plane UV coordinates with corners at +/-(W, H)/diagonal.""" + if aspect_ratio is None: + aspect_ratio = width / height + span_x = aspect_ratio / (1 + aspect_ratio ** 2) ** 0.5 + span_y = 1.0 / (1 + aspect_ratio ** 2) ** 0.5 + u = torch.linspace(-span_x * (width - 1) / width, span_x * (width - 1) / width, width, dtype=dtype, device=device) + v = torch.linspace(-span_y * (height - 1) / height, span_y * (height - 1) / height, height, dtype=dtype, device=device) + u, v = torch.meshgrid(u, v, indexing="xy") + return torch.stack([u, v], dim=-1) + + +def intrinsics_from_focal_center(fx: torch.Tensor, fy: torch.Tensor, cx: torch.Tensor, cy: torch.Tensor) -> torch.Tensor: + """Assemble (..., 3, 3) intrinsics from broadcastable fx, fy, cx, cy.""" + fx, fy, cx, cy = [torch.as_tensor(v) for v in (fx, fy, cx, cy)] + fx, fy, cx, cy = torch.broadcast_tensors(fx, fy, cx, cy) + zero = torch.zeros_like(fx) + one = torch.ones_like(fx) + return torch.stack([ + torch.stack([fx, zero, cx], dim=-1), + torch.stack([zero, fy, cy], dim=-1), + torch.stack([zero, zero, one], dim=-1), + ], dim=-2) + + +def depth_map_to_point_map(depth: torch.Tensor, intrinsics: torch.Tensor) -> torch.Tensor: + """Back-project a (..., H, W) depth map through K^-1 to (..., H, W, 3) camera-space points. + + Intrinsics use normalized image coords (x in [0, 1] left->right, y in [0, 1] top->bottom). + """ + H, W = depth.shape[-2:] + device, dtype = depth.device, depth.dtype + u = (torch.arange(W, dtype=dtype, device=device) + 0.5) / W + v = (torch.arange(H, dtype=dtype, device=device) + 0.5) / H + grid_v, grid_u = torch.meshgrid(v, u, indexing="ij") + pix = torch.stack([grid_u, grid_v, torch.ones_like(grid_u)], dim=-1) + K_inv = torch.linalg.inv(intrinsics) + rays = torch.einsum("...ij,hwj->...hwi", K_inv, pix) + return rays * depth.unsqueeze(-1) + + +def _solve_optimal_shift(uv: np.ndarray, xyz: np.ndarray, + focal: Optional[float] = None) -> Tuple[float, float]: + """LM-solve for z-shift; when focal is None, also recovers the optimal focal.""" + uv = uv.reshape(-1, 2) + xy = xyz[..., :2].reshape(-1, 2) + z = xyz[..., 2].reshape(-1) + + def fn(shift): + xy_proj = xy / (z + shift)[:, None] + f = focal if focal is not None else (xy_proj * uv).sum() / np.square(xy_proj).sum() + return (f * xy_proj - uv).ravel() + + sol = least_squares(fn, x0=0.0, ftol=1e-3, method="lm") + shift = float(np.asarray(sol["x"]).squeeze()) + if focal is None: + xy_proj = xy / (z + shift)[:, None] + focal = float((xy_proj * uv).sum() / np.square(xy_proj).sum()) + return shift, focal + + +def recover_focal_shift(points: torch.Tensor, mask: Optional[torch.Tensor] = None, + focal: Optional[torch.Tensor] = None, downsample_size: Tuple[int, int] = (64, 64) + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Recover the focal length and z-shift that turn points into a metric point map. + + Optical center is at the image center; returned focal is relative to half the image diagonal. + Returns (focal, shift) on the same device/dtype as points. + """ + shape = points.shape + H, W = shape[-3], shape[-2] + points_b = points.reshape(-1, H, W, 3) + mask_b = None if mask is None else mask.reshape(-1, H, W) + focal_b = None if focal is None else focal.reshape(-1) + + uv = normalized_view_plane_uv(W, H, dtype=points.dtype, device=points.device) + + points_lr = F.interpolate(points_b.permute(0, 3, 1, 2), downsample_size, mode="nearest").permute(0, 2, 3, 1) + uv_lr = F.interpolate(uv.unsqueeze(0).permute(0, 3, 1, 2), downsample_size, mode="nearest").squeeze(0).permute(1, 2, 0) + mask_lr = None + if mask_b is not None: + mask_lr = F.interpolate(mask_b.to(torch.float32).unsqueeze(1), downsample_size, mode="nearest").squeeze(1) > 0 + + uv_np = uv_lr.detach().cpu().numpy() + points_np = points_lr.detach().cpu().numpy() + mask_np = None if mask_lr is None else mask_lr.detach().cpu().numpy() + focal_np = None if focal_b is None else focal_b.detach().cpu().numpy() + + out_focal: list = [] + out_shift: list = [] + for i in range(points_b.shape[0]): + if mask_np is None: + xyz_i = points_np[i].reshape(-1, 3) + uv_i = uv_np.reshape(-1, 2) + else: + sel = mask_np[i] + if sel.sum() < 2: + out_focal.append(1.0) + out_shift.append(0.0) + continue + xyz_i = points_np[i][sel] + uv_i = uv_np[sel] + if focal_np is None: + shift_i, focal_i = _solve_optimal_shift(uv_i, xyz_i) + out_focal.append(focal_i) + else: + shift_i, _ = _solve_optimal_shift(uv_i, xyz_i, focal=float(focal_np[i])) + out_shift.append(shift_i) + + shift_t = torch.tensor(out_shift, device=points.device, dtype=points.dtype).reshape(shape[:-3]) + if focal is None: + focal_t = torch.tensor(out_focal, device=points.device, dtype=points.dtype).reshape(shape[:-3]) + else: + focal_t = focal.reshape(shape[:-3]) + return focal_t, shift_t + + +def depth_map_edge(depth: torch.Tensor, atol: Optional[float] = None, rtol: Optional[float] = None, kernel_size: int = 3) -> torch.Tensor: + """Per-pixel boolean: True where the local depth window's max-min span exceeds atol or rtol*depth.""" + shape = depth.shape + d = depth.reshape(-1, 1, *shape[-2:]) + pad = kernel_size // 2 + diff = F.max_pool2d(d, kernel_size, stride=1, padding=pad) + F.max_pool2d(-d, kernel_size, stride=1, padding=pad) + edge = torch.zeros_like(d, dtype=torch.bool) + if atol is not None: + edge |= diff > atol + if rtol is not None: + edge |= (diff / d.clamp_min(1e-6)).nan_to_num_() > rtol + return edge.reshape(*shape) + + +def triangulate_grid_mesh(points: torch.Tensor, mask: Optional[torch.Tensor] = None, decimation: int = 1, discontinuity_threshold: float = 0.04, + depth: Optional[torch.Tensor] = None) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Triangulate a (H, W, 3) point map into (vertices, faces, uvs) on CPU. + + Vertices: pixels with finite coords (passing optional mask). Quads with four valid corners + become two triangles. depth overrides the scalar used for the rtol edge check; pass radial + depth for panoramas (the default points[..., 2] goes negative below the equator). + """ + points = points.detach().cpu() + finite = torch.isfinite(points).all(dim=-1) + if mask is None: + mask = finite + else: + mask = mask.detach().cpu().to(torch.bool) & finite + + if discontinuity_threshold > 0: + d = depth.detach().cpu() if depth is not None else points[..., 2] + # Replace inf with 0 so max-pool doesn't poison neighbourhoods (mask above already excludes those pixels). + d_finite = torch.where(finite, d, torch.zeros_like(d)) + edge = depth_map_edge(d_finite, rtol=discontinuity_threshold) + mask = mask & ~edge + + if decimation > 1: + points = points[::decimation, ::decimation].contiguous() + mask = mask[::decimation, ::decimation].contiguous() + H, W = points.shape[:2] + + flat_mask = mask.reshape(-1) + idx = torch.full((H * W,), -1, dtype=torch.long) + n_valid = int(flat_mask.sum().item()) + idx[flat_mask] = torch.arange(n_valid, dtype=torch.long) + idx = idx.reshape(H, W) + + vertices = points.reshape(-1, 3)[flat_mask].contiguous() + + yy, xx = torch.meshgrid(torch.arange(H), torch.arange(W), indexing="ij") + u = xx.float() / max(W - 1, 1) + v = yy.float() / max(H - 1, 1) + uvs = torch.stack([u, v], dim=-1).reshape(-1, 2)[flat_mask].contiguous() + + a, b, c, d = idx[:-1, :-1], idx[:-1, 1:], idx[1:, 1:], idx[1:, :-1] + quad_ok = (a >= 0) & (b >= 0) & (c >= 0) & (d >= 0) + a, b, c, d = a[quad_ok], b[quad_ok], c[quad_ok], d[quad_ok] + faces = torch.cat([torch.stack([a, b, c], dim=-1), torch.stack([a, c, d], dim=-1)], dim=0).contiguous() + return vertices, faces, uvs diff --git a/comfy/ldm/moge/model.py b/comfy/ldm/moge/model.py new file mode 100644 index 000000000..1695626bc --- /dev/null +++ b/comfy/ldm/moge/model.py @@ -0,0 +1,346 @@ +"""MoGe v1 / v2 inference modules and a state-dict-driven builder. + +V1: DINOv2 backbone + multi-output head (points, mask). +V2: DINOv2 encoder + neck + per-output heads (points, mask, normal, optional metric-scale MLP). +""" + + +from numbers import Number +from typing import Any, Dict, List, Optional, Tuple, Union + +import torch +import torch.nn as nn +import torch.nn.functional as F + +import comfy.ops +import comfy.model_management +import comfy.model_patcher + +from comfy.image_encoders.dino2 import Dinov2Model + +from .geometry import depth_map_to_point_map, intrinsics_from_focal_center, recover_focal_shift +from .modules import ConvStack, DINOv2Encoder, HeadV1, MLP, _view_plane_uv_grid + + +def _remap_points(points: torch.Tensor) -> torch.Tensor: + """Apply the exp remap: z -> exp(z), xy stays linear and gets scaled by the new z.""" + xy, z = points.split([2, 1], dim=-1) + z = torch.exp(z) + return torch.cat([xy * z, z], dim=-1) + + +def _detect_dinov2(sd: dict, prefix: str) -> Dict[str, Any]: + # All shipped MoGe checkpoints use plain DINOv2 + hidden = sd[prefix + "embeddings.cls_token"].shape[-1] + layer_prefix = prefix + "encoder.layer." + depth = 1 + max(int(k[len(layer_prefix):].split(".")[0]) for k in sd if k.startswith(layer_prefix)) + return { + "hidden_size": hidden, + "num_attention_heads": hidden // 64, + "num_hidden_layers": depth, + "layer_norm_eps": 1e-6, + "use_swiglu_ffn": False, + } + + +class MoGeModelV1(nn.Module): + """MoGe v1: DINOv2 backbone + HeadV1 (points, mask).""" + + image_mean: torch.Tensor + image_std: torch.Tensor + + intermediate_layers = 4 + num_tokens_range: Tuple[Number, Number] = (1200, 2500) + mask_threshold = 0.5 + + def __init__(self, backbone: Dict[str, Any], dim_upsample: List[int] = (256, 128, 128), + num_res_blocks: int = 1, dim_times_res_block_hidden: int = 1, + dtype=None, device=None, operations=comfy.ops.manual_cast): + super().__init__() + self.backbone = Dinov2Model(backbone, dtype, device, operations) + self.head = HeadV1(dim_in=backbone["hidden_size"], dim_upsample=list(dim_upsample), + num_res_blocks=num_res_blocks, dim_times_res_block_hidden=dim_times_res_block_hidden, + dtype=dtype, device=device, operations=operations) + self.register_buffer("image_mean", torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)) + self.register_buffer("image_std", torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1)) + + def forward(self, image: torch.Tensor, num_tokens: int) -> Dict[str, torch.Tensor]: + H, W = image.shape[-2:] + resize = ((num_tokens * 14 ** 2) / (H * W)) ** 0.5 + rh, rw = int(H * resize), int(W * resize) + x = F.interpolate(image, (rh, rw), mode="bicubic", align_corners=False, antialias=True) + x = (x - self.image_mean) / self.image_std + x14 = F.interpolate(x, (rh // 14 * 14, rw // 14 * 14), mode="bilinear", align_corners=False, antialias=True) + + n_layers = len(self.backbone.encoder.layer) + indices = list(range(n_layers - self.intermediate_layers, n_layers)) + feats = self.backbone.get_intermediate_layers(x14, indices, apply_norm=True) + + points, mask = self.head(feats, x) + points = F.interpolate(points.float(), (H, W), mode="bilinear", align_corners=False) + points = _remap_points(points.permute(0, 2, 3, 1)) + + mask = F.interpolate(mask.float(), (H, W), mode="bilinear", align_corners=False).squeeze(1) + + return {"points": points, "mask": mask} + + @classmethod + def from_state_dict(cls, sd, dtype=None, device=None, operations=comfy.ops.manual_cast): + """Detect the v1 head config from sd, build a model, and load weights.""" + n_up = 1 + max(int(k.split(".")[2]) for k in sd if k.startswith("head.upsample_blocks.")) + dim_upsample = [sd[f"head.upsample_blocks.{i}.0.0.weight"].shape[1] for i in range(n_up)] + # Each upsample stage is Sequential[upsampler, *res_blocks]; count res blocks at level 0. + num_res_blocks = max({int(k.split(".")[3]) for k in sd if k.startswith("head.upsample_blocks.0.")}) + hidden_out = sd["head.upsample_blocks.0.1.layers.2.weight"].shape[0] + dim_times = max(hidden_out // dim_upsample[0], 1) + model = cls(backbone=_detect_dinov2(sd, prefix="backbone."), + dim_upsample=dim_upsample, num_res_blocks=num_res_blocks, dim_times_res_block_hidden=dim_times, + dtype=dtype, device=device, operations=operations) + model.load_state_dict(sd, strict=True) + return model + + +class MoGeModelV2(nn.Module): + """MoGe v2: DINOv2 encoder + neck + per-output heads (points/mask/normal/metric-scale).""" + + intermediate_layers = 4 + num_tokens_range: Tuple[Number, Number] = (1200, 3600) + + def __init__(self, + encoder: Dict[str, Any], + neck: Dict[str, Any], + points_head: Dict[str, Any], + mask_head: Dict[str, Any], + scale_head: Dict[str, Any], + normal_head: Optional[Dict[str, Any]] = None, + dtype=None, device=None, operations=comfy.ops.manual_cast): + super().__init__() + self.encoder = DINOv2Encoder(**encoder, dtype=dtype, device=device, operations=operations) + self.neck = ConvStack(**neck, dtype=dtype, device=device, operations=operations) + self.points_head = ConvStack(**points_head, dtype=dtype, device=device, operations=operations) + self.mask_head = ConvStack(**mask_head, dtype=dtype, device=device, operations=operations) + self.scale_head = MLP(**scale_head, dtype=dtype, device=device, operations=operations) + if normal_head is not None: + self.normal_head = ConvStack(**normal_head, dtype=dtype, device=device, operations=operations) + + def forward(self, image: torch.Tensor, num_tokens: int) -> Dict[str, torch.Tensor]: + B, _, H, W = image.shape + device, dtype = image.device, image.dtype + aspect_ratio = W / H + base_h = round((num_tokens / aspect_ratio) ** 0.5) + base_w = round((num_tokens * aspect_ratio) ** 0.5) + + feat_top, cls_token = self.encoder(image, base_h, base_w, return_class_token=True) + + # 5-level pyramid: feat at level 0 concatenated with UV, other levels UV-only. + levels = [_view_plane_uv_grid(B, base_h * (2 ** L), base_w * (2 ** L), aspect_ratio, dtype, device) + for L in range(5)] + levels[0] = torch.cat([feat_top, levels[0]], dim=1) + + feats = self.neck(levels) + + def _resize(v): + return F.interpolate(v, (H, W), mode="bilinear", align_corners=False) + + points = _remap_points(_resize(self.points_head(feats)[-1]).permute(0, 2, 3, 1)) + mask = _resize(self.mask_head(feats)[-1]).squeeze(1).sigmoid() + metric_scale = self.scale_head(cls_token).squeeze(1).exp() + + result = {"points": points, "mask": mask, "metric_scale": metric_scale} + if hasattr(self, "normal_head"): + normal = _resize(self.normal_head(feats)[-1]) + result["normal"] = F.normalize(normal.permute(0, 2, 3, 1), dim=-1) + return result + + @classmethod + def from_state_dict(cls, sd, dtype=None, device=None, operations=comfy.ops.manual_cast): + """Detect the v2 encoder/neck/heads config from sd, build a model, and load weights.""" + backbone = _detect_dinov2(sd, prefix="encoder.backbone.") + depth = backbone["num_hidden_layers"] + n = cls.intermediate_layers + encoder = { + "backbone": backbone, + "intermediate_layers": [(depth // n) * (i + 1) - 1 for i in range(n)], + "dim_out": sd["encoder.output_projections.0.weight"].shape[0], + } + # scale_head is an MLP: Sequential of [Linear, ReLU, ..., Linear]; Linear weight is (out, in). + scale_idxs = sorted({int(k.split(".")[1]) for k in sd if k.startswith("scale_head.")}) + scale_first = sd[f"scale_head.{scale_idxs[0]}.weight"] + cfg: Dict[str, Any] = { + "encoder": encoder, + "neck": cls._detect_convstack(sd, "neck."), + "points_head": cls._detect_convstack(sd, "points_head."), + "mask_head": cls._detect_convstack(sd, "mask_head."), + "scale_head": {"dims": [scale_first.shape[1]] + [sd[f"scale_head.{i}.weight"].shape[0] for i in scale_idxs]}, + } + if any(k.startswith("normal_head.") for k in sd): + cfg["normal_head"] = cls._detect_convstack(sd, "normal_head.") + model = cls(**cfg, dtype=dtype, device=device, operations=operations) + model.load_state_dict(sd, strict=True) + return model + + @staticmethod + def _detect_convstack(sd: dict, prefix: str) -> Dict[str, Any]: + """Reconstruct a ConvStack config from the keys under prefix""" + in_keys = [k for k in sd if k.startswith(f"{prefix}input_blocks.") and k.endswith(".weight")] + n = 1 + max(int(k[len(f"{prefix}input_blocks."):].split(".")[0]) for k in in_keys) + + in_shapes = [sd[f"{prefix}input_blocks.{i}.weight"].shape for i in range(n)] + has_out = lambda i: f"{prefix}output_blocks.{i}.weight" in sd + has_norm = f"{prefix}res_blocks.0.0.layers.0.weight" in sd + + def num_res_at(i): + rb_prefix = f"{prefix}res_blocks.{i}." + return len({int(k[len(rb_prefix):].split(".")[0]) for k in sd if k.startswith(rb_prefix)}) + + return { + "dim_in": [s[1] for s in in_shapes], + "dim_res_blocks": [s[0] for s in in_shapes], + "dim_out": [sd[f"{prefix}output_blocks.{i}.weight"].shape[0] if has_out(i) else None for i in range(n)], + "num_res_blocks": [num_res_at(i) for i in range(n)], + "resamplers": ["conv_transpose" if f"{prefix}resamplers.{i}.0.weight" in sd else "bilinear" + for i in range(n - 1)], + "res_block_in_norm": "layer_norm" if has_norm else "none", + "res_block_hidden_norm": "group_norm" if has_norm else "none", + } + + +# Translate the Meta-style DINOv2 keys MoGe ships to the naming ComfyUI DINOv2 port expects, +# and split each fused qkv tensor into Q/K/V. +_DINOV2_TOPLEVEL_RENAMES = { + "patch_embed.proj.weight": "embeddings.patch_embeddings.projection.weight", + "patch_embed.proj.bias": "embeddings.patch_embeddings.projection.bias", + "cls_token": "embeddings.cls_token", + "pos_embed": "embeddings.position_embeddings", + "register_tokens": "embeddings.register_tokens", + "mask_token": "embeddings.mask_token", + "norm.weight": "layernorm.weight", + "norm.bias": "layernorm.bias", +} +_DINOV2_BLOCK_RENAMES = [ + ("ls1.gamma", "layer_scale1.lambda1"), + ("ls2.gamma", "layer_scale2.lambda1"), + ("attn.proj.", "attention.output.dense."), + ("mlp.w12.", "mlp.weights_in."), + ("mlp.w3.", "mlp.weights_out."), +] + + +def _remap_state_dict(sd: dict) -> dict: + if "model" in sd and "model_config" in sd: + sd = sd["model"] + prefix = "encoder.backbone." if any(k.startswith("encoder.backbone.") for k in sd) else "backbone." + out: dict = {} + for k, v in sd.items(): + if not k.startswith(prefix): + out[k] = v + continue + rel = k[len(prefix):] + if rel in _DINOV2_TOPLEVEL_RENAMES: + out[prefix + _DINOV2_TOPLEVEL_RENAMES[rel]] = v + continue + if not rel.startswith("blocks."): + out[k] = v + continue + _, idx, sub = rel.split(".", 2) + if sub in ("attn.qkv.weight", "attn.qkv.bias"): + tail = sub.rsplit(".", 1)[1] + q, kw, vw = v.chunk(3, dim=0) + base = f"{prefix}encoder.layer.{idx}.attention.attention" + out[f"{base}.query.{tail}"] = q + out[f"{base}.key.{tail}"] = kw + out[f"{base}.value.{tail}"] = vw + continue + for old, new in _DINOV2_BLOCK_RENAMES: + sub = sub.replace(old, new) + out[f"{prefix}encoder.layer.{idx}.{sub}"] = v + return out + + +def build_from_state_dict(sd: dict, dtype=None, device=None, operations=comfy.ops.manual_cast) -> nn.Module: + """Dispatch to v1 or v2 based on the DINOv2 backbone prefix.""" + sd = _remap_state_dict(sd) + cls = MoGeModelV2 if any(k.startswith("encoder.backbone.") for k in sd) else MoGeModelV1 + return cls.from_state_dict(sd, dtype=dtype, device=device, operations=operations) + + +class MoGeModel: + """Loaded MoGe model + ComfyUI memory management.""" + + def __init__(self, state_dict: dict): + # text encoder dtype closest match + self.load_device = comfy.model_management.text_encoder_device() + offload_device = comfy.model_management.text_encoder_offload_device() + self.dtype = comfy.model_management.text_encoder_dtype(self.load_device) + + self.model = build_from_state_dict(state_dict, dtype=self.dtype, device=offload_device, operations=comfy.ops.manual_cast).eval() + self.patcher = comfy.model_patcher.CoreModelPatcher(self.model, load_device=self.load_device, offload_device=offload_device) + self.version = "v2" if hasattr(self.model, "encoder") else "v1" + self.mask_threshold = float(getattr(self.model, "mask_threshold", 0.5)) + nt = getattr(self.model, "num_tokens_range", (1200, 2500 if self.version == "v1" else 3600)) + self.num_tokens_range = (int(nt[0]), int(nt[1])) + + def infer(self, image: torch.Tensor, num_tokens: Optional[int] = None, + resolution_level: int = 9, fov_x: Optional[Union[Number, torch.Tensor]] = None, + force_projection: bool = True, apply_mask: bool = True, + apply_metric_scale: bool = True + ) -> Dict[str, torch.Tensor]: + """Run a single MoGe forward + post-process pass. image is (B, 3, H, W) in [0, 1].""" + comfy.model_management.load_model_gpu(self.patcher) + image = image.to(device=self.load_device, dtype=self.dtype) + H, W = image.shape[-2:] + aspect_ratio = W / H + + if num_tokens is None: + lo, hi = self.num_tokens_range + num_tokens = int(lo + (resolution_level / 9) * (hi - lo)) + + out = self.model.forward(image, num_tokens=num_tokens) + points = out["points"].float() # recover_focal_shift goes through scipy on CPU; needs fp32. + mask_binary = out["mask"] > self.mask_threshold + normal = out.get("normal") + metric_scale = out.get("metric_scale") + + diag = (1 + aspect_ratio ** 2) ** 0.5 + + def focal_from_fov_deg(deg): + fov = torch.as_tensor(deg, device=points.device, dtype=points.dtype) + return aspect_ratio / diag / torch.tan(torch.deg2rad(fov / 2)) + + if fov_x is None: + focal, shift = recover_focal_shift(points, mask_binary) + # Fall back to 60 deg FoV when the least-squares solver flips the focal sign. + bad = ~torch.isfinite(focal) | (focal <= 0) + if bool(bad.any()): + focal = torch.where(bad, focal_from_fov_deg(60.0), focal) + _, shift = recover_focal_shift(points, mask_binary, focal=focal) + else: + focal = focal_from_fov_deg(fov_x).expand(points.shape[0]) + _, shift = recover_focal_shift(points, mask_binary, focal=focal) + + f_diag = focal / 2 * diag + half = torch.tensor(0.5, device=points.device, dtype=points.dtype) + intrinsics = intrinsics_from_focal_center(f_diag / aspect_ratio, f_diag, half, half) + points[..., 2] = points[..., 2] + shift[..., None, None] + # v2 only: filter mask by depth>0 to drop metric-scale negative-depth artifacts. + if self.version == "v2": + mask_binary = mask_binary & (points[..., 2] > 0) + depth = points[..., 2].clone() + + if force_projection: + points = depth_map_to_point_map(depth, intrinsics=intrinsics) + + if apply_metric_scale and metric_scale is not None: + points = points * metric_scale[:, None, None, None] + depth = depth * metric_scale[:, None, None] + + if apply_mask: + points = torch.where(mask_binary[..., None], points, torch.full_like(points, float("inf"))) + depth = torch.where(mask_binary, depth, torch.full_like(depth, float("inf"))) + if normal is not None: + normal = torch.where(mask_binary[..., None], normal, torch.zeros_like(normal)) + + result = {"points": points, "depth": depth, "intrinsics": intrinsics, "mask": mask_binary} + if normal is not None: + result["normal"] = normal + return result diff --git a/comfy/ldm/moge/modules.py b/comfy/ldm/moge/modules.py new file mode 100644 index 000000000..f6443d65a --- /dev/null +++ b/comfy/ldm/moge/modules.py @@ -0,0 +1,203 @@ +"""Building blocks for MoGe: residual conv stack, resamplers, MLP, DINOv2 encoder, v1 head.""" + + +from typing import List, Optional, Sequence, Tuple, Union + +import torch +import torch.nn as nn +import torch.nn.functional as F + +import comfy.ops +from comfy.image_encoders.dino2 import Dinov2Model + +from .geometry import normalized_view_plane_uv + + +def _conv2d(operations, c_in: int, c_out: int, k: int = 3, *, dtype=None, device=None): + return operations.Conv2d(c_in, c_out, kernel_size=k, padding=k // 2, padding_mode="replicate", dtype=dtype, device=device) + + +def _view_plane_uv_grid(batch: int, height: int, width: int, aspect_ratio: float, dtype, device) -> torch.Tensor: + """Batched normalized view-plane UV grid as a (B, 2, H, W) tensor.""" + uv = normalized_view_plane_uv(width, height, aspect_ratio=aspect_ratio, dtype=dtype, device=device) + return uv.permute(2, 0, 1).unsqueeze(0).expand(batch, -1, -1, -1) + + +def _concat_view_plane_uv(x: torch.Tensor, aspect_ratio: float) -> torch.Tensor: + """Append a 2-channel normalized view-plane UV grid to x along the channel dim.""" + uv = _view_plane_uv_grid(x.shape[0], x.shape[-2], x.shape[-1], aspect_ratio, x.dtype, x.device) + return torch.cat([x, uv], dim=1) + + +class ResidualConvBlock(nn.Module): + def __init__(self, channels: int, hidden_channels: Optional[int] = None, in_norm: str = "layer_norm", hidden_norm: str = "group_norm", + dtype=None, device=None, operations=comfy.ops.manual_cast): + super().__init__() + hidden_channels = hidden_channels if hidden_channels is not None else channels + + in_norm_layer = operations.GroupNorm(1, channels, dtype=dtype, device=device) if in_norm == "layer_norm" else nn.Identity() + hidden_norm_layer = (operations.GroupNorm(max(hidden_channels // 32, 1), hidden_channels, dtype=dtype, device=device) + if hidden_norm == "group_norm" else nn.Identity()) + + self.layers = nn.Sequential( + in_norm_layer, nn.ReLU(), _conv2d(operations, channels, hidden_channels, dtype=dtype, device=device), + hidden_norm_layer, nn.ReLU(), _conv2d(operations, hidden_channels, channels, dtype=dtype, device=device), + ) + + def forward(self, x): + return self.layers(x) + x + + +class Resampler(nn.Sequential): + """2x upsampler: ConvTranspose2d(2x2) or bilinear upsample, followed by a 3x3 conv.""" + + def __init__(self, in_channels: int, out_channels: int, type_: str, dtype=None, device=None, operations=comfy.ops.manual_cast): + if type_ == "conv_transpose": + up = operations.ConvTranspose2d(in_channels, out_channels, kernel_size=2, stride=2, dtype=dtype, device=device) + conv_in = out_channels + else: # "bilinear" + up = nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False) + conv_in = in_channels + super().__init__(up, _conv2d(operations, conv_in, out_channels, dtype=dtype, device=device)) + + +class MLP(nn.Sequential): + def __init__(self, dims: Sequence[int], dtype=None, device=None, operations=comfy.ops.manual_cast): + layers = [] + for d_in, d_out in zip(dims[:-2], dims[1:-1]): + layers.append(operations.Linear(d_in, d_out, dtype=dtype, device=device)) + layers.append(nn.ReLU(inplace=True)) + layers.append(operations.Linear(dims[-2], dims[-1], dtype=dtype, device=device)) + super().__init__(*layers) + + +class ConvStack(nn.Module): + def __init__(self, dim_in: List[Optional[int]], dim_res_blocks: List[int], dim_out: List[Optional[int]], resamplers: List[str], + num_res_blocks: List[int], dim_times_res_block_hidden: int = 1, res_block_in_norm: str = "layer_norm", res_block_hidden_norm: str = "group_norm", + dtype=None, device=None, operations=comfy.ops.manual_cast): + super().__init__() + + self.input_blocks = nn.ModuleList([ + (_conv2d(operations, d_in, d_res, k=1, dtype=dtype, device=device) + if d_in is not None else nn.Identity()) + for d_in, d_res in zip(dim_in, dim_res_blocks) + ]) + + self.resamplers = nn.ModuleList([ + Resampler(prev, succ, type_=r, dtype=dtype, device=device, operations=operations) + for prev, succ, r in zip(dim_res_blocks[:-1], dim_res_blocks[1:], resamplers) + ]) + + self.res_blocks = nn.ModuleList([ + nn.Sequential(*[ + ResidualConvBlock(d_res, dim_times_res_block_hidden * d_res, in_norm=res_block_in_norm, hidden_norm=res_block_hidden_norm, dtype=dtype, device=device, operations=operations) + for _ in range(num_res_blocks[i]) + ]) + for i, d_res in enumerate(dim_res_blocks) + ]) + + self.output_blocks = nn.ModuleList([ + (_conv2d(operations, d_res, d_out, k=1, dtype=dtype, device=device) + if d_out is not None else nn.Identity()) + for d_out, d_res in zip(dim_out, dim_res_blocks) + ]) + + def forward(self, in_features: List[Optional[torch.Tensor]]): + out_features = [] + x = None + for i in range(len(self.res_blocks)): + feat = self.input_blocks[i](in_features[i]) if in_features[i] is not None else None + if i == 0: + x = feat + elif feat is not None: + x = x + feat + x = self.res_blocks[i](x) + out_features.append(self.output_blocks[i](x)) + if i < len(self.res_blocks) - 1: + x = self.resamplers[i](x) + return out_features + + +class DINOv2Encoder(nn.Module): + """Comfy DINOv2 backbone with per-layer 1x1 projection heads.""" + + def __init__(self, backbone: dict, intermediate_layers: List[int], dim_out: int, dtype=None, device=None, operations=comfy.ops.manual_cast): + super().__init__() + self.intermediate_layers = list(intermediate_layers) + dim_features = backbone["hidden_size"] + self.backbone = Dinov2Model(backbone, dtype, device, operations) + self.output_projections = nn.ModuleList([ + _conv2d(operations, dim_features, dim_out, k=1, dtype=dtype, device=device) + for _ in range(len(self.intermediate_layers)) + ]) + self.register_buffer("image_mean", torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)) + self.register_buffer("image_std", torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1)) + + def forward(self, image: torch.Tensor, token_rows: int, token_cols: int, + return_class_token: bool = False) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + image_14 = F.interpolate(image, (token_rows * 14, token_cols * 14), mode="bilinear", align_corners=False, antialias=True) + image_14 = (image_14 - self.image_mean) / self.image_std + feats = self.backbone.get_intermediate_layers(image_14, self.intermediate_layers, apply_norm=True) + x = torch.stack([ + proj(feat.permute(0, 2, 1).unflatten(2, (token_rows, token_cols)).contiguous()) + for proj, (feat, _cls) in zip(self.output_projections, feats) + ], dim=1).sum(dim=1) + if return_class_token: + return x, feats[-1][1] + return x + + +class HeadV1(nn.Module): + """v1 head: 4 backbone-feature projections -> shared upsample stack -> per-target output convs (points, mask).""" + + NUM_FEATURES = 4 + DIM_PROJ = 512 + DIM_OUT = (3, 1) # 3 channels for points, 1 for mask + LAST_CONV_CHANNELS = 32 + + def __init__(self, dim_in: int, dim_upsample: List[int] = (256, 128, 128), num_res_blocks: int = 1, dim_times_res_block_hidden: int = 1, + dtype=None, device=None, operations=comfy.ops.manual_cast): + super().__init__() + self.projects = nn.ModuleList([ + _conv2d(operations, dim_in, self.DIM_PROJ, k=1, dtype=dtype, device=device) + for _ in range(self.NUM_FEATURES) + ]) + def upsampler(in_ch, out_ch): + return nn.Sequential( + operations.ConvTranspose2d(in_ch, out_ch, kernel_size=2, stride=2, dtype=dtype, device=device), + _conv2d(operations, out_ch, out_ch, dtype=dtype, device=device), + ) + + in_chs = [self.DIM_PROJ] + list(dim_upsample[:-1]) + self.upsample_blocks = nn.ModuleList([ + nn.Sequential( + upsampler(in_ch + 2, out_ch), + *(ResidualConvBlock(out_ch, dim_times_res_block_hidden * out_ch, dtype=dtype, device=device, operations=operations) + for _ in range(num_res_blocks)) + ) + for in_ch, out_ch in zip(in_chs, dim_upsample) + ]) + self.output_block = nn.ModuleList([ + nn.Sequential( + _conv2d(operations, dim_upsample[-1] + 2, self.LAST_CONV_CHANNELS, dtype=dtype, device=device), + nn.ReLU(inplace=True), + _conv2d(operations, self.LAST_CONV_CHANNELS, d_out, k=1, dtype=dtype, device=device), + ) + for d_out in self.DIM_OUT + ]) + + def forward(self, hidden_states, image: torch.Tensor): + img_h, img_w = image.shape[-2:] + patch_h, patch_w = img_h // 14, img_w // 14 + aspect = img_w / img_h + x = torch.stack([ + proj(feat.permute(0, 2, 1).unflatten(2, (patch_h, patch_w)).contiguous()) + for proj, (feat, _cls) in zip(self.projects, hidden_states) + ], dim=1).sum(dim=1) + + for block in self.upsample_blocks: + x = block(_concat_view_plane_uv(x, aspect)) + + x = F.interpolate(x, (img_h, img_w), mode="bilinear", align_corners=False) + x = _concat_view_plane_uv(x, aspect) + return [block(x) for block in self.output_block] diff --git a/comfy/ldm/moge/panorama.py b/comfy/ldm/moge/panorama.py new file mode 100644 index 000000000..18d0cb665 --- /dev/null +++ b/comfy/ldm/moge/panorama.py @@ -0,0 +1,312 @@ +"""Panorama (equirectangular) inference helpers for MoGe. + +Splits an equirect into 12 perspective views via an icosahedron camera rig, runs +the model per view, and stitches per-view distance maps back into a single +equirect distance map via a multi-scale Poisson + gradient sparse solve. +Image sampling uses F.grid_sample (GPU); the sparse solve uses lsmr (CPU). +""" + + +from typing import Callable, List, Optional, Tuple + +import numpy as np +import torch +import torch.nn.functional as F + +from scipy.ndimage import convolve, map_coordinates +from scipy.sparse import vstack, csr_array +from scipy.sparse.linalg import lsmr + + +def _icosahedron_directions() -> np.ndarray: + """12 icosahedron-vertex directions (non-normalised, matching upstream's vertex order).""" + A = (1.0 + np.sqrt(5.0)) / 2.0 + return np.array([ + [0, 1, A], [0, -1, A], [0, 1, -A], [0, -1, -A], + [1, A, 0], [-1, A, 0], [1, -A, 0], [-1, -A, 0], + [A, 0, 1], [A, 0, -1], [-A, 0, 1], [-A, 0, -1], + ], dtype=np.float32) + + +def _intrinsics_from_fov(fov_x_rad: float, fov_y_rad: float) -> np.ndarray: + """Normalised-image (unit-square) K matrix.""" + fx = 0.5 / np.tan(fov_x_rad / 2) + fy = 0.5 / np.tan(fov_y_rad / 2) + return np.array([[fx, 0, 0.5], [0, fy, 0.5], [0, 0, 1]], dtype=np.float32) + + +def _extrinsics_look_at(eye: np.ndarray, target: np.ndarray, up: np.ndarray) -> np.ndarray: + """OpenCV-convention world->camera extrinsics for an array of look-at targets (N, 4, 4).""" + eye = np.asarray(eye, dtype=np.float32) + target = np.asarray(target, dtype=np.float32) + up = np.asarray(up, dtype=np.float32) + if target.ndim == 1: + target = target[None] + + fwd = target - eye + fwd = fwd / np.linalg.norm(fwd, axis=-1, keepdims=True).clip(1e-12) + right = np.cross(fwd, up) + right_norm = np.linalg.norm(right, axis=-1, keepdims=True) + # Fall back to an arbitrary perpendicular if forward is parallel to up. + parallel = right_norm.squeeze(-1) < 1e-6 + if parallel.any(): + alt_up = np.array([1, 0, 0], dtype=np.float32) + right = np.where(parallel[:, None], np.cross(fwd, alt_up), right) + right_norm = np.linalg.norm(right, axis=-1, keepdims=True) + right = right / right_norm.clip(1e-12) + new_up = np.cross(fwd, right) + + R = np.stack([right, new_up, fwd], axis=-2) + t = -np.einsum("nij,j->ni", R, eye) + E = np.zeros((R.shape[0], 4, 4), dtype=np.float32) + E[:, :3, :3] = R + E[:, :3, 3] = t + E[:, 3, 3] = 1.0 + return E + + +def get_panorama_cameras() -> Tuple[np.ndarray, List[np.ndarray]]: + """Returns (extrinsics (12, 4, 4), [intrinsics] * 12) for icosahedron views at 90 deg FoV.""" + targets = _icosahedron_directions() + eye = np.zeros(3, dtype=np.float32) + up = np.array([0, 0, 1], dtype=np.float32) + extrinsics = _extrinsics_look_at(eye, targets, up) + K = _intrinsics_from_fov(np.deg2rad(90.0), np.deg2rad(90.0)) + return extrinsics, [K] * len(targets) + + +def spherical_uv_to_directions(uv: np.ndarray) -> np.ndarray: + """Equirect UV in [0, 1] -> 3D unit-direction (Z up).""" + theta = (1 - uv[..., 0]) * (2 * np.pi) + phi = uv[..., 1] * np.pi + return np.stack([ + np.sin(phi) * np.cos(theta), + np.sin(phi) * np.sin(theta), + np.cos(phi), + ], axis=-1).astype(np.float32) + + +def directions_to_spherical_uv(directions: np.ndarray) -> np.ndarray: + """3D direction -> equirect UV in [0, 1].""" + n = np.linalg.norm(directions, axis=-1, keepdims=True).clip(1e-12) + d = directions / n + u = 1 - np.arctan2(d[..., 1], d[..., 0]) / (2 * np.pi) % 1.0 + v = np.arccos(d[..., 2].clip(-1, 1)) / np.pi + return np.stack([u, v], axis=-1).astype(np.float32) + + +def _uv_grid(H: int, W: int) -> np.ndarray: + """Pixel-center UV grid in [0, 1]; (H, W, 2).""" + u = (np.arange(W, dtype=np.float32) + 0.5) / W + v = (np.arange(H, dtype=np.float32) + 0.5) / H + return np.stack(np.meshgrid(u, v, indexing="xy"), axis=-1) + + +def _unproject_cv(uv: np.ndarray, depth: np.ndarray, + extrinsics: np.ndarray, intrinsics: np.ndarray) -> np.ndarray: + """Back-project pixels into world coords (OpenCV convention).""" + pix = np.concatenate([uv, np.ones_like(uv[..., :1])], axis=-1) + K_inv = np.linalg.inv(intrinsics) + cam = pix @ K_inv.T * depth[..., None] + cam_h = np.concatenate([cam, np.ones_like(cam[..., :1])], axis=-1) + E_inv = np.linalg.inv(extrinsics) + return (cam_h @ E_inv.T)[..., :3] + + +def _project_cv(points: np.ndarray, extrinsics: np.ndarray, intrinsics: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """World coords -> (uv, depth) in the camera (OpenCV convention).""" + pts_h = np.concatenate([points, np.ones_like(points[..., :1])], axis=-1) + cam = pts_h @ extrinsics.T + cam_xyz = cam[..., :3] + depth = cam_xyz[..., 2] + proj = cam_xyz @ intrinsics.T + uv = proj[..., :2] / proj[..., 2:3].clip(1e-12) + return uv.astype(np.float32), depth.astype(np.float32) + + +def _grid_sample_uv(img_bchw: torch.Tensor, uv: torch.Tensor, mode: str = "bilinear") -> torch.Tensor: + """Sample img_bchw at UV-in-[0,1] coords uv of shape (B, H, W, 2); replicate-border.""" + grid = uv * 2.0 - 1.0 + return F.grid_sample(img_bchw, grid, mode=mode, padding_mode="border", align_corners=False) + + +def split_panorama_image(image: torch.Tensor, extrinsics: np.ndarray, intrinsics: List[np.ndarray], resolution: int) -> torch.Tensor: + """(3, Hp, Wp) equirect on any device -> (N, 3, R, R) perspective crops on the same device.""" + device = image.device + N = len(extrinsics) + uv = _uv_grid(resolution, resolution) + sample_uvs = [] + for i in range(N): + world = _unproject_cv(uv, np.ones(uv.shape[:-1], dtype=np.float32), extrinsics[i], intrinsics[i]) + sample_uvs.append(directions_to_spherical_uv(world)) + sample_uvs = np.stack(sample_uvs, axis=0) + + img_bchw = image.unsqueeze(0).expand(N, -1, -1, -1).contiguous() + sample_uvs_t = torch.from_numpy(sample_uvs).to(device=device, dtype=image.dtype) + return _grid_sample_uv(img_bchw, sample_uvs_t, mode="bilinear") + + +def _poisson_equation(W: int, H: int, wrap_x: bool = False, wrap_y: bool = False): + """Sparse Laplacian operator over the H x W grid.""" + grid_index = np.arange(H * W).reshape(H, W) + grid_index = np.pad(grid_index, ((0, 0), (1, 1)), mode="wrap" if wrap_x else "edge") + grid_index = np.pad(grid_index, ((1, 1), (0, 0)), mode="wrap" if wrap_y else "edge") + + data = np.array([[-4, 1, 1, 1, 1]], dtype=np.float32).repeat(H * W, axis=0).reshape(-1) + indices = np.stack([ + grid_index[1:-1, 1:-1], + grid_index[:-2, 1:-1], grid_index[2:, 1:-1], + grid_index[1:-1, :-2], grid_index[1:-1, 2:], + ], axis=-1).reshape(-1) + indptr = np.arange(0, H * W * 5 + 1, 5) + return csr_array((data, indices, indptr), shape=(H * W, H * W)) + + +def _grad_equation(W: int, H: int, wrap_x: bool = False, wrap_y: bool = False): + """Sparse forward-difference operator over the H x W grid.""" + grid_index = np.arange(W * H).reshape(H, W) + if wrap_x: + grid_index = np.pad(grid_index, ((0, 0), (0, 1)), mode="wrap") + if wrap_y: + grid_index = np.pad(grid_index, ((0, 1), (0, 0)), mode="wrap") + + data = np.concatenate([ + np.concatenate([ + np.ones((grid_index.shape[0], grid_index.shape[1] - 1), dtype=np.float32).reshape(-1, 1), + -np.ones((grid_index.shape[0], grid_index.shape[1] - 1), dtype=np.float32).reshape(-1, 1), + ], axis=1).reshape(-1), + np.concatenate([ + np.ones((grid_index.shape[0] - 1, grid_index.shape[1]), dtype=np.float32).reshape(-1, 1), + -np.ones((grid_index.shape[0] - 1, grid_index.shape[1]), dtype=np.float32).reshape(-1, 1), + ], axis=1).reshape(-1), + ]) + indices = np.concatenate([ + np.concatenate([grid_index[:, :-1].reshape(-1, 1), grid_index[:, 1:].reshape(-1, 1)], axis=1).reshape(-1), + np.concatenate([grid_index[:-1, :].reshape(-1, 1), grid_index[1:, :].reshape(-1, 1)], axis=1).reshape(-1), + ]) + nx = grid_index.shape[0] * (grid_index.shape[1] - 1) + ny = (grid_index.shape[0] - 1) * grid_index.shape[1] + indptr = np.arange(0, nx * 2 + ny * 2 + 1, 2) + return csr_array((data, indices, indptr), shape=(nx + ny, H * W)) + + +def _scipy_remap_bilinear(img: np.ndarray, sample_pixels: np.ndarray, mode: str = "bilinear") -> np.ndarray: + """Bilinear/nearest sampling at fractional pixel coords; out-of-range clamps to nearest border.""" + H, W = img.shape[:2] + yy = np.clip(sample_pixels[..., 1], 0, H - 1) + xx = np.clip(sample_pixels[..., 0], 0, W - 1) + order = 1 if mode == "bilinear" else 0 + if img.ndim == 2: + return map_coordinates(img, [yy, xx], order=order, mode="nearest").astype(img.dtype) + out = np.stack([ + map_coordinates(img[..., c], [yy, xx], order=order, mode="nearest") + for c in range(img.shape[-1]) + ], axis=-1) + return out.astype(img.dtype) + + +def merge_panorama_depth(width: int, height: int, + distance_maps: List[np.ndarray], pred_masks: List[np.ndarray], + extrinsics: List[np.ndarray], intrinsics: List[np.ndarray], + on_view: Optional[Callable[[], None]] = None, + on_solve_start: Optional[Callable[[int, int], None]] = None, + on_solve_end: Optional[Callable[[int, int], None]] = None, + ) -> Tuple[np.ndarray, np.ndarray]: + """Stitch per-view distance maps into a single equirect distance map. + + Recursive multi-scale solve: solves at half resolution first and uses that as the lsmr init + for the full-resolution solve. Optional callbacks fire per view processed and around each + lsmr solve so callers can drive a progress bar. + """ + + if max(width, height) > 256: + coarse_depth, _ = merge_panorama_depth(width // 2, height // 2, + distance_maps, pred_masks, extrinsics, intrinsics, + on_view=on_view, + on_solve_start=on_solve_start, + on_solve_end=on_solve_end) + t = torch.from_numpy(coarse_depth).unsqueeze(0).unsqueeze(0) + t = F.interpolate(t, size=(height, width), mode="bilinear", align_corners=False) + depth_init = t.squeeze().numpy().astype(np.float32) + else: + depth_init = None + + spherical_directions = spherical_uv_to_directions(_uv_grid(height, width)) + + pano_log_grad_maps, pano_grad_masks = [], [] + pano_log_lap_maps, pano_lap_masks = [], [] + pano_pred_masks: List[np.ndarray] = [] + + for i in range(len(distance_maps)): + proj_uv, proj_depth = _project_cv(spherical_directions, extrinsics[i], intrinsics[i]) + proj_valid = (proj_depth > 0) & (proj_uv > 0).all(axis=-1) & (proj_uv < 1).all(axis=-1) + + Hd, Wd = distance_maps[i].shape[:2] + proj_pixels = np.clip(proj_uv, 0, 1) * np.array([Wd - 1, Hd - 1], dtype=np.float32) + + log_dist = np.log(np.clip(distance_maps[i], 1e-6, None)) + sampled = _scipy_remap_bilinear(log_dist, proj_pixels, mode="bilinear") + pano_log = np.where(proj_valid, sampled, 0.0).astype(np.float32) + + sampled_mask = _scipy_remap_bilinear(pred_masks[i].astype(np.uint8), proj_pixels, mode="nearest") + pano_pred = proj_valid & (sampled_mask > 0) + + # Equirect wraps horizontally but not vertically: wrap pad along x, edge pad along y. + padded = np.pad(pano_log, ((0, 0), (0, 1)), mode="wrap") + gx, gy = padded[:, :-1] - padded[:, 1:], padded[:-1, :] - padded[1:, :] + padded_m = np.pad(pano_pred, ((0, 0), (0, 1)), mode="wrap") + mx, my = padded_m[:, :-1] & padded_m[:, 1:], padded_m[:-1, :] & padded_m[1:, :] + pano_log_grad_maps.append((gx, gy)) + pano_grad_masks.append((mx, my)) + + padded = np.pad(pano_log, ((1, 1), (0, 0)), mode="edge") + padded = np.pad(padded, ((0, 0), (1, 1)), mode="wrap") + lap_kernel = np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]], dtype=np.float32) + lap = convolve(padded, lap_kernel)[1:-1, 1:-1] + padded_m = np.pad(pano_pred, ((1, 1), (0, 0)), mode="edge") + padded_m = np.pad(padded_m, ((0, 0), (1, 1)), mode="wrap") + m_kernel = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.uint8) + lap_mask = convolve(padded_m.astype(np.uint8), m_kernel)[1:-1, 1:-1] == 5 + pano_log_lap_maps.append(lap) + pano_lap_masks.append(lap_mask) + pano_pred_masks.append(pano_pred) + + if on_view is not None: + on_view() + + gx = np.stack([m[0] for m in pano_log_grad_maps], axis=0) + gy = np.stack([m[1] for m in pano_log_grad_maps], axis=0) + mx = np.stack([m[0] for m in pano_grad_masks], axis=0) + my = np.stack([m[1] for m in pano_grad_masks], axis=0) + gx_avg = (gx * mx).sum(axis=0) / mx.sum(axis=0).clip(1e-3) + gy_avg = (gy * my).sum(axis=0) / my.sum(axis=0).clip(1e-3) + + laps = np.stack(pano_log_lap_maps, axis=0) + lap_masks = np.stack(pano_lap_masks, axis=0) + lap_avg = (laps * lap_masks).sum(axis=0) / lap_masks.sum(axis=0).clip(1e-3) + + grad_x_mask = mx.any(axis=0).reshape(-1) + grad_y_mask = my.any(axis=0).reshape(-1) + grad_mask = np.concatenate([grad_x_mask, grad_y_mask]) + lap_mask_flat = lap_masks.any(axis=0).reshape(-1) + + A = vstack([ + _grad_equation(width, height, wrap_x=True, wrap_y=False)[grad_mask], + _poisson_equation(width, height, wrap_x=True, wrap_y=False)[lap_mask_flat], + ]) + b = np.concatenate([ + gx_avg.reshape(-1)[grad_x_mask], + gy_avg.reshape(-1)[grad_y_mask], + lap_avg.reshape(-1)[lap_mask_flat], + ]) + x0 = np.log(np.clip(depth_init, 1e-6, None)).reshape(-1) if depth_init is not None else None + + if on_solve_start is not None: + on_solve_start(width, height) + x, *_ = lsmr(A, b, atol=1e-5, btol=1e-5, x0=x0, show=False) + if on_solve_end is not None: + on_solve_end(width, height) + + pano_depth = np.exp(x).reshape(height, width).astype(np.float32) + pano_mask = np.any(pano_pred_masks, axis=0) + return pano_depth, pano_mask diff --git a/comfy/ldm/sam3/detector.py b/comfy/ldm/sam3/detector.py index 12d3a01ab..23a972ac7 100644 --- a/comfy/ldm/sam3/detector.py +++ b/comfy/ldm/sam3/detector.py @@ -561,7 +561,8 @@ class SAM3Model(nn.Module): return high_res_masks def forward_video(self, images, initial_masks, pbar=None, text_prompts=None, - new_det_thresh=0.5, max_objects=0, detect_interval=1): + new_det_thresh=0.5, max_objects=0, detect_interval=1, + target_device=None, target_dtype=None): """Track video with optional per-frame text-prompted detection.""" bb = self.detector.backbone["vision_backbone"] @@ -589,8 +590,10 @@ class SAM3Model(nn.Module): return self.tracker.track_video_with_detection( backbone_fn, images, initial_masks, detect_fn, new_det_thresh=new_det_thresh, max_objects=max_objects, - detect_interval=detect_interval, backbone_obj=bb, pbar=pbar) + detect_interval=detect_interval, backbone_obj=bb, pbar=pbar, + target_device=target_device, target_dtype=target_dtype) # SAM3 (non-multiplex) — no detection support, requires initial masks if initial_masks is None: raise ValueError("SAM3 (non-multiplex) requires initial_mask for video tracking") - return self.tracker.track_video(backbone_fn, images, initial_masks, pbar=pbar, backbone_obj=bb) + return self.tracker.track_video(backbone_fn, images, initial_masks, pbar=pbar, backbone_obj=bb, + target_device=target_device, target_dtype=target_dtype) diff --git a/comfy/ldm/sam3/tracker.py b/comfy/ldm/sam3/tracker.py index 8f7481003..8456e90a6 100644 --- a/comfy/ldm/sam3/tracker.py +++ b/comfy/ldm/sam3/tracker.py @@ -200,8 +200,13 @@ def pack_masks(masks): def unpack_masks(packed): """Unpack bit-packed [*, H, W//8] uint8 to bool [*, H, W*8].""" - shifts = torch.arange(8, device=packed.device) - return ((packed.unsqueeze(-1) >> shifts) & 1).view(*packed.shape[:-1], -1).bool() + bits = torch.tensor([1, 2, 4, 8, 16, 32, 64, 128], dtype=torch.uint8, device=packed.device) + return (packed.unsqueeze(-1) & bits).bool().view(*packed.shape[:-1], -1) + + +def _prep_frame(images, idx, device, dt, size): + """Slice CPU full-res frames, transfer to GPU in target dtype, and resize to (size, size).""" + return comfy.utils.common_upscale(images[idx].to(device=device, dtype=dt), size, size, "bicubic", crop="disabled") def _compute_backbone(backbone_fn, frame, frame_idx=None): @@ -1078,16 +1083,19 @@ class SAM3Tracker(nn.Module): # SAM3: drop last FPN level return vision_feats[:-1], vision_pos[:-1], feat_sizes[:-1] - def _track_single_object(self, backbone_fn, images, initial_mask, pbar=None): + def _track_single_object(self, backbone_fn, images, initial_mask, pbar=None, + target_device=None, target_dtype=None): """Track one object, computing backbone per frame to save VRAM.""" N = images.shape[0] - device, dt = images.device, images.dtype + device = target_device if target_device is not None else images.device + dt = target_dtype if target_dtype is not None else images.dtype + size = self.image_size output_dict = {"cond_frame_outputs": {}, "non_cond_frame_outputs": {}} all_masks = [] for frame_idx in tqdm(range(N), desc="tracking"): vision_feats, vision_pos, feat_sizes = self._compute_backbone_frame( - backbone_fn, images[frame_idx:frame_idx + 1], frame_idx=frame_idx) + backbone_fn, _prep_frame(images, slice(frame_idx, frame_idx + 1), device, dt, size), frame_idx=frame_idx) mask_input = None if frame_idx == 0: mask_input = F.interpolate(initial_mask.to(device=device, dtype=dt), @@ -1114,12 +1122,13 @@ class SAM3Tracker(nn.Module): return torch.cat(all_masks, dim=0) # [N, 1, H, W] - def track_video(self, backbone_fn, images, initial_masks, pbar=None, **kwargs): + def track_video(self, backbone_fn, images, initial_masks, pbar=None, + target_device=None, target_dtype=None, **kwargs): """Track one or more objects across video frames. Args: backbone_fn: callable that returns (sam2_features, sam2_positions, trunk_out) for a frame - images: [N, 3, 1008, 1008] video frames + images: [N, 3, H, W] CPU full-res video frames (resized per-frame to self.image_size) initial_masks: [N_obj, 1, H, W] binary masks for first frame (one per object) pbar: optional progress bar @@ -1130,7 +1139,8 @@ class SAM3Tracker(nn.Module): per_object = [] for obj_idx in range(N_obj): obj_masks = self._track_single_object( - backbone_fn, images, initial_masks[obj_idx:obj_idx + 1], pbar=pbar) + backbone_fn, images, initial_masks[obj_idx:obj_idx + 1], pbar=pbar, + target_device=target_device, target_dtype=target_dtype) per_object.append(obj_masks) return torch.cat(per_object, dim=1) # [N, N_obj, H, W] @@ -1632,11 +1642,18 @@ class SAM31Tracker(nn.Module): return det_scores[new_dets].tolist() if det_scores is not None else [0.0] * new_dets.sum().item() return [] + INTERNAL_MAX_OBJECTS = 64 # Hard ceiling on accumulated tracks; max_objects=0 or any value above this is clamped here. + def track_video_with_detection(self, backbone_fn, images, initial_masks, detect_fn=None, new_det_thresh=0.5, max_objects=0, detect_interval=1, - backbone_obj=None, pbar=None): + backbone_obj=None, pbar=None, target_device=None, target_dtype=None): """Track with optional per-frame detection. Returns [N, max_N_obj, H, W] mask logits.""" - N, device, dt = images.shape[0], images.device, images.dtype + if max_objects <= 0 or max_objects > self.INTERNAL_MAX_OBJECTS: + max_objects = self.INTERNAL_MAX_OBJECTS + N = images.shape[0] + device = target_device if target_device is not None else images.device + dt = target_dtype if target_dtype is not None else images.dtype + size = self.image_size output_dict = {"cond_frame_outputs": {}, "non_cond_frame_outputs": {}} all_masks = [] idev = comfy.model_management.intermediate_device() @@ -1656,7 +1673,7 @@ class SAM31Tracker(nn.Module): prefetch = True except RuntimeError: pass - cur_bb = self._compute_backbone_frame(backbone_fn, images[0:1], frame_idx=0) + cur_bb = self._compute_backbone_frame(backbone_fn, _prep_frame(images, slice(0, 1), device, dt, size), frame_idx=0) for frame_idx in tqdm(range(N), desc="tracking"): vision_feats, vision_pos, feat_sizes, high_res_prop, trunk_out = cur_bb @@ -1666,7 +1683,7 @@ class SAM31Tracker(nn.Module): backbone_stream.wait_stream(torch.cuda.current_stream(device)) with torch.cuda.stream(backbone_stream): next_bb = self._compute_backbone_frame( - backbone_fn, images[frame_idx + 1:frame_idx + 2], frame_idx=frame_idx + 1) + backbone_fn, _prep_frame(images, slice(frame_idx + 1, frame_idx + 2), device, dt, size), frame_idx=frame_idx + 1) # Per-frame detection with NMS (skip if no detect_fn, or interval/max not met) det_masks = torch.empty(0, device=device) @@ -1687,7 +1704,7 @@ class SAM31Tracker(nn.Module): current_out = self._condition_with_masks( initial_masks.to(device=device, dtype=dt), frame_idx, vision_feats, vision_pos, feat_sizes, high_res_prop, output_dict, N, mux_state, backbone_obj, - images[frame_idx:frame_idx + 1], trunk_out) + _prep_frame(images, slice(frame_idx, frame_idx + 1), device, dt, size), trunk_out) last_occluded = torch.full((mux_state.total_valid_entries,), -1, device=device, dtype=torch.long) obj_scores = [1.0] * mux_state.total_valid_entries if keep_alive is not None: @@ -1702,7 +1719,7 @@ class SAM31Tracker(nn.Module): current_out = self._condition_with_masks( det_masks, frame_idx, vision_feats, vision_pos, feat_sizes, high_res_prop, output_dict, N, mux_state, backbone_obj, - images[frame_idx:frame_idx + 1], trunk_out, threshold=0.0) + _prep_frame(images, slice(frame_idx, frame_idx + 1), device, dt, size), trunk_out, threshold=0.0) last_occluded = torch.full((mux_state.total_valid_entries,), -1, device=device, dtype=torch.long) obj_scores = det_scores[:mux_state.total_valid_entries].tolist() if keep_alive is not None: @@ -1718,7 +1735,7 @@ class SAM31Tracker(nn.Module): torch.cuda.current_stream(device).wait_stream(backbone_stream) cur_bb = next_bb else: - cur_bb = self._compute_backbone_frame(backbone_fn, images[frame_idx + 1:frame_idx + 2], frame_idx=frame_idx + 1) + cur_bb = self._compute_backbone_frame(backbone_fn, _prep_frame(images, slice(frame_idx + 1, frame_idx + 2), device, dt, size), frame_idx=frame_idx + 1) continue else: N_obj = mux_state.total_valid_entries @@ -1768,7 +1785,7 @@ class SAM31Tracker(nn.Module): torch.cuda.current_stream(device).wait_stream(backbone_stream) cur_bb = next_bb else: - cur_bb = self._compute_backbone_frame(backbone_fn, images[frame_idx + 1:frame_idx + 2], frame_idx=frame_idx + 1) + cur_bb = self._compute_backbone_frame(backbone_fn, _prep_frame(images, slice(frame_idx + 1, frame_idx + 2), device, dt, size), frame_idx=frame_idx + 1) if not all_masks or all(m is None for m in all_masks): return {"packed_masks": None, "n_frames": N, "scores": []} diff --git a/comfy/ldm/wan/model.py b/comfy/ldm/wan/model.py index b2287dba9..70dfe7b16 100644 --- a/comfy/ldm/wan/model.py +++ b/comfy/ldm/wan/model.py @@ -1135,7 +1135,7 @@ class AudioInjector_WAN(nn.Module): self.injector_adain_output_layers = nn.ModuleList( [operations.Linear(dim, dim, dtype=dtype, device=device) for _ in range(audio_injector_id)]) - def forward(self, x, block_id, audio_emb, audio_emb_global, seq_len): + def forward(self, x, block_id, audio_emb, audio_emb_global, seq_len, scale=1.0): audio_attn_id = self.injected_block_id.get(block_id, None) if audio_attn_id is None: return x @@ -1148,12 +1148,15 @@ class AudioInjector_WAN(nn.Module): attn_hidden_states = adain_hidden_states else: attn_hidden_states = self.injector_pre_norm_feat[audio_attn_id](input_hidden_states) - audio_emb = rearrange(audio_emb, "b t n c -> (b t) n c", t=num_frames) - attn_audio_emb = audio_emb + + if audio_emb.dim() == 3: # WanDancer case + attn_audio_emb = rearrange(audio_emb, "b t c -> (b t) 1 c", t=num_frames) + else: # S2V case + attn_audio_emb = rearrange(audio_emb, "b t n c -> (b t) n c", t=num_frames) + residual_out = self.injector[audio_attn_id](x=attn_hidden_states, context=attn_audio_emb) - residual_out = rearrange( - residual_out, "(b t) n c -> b (t n) c", t=num_frames) - x[:, :seq_len] = x[:, :seq_len] + residual_out + residual_out = rearrange(residual_out, "(b t) n c -> b (t n) c", t=num_frames) + x[:, :seq_len] = x[:, :seq_len] + residual_out * scale return x diff --git a/comfy/ldm/wan/model_wandancer.py b/comfy/ldm/wan/model_wandancer.py new file mode 100644 index 000000000..3caef6dc5 --- /dev/null +++ b/comfy/ldm/wan/model_wandancer.py @@ -0,0 +1,251 @@ +import torch +import torch.nn as nn +import comfy +from comfy.ldm.modules.attention import optimized_attention +from comfy.ldm.flux.math import apply_rope1 +from comfy.ldm.flux.layers import EmbedND + +from .model import AudioInjector_WAN, WanModel, MLPProj, Head, sinusoidal_embedding_1d + + +class MusicSelfAttention(nn.Module): + def __init__(self, dim, num_heads, device=None, dtype=None, operations=None): + assert dim % num_heads == 0 + super().__init__() + self.embed_dim = dim + self.num_heads = num_heads + self.head_dim = dim // num_heads + + self.q_proj = operations.Linear(dim, dim, device=device, dtype=dtype) + self.k_proj = operations.Linear(dim, dim, device=device, dtype=dtype) + self.v_proj = operations.Linear(dim, dim, device=device, dtype=dtype) + self.out_proj = operations.Linear(dim, dim, device=device, dtype=dtype) + + def forward(self, x, freqs): + b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim + + q = self.q_proj(x).view(b, s, n, d) + q = apply_rope1(q, freqs) + + k = self.k_proj(x).view(b, s, n, d) + k = apply_rope1(k, freqs) + + x = optimized_attention( + q.view(b, s, n * d), + k.view(b, s, n * d), + self.v_proj(x).view(b, s, n * d), + heads=self.num_heads, + ) + + return self.out_proj(x) + + +class MusicEncoderLayer(nn.Module): + def __init__(self, dim: int, num_heads: int, ffn_dim: int, device=None, dtype=None, operations=None): + super().__init__() + self.self_attn = MusicSelfAttention(dim, num_heads, device=device, dtype=dtype, operations=operations) + + self.linear1 = operations.Linear(dim, ffn_dim, device=device, dtype=dtype) + self.linear2 = operations.Linear(ffn_dim, dim, device=device, dtype=dtype) + + self.norm1 = operations.LayerNorm(dim, device=device, dtype=dtype) + self.norm2 = operations.LayerNorm(dim, device=device, dtype=dtype) + + def forward(self, x: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor: + x = x + self.self_attn(self.norm1(x), freqs=freqs) + x = x + self.linear2(torch.nn.functional.gelu(self.linear1(self.norm2(x)))) # ffn + return x + + +class WanDancerModel(WanModel): + def __init__(self, + model_type='wandancer', + patch_size=(1, 2, 2), + text_len=512, + in_dim=16, + dim=5120, + ffn_dim=8192, + freq_dim=256, + text_dim=4096, + out_dim=16, + num_heads=16, + num_layers=40, + window_size=(-1, -1), + qk_norm=True, + cross_attn_norm=True, + eps=1e-6, + in_dim_ref_conv=None, + image_model=None, + device=None, dtype=None, operations=None, + audio_inject_layers=[0, 4, 8, 12, 16, 20, 24, 27], + music_dim = 256, + music_heads = 4, + music_feature_dim = 35, + music_latent_dim = 256 + ): + + super().__init__(model_type='i2v', patch_size=patch_size, text_len=text_len, in_dim=in_dim, dim=dim, ffn_dim=ffn_dim, freq_dim=freq_dim, text_dim=text_dim, out_dim=out_dim, + num_heads=num_heads, num_layers=num_layers, window_size=window_size, qk_norm=qk_norm, cross_attn_norm=cross_attn_norm, eps=eps, image_model=image_model, in_dim_ref_conv=in_dim_ref_conv, + device=device, dtype=dtype, operations=operations) + + self.dtype = dtype + operation_settings = {"operations": operations, "device": device, "dtype": dtype} + + self.patch_embedding_global = operations.Conv3d(in_dim, dim, kernel_size=patch_size, stride=patch_size, device=operation_settings.get("device"), dtype=torch.float32) + self.img_emb_refimage = MLPProj(1280, dim, operation_settings=operation_settings) + self.head_global = Head(dim, out_dim, patch_size, eps, operation_settings=operation_settings) + + self.music_injector = AudioInjector_WAN( + dim=self.dim, + num_heads=self.num_heads, + inject_layer=audio_inject_layers, + root_net=self, + enable_adain=False, + dtype=dtype, device=device, operations=operations + ) + + self.music_projection = operations.Linear(music_feature_dim, music_latent_dim, device=device, dtype=dtype) + self.music_encoder = nn.ModuleList([MusicEncoderLayer(dim=music_dim, num_heads=music_heads, ffn_dim=1024, device=device, dtype=dtype, operations=operations) for _ in range(2)]) + music_head_dim = music_dim // music_heads + self.music_rope_embedder = EmbedND(dim=music_head_dim, theta=10000.0, axes_dim=[music_head_dim]) + + def forward_orig(self, x, t, context, clip_fea=None, clip_fea_ref=None, freqs=None, audio_embed=None, fps=30, audio_inject_scale=1.0, transformer_options={}, **kwargs): + # embeddings + if int(fps + 0.5) != 30: + x = self.patch_embedding_global(x.float()).to(x.dtype) + else: + x = self.patch_embedding(x.float()).to(x.dtype) + + grid_sizes = x.shape[2:] + latent_frames = grid_sizes[0] + transformer_options["grid_sizes"] = grid_sizes + x = x.flatten(2).transpose(1, 2) + seq_len = x.size(1) + + # time embeddings + e = self.time_embedding(sinusoidal_embedding_1d(self.freq_dim, t.flatten()).to(dtype=x[0].dtype)) + e = e.reshape(t.shape[0], -1, e.shape[-1]) + e0 = self.time_projection(e).unflatten(2, (6, self.dim)) + + full_ref = None + if self.ref_conv is not None: # model has the weight, but this wasn't used in the original pipeline + full_ref = kwargs.get("reference_latent", None) + if full_ref is not None: + full_ref = self.ref_conv(full_ref).flatten(2).transpose(1, 2) + x = torch.concat((full_ref, x), dim=1) + + # context + context = self.text_embedding(context) + + audio_emb = None + if audio_embed is not None: # encode music feature,[1, frame_num, 35] -> [1, F*8, dim] + music_feature = self.music_projection(audio_embed) + + music_seq_len = music_feature.shape[1] + music_ids = torch.arange(music_seq_len, device=music_feature.device, dtype=music_feature.dtype).reshape(1, -1, 1) # create 1D position IDs + music_freqs = self.music_rope_embedder(music_ids).movedim(1, 2) + + # apply encoder layers + for layer in self.music_encoder: + music_feature = layer(music_feature, music_freqs) + + # interpolate + audio_emb = torch.nn.functional.interpolate(music_feature.unsqueeze(1), size=(latent_frames * 8, self.dim), mode='bilinear').squeeze(1) + + context_img_len = 0 + if self.img_emb is not None and clip_fea is not None: + context_clip = self.img_emb(clip_fea) # bs x 257 x dim + context = torch.cat([context_clip, context], dim=1) + context_img_len += clip_fea.shape[-2] + if self.img_emb_refimage is not None and clip_fea_ref is not None: + context_clip_ref = self.img_emb_refimage(clip_fea_ref) + context = torch.cat([context_clip_ref, context], dim=1) + context_img_len += clip_fea_ref.shape[-2] + + patches_replace = transformer_options.get("patches_replace", {}) + blocks_replace = patches_replace.get("dit", {}) + transformer_options["total_blocks"] = len(self.blocks) + transformer_options["block_type"] = "double" + for i, block in enumerate(self.blocks): + transformer_options["block_index"] = i + if ("double_block", i) in blocks_replace: + def block_wrap(args): + out = {} + out["img"] = block(args["img"], context=args["txt"], e=args["vec"], freqs=args["pe"], context_img_len=context_img_len, transformer_options=args["transformer_options"]) + return out + out = blocks_replace[("double_block", i)]({"img": x, "txt": context, "vec": e0, "pe": freqs, "transformer_options": transformer_options}, {"original_block": block_wrap}) + x = out["img"] + else: + x = block(x, e=e0, freqs=freqs, context=context, context_img_len=context_img_len, transformer_options=transformer_options) + if audio_emb is not None: + x = self.music_injector(x, i, audio_emb, audio_emb_global=None, seq_len=seq_len, scale=audio_inject_scale) + + # head + if int(fps + 0.5) != 30: + x = self.head_global(x, e) + else: + x = self.head(x, e) + + if full_ref is not None: + x = x[:, full_ref.shape[1]:] + + # unpatchify + x = self.unpatchify(x, grid_sizes) + return x + + def _forward(self, x, timestep, context, clip_fea=None, time_dim_concat=None, transformer_options={}, clip_fea_ref=None, fps=30, audio_inject_scale=1.0, **kwargs): + bs, c, t, h, w = x.shape + x = comfy.ldm.common_dit.pad_to_patch_size(x, self.patch_size) + + t_len = t + if time_dim_concat is not None: + time_dim_concat = comfy.ldm.common_dit.pad_to_patch_size(time_dim_concat, self.patch_size) + x = torch.cat([x, time_dim_concat], dim=2) + t_len = x.shape[2] + + freqs = self.rope_encode(t_len, h, w, device=x.device, dtype=x.dtype, fps=fps, transformer_options=transformer_options) + return self.forward_orig(x, timestep, context, clip_fea=clip_fea, clip_fea_ref=clip_fea_ref, freqs=freqs, fps=fps, audio_inject_scale=audio_inject_scale, transformer_options=transformer_options, **kwargs)[:, :, :t, :h, :w] + + def rope_encode(self, t, h, w, t_start=0, steps_t=None, steps_h=None, steps_w=None, fps=30, device=None, dtype=None, transformer_options={}): + patch_size = self.patch_size + t_len = ((t + (patch_size[0] // 2)) // patch_size[0]) + h_len = ((h + (patch_size[1] // 2)) // patch_size[1]) + w_len = ((w + (patch_size[2] // 2)) // patch_size[2]) + + if steps_t is None: + steps_t = t_len + if steps_h is None: + steps_h = h_len + if steps_w is None: + steps_w = w_len + + h_start = 0 + w_start = 0 + rope_options = transformer_options.get("rope_options", None) + if rope_options is not None: + t_len = (t_len - 1.0) * rope_options.get("scale_t", 1.0) + 1.0 + h_len = (h_len - 1.0) * rope_options.get("scale_y", 1.0) + 1.0 + w_len = (w_len - 1.0) * rope_options.get("scale_x", 1.0) + 1.0 + + t_start += rope_options.get("shift_t", 0.0) + h_start += rope_options.get("shift_y", 0.0) + w_start += rope_options.get("shift_x", 0.0) + + img_ids = torch.zeros((steps_t, steps_h, steps_w, 3), device=device, dtype=dtype) + + if int(fps + 0.5) != 30: + time_scale = 30.0 / fps # how many time units each frame represents relative to 30fps + positions_new = torch.arange(steps_t, device=device, dtype=dtype) * time_scale + t_start + total_frames_at_30fps = int(time_scale * steps_t + 0.5) + positions_new[-1] = t_start + (total_frames_at_30fps - 1) + + img_ids[:, :, :, 0] = img_ids[:, :, :, 0] + positions_new.reshape(-1, 1, 1) + else: + img_ids[:, :, :, 0] = img_ids[:, :, :, 0] + torch.linspace(t_start, t_start + (t_len - 1), steps=steps_t, device=device, dtype=dtype).reshape(-1, 1, 1) + + img_ids[:, :, :, 1] = img_ids[:, :, :, 1] + torch.linspace(h_start, h_start + (h_len - 1), steps=steps_h, device=device, dtype=dtype).reshape(1, -1, 1) + img_ids[:, :, :, 2] = img_ids[:, :, :, 2] + torch.linspace(w_start, w_start + (w_len - 1), steps=steps_w, device=device, dtype=dtype).reshape(1, 1, -1) + img_ids = img_ids.reshape(1, -1, img_ids.shape[-1]) + + freqs = self.rope_embedder(img_ids).movedim(1, 2) + return freqs diff --git a/comfy/lora.py b/comfy/lora.py index db8f16bcb..4e0ea29e0 100644 --- a/comfy/lora.py +++ b/comfy/lora.py @@ -16,7 +16,6 @@ along with this program. If not, see . """ -from __future__ import annotations import comfy.memory_management import comfy.utils import comfy.model_management @@ -97,12 +96,14 @@ def load_lora(lora, to_load, log_missing=True): def model_lora_keys_clip(model, key_map={}): sdk = model.state_dict().keys() + prefix_set = set() for k in sdk: if k.endswith(".weight"): key_map["text_encoders.{}".format(k[:-len(".weight")])] = k #generic lora format without any weird key names tp = k.find(".transformer.") #also map without wrapper prefix for composite text encoder models if tp > 0 and not k.startswith("clip_"): key_map["text_encoders.{}".format(k[tp + 1:-len(".weight")])] = k + prefix_set.add(k.split('.')[0]) text_model_lora_key = "lora_te_text_model_encoder_layers_{}_{}" clip_l_present = False @@ -163,6 +164,13 @@ def model_lora_keys_clip(model, key_map={}): lora_key = "lora_te1_{}".format(l_key.replace(".", "_")) key_map[lora_key] = k + if len(prefix_set) == 1: + full_prefix = "{}.transformer.model.".format(next(iter(prefix_set))) # kohya anima and maybe other single TE models that use a single llama arch based te + for k in sdk: + if k.endswith(".weight"): + if k.startswith(full_prefix): + l_key = k[len(full_prefix):-len(".weight")] + key_map["lora_te_{}".format(l_key.replace(".", "_"))] = k k = "clip_g.transformer.text_projection.weight" if k in sdk: @@ -475,16 +483,23 @@ def calculate_weight(patches, weight, key, intermediate_dtype=torch.float32, ori return weight -def prefetch_prepared_value(value, allocate_buffer, stream): +def prefetch_prepared_value(value, counter, destination, stream, copy): if isinstance(value, torch.Tensor): - dest = allocate_buffer(comfy.memory_management.vram_aligned_size(value)) - comfy.model_management.cast_to_gathered([value], dest, non_blocking=True, stream=stream) + size = comfy.memory_management.vram_aligned_size(value) + offset = counter[0] + counter[0] += size + if destination is None: + return value + + dest = destination[offset:offset + size] + if copy: + comfy.model_management.cast_to_gathered([value], dest, non_blocking=True, stream=stream) return comfy.memory_management.interpret_gathered_like([value], dest)[0] elif isinstance(value, weight_adapter.WeightAdapterBase): - return type(value)(value.loaded_keys, prefetch_prepared_value(value.weights, allocate_buffer, stream)) + return type(value)(value.loaded_keys, prefetch_prepared_value(value.weights, counter, destination, stream, copy)) elif isinstance(value, tuple): - return tuple(prefetch_prepared_value(item, allocate_buffer, stream) for item in value) + return tuple(prefetch_prepared_value(item, counter, destination, stream, copy) for item in value) elif isinstance(value, list): - return [prefetch_prepared_value(item, allocate_buffer, stream) for item in value] + return [prefetch_prepared_value(item, counter, destination, stream, copy) for item in value] return value diff --git a/comfy/memory_management.py b/comfy/memory_management.py index 48e3c11da..962addb27 100644 --- a/comfy/memory_management.py +++ b/comfy/memory_management.py @@ -1,6 +1,5 @@ import math import ctypes -import threading import dataclasses import torch from typing import NamedTuple @@ -10,12 +9,12 @@ from comfy.quant_ops import QuantizedTensor class TensorFileSlice(NamedTuple): file_ref: object - thread_id: int + lock: object offset: int size: int -def read_tensor_file_slice_into(tensor, destination): +def read_tensor_file_slice_into(tensor, destination, stream=None, destination2=None): if isinstance(tensor, QuantizedTensor): if not isinstance(destination, QuantizedTensor): @@ -23,12 +22,17 @@ def read_tensor_file_slice_into(tensor, destination): if tensor._layout_cls != destination._layout_cls: return False - if not read_tensor_file_slice_into(tensor._qdata, destination._qdata): + if not read_tensor_file_slice_into(tensor._qdata, destination._qdata, stream=stream, + destination2=(destination2._qdata if destination2 is not None else None)): return False dst_orig_dtype = destination._params.orig_dtype destination._params.copy_from(tensor._params, non_blocking=False) destination._params = dataclasses.replace(destination._params, orig_dtype=dst_orig_dtype) + if destination2 is not None: + dst_orig_dtype = destination2._params.orig_dtype + destination2._params.copy_from(destination._params, non_blocking=True) + destination2._params = dataclasses.replace(destination2._params, orig_dtype=dst_orig_dtype) return True info = getattr(tensor.untyped_storage(), "_comfy_tensor_file_slice", None) @@ -38,7 +42,6 @@ def read_tensor_file_slice_into(tensor, destination): file_obj = info.file_ref if (destination.device.type != "cpu" or file_obj is None - or threading.get_ident() != info.thread_id or destination.numel() * destination.element_size() < info.size or tensor.numel() * tensor.element_size() != info.size or tensor.storage_offset() != 0 @@ -48,20 +51,33 @@ def read_tensor_file_slice_into(tensor, destination): if info.size == 0: return True + hostbuf = getattr(destination.untyped_storage(), "_comfy_hostbuf", None) + if hostbuf is not None: + stream_ptr = getattr(stream, "cuda_stream", 0) if stream is not None else 0 + device_ptr = destination2.data_ptr() if destination2 is not None else 0 + with info.lock: + hostbuf.read_file_slice(file_obj, info.offset, info.size, + offset=destination.data_ptr() - hostbuf.get_raw_address(), + stream=stream_ptr, + device_ptr=device_ptr, + device=None if destination2 is None else destination2.device.index) + return True + buf_type = ctypes.c_ubyte * info.size view = memoryview(buf_type.from_address(destination.data_ptr())) try: - file_obj.seek(info.offset) - done = 0 - while done < info.size: - try: - n = file_obj.readinto(view[done:]) - except OSError: - return False - if n <= 0: - return False - done += n + with info.lock: + file_obj.seek(info.offset) + done = 0 + while done < info.size: + try: + n = file_obj.readinto(view[done:]) + except OSError: + return False + if n <= 0: + return False + done += n return True finally: view.release() @@ -151,7 +167,7 @@ def set_ram_cache_release_state(callback, headroom): extra_ram_release_callback = callback RAM_CACHE_HEADROOM = max(0, int(headroom)) -def extra_ram_release(target): +def extra_ram_release(target, free_active=False): if extra_ram_release_callback is None: return 0 - return extra_ram_release_callback(target) + return extra_ram_release_callback(target, free_active=free_active) diff --git a/comfy/model_base.py b/comfy/model_base.py index afca4d44e..d10e90399 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -36,6 +36,7 @@ import comfy.ldm.hydit.models import comfy.ldm.audio.dit import comfy.ldm.audio.embedders import comfy.ldm.flux.model +import comfy.ldm.lens.model import comfy.ldm.lightricks.model import comfy.ldm.hunyuan_video.model import comfy.ldm.cosmos.model @@ -44,6 +45,7 @@ import comfy.ldm.lumina.model import comfy.ldm.wan.model import comfy.ldm.wan.model_animate import comfy.ldm.wan.ar_model +import comfy.ldm.wan.model_wandancer import comfy.ldm.hunyuan3d.model import comfy.ldm.hidream.model import comfy.ldm.chroma.model @@ -58,6 +60,8 @@ import comfy.ldm.cogvideo.model import comfy.ldm.rt_detr.rtdetr_v4 import comfy.ldm.ernie.model import comfy.ldm.sam3.detector +import comfy.ldm.hidream_o1.model +from comfy.ldm.hidream_o1.conditioning import build_extra_conds import comfy.model_management import comfy.patcher_extension @@ -811,6 +815,85 @@ class StableAudio1(BaseModel): sd["{}{}".format(k, l)] = s[l] return sd +class StableAudio3(BaseModel): + def __init__(self, model_config, seconds_total_embedder_weights, padding_embedding=None, model_type=ModelType.FLOW, device=None): + super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.audio.dit.AudioDiffusionTransformer) + self.seconds_total_embedder = comfy.ldm.audio.embedders.NumberConditioner(768, min_val=0, max_val=384, fourier_features_type=model_config.unet_config["timestep_features_type"]) + self.seconds_total_embedder.load_state_dict(seconds_total_embedder_weights) + if padding_embedding is not None: + self.padding_embedding = torch.nn.Parameter(padding_embedding, requires_grad=False) + else: + self.padding_embedding = None + + def concat_cond(self, **kwargs): + noise = kwargs.get("noise", None) + image = kwargs.get("concat_latent_image", None) + + if image is None: + shape_image = list(noise.shape) + image = torch.zeros(shape_image, dtype=noise.dtype, layout=noise.layout, device=noise.device) + else: + image = self.process_latent_in(image) + # TODO: scale if not match + image = utils.resize_to_batch_size(image, noise.shape[0]) + + mask = kwargs.get("concat_mask", kwargs.get("denoise_mask", None)) + if mask is None: + mask = torch.zeros_like(noise)[:, :1] + else: + if mask.shape[1] != 1: + mask = torch.mean(mask, dim=1, keepdim=True) + mask = 1.0 - mask + # TODO: scale if not match + mask = utils.resize_to_batch_size(mask, noise.shape[0]) + + return torch.cat((mask, image), dim=1) + + def extra_conds(self, **kwargs): + out = {} + + concat_cond = self.concat_cond(**kwargs) + if concat_cond is not None: + out['local_add_cond'] = comfy.conds.CONDNoiseShape(concat_cond) + + noise = kwargs.get("noise", None) + device = kwargs["device"] + + seconds_total = kwargs.get("seconds_total", int(noise.shape[-1] / 10.7666)) + seconds_total_embed = self.seconds_total_embedder([seconds_total])[0].to(device) + + global_embed = seconds_total_embed.reshape((1, -1)) + out['global_embed'] = comfy.conds.CONDRegular(global_embed) + + cross_attn = kwargs.get("cross_attn", None) + if cross_attn is not None: + cross_attn = cross_attn.to(device) + if self.padding_embedding is not None: + pe = self.padding_embedding.to(device=device, dtype=cross_attn.dtype) + max_text_tokens = self.model_config.unet_config.get("max_text_tokens", 256) + n_text = cross_attn.shape[1] + if n_text < max_text_tokens: + pad = pe.view(1, 1, -1).expand(cross_attn.shape[0], max_text_tokens - n_text, -1) + cross_attn = torch.cat([cross_attn, pad], dim=1) + cross_attn = torch.cat([cross_attn, seconds_total_embed.repeat((cross_attn.shape[0], 1, 1))], dim=1) + out['c_crossattn'] = comfy.conds.CONDRegular(cross_attn) + + return out + + def state_dict_for_saving(self, unet_state_dict, clip_state_dict=None, vae_state_dict=None, clip_vision_state_dict=None): + sd = super().state_dict_for_saving(unet_state_dict, clip_state_dict=clip_state_dict, vae_state_dict=vae_state_dict, clip_vision_state_dict=clip_vision_state_dict) + + d = {"conditioner.conditioners.seconds_total.": self.seconds_total_embedder.state_dict()} + + for k in d: + s = d[k] + for l in s: + sd["{}{}".format(k, l)] = s[l] + + if self.padding_embedding is not None: + sd["conditioner.conditioners.prompt.padding_embedding"] = self.padding_embedding.data + return sd + class HunyuanDiT(BaseModel): def __init__(self, model_config, model_type=ModelType.V_PREDICTION, device=None): @@ -977,6 +1060,27 @@ class Flux2(Flux): out['c_crossattn'] = comfy.conds.CONDRegular(cross_attn) return out + +class Lens(BaseModel): + def __init__(self, model_config, model_type=ModelType.FLUX, device=None): + super().__init__( + model_config, model_type, device=device, + unet_model=comfy.ldm.lens.model.LensTransformer2DModel, + ) + + def encode_adm(self, **kwargs): + return None # Lens has no pooled/ADM conditioning. + + def extra_conds(self, **kwargs): + out = super().extra_conds(**kwargs) + cross_attn = kwargs.get("cross_attn", None) + if cross_attn is not None: + out['c_crossattn'] = comfy.conds.CONDRegular(cross_attn) + attention_mask = kwargs.get("attention_mask", None) + if attention_mask is not None: + out['attention_mask'] = comfy.conds.CONDRegular(attention_mask) + return out + class GenmoMochi(BaseModel): def __init__(self, model_config, model_type=ModelType.FLOW, device=None): super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.genmo.joint_model.asymm_models_joint.AsymmDiTJoint) @@ -1721,6 +1825,30 @@ class WAN21_SCAIL(WAN21): return out +class WAN22_WanDancer(WAN21): + def __init__(self, model_config, model_type=ModelType.FLOW, image_to_video=True, device=None): + super(WAN21, self).__init__(model_config, model_type, device=device, unet_model=comfy.ldm.wan.model_wandancer.WanDancerModel) + self.image_to_video = image_to_video + + def extra_conds(self, **kwargs): + out = super().extra_conds(**kwargs) + audio_embed = kwargs.get("audio_embed", None) + if audio_embed is not None: + out['audio_embed'] = comfy.conds.CONDRegular(audio_embed) + + clip_vision_output_ref = kwargs.get("clip_vision_output_ref", None) + if clip_vision_output_ref is not None: + out['clip_fea_ref'] = comfy.conds.CONDRegular(clip_vision_output_ref.penultimate_hidden_states) + + fps = kwargs.get("fps", None) + if fps is not None: + out['fps'] = comfy.conds.CONDRegular(torch.FloatTensor([fps])) + + audio_inject_scale = kwargs.get("audio_inject_scale", None) + if audio_inject_scale is not None: + out['audio_inject_scale'] = comfy.conds.CONDRegular(torch.FloatTensor([audio_inject_scale])) + return out + class Hunyuan3Dv2(BaseModel): def __init__(self, model_config, model_type=ModelType.FLOW, device=None): super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.hunyuan3d.model.Hunyuan3Dv2) @@ -1771,6 +1899,39 @@ class HiDream(BaseModel): out['image_cond'] = comfy.conds.CONDNoiseShape(self.process_latent_in(image_cond)) return out +class HiDreamO1(BaseModel): + """HiDream-O1-Image: pixel-space DiT (no VAE). Refs from HiDreamO1ReferenceImages and tokens from the stub TE flow through + extra_conds; the heavy preprocessing lives in comfy.ldm.hidream_o1.conditioning.""" + PATCH_SIZE = 32 + + def __init__(self, model_config, model_type=ModelType.FLOW, device=None): + super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.hidream_o1.model.HiDreamO1Transformer) + + def extra_conds(self, **kwargs): + out = super().extra_conds(**kwargs) + text_input_ids = kwargs.get("text_input_ids", None) + noise = kwargs.get("noise", None) + if text_input_ids is None or noise is None: + return out + + # handle area conds + area = kwargs.get("area", None) + if area is not None: + crop_h = min(noise.shape[-2] - area[2], area[0]) + crop_w = min(noise.shape[-1] - area[3], area[1]) + noise = torch.empty((noise.shape[0], 3, crop_h, crop_w), dtype=noise.dtype, device=noise.device) + + conds = build_extra_conds( + text_input_ids, noise, + ref_images=kwargs.get("reference_latents", None), + target_patch_size=self.PATCH_SIZE, + ) + for k, v in conds.items(): + # ar_len is a Python int (precomputed to avoid a GPU sync in forward). + cls = comfy.conds.CONDConstant if k == "ar_len" else comfy.conds.CONDRegular + out[k] = cls(v) + return out + class Chroma(Flux): def __init__(self, model_config, model_type=ModelType.FLUX, device=None, unet_model=comfy.ldm.chroma.model.Chroma): super().__init__(model_config, model_type, device=device, unet_model=unet_model) diff --git a/comfy/model_detection.py b/comfy/model_detection.py index d9b67dcdf..2b0b98cd8 100644 --- a/comfy/model_detection.py +++ b/comfy/model_detection.py @@ -116,6 +116,45 @@ def detect_unet_config(state_dict, key_prefix, metadata=None): if '{}transformer.rotary_pos_emb.inv_freq'.format(key_prefix) in state_dict_keys: #stable audio dit unet_config = {} unet_config["audio_model"] = "dit1.0" + unet_config["global_cond_dim"] = state_dict['{}to_global_embed.0.weight'.format(key_prefix)].shape[1] + cond_embed = state_dict['{}to_cond_embed.0.weight'.format(key_prefix)] + unet_config["project_cond_tokens"] = cond_embed.shape[0] != cond_embed.shape[1] + unet_config["embed_dim"] = state_dict['{}to_timestep_embed.0.weight'.format(key_prefix)].shape[0] + mem_tokens = state_dict.get('{}transformer.memory_tokens'.format(key_prefix), None) + to_qkv = state_dict.get('{}transformer.layers.0.self_attn.to_qkv.weight'.format(key_prefix), None) + differential = False + if to_qkv is not None: + if to_qkv.shape[0] == to_qkv.shape[1] * 5: + differential = True + if mem_tokens is not None: + unet_config["num_memory_tokens"] = mem_tokens.shape[0] + if '{}transformer.layers.0.self_attn.q_norm.weight'.format(key_prefix) in state_dict: + unet_config["attn_kwargs"] = {"qk_norm": "ln", "feat_scale": True} + rms_norm = state_dict.get('{}transformer.layers.0.self_attn.q_norm.gamma'.format(key_prefix), None) + if rms_norm is not None: + unet_config["attn_kwargs"] = {"qk_norm": "rms", "differential": differential} + unet_config["norm_type"] = "rms_norm" + unet_config["num_heads"] = unet_config["embed_dim"] // rms_norm.shape[0] + + if '{}timestep_features.weight'.format(key_prefix) in state_dict: + unet_config["timestep_features_type"] = "learned" + else: + unet_config["timestep_features_type"] = "expo" + + io_channels = state_dict['{}postprocess_conv.weight'.format(key_prefix)].shape[0] + unet_config["io_channels"] = io_channels + unet_config["input_concat_dim"] = state_dict['{}transformer.project_in.weight'.format(key_prefix)].shape[1] - io_channels + + local_add_cond = state_dict.get('{}transformer.layers.0.to_local_embed.0.weight'.format(key_prefix), None) + if local_add_cond is not None: + unet_config["local_add_cond_dim"] = local_add_cond.shape[1] + + global_cond_embed = state_dict.get('{}transformer.global_cond_embedder.0.weight'.format(key_prefix), None) + if global_cond_embed is not None: + unet_config["global_cond_shared_embed"] = True + unet_config["global_cond_type"] = "adaLN" + + unet_config["depth"] = count_blocks(state_dict_keys, '{}transformer.layers.'.format(key_prefix) + '{}.') return unet_config if '{}double_layers.0.attn.w1q.weight'.format(key_prefix) in state_dict_keys: #aura flow dit @@ -572,6 +611,8 @@ def detect_unet_config(state_dict, key_prefix, metadata=None): dit_config["model_type"] = "animate" elif '{}patch_embedding_pose.weight'.format(key_prefix) in state_dict_keys: dit_config["model_type"] = "scail" + elif '{}patch_embedding_global.weight'.format(key_prefix) in state_dict_keys: + dit_config["model_type"] = "wandancer" else: if '{}img_emb.proj.0.bias'.format(key_prefix) in state_dict_keys: dit_config["model_type"] = "i2v" @@ -618,6 +659,9 @@ def detect_unet_config(state_dict, key_prefix, metadata=None): dit_config["guidance_cond_proj_dim"] = None#f"{key_prefix}t_embedder.cond_proj.weight" in state_dict_keys return dit_config + if '{}t_embedder1.mlp.0.weight'.format(key_prefix) in state_dict_keys and '{}x_embedder.proj1.weight'.format(key_prefix) in state_dict_keys: # HiDream-O1 + return {"image_model": "hidream_o1"} + if '{}caption_projection.0.linear.weight'.format(key_prefix) in state_dict_keys: # HiDream dit_config = {} dit_config["image_model"] = "hidream" @@ -711,6 +755,30 @@ def detect_unet_config(state_dict, key_prefix, metadata=None): dit_config["timestep_scale"] = 1000.0 return dit_config + if '{}transformer_blocks.0.attn.norm_added_q.weight'.format(key_prefix) in state_dict_keys \ + and '{}transformer_blocks.0.img_mlp.w1.weight'.format(key_prefix) in state_dict_keys: # Lens + img_in_w = state_dict['{}img_in.weight'.format(key_prefix)] + proj_out_w = state_dict['{}proj_out.weight'.format(key_prefix)] + multi_layer = '{}txt_norm.0.weight'.format(key_prefix) in state_dict_keys + if multi_layer: + enc_hidden_dim = state_dict['{}txt_norm.0.weight'.format(key_prefix)].shape[0] + # Indices are TE-side; the DiT just consumes L layers in order. + selected_layer_index = tuple(range(count_blocks(state_dict_keys, '{}txt_norm.'.format(key_prefix) + '{}.'))) + else: + enc_hidden_dim = state_dict['{}txt_norm.weight'.format(key_prefix)].shape[0] + selected_layer_index = (0,) + + return { + "image_model": "lens", + "in_channels": img_in_w.shape[1], + "out_channels": proj_out_w.shape[0] // 4, # patch_size ** 2 (=2² default) + "num_layers": count_blocks(state_dict_keys, '{}transformer_blocks.'.format(key_prefix) + '{}.'), + "num_attention_heads": img_in_w.shape[0] // 64, # // attention_head_dim default + "enc_hidden_dim": enc_hidden_dim, + "multi_layer_encoder_feature": multi_layer, + "selected_layer_index": selected_layer_index, + } + if '{}txt_norm.weight'.format(key_prefix) in state_dict_keys: # Qwen Image dit_config = {} dit_config["image_model"] = "qwen_image" diff --git a/comfy/model_management.py b/comfy/model_management.py index 21738a4c7..b01c4d7fa 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import psutil import logging @@ -27,12 +28,18 @@ import platform import weakref import gc import os -from contextlib import nullcontext +from contextlib import contextmanager, nullcontext import comfy.memory_management import comfy.utils import comfy.quant_ops +import comfy_aimdo.host_buffer import comfy_aimdo.vram_buffer +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from comfy.model_patcher import ModelPatcher + + class VRAMState(Enum): DISABLED = 0 #No vram present: no need to move models to vram NO_VRAM = 1 #Very low vram: enable all the options to save vram @@ -203,6 +210,107 @@ def get_torch_device(): else: return torch.device(torch.cuda.current_device()) +def get_all_torch_devices(exclude_current=False): + global cpu_state + devices = [] + if cpu_state == CPUState.GPU: + # NVIDIA + AMD/ROCm both expose their GPUs through torch.cuda.*; + # without the AMD arm, single-GPU ROCm users get an empty list + # which silently turns unload_all_models() into a no-op. + if is_nvidia() or is_amd(): + for i in range(torch.cuda.device_count()): + devices.append(torch.device("cuda", i)) + elif is_intel_xpu(): + for i in range(torch.xpu.device_count()): + devices.append(torch.device("xpu", i)) + elif is_ascend_npu(): + for i in range(torch.npu.device_count()): + devices.append(torch.device("npu", i)) + elif is_mlu(): + for i in range(torch.mlu.device_count()): + devices.append(torch.device("mlu", i)) + else: + # Fallback for unhandled GPU backends (e.g. DirectML): at least + # report the current device so callers like unload_all_models() + # do not silently no-op. + devices.append(get_torch_device()) + else: + devices.append(get_torch_device()) + if exclude_current: + current = get_torch_device() + if current in devices: + devices.remove(current) + return devices + +def get_gpu_device_options(): + """Return list of device option strings for node widgets. + + Always includes "default" and "cpu". When multiple GPUs are present, + adds "gpu:0", "gpu:1", etc. (vendor-agnostic labels). + """ + options = ["default", "cpu"] + devices = get_all_torch_devices() + if len(devices) > 1: + for i in range(len(devices)): + options.append(f"gpu:{i}") + return options + +def get_gpu_device_options_no_cpu(): + """Variant of get_gpu_device_options that omits "cpu". + + Intended for components like the VAE selector where running on CPU + is impractical and should not be offered as a choice. + """ + return [o for o in get_gpu_device_options() if o != "cpu"] + +def resolve_gpu_device_option(option: str): + """Resolve a device option string to a torch.device. + + Returns None for "default" (let the caller use its normal default). + Returns torch.device("cpu") for "cpu". + For "gpu:N", returns the Nth torch device. Returns None if the + index is out of range, the option string is malformed, or + unrecognized (callers are expected to log their own context-rich + message before falling back to the default device). + """ + if option is None or option == "default": + return None + if option == "cpu": + return torch.device("cpu") + if option.startswith("gpu:"): + try: + idx = int(option[4:]) + except ValueError: + return None + devices = get_all_torch_devices() + if 0 <= idx < len(devices): + return devices[idx] + return None + +@contextmanager +def cuda_device_context(device): + """Context manager that sets torch.cuda.current_device to match *device*. + + Used when running operations on a non-default CUDA device so that custom + CUDA kernels (e.g. comfy_kitchen fp8 quantization) pick up the correct + device index. The previous device is restored on exit. + + No-op when *device* is not CUDA, has no explicit index, or already matches + the current device. + """ + prev = None + if device.type == "cuda" and device.index is not None: + prev = torch.cuda.current_device() + if prev != device.index: + torch.cuda.set_device(device) + else: + prev = None + try: + yield + finally: + if prev is not None: + torch.cuda.set_device(prev) + def get_total_memory(dev=None, torch_total_too=False): global directml_enabled if dev is None: @@ -491,9 +599,21 @@ try: logging.info("Device: {}".format(get_torch_device_name(get_torch_device()))) except: logging.warning("Could not pick default device.") +try: + for device in get_all_torch_devices(exclude_current=True): + logging.info("Device: {}".format(get_torch_device_name(device))) +except: + pass +current_loaded_models: list[LoadedModel] = [] -current_loaded_models = [] +DIRTY_MMAPS = set() + +PIN_PRESSURE_HYSTERESIS = 256 * 1024 * 1024 + +#Freeing registerables on pressure does imply a GPU sync, so go big on +#the hysteresis so each expensive sync gives us back a good chunk. +REGISTERABLE_PIN_HYSTERESIS = 2048 * 1024 * 1024 def module_size(module): module_mem = 0 @@ -503,30 +623,49 @@ def module_size(module): module_mem += t.nbytes return module_mem -def module_mmap_residency(module, free=False): - mmap_touched_mem = 0 - module_mem = 0 - bounced_mmaps = set() - sd = module.state_dict() - for k in sd: - t = sd[k] - module_mem += t.nbytes - storage = t._qdata.untyped_storage() if isinstance(t, comfy.quant_ops.QuantizedTensor) else t.untyped_storage() - if not getattr(storage, "_comfy_tensor_mmap_touched", False): - continue - mmap_touched_mem += t.nbytes - if not free: - continue - storage._comfy_tensor_mmap_touched = False - mmap_obj = storage._comfy_tensor_mmap_refs[0] - if mmap_obj in bounced_mmaps: - continue - mmap_obj.bounce() - bounced_mmaps.add(mmap_obj) - return mmap_touched_mem, module_mem +def mark_mmap_dirty(storage): + mmap_refs = getattr(storage, "_comfy_tensor_mmap_refs", None) + if mmap_refs is not None: + DIRTY_MMAPS.add(mmap_refs[0]) + +def free_pins(size, evict_active=False): + freed_total = 0 + for loaded_model in reversed(current_loaded_models): + if size <= 0: + return freed_total + model = loaded_model.model + if model is not None and model.is_dynamic() and (evict_active or not model.model.dynamic_pins[model.load_device]["active"]): + freed = model.partially_unload_ram(size) + freed_total += freed + size -= freed + return freed_total + +def ensure_pin_budget(size, evict_active=False): + shortfall = size + comfy.memory_management.RAM_CACHE_HEADROOM / 2 - psutil.virtual_memory().available + if shortfall <= 0: + return True + + to_free = shortfall + PIN_PRESSURE_HYSTERESIS + return free_pins(to_free, evict_active=evict_active) >= shortfall + +def ensure_pin_registerable(size, evict_active=False): + shortfall = TOTAL_PINNED_MEMORY + size - MAX_PINNED_MEMORY + if MAX_PINNED_MEMORY <= 0: + return False + if shortfall <= 0: + return True + + shortfall += REGISTERABLE_PIN_HYSTERESIS + for loaded_model in reversed(current_loaded_models): + model = loaded_model.model + if model is not None and model.is_dynamic() and (evict_active or not model.model.dynamic_pins[model.load_device]["active"]): + shortfall -= model.unregister_inactive_pins(shortfall) + if shortfall <= 0: + return True + return shortfall <= REGISTERABLE_PIN_HYSTERESIS class LoadedModel: - def __init__(self, model): + def __init__(self, model: ModelPatcher): self._set_model(model) self.device = model.load_device self.real_model = None @@ -534,7 +673,7 @@ class LoadedModel: self.model_finalizer = None self._patcher_finalizer = None - def _set_model(self, model): + def _set_model(self, model: ModelPatcher): self._model = weakref.ref(model) if model.parent is not None: self._parent_model = weakref.ref(model.parent) @@ -545,6 +684,7 @@ class LoadedModel: model = self._parent_model() if model is not None: self._set_model(model) + self.device = model.load_device @property def model(self): @@ -553,9 +693,6 @@ class LoadedModel: def model_memory(self): return self.model.model_size() - def model_mmap_residency(self, free=False): - return self.model.model_mmap_residency(free=free) - def model_loaded_memory(self): return self.model.loaded_size() @@ -635,15 +772,9 @@ WINDOWS = any(platform.win32_ver()) EXTRA_RESERVED_VRAM = 400 * 1024 * 1024 if WINDOWS: - import comfy.windows EXTRA_RESERVED_VRAM = 600 * 1024 * 1024 #Windows is higher because of the shared vram issue if total_vram > (15 * 1024): # more extra reserved vram on 16GB+ cards EXTRA_RESERVED_VRAM += 100 * 1024 * 1024 - def get_free_ram(): - return comfy.windows.get_free_ram() -else: - def get_free_ram(): - return psutil.virtual_memory().available if args.reserve_vram is not None: EXTRA_RESERVED_VRAM = args.reserve_vram * 1024 * 1024 * 1024 @@ -657,7 +788,6 @@ def minimum_inference_memory(): def free_memory(memory_required, device, keep_loaded=[], for_dynamic=False, pins_required=0, ram_required=0): cleanup_models_gc() - comfy.memory_management.extra_ram_release(max(pins_required, ram_required)) unloaded_model = [] can_unload = [] unloaded_models = [] @@ -673,11 +803,9 @@ def free_memory(memory_required, device, keep_loaded=[], for_dynamic=False, pins for x in can_unload_sorted: i = x[-1] memory_to_free = 1e32 - pins_to_free = 1e32 - if not DISABLE_SMART_MEMORY or device is None: + if current_loaded_models[i].model.is_dynamic() and (not DISABLE_SMART_MEMORY or device is None): memory_to_free = 0 if device is None else memory_required - get_free_memory(device) - pins_to_free = pins_required - get_free_ram() - if current_loaded_models[i].model.is_dynamic() and for_dynamic: + if for_dynamic: #don't actually unload dynamic models for the sake of other dynamic models #as that works on-demand. memory_required -= current_loaded_models[i].model.loaded_size() @@ -685,18 +813,6 @@ def free_memory(memory_required, device, keep_loaded=[], for_dynamic=False, pins if memory_to_free > 0 and current_loaded_models[i].model_unload(memory_to_free): logging.debug(f"Unloading {current_loaded_models[i].model.model.__class__.__name__}") unloaded_model.append(i) - if pins_to_free > 0: - logging.debug(f"PIN Unloading {current_loaded_models[i].model.model.__class__.__name__}") - current_loaded_models[i].model.partially_unload_ram(pins_to_free) - - for x in can_unload_sorted: - i = x[-1] - ram_to_free = ram_required - psutil.virtual_memory().available - if ram_to_free <= 0 and i not in unloaded_model: - continue - resident_memory, _ = current_loaded_models[i].model_mmap_residency(free=True) - if resident_memory > 0: - logging.debug(f"RAM Unloading {current_loaded_models[i].model.model.__class__.__name__}") for i in sorted(unloaded_model, reverse=True): unloaded_models.append(current_loaded_models.pop(i)) @@ -762,29 +878,16 @@ def load_models_gpu(models, memory_required=0, force_patch_weights=False, minimu model_to_unload.model.detach(unpatch_all=False) model_to_unload.model_finalizer.detach() - total_memory_required = {} - total_pins_required = {} - total_ram_required = {} for loaded_model in models_to_load: device = loaded_model.device total_memory_required[device] = total_memory_required.get(device, 0) + loaded_model.model_memory_required(device) - resident_memory, model_memory = loaded_model.model_mmap_residency() - pinned_memory = loaded_model.model.pinned_memory_size() - #FIXME: This can over-free the pins as it budgets to pin the entire model. We should - #make this JIT to keep as much pinned as possible. - pins_required = model_memory - pinned_memory - ram_required = model_memory - resident_memory - total_pins_required[device] = total_pins_required.get(device, 0) + pins_required - total_ram_required[device] = total_ram_required.get(device, 0) + ram_required for device in total_memory_required: if device != torch.device("cpu"): free_memory(total_memory_required[device] * 1.1 + extra_mem, device, - for_dynamic=free_for_dynamic, - pins_required=total_pins_required[device], - ram_required=total_ram_required[device]) + for_dynamic=free_for_dynamic) for device in total_memory_required: if device != torch.device("cpu"): @@ -1180,6 +1283,7 @@ STREAM_CAST_BUFFERS = {} LARGEST_CASTED_WEIGHT = (None, 0) STREAM_AIMDO_CAST_BUFFERS = {} LARGEST_AIMDO_CASTED_WEIGHT = (None, 0) +STREAM_PIN_BUFFERS = {} DEFAULT_AIMDO_CAST_BUFFER_RESERVATION_SIZE = 16 * 1024 ** 3 @@ -1220,21 +1324,66 @@ def get_aimdo_cast_buffer(offload_stream, device): if cast_buffer is None: cast_buffer = comfy_aimdo.vram_buffer.VRAMBuffer(DEFAULT_AIMDO_CAST_BUFFER_RESERVATION_SIZE, device.index) STREAM_AIMDO_CAST_BUFFERS[offload_stream] = cast_buffer - return cast_buffer + +def get_pin_buffer(offload_stream): + pin_buffer = STREAM_PIN_BUFFERS.get(offload_stream, None) + if pin_buffer is None: + pin_buffer = comfy_aimdo.host_buffer.HostBuffer(0, 0, pinned_hostbuf_size(8 * 1024**3), mark_cold=False) + STREAM_PIN_BUFFERS[offload_stream] = pin_buffer + elif offload_stream is not None: + event = getattr(pin_buffer, "_comfy_event", None) + if event is not None: + event.synchronize() + delattr(pin_buffer, "_comfy_event") + return pin_buffer + +def resize_pin_buffer(pin_buffer, size): + global TOTAL_PINNED_MEMORY + old_size = pin_buffer.size + if size <= old_size: + return True + growth = size - old_size + comfy.memory_management.extra_ram_release(comfy.memory_management.RAM_CACHE_HEADROOM) + ensure_pin_budget(growth, evict_active=True) + ensure_pin_registerable(growth, evict_active=True) + try: + pin_buffer.extend(size=size, reallocate=True) + except RuntimeError: + return False + TOTAL_PINNED_MEMORY += pin_buffer.size - old_size + return True + def reset_cast_buffers(): + global TOTAL_PINNED_MEMORY global LARGEST_CASTED_WEIGHT global LARGEST_AIMDO_CASTED_WEIGHT LARGEST_CASTED_WEIGHT = (None, 0) LARGEST_AIMDO_CASTED_WEIGHT = (None, 0) - for offload_stream in set(STREAM_CAST_BUFFERS) | set(STREAM_AIMDO_CAST_BUFFERS): + for offload_stream in set(STREAM_CAST_BUFFERS) | set(STREAM_AIMDO_CAST_BUFFERS) | set(STREAM_PIN_BUFFERS): if offload_stream is not None: offload_stream.synchronize() synchronize() + for mmap_obj in DIRTY_MMAPS: + mmap_obj.bounce() + DIRTY_MMAPS.clear() + + for pin_buffer in STREAM_PIN_BUFFERS.values(): + TOTAL_PINNED_MEMORY -= pin_buffer.size + TOTAL_PINNED_MEMORY = max(0, TOTAL_PINNED_MEMORY) + + for loaded_model in current_loaded_models: + model = loaded_model.model + if model is not None and model.is_dynamic(): + model.model.dynamic_pins[model.load_device]["active"] = False + model.partially_unload_ram(1e30, subsets=[ "patches" ]) + model.model.dynamic_pins[model.load_device]["patches"] = (comfy_aimdo.host_buffer.HostBuffer(0, 8 * 1024 * 1024, pinned_hostbuf_size(model.model_size())), [], [-1], [0]) + STREAM_CAST_BUFFERS.clear() STREAM_AIMDO_CAST_BUFFERS.clear() + STREAM_PIN_BUFFERS.clear() soft_empty_cache() def get_offload_stream(device): @@ -1280,7 +1429,7 @@ def sync_stream(device, stream): current_stream(device).wait_stream(stream) -def cast_to_gathered(tensors, r, non_blocking=False, stream=None): +def cast_to_gathered(tensors, r, non_blocking=False, stream=None, r2=None): wf_context = nullcontext() if stream is not None: wf_context = stream @@ -1288,17 +1437,20 @@ def cast_to_gathered(tensors, r, non_blocking=False, stream=None): wf_context = wf_context.as_context(stream) dest_views = comfy.memory_management.interpret_gathered_like(tensors, r) + dest2_views = comfy.memory_management.interpret_gathered_like(tensors, r2) if r2 is not None else None with wf_context: for tensor in tensors: dest_view = dest_views.pop(0) + dest2_view = dest2_views.pop(0) if dest2_views is not None else None if tensor is None: continue - if comfy.memory_management.read_tensor_file_slice_into(tensor, dest_view): + if comfy.memory_management.read_tensor_file_slice_into(tensor, dest_view, stream=stream, destination2=dest2_view): continue storage = tensor._qdata.untyped_storage() if isinstance(tensor, comfy.quant_ops.QuantizedTensor) else tensor.untyped_storage() - if hasattr(storage, "_comfy_tensor_mmap_touched"): - storage._comfy_tensor_mmap_touched = True + mark_mmap_dirty(storage) dest_view.copy_(tensor, non_blocking=non_blocking) + if dest2_view is not None: + dest2_view.copy_(dest_view, non_blocking=non_blocking) def cast_to(weight, dtype=None, device=None, non_blocking=False, copy=False, stream=None, r=None): @@ -1339,14 +1491,18 @@ TOTAL_PINNED_MEMORY = 0 MAX_PINNED_MEMORY = -1 if not args.disable_pinned_memory: if is_nvidia() or is_amd(): + ram = get_total_memory(torch.device("cpu")) if WINDOWS: - MAX_PINNED_MEMORY = get_total_memory(torch.device("cpu")) * 0.40 # Windows limit is apparently 50% + MAX_PINNED_MEMORY = ram * 0.40 # Windows limit is apparently 50% else: - MAX_PINNED_MEMORY = get_total_memory(torch.device("cpu")) * 0.90 + MAX_PINNED_MEMORY = ram * 0.90 logging.info("Enabled pinned memory {}".format(MAX_PINNED_MEMORY // (1024 * 1024))) PINNING_ALLOWED_TYPES = set(["Tensor", "Parameter", "QuantizedTensor"]) +def pinned_hostbuf_size(size): + return max(0, int(min(size, MAX_PINNED_MEMORY) * 2)) + def discard_cuda_async_error(): try: a = torch.tensor([1], dtype=torch.uint8, device=get_torch_device()) @@ -1378,8 +1534,8 @@ def pin_memory(tensor): return False size = tensor.nbytes - if (TOTAL_PINNED_MEMORY + size) > MAX_PINNED_MEMORY: - return False + comfy.memory_management.extra_ram_release(comfy.memory_management.RAM_CACHE_HEADROOM) + ensure_pin_registerable(size) ptr = tensor.data_ptr() if ptr == 0: @@ -1416,7 +1572,8 @@ def unpin_memory(tensor): return False if torch.cuda.cudart().cudaHostUnregister(ptr) == 0: - TOTAL_PINNED_MEMORY -= PINNED_MEMORY.pop(ptr) + size = PINNED_MEMORY.pop(ptr) + TOTAL_PINNED_MEMORY -= size return True else: logging.warning("Unpin error.") @@ -1803,7 +1960,34 @@ def soft_empty_cache(force=False): torch.cuda.ipc_collect() def unload_all_models(): - free_memory(1e30, get_torch_device()) + for device in get_all_torch_devices(): + free_memory(1e30, device) + +def unload_model_and_clones(model: ModelPatcher, unload_additional_models=True, all_devices=False): + 'Unload only model and its clones - primarily for multigpu cloning purposes.' + initial_keep_loaded: list[LoadedModel] = current_loaded_models.copy() + additional_models = [] + if unload_additional_models: + additional_models = model.get_nested_additional_models() + keep_loaded = [] + for loaded_model in initial_keep_loaded: + if loaded_model.model is not None: + if model.clone_base_uuid == loaded_model.model.clone_base_uuid: + continue + # check additional models if they are a match + skip = False + for add_model in additional_models: + if add_model.clone_base_uuid == loaded_model.model.clone_base_uuid: + skip = True + break + if skip: + continue + keep_loaded.append(loaded_model) + if not all_devices: + free_memory(1e30, get_torch_device(), keep_loaded) + else: + for device in get_all_torch_devices(): + free_memory(1e30, device, keep_loaded) def debug_memory_summary(): if is_amd() or is_nvidia(): diff --git a/comfy/model_patcher.py b/comfy/model_patcher.py index 7d2d6883f..00a15fa63 100644 --- a/comfy/model_patcher.py +++ b/comfy/model_patcher.py @@ -26,6 +26,7 @@ import uuid from typing import Callable, Optional import torch +import tqdm import comfy.float import comfy.hooks @@ -34,6 +35,7 @@ import comfy.model_management import comfy.ops import comfy.patcher_extension import comfy.utils +import comfy_aimdo.host_buffer from comfy.comfy_types import UnetWrapperFunction from comfy.quant_ops import QuantizedTensor from comfy.patcher_extension import CallbacksMP, PatcherInjection, WrappersMP @@ -76,12 +78,15 @@ def set_model_options_pre_cfg_function(model_options, pre_cfg_function, disable_ def create_model_options_clone(orig_model_options: dict): return comfy.patcher_extension.copy_nested_dicts(orig_model_options) -def create_hook_patches_clone(orig_hook_patches): +def create_hook_patches_clone(orig_hook_patches, copy_tuples=False): new_hook_patches = {} for hook_ref in orig_hook_patches: new_hook_patches[hook_ref] = {} for k in orig_hook_patches[hook_ref]: new_hook_patches[hook_ref][k] = orig_hook_patches[hook_ref][k][:] + if copy_tuples: + for i in range(len(new_hook_patches[hook_ref][k])): + new_hook_patches[hook_ref][k][i] = tuple(new_hook_patches[hook_ref][k][i]) return new_hook_patches def wipe_lowvram_weight(m): @@ -116,6 +121,8 @@ def string_to_seed(data): return comfy.utils.string_to_seed(data) class LowVramPatch: + is_lowvram_patch = True + def __init__(self, key, patches, convert_func=None, set_func=None): self.key = key self.patches = patches @@ -123,11 +130,21 @@ class LowVramPatch: self.set_func = set_func self.prepared_patches = None - def prepare(self, allocate_buffer, stream): - self.prepared_patches = [ - (patch[0], comfy.lora.prefetch_prepared_value(patch[1], allocate_buffer, stream), patch[2], patch[3], patch[4]) + def memory_required(self): + counter = [0] + for patch in self.patches[self.key]: + comfy.lora.prefetch_prepared_value(patch[1], counter, None, None, False) + return counter[0] + + def prepare(self, destination, stream, copy=True, commit=True): + counter = [0] + prepared_patches = [ + (patch[0], comfy.lora.prefetch_prepared_value(patch[1], counter, destination, stream, copy), patch[2], patch[3], patch[4]) for patch in self.patches[self.key] ] + if commit: + self.prepared_patches = prepared_patches + return prepared_patches def clear_prepared(self): self.prepared_patches = None @@ -241,6 +258,37 @@ class LazyCastingParam(torch.nn.Parameter): return self.model.patch_weight_to_device(self.key, device_to=self.model.load_device, return_weight=True).to("cpu") +class LazyCastingQuantizedParam: + def __init__(self, model, key): + self.model = model + self.key = key + self.cpu_state_dict = None + + def state_dict_tensor(self, state_dict_key): + if self.cpu_state_dict is None: + weight = self.model.patch_weight_to_device(self.key, device_to=self.model.load_device, return_weight=True) + self.cpu_state_dict = {k: v.to("cpu") for k, v in weight.state_dict(self.key).items()} + return self.cpu_state_dict[state_dict_key] + + +class LazyCastingParamPiece(torch.nn.Parameter): + def __new__(cls, caster, state_dict_key, tensor): + return super().__new__(cls, tensor) + + def __init__(self, caster, state_dict_key, tensor): + self.caster = caster + self.state_dict_key = state_dict_key + + @property + def device(self): + return CustomTorchDevice + + def to(self, *args, **kwargs): + caster = self.caster + del self.caster + return caster.state_dict_tensor(self.state_dict_key) + + class ModelPatcher: def __init__(self, model, load_device, offload_device, size=0, weight_inplace_update=False): self.size = size @@ -284,7 +332,10 @@ class ModelPatcher: self.is_clip = False self.hook_mode = comfy.hooks.EnumHookMode.MaxSpeed - self.cached_patcher_init: tuple[Callable, tuple] | None = None + self.cached_patcher_init: tuple[Callable, tuple] | tuple[Callable, tuple, int] | None = None + self.is_multigpu_base_clone = False + self.clone_base_uuid = uuid.uuid4() + if not hasattr(self.model, 'model_loaded_weight_memory'): self.model.model_loaded_weight_memory = 0 @@ -309,9 +360,6 @@ class ModelPatcher: self.size = comfy.model_management.module_size(self.model) return self.size - def model_mmap_residency(self, free=False): - return comfy.model_management.module_mmap_residency(self.model, free=free) - def loaded_size(self): return self.model.model_loaded_weight_memory @@ -324,7 +372,8 @@ class ModelPatcher: #than pays for CFG. So return everything both torch and Aimdo could give us aimdo_mem = 0 if comfy.memory_management.aimdo_enabled: - aimdo_mem = comfy_aimdo.model_vbar.vbars_analyze() + aimdo_device = device.index if getattr(device, "type", None) == "cuda" else None + aimdo_mem = comfy_aimdo.model_vbar.vbars_analyze(aimdo_device) return comfy.model_management.get_free_memory(device) + aimdo_mem def get_clone_model_override(self): @@ -338,6 +387,8 @@ class ModelPatcher: if self.cached_patcher_init is None: raise RuntimeError("Cannot create non-dynamic delegate: cached_patcher_init is not initialized.") temp_model_patcher = self.cached_patcher_init[0](*self.cached_patcher_init[1], disable_dynamic=True) + if len(self.cached_patcher_init) > 2: + temp_model_patcher = temp_model_patcher[self.cached_patcher_init[2]] model_override = temp_model_patcher.get_clone_model_override() if model_override is None: model_override = self.get_clone_model_override() @@ -396,19 +447,113 @@ class ModelPatcher: n.hook_mode = self.hook_mode n.cached_patcher_init = self.cached_patcher_init + n.is_multigpu_base_clone = self.is_multigpu_base_clone + n.clone_base_uuid = self.clone_base_uuid for callback in self.get_all_callbacks(CallbacksMP.ON_CLONE): callback(self, n) return n + def deepclone_multigpu(self, new_load_device=None, models_cache: dict[uuid.UUID,ModelPatcher]=None): + logging.info(f"Creating deepclone of {self.model.__class__.__name__} for {new_load_device if new_load_device else self.load_device}.") + if self.cached_patcher_init is None: + raise RuntimeError( + f"Cannot create multigpu deepclone of {self.model.__class__.__name__}: " + "the loader that produced this model does not support multigpu " + "(cached_patcher_init is not initialized). Use a core loader " + "(CheckpointLoaderSimple, UNETLoader, CLIPLoader/DualCLIPLoader, VAELoader), " + "or have the custom loader register a cached_patcher_init factory." + ) + comfy.model_management.unload_model_and_clones(self) + # Produce a freshly-loaded patcher from the loader factory so the multigpu + # clone owns its own untainted model weights (rather than relying on + # copy.deepcopy of an already-patched/already-loaded module). + temp_model_patcher: ModelPatcher | list[ModelPatcher] = self.cached_patcher_init[0](*self.cached_patcher_init[1]) + if len(self.cached_patcher_init) > 2: + temp_model_patcher = temp_model_patcher[self.cached_patcher_init[2]] + # Override clone()'s normal "share self.model + share backup containers" with + # the pristine model from temp_model_patcher plus empty backup containers -- + # the fresh model has no patches applied, so any deepcopy of self's stale + # backup/object_patches_backup/pinned would just propagate dead state that + # no longer corresponds to anything in n.model. + model_override = (temp_model_patcher.model, ({}, {}, {}, set())) + n = self.clone(model_override=model_override) + # clone() copies hook_backup by reference from self; reset since model is pristine. + n.hook_backup = {} + # set load device, if present + if new_load_device is not None: + n.load_device = new_load_device + # Ensure any per-device bookkeeping (e.g. ModelPatcherDynamic.dynamic_pins) + # has an entry for n.load_device on the freshly-loaded n.model. temp_model_patcher's + # __init__ only registered its own (default) load_device. + if hasattr(n, "register_load_device"): + n.register_load_device(n.load_device) + # multigpu clone should not have multigpu additional_models entry + n.remove_additional_models("multigpu") + # multigpu_clone all stored additional_models; make sure circular references are properly handled + if models_cache is None: + models_cache = {} + for key, model_list in n.additional_models.items(): + for i in range(len(model_list)): + add_model = n.additional_models[key][i] + if add_model.clone_base_uuid not in models_cache: + models_cache[add_model.clone_base_uuid] = add_model.deepclone_multigpu(new_load_device=new_load_device, models_cache=models_cache) + n.additional_models[key][i] = models_cache[add_model.clone_base_uuid] + for callback in self.get_all_callbacks(CallbacksMP.ON_DEEPCLONE_MULTIGPU): + callback(self, n) + return n + + def match_multigpu_clones(self): + multigpu_models = self.get_additional_models_with_key("multigpu") + if len(multigpu_models) > 0: + new_multigpu_models = [] + for mm in multigpu_models: + # clone main model, but bring over relevant props from existing multigpu clone + n = self.clone() + n.load_device = mm.load_device + n.backup = mm.backup + n.object_patches_backup = mm.object_patches_backup + n.hook_backup = mm.hook_backup + n.model = mm.model + n.is_multigpu_base_clone = mm.is_multigpu_base_clone + n.remove_additional_models("multigpu") + orig_additional_models: dict[str, list[ModelPatcher]] = comfy.patcher_extension.copy_nested_dicts(n.additional_models) + n.additional_models = comfy.patcher_extension.copy_nested_dicts(mm.additional_models) + # figure out which additional models are not present in multigpu clone + models_cache = {} + for mm_add_model in mm.get_additional_models(): + models_cache[mm_add_model.clone_base_uuid] = mm_add_model + remove_models_uuids = set(list(models_cache.keys())) + for key, model_list in orig_additional_models.items(): + for orig_add_model in model_list: + if orig_add_model.clone_base_uuid not in models_cache: + models_cache[orig_add_model.clone_base_uuid] = orig_add_model.deepclone_multigpu(new_load_device=n.load_device, models_cache=models_cache) + existing_list = n.get_additional_models_with_key(key) + existing_list.append(models_cache[orig_add_model.clone_base_uuid]) + n.set_additional_models(key, existing_list) + if orig_add_model.clone_base_uuid in remove_models_uuids: + remove_models_uuids.remove(orig_add_model.clone_base_uuid) + # remove duplicate additional models + for key, model_list in n.additional_models.items(): + new_model_list = [x for x in model_list if x.clone_base_uuid not in remove_models_uuids] + n.set_additional_models(key, new_model_list) + for callback in self.get_all_callbacks(CallbacksMP.ON_MATCH_MULTIGPU_CLONES): + callback(self, n) + new_multigpu_models.append(n) + self.set_additional_models("multigpu", new_multigpu_models) + def is_clone(self, other): if hasattr(other, 'model') and self.model is other.model: return True return False - def clone_has_same_weights(self, clone: 'ModelPatcher'): - if not self.is_clone(clone): - return False + def clone_has_same_weights(self, clone: ModelPatcher, allow_multigpu=False): + if allow_multigpu: + if self.clone_base_uuid != clone.clone_base_uuid: + return False + else: + if not self.is_clone(clone): + return False if self.current_hooks != clone.current_hooks: return False @@ -1086,8 +1231,12 @@ class ModelPatcher: # Pinned memory pressure tracking is only implemented for DynamicVram loading return 0 + def loaded_ram_size(self): + # Loaded RAM pressure tracking is only implemented for DynamicVram loading + return 0 + def partially_unload_ram(self, ram_to_unload): - pass + return 0 def detach(self, unpatch_all=True): self.eject_model() @@ -1186,7 +1335,7 @@ class ModelPatcher: return self.additional_models.get(key, []) def get_additional_models(self): - all_models = [] + all_models: list[ModelPatcher] = [] for models in self.additional_models.values(): all_models.extend(models) return all_models @@ -1240,9 +1389,18 @@ class ModelPatcher: for callback in self.get_all_callbacks(CallbacksMP.ON_PRE_RUN): callback(self) - def prepare_state(self, timestep): + def prepare_state(self, timestep, model_options): + ignore_multigpu = model_options.get("ignore_multigpu", False) for callback in self.get_all_callbacks(CallbacksMP.ON_PREPARE_STATE): - callback(self, timestep) + callback(self, timestep, model_options) + if not ignore_multigpu and "multigpu_clones" in model_options: + model_options["ignore_multigpu"] = True + try: + for p in model_options["multigpu_clones"].values(): + p: ModelPatcher + p.prepare_state(timestep, model_options) + finally: + model_options.pop("ignore_multigpu", None) def restore_hook_patches(self): if self.hook_patches_backup is not None: @@ -1255,12 +1413,18 @@ class ModelPatcher: def prepare_hook_patches_current_keyframe(self, t: torch.Tensor, hook_group: comfy.hooks.HookGroup, model_options: dict[str]): curr_t = t[0] reset_current_hooks = False + multigpu_kf_changed_cache = None transformer_options = model_options.get("transformer_options", {}) for hook in hook_group.hooks: changed = hook.hook_keyframe.prepare_current_keyframe(curr_t=curr_t, transformer_options=transformer_options) # if keyframe changed, remove any cached HookGroups that contain hook with the same hook_ref; # this will cause the weights to be recalculated when sampling if changed: + # cache changed for multigpu usage + if "multigpu_clones" in model_options: + if multigpu_kf_changed_cache is None: + multigpu_kf_changed_cache = [] + multigpu_kf_changed_cache.append(hook) # reset current_hooks if contains hook that changed if self.current_hooks is not None: for current_hook in self.current_hooks.hooks: @@ -1272,6 +1436,28 @@ class ModelPatcher: self.cached_hook_patches.pop(cached_group) if reset_current_hooks: self.patch_hooks(None) + if "multigpu_clones" in model_options: + for p in model_options["multigpu_clones"].values(): + p: ModelPatcher + p._handle_changed_hook_keyframes(multigpu_kf_changed_cache) + + def _handle_changed_hook_keyframes(self, kf_changed_cache: list[comfy.hooks.Hook]): + 'Used to handle multigpu behavior inside prepare_hook_patches_current_keyframe.' + if kf_changed_cache is None: + return + reset_current_hooks = False + # reset current_hooks if contains hook that changed + for hook in kf_changed_cache: + if self.current_hooks is not None: + for current_hook in self.current_hooks.hooks: + if current_hook == hook: + reset_current_hooks = True + break + for cached_group in list(self.cached_hook_patches.keys()): + if cached_group.contains(hook): + self.cached_hook_patches.pop(cached_group) + if reset_current_hooks: + self.patch_hooks(None) def register_all_hook_patches(self, hooks: comfy.hooks.HookGroup, target_dict: dict[str], model_options: dict=None, registered: comfy.hooks.HookGroup = None): @@ -1461,21 +1647,45 @@ class ModelPatcher: self.unpatch_hooks() self.clear_cached_hook_weights() - def state_dict_for_saving(self, clip_state_dict=None, vae_state_dict=None, clip_vision_state_dict=None): - unet_state_dict = self.model.diffusion_model.state_dict() - for k, v in unet_state_dict.items(): + def model_state_dict_for_saving(self, model=None, prefix=""): + if model is None: + model = self.model + + original_state_dict = model.state_dict() + output_state_dict = {} + keys = list(original_state_dict) + while len(keys) > 0: + k = keys.pop(0) + v = original_state_dict[k] op_keys = k.rsplit('.', 1) if (len(op_keys) < 2) or op_keys[1] not in ["weight", "bias"]: + output_state_dict[k] = v continue try: - op = comfy.utils.get_attr(self.model.diffusion_model, op_keys[0]) + op = comfy.utils.get_attr(model, op_keys[0]) except: + output_state_dict[k] = v continue if not op or not hasattr(op, "comfy_cast_weights") or \ (hasattr(op, "comfy_patched_weights") and op.comfy_patched_weights == True): + output_state_dict[k] = v continue - key = "diffusion_model." + k - unet_state_dict[k] = LazyCastingParam(self, key, comfy.utils.get_attr(self.model, key)) + key = prefix + k + weight = comfy.utils.get_attr(self.model, key) + if isinstance(weight, QuantizedTensor) and k in original_state_dict: + qt_state_dict = weight.state_dict(k) + caster = LazyCastingQuantizedParam(self, key) + for group_key in (x for x in qt_state_dict if x in original_state_dict): + if group_key in keys: + keys.remove(group_key) + output_state_dict.pop(group_key, "") + output_state_dict[group_key] = LazyCastingParamPiece(caster, prefix + group_key, original_state_dict[group_key]) + continue + output_state_dict[k] = LazyCastingParam(self, key, weight) + return output_state_dict + + def state_dict_for_saving(self, clip_state_dict=None, vae_state_dict=None, clip_vision_state_dict=None): + unet_state_dict = self.model_state_dict_for_saving(self.model.diffusion_model, "diffusion_model.") return self.model.state_dict_for_saving(unet_state_dict, clip_state_dict=clip_state_dict, vae_state_dict=vae_state_dict, clip_vision_state_dict=clip_vision_state_dict) def __del__(self): @@ -1494,9 +1704,30 @@ class ModelPatcherDynamic(ModelPatcher): super().__init__(model, load_device, offload_device, size, weight_inplace_update) if not hasattr(self.model, "dynamic_vbars"): self.model.dynamic_vbars = {} + if not hasattr(self.model, "dynamic_pins"): + self.model.dynamic_pins = {} + self.register_load_device(self.load_device) self.non_dynamic_delegate_model = None assert load_device is not None + def register_load_device(self, device): + """Ensure dynamic_pins has an entry for *device*. + + Called from __init__ and also from any code that retargets an + already-constructed patcher to a new load_device (e.g. the + Select{Model,CLIP,VAE}Device selector nodes); without this entry + partially_unload_ram() raises KeyError when it tries to read the + per-device pin state. + """ + if device not in self.model.dynamic_pins: + self.model.dynamic_pins[device] = { + "weights": (comfy_aimdo.host_buffer.HostBuffer(0, 0, 0), [], [-1], [0]), + "patches": (comfy_aimdo.host_buffer.HostBuffer(0, 0, 0), [], [-1], [0]), + "hostbufs_initialized": False, + "failed": False, + "active": False, + } + def is_dynamic(self): return True @@ -1533,6 +1764,16 @@ class ModelPatcherDynamic(ModelPatcher): #use all ModelPatcherDynamic this is ignored and its all done dynamically. return super().memory_required(input_shape=input_shape) * 1.3 + (1024 ** 3) + def restore_loaded_backups(self): + restored = self.model.model_loaded_weight_memory + for key in list(self.backup.keys()): + bk = self.backup.pop(key) + comfy.utils.set_attr_param(self.model, key, bk.weight) + for key in list(self.backup_buffers.keys()): + comfy.utils.set_attr_buffer(self.model, key, self.backup_buffers.pop(key)) + self.model.model_loaded_weight_memory = 0 + return restored + def load(self, device_to=None, lowvram_model_memory=0, force_patch_weights=False, full_load=False, dirty=False): @@ -1549,12 +1790,20 @@ class ModelPatcherDynamic(ModelPatcher): num_patches = 0 allocated_size = 0 - self.model.model_loaded_weight_memory = 0 + self.restore_loaded_backups() with self.use_ejected(): self.unpatch_hooks() vbar = self._vbar_get(create=True) + pin_state = self.model.dynamic_pins[self.load_device] + if not pin_state["hostbufs_initialized"]: + hostbuf_size = comfy.model_management.pinned_hostbuf_size(self.model_size()) + pin_state["weights"] = (comfy_aimdo.host_buffer.HostBuffer(0, 64 * 1024 * 1024, hostbuf_size), [], [-1], [0]) + pin_state["patches"] = (comfy_aimdo.host_buffer.HostBuffer(0, 8 * 1024 * 1024, hostbuf_size), [], [-1], [0]) + pin_state["hostbufs_initialized"] = True + pin_state["failed"] = False + pin_state["active"] = True if vbar is not None: vbar.prioritize() @@ -1580,7 +1829,9 @@ class ModelPatcherDynamic(ModelPatcher): if key in self.patches: if comfy.lora.calculate_shape(self.patches[key], weight, key) != weight.shape: return (True, 0) - setattr(m, param_key + "_lowvram_function", LowVramPatch(key, self.patches)) + lowvram_patch = LowVramPatch(key, self.patches) + lowvram_patch._pin_state = pin_state + setattr(m, param_key + "_lowvram_function", lowvram_patch) num_patches += 1 else: setattr(m, param_key + "_lowvram_function", None) @@ -1597,6 +1848,9 @@ class ModelPatcherDynamic(ModelPatcher): def force_load_param(self, param_key, device_to): key = key_param_name_to_key(n, param_key) + weight, _, _ = get_key_weight(self.model, key) + if weight is None: + return if key in self.backup: comfy.utils.set_attr_param(self.model, key, self.backup[key].weight) self.patch_weight_to_device(key, device_to=device_to, force_cast=True) @@ -1606,17 +1860,26 @@ class ModelPatcherDynamic(ModelPatcher): if hasattr(m, "comfy_cast_weights"): m.comfy_cast_weights = True - m.pin_failed = False m.seed_key = n + m._pin_state = pin_state set_dirty(m, dirty) - force_load, v_weight_size = setup_param(self, m, n, "weight") - force_load_bias, v_weight_bias = setup_param(self, m, n, "bias") - force_load = force_load or force_load_bias - v_weight_size += v_weight_bias + #Models that mix tiny and giant weights can causing lopsided stream buffer + #rotations and stall. force the tinys over. + if module_mem > 16 * 1024: + force_load, v_weight_size = setup_param(self, m, n, "weight") + force_load_bias, v_weight_bias = setup_param(self, m, n, "bias") + force_load = force_load or force_load_bias + v_weight_size += v_weight_bias + if force_load: + logging.info(f"Module {n} has resizing Lora - force loading") + else: + force_load=True if force_load: - logging.info(f"Module {n} has resizing Lora - force loading") + if hasattr(m, "_v"): + comfy_aimdo.model_vbar.vbar_unpin(m._v) + delattr(m, "_v") force_load_param(self, "weight", device_to) force_load_param(self, "bias", device_to) else: @@ -1651,7 +1914,11 @@ class ModelPatcherDynamic(ModelPatcher): self.model.model_loaded_weight_memory += casted_buf.numel() * casted_buf.element_size() force_load_stat = f" Force pre-loaded {len(self.backup)} weights: {self.model.model_loaded_weight_memory // 1024} KB." if len(self.backup) > 0 else "" - logging.info(f"Model {self.model.__class__.__name__} prepared for dynamic VRAM loading. {allocated_size // (1024 ** 2)}MB Staged. {num_patches} patches attached.{force_load_stat}") + log_key = (self.patches_uuid, allocated_size, num_patches, len(self.backup), self.model.model_loaded_weight_memory) + in_loop = bool(getattr(tqdm.tqdm, "_instances", None)) + level = logging.DEBUG if in_loop and getattr(self, "_last_prepare_log_key", None) == log_key else logging.INFO + self._last_prepare_log_key = log_key + logging.log(level, f"Model {self.model.__class__.__name__} prepared for dynamic VRAM loading. {allocated_size // (1024 ** 2)}MB Staged. {num_patches} patches attached.{force_load_stat}") self.model.device = device_to self.model.current_weight_patches_uuid = self.patches_uuid @@ -1670,33 +1937,62 @@ class ModelPatcherDynamic(ModelPatcher): freed = 0 if vbar is None else vbar.free_memory(memory_to_free) if freed < memory_to_free: - for key in list(self.backup.keys()): - bk = self.backup.pop(key) - comfy.utils.set_attr_param(self.model, key, bk.weight) - for key in list(self.backup_buffers.keys()): - comfy.utils.set_attr_buffer(self.model, key, self.backup_buffers.pop(key)) - freed += self.model.model_loaded_weight_memory - self.model.model_loaded_weight_memory = 0 + freed += self.restore_loaded_backups() return freed - def pinned_memory_size(self): - total = 0 - loading = self._load_list(for_dynamic=True) - for x in loading: - _, _, _, _, m, _ = x - pin = comfy.pinned_memory.get_pin(m) - if pin is not None: - total += pin.numel() * pin.element_size() - return total + def loaded_ram_size(self): + return (self.model.dynamic_pins[self.load_device]["weights"][0].size + + self.model.dynamic_pins[self.load_device]["patches"][0].size) - def partially_unload_ram(self, ram_to_unload): - loading = self._load_list(for_dynamic=True, default_device=self.offload_device) - for x in loading: - *_, m, _ = x - ram_to_unload -= comfy.pinned_memory.unpin_memory(m) - if ram_to_unload <= 0: - return + def pinned_memory_size(self): + return (self.model.dynamic_pins[self.load_device]["weights"][3][0] + + self.model.dynamic_pins[self.load_device]["patches"][3][0]) + + def unregister_inactive_pins(self, ram_to_unload, subsets=[ "weights", "patches" ]): + freed = 0 + pin_state = self.model.dynamic_pins[self.load_device] + for subset in subsets: + hostbuf, stack, stack_split, pinned_size = pin_state[subset] + split = stack_split[0] + while split >= 0: + module, offset = stack[split] + split -= 1 + stack_split[0] = split + if not module._pin_registered: + continue + size = module._pin.numel() * module._pin.element_size() + if torch.cuda.cudart().cudaHostUnregister(module._pin.data_ptr()) != 0: + comfy.model_management.discard_cuda_async_error() + continue + module._pin_registered = False + comfy.model_management.TOTAL_PINNED_MEMORY = max(0, comfy.model_management.TOTAL_PINNED_MEMORY - size) + pinned_size[0] = max(0, pinned_size[0] - size) + freed += size + ram_to_unload -= size + if ram_to_unload <= 0: + return freed + return freed + + def partially_unload_ram(self, ram_to_unload, subsets=[ "weights", "patches" ]): + freed = 0 + pin_state = self.model.dynamic_pins[self.load_device] + for subset in subsets: + hostbuf, stack, stack_split, pinned_size = pin_state[subset] + while len(stack) > 0: + module, offset = stack.pop() + size = module._pin.numel() * module._pin.element_size() + del module._pin + hostbuf.truncate(offset, do_unregister=module._pin_registered) + stack_split[0] = min(stack_split[0], len(stack) - 1) + if module._pin_registered: + comfy.model_management.TOTAL_PINNED_MEMORY = max(0, comfy.model_management.TOTAL_PINNED_MEMORY - size) + pinned_size[0] = max(0, pinned_size[0] - size) + freed += size + ram_to_unload -= size + if ram_to_unload <= 0: + return freed + return freed def patch_model(self, device_to=None, lowvram_model_memory=0, load_weights=True, force_patch_weights=False): #This isn't used by the core at all and can only be to load a model out of diff --git a/comfy/model_sampling.py b/comfy/model_sampling.py index cf2b5db5f..5af336e76 100644 --- a/comfy/model_sampling.py +++ b/comfy/model_sampling.py @@ -93,7 +93,8 @@ class CONST: def noise_scaling(self, sigma, noise, latent_image, max_denoise=False): sigma = reshape_sigma(sigma, noise.ndim) - return sigma * noise + (1.0 - sigma) * latent_image + s = getattr(self, "noise_scale", 1.0) + return sigma * (s * noise) + (1.0 - sigma) * latent_image def inverse_noise_scaling(self, sigma, latent): sigma = reshape_sigma(sigma, latent.ndim) @@ -288,7 +289,11 @@ class ModelSamplingDiscreteFlow(torch.nn.Module): else: sampling_settings = {} - self.set_parameters(shift=sampling_settings.get("shift", 1.0), multiplier=sampling_settings.get("multiplier", 1000)) + self.set_noise_scale(sampling_settings.get("noise_scale", 1.0)) + self.set_parameters( + shift=sampling_settings.get("shift", 1.0), + multiplier=sampling_settings.get("multiplier", 1000), + ) def set_parameters(self, shift=1.0, timesteps=1000, multiplier=1000): self.shift = shift @@ -296,6 +301,9 @@ class ModelSamplingDiscreteFlow(torch.nn.Module): ts = self.sigma((torch.arange(1, timesteps + 1, 1) / timesteps) * multiplier) self.register_buffer('sigmas', ts) + def set_noise_scale(self, noise_scale): + self.noise_scale = float(noise_scale) + @property def sigma_min(self): return self.sigmas[0] diff --git a/comfy/multigpu.py b/comfy/multigpu.py new file mode 100644 index 000000000..e7f5b3d6f --- /dev/null +++ b/comfy/multigpu.py @@ -0,0 +1,248 @@ +from __future__ import annotations +import queue +import threading +import torch +import logging + +from collections import namedtuple +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from comfy.model_patcher import ModelPatcher +import comfy.utils +import comfy.patcher_extension +import comfy.model_management + + +class MultiGPUThreadPool: + """Persistent thread pool for multi-GPU work distribution. + + Maintains one worker thread per extra GPU device. Each thread calls + torch.cuda.set_device() once at startup so that compiled kernel caches + (inductor/triton) stay warm across diffusion steps. + """ + + def __init__(self, devices: list[torch.device]): + self._workers: list[threading.Thread] = [] + self._work_queues: dict[torch.device, queue.Queue] = {} + self._result_queues: dict[torch.device, queue.Queue] = {} + + for device in devices: + wq = queue.Queue() + rq = queue.Queue() + self._work_queues[device] = wq + self._result_queues[device] = rq + t = threading.Thread(target=self._worker_loop, args=(device, wq, rq), daemon=True) + t.start() + self._workers.append(t) + + def _worker_loop(self, device: torch.device, work_q: queue.Queue, result_q: queue.Queue): + try: + torch.cuda.set_device(device) + except Exception as e: + logging.error(f"MultiGPUThreadPool: failed to set device {device}: {e}") + while True: + item = work_q.get() + if item is None: + return + result_q.put((None, e)) + return + while True: + item = work_q.get() + if item is None: + break + fn, args, kwargs = item + try: + result = fn(*args, **kwargs) + result_q.put((result, None)) + except Exception as e: + result_q.put((None, e)) + + def submit(self, device: torch.device, fn, *args, **kwargs): + self._work_queues[device].put((fn, args, kwargs)) + + def get_result(self, device: torch.device): + return self._result_queues[device].get() + + @property + def devices(self) -> list[torch.device]: + return list(self._work_queues.keys()) + + def shutdown(self): + for wq in self._work_queues.values(): + wq.put(None) # sentinel + for t in self._workers: + t.join(timeout=5.0) + + +class GPUOptions: + def __init__(self, device_index: int, relative_speed: float): + self.device_index = device_index + self.relative_speed = relative_speed + + def clone(self): + return GPUOptions(self.device_index, self.relative_speed) + + def create_dict(self): + return { + "relative_speed": self.relative_speed + } + +class GPUOptionsGroup: + def __init__(self): + self.options: dict[int, GPUOptions] = {} + + def add(self, info: GPUOptions): + self.options[info.device_index] = info + + def clone(self): + c = GPUOptionsGroup() + for opt in self.options.values(): + c.add(opt) + return c + + def register(self, model: ModelPatcher): + opts_dict = {} + # get devices that are valid for this model + devices: list[torch.device] = [model.load_device] + for extra_model in model.get_additional_models_with_key("multigpu"): + extra_model: ModelPatcher + devices.append(extra_model.load_device) + # create dictionary with actual device mapped to its GPUOptions + device_opts_list: list[GPUOptions] = [] + for device in devices: + device_opts = self.options.get(device.index, GPUOptions(device_index=device.index, relative_speed=1.0)) + opts_dict[device] = device_opts.create_dict() + device_opts_list.append(device_opts) + # make relative_speed relative to 1.0 + min_speed = min([x.relative_speed for x in device_opts_list]) + for value in opts_dict.values(): + value['relative_speed'] /= min_speed + model.model_options['multigpu_options'] = opts_dict + + +def create_multigpu_deepclones(model: ModelPatcher, max_gpus: int, gpu_options: GPUOptionsGroup=None, reuse_loaded=False): + 'Prepare ModelPatcher to contain deepclones of its BaseModel and related properties.' + model = model.clone() + # check if multigpu is already prepared - get the load devices from them if possible to exclude + skip_devices = set() + multigpu_models = model.get_additional_models_with_key("multigpu") + if len(multigpu_models) > 0: + for mm in multigpu_models: + skip_devices.add(mm.load_device) + skip_devices = list(skip_devices) + + # Exclude the primary model's actual device, not the global current device: + # after SelectModelDevice(gpu:N) the primary may not live on the process's + # current CUDA device, and excluding the wrong device picks bad extras. + all_devices = comfy.model_management.get_all_torch_devices(exclude_current=False) + full_extra_devices = [d for d in all_devices if d != model.load_device] + limit_extra_devices = full_extra_devices[:max_gpus-1] + extra_devices = limit_extra_devices.copy() + # exclude skipped devices + for skip in skip_devices: + if skip in extra_devices: + extra_devices.remove(skip) + # create new deepclones + if len(extra_devices) > 0: + for device in extra_devices: + device_patcher = None + if reuse_loaded: + # Only reuse a previously-loaded MultiGPU clone. A SelectModelDevice + # patcher on the same device shares clone_base_uuid but has + # is_multigpu_base_clone=False, which would later be filtered out by + # prepare_model_patcher_multigpu_clones() and silently shrink the + # work split back to one GPU. + loaded_models: list[ModelPatcher] = comfy.model_management.loaded_models() + for lm in loaded_models: + if lm.model is None: + continue + if lm.load_device != device: + continue + if lm.clone_base_uuid != model.clone_base_uuid: + continue + if not getattr(lm, "is_multigpu_base_clone", False): + continue + device_patcher = lm.clone() + logging.info(f"Reusing loaded multigpu deepclone of {device_patcher.model.__class__.__name__} for {device}") + break + if device_patcher is None: + device_patcher = model.deepclone_multigpu(new_load_device=device) + # Always flag the clone; whether reused or freshly deepcloned, it must + # advertise itself as a MultiGPU base clone so the cond scheduler picks + # it up in prepare_model_patcher_multigpu_clones(). + device_patcher.is_multigpu_base_clone = True + multigpu_models = model.get_additional_models_with_key("multigpu") + multigpu_models.append(device_patcher) + model.set_additional_models("multigpu", multigpu_models) + model.match_multigpu_clones() + if gpu_options is None: + gpu_options = GPUOptionsGroup() + gpu_options.register(model) + else: + logging.info("No extra torch devices need initialization, skipping initializing MultiGPU Work Units.") + # only keep model clones that don't go 'past' the intended max_gpu count; + # this prunes any inherited multigpu clones whose load_device is no longer allowed + # when max_gpus is lowered between runs. + allowed_devices = set(limit_extra_devices) + allowed_devices.add(model.load_device) + multigpu_models = model.get_additional_models_with_key("multigpu") + new_multigpu_models = [m for m in multigpu_models if m.load_device in allowed_devices] + if len(new_multigpu_models) != len(multigpu_models): + model.set_additional_models("multigpu", new_multigpu_models) + model.match_multigpu_clones() + return model + + +LoadBalance = namedtuple('LoadBalance', ['work_per_device', 'idle_time']) +def load_balance_devices(model_options: dict[str], total_work: int, return_idle_time=False, work_normalized: int=None): + 'Optimize work assigned to different devices, accounting for their relative speeds and splittable work.' + opts_dict = model_options['multigpu_options'] + devices = list(model_options['multigpu_clones'].keys()) + speed_per_device = [] + work_per_device = [] + # get sum of each device's relative_speed + total_speed = 0.0 + for opts in opts_dict.values(): + total_speed += opts['relative_speed'] + # get relative work for each device; + # obtained by w = (W*r)/R + for device in devices: + relative_speed = opts_dict[device]['relative_speed'] + relative_work = (total_work*relative_speed) / total_speed + speed_per_device.append(relative_speed) + work_per_device.append(relative_work) + # relative work must be expressed in whole numbers, but likely is a decimal; + # perform rounding while maintaining total sum equal to total work (sum of relative works) + work_per_device = round_preserved(work_per_device) + dict_work_per_device = {} + for device, relative_work in zip(devices, work_per_device): + dict_work_per_device[device] = relative_work + if not return_idle_time: + return LoadBalance(dict_work_per_device, None) + # divide relative work by relative speed to get estimated completion time of said work by each device; + # time here is relative and does not correspond to real-world units + completion_time = [w/r for w,r in zip(work_per_device, speed_per_device)] + # calculate relative time spent by the devices waiting on each other after their work is completed + idle_time = abs(min(completion_time) - max(completion_time)) + # if need to compare work idle time, need to normalize to a common total work + if work_normalized: + idle_time *= (work_normalized/total_work) + + return LoadBalance(dict_work_per_device, idle_time) + +def round_preserved(values: list[float]): + 'Round all values in a list, preserving the combined sum of values.' + # get floor of values; casting to int does it too + floored = [int(x) for x in values] + total_floored = sum(floored) + # get remainder to distribute + remainder = round(sum(values)) - total_floored + # pair values with fractional portions + fractional = [(i, x-floored[i]) for i, x in enumerate(values)] + # sort by fractional part in descending order + fractional.sort(key=lambda x: x[1], reverse=True) + # distribute the remainder + for i in range(remainder): + index = fractional[i][0] + floored[index] += 1 + return floored diff --git a/comfy/ops.py b/comfy/ops.py index 585c185a3..56445be8d 100644 --- a/comfy/ops.py +++ b/comfy/ops.py @@ -18,6 +18,7 @@ import torch import logging +import contextlib import comfy.model_management from comfy.cli_args import args, PerformanceFeature import comfy.float @@ -75,6 +76,8 @@ except: cast_to = comfy.model_management.cast_to #TODO: remove once no more references +STREAM_PIN_BUFFER_HEADROOM = 8 * 1024 * 1024 + def cast_to_input(weight, input, non_blocking=False, copy=True): return comfy.model_management.cast_to(weight, input.dtype, input.device, non_blocking=non_blocking, copy=copy) @@ -91,6 +94,9 @@ def cast_modules_with_vbar(comfy_modules, dtype, device, bias_dtype, non_blockin offload_stream = None cast_buffer = None cast_buffer_offset = 0 + stream_pin_hostbuf = None + stream_pin_offset = 0 + stream_pin_queue = [] def ensure_offload_stream(module, required_size, check_largest): nonlocal offload_stream @@ -124,6 +130,22 @@ def cast_modules_with_vbar(comfy_modules, dtype, device, bias_dtype, non_blockin cast_buffer_offset += buffer_size return buffer + def get_stream_pin_buffer_offset(buffer_size): + nonlocal stream_pin_hostbuf + nonlocal stream_pin_offset + + if buffer_size == 0 or offload_stream is None: + return None + + if stream_pin_hostbuf is None: + stream_pin_hostbuf = comfy.model_management.get_pin_buffer(offload_stream) + if stream_pin_hostbuf is None: + return None + + offset = stream_pin_offset + stream_pin_offset += buffer_size + return offset + for s in comfy_modules: signature = comfy_aimdo.model_vbar.vbar_fault(s._v) resident = comfy_aimdo.model_vbar.vbar_signature_compare(signature, s._v_signature) @@ -162,23 +184,47 @@ def cast_modules_with_vbar(comfy_modules, dtype, device, bias_dtype, non_blockin if xfer_dest is None: xfer_dest = get_cast_buffer(dest_size) - if signature is None and pin is None: - comfy.pinned_memory.pin_memory(s) - pin = comfy.pinned_memory.get_pin(s) - else: - pin = None + def cast_maybe_lowvram_patch(xfer_source, xfer_dest, stream): + if xfer_source is not None: + if getattr(xfer_source, "is_lowvram_patch", False): + xfer_source.prepare(xfer_dest, stream, copy=True, commit=False) + else: + comfy.model_management.cast_to_gathered(xfer_source, xfer_dest, non_blocking=non_blocking, stream=stream) - if pin is not None: - comfy.model_management.cast_to_gathered(xfer_source, pin) - xfer_source = [ pin ] - #send it over - comfy.model_management.cast_to_gathered(xfer_source, xfer_dest, non_blocking=non_blocking, stream=offload_stream) + def handle_pin(m, pin, source, dest, subset="weights", size=None): + if pin is not None: + cast_maybe_lowvram_patch([pin], dest, offload_stream) + return + if signature is None: + comfy.pinned_memory.pin_memory(m, subset=subset, size=size) + pin = comfy.pinned_memory.get_pin(m, subset=subset) + if pin is not None: + if isinstance(source, list): + comfy.model_management.cast_to_gathered(source, pin, non_blocking=non_blocking, stream=offload_stream, r2=dest) + else: + cast_maybe_lowvram_patch(source, pin, None) + cast_maybe_lowvram_patch([ pin ], dest, offload_stream) + return + if pin is None: + pin_offset = get_stream_pin_buffer_offset(size) + if pin_offset is not None: + stream_pin_queue.append((source, pin_offset, size, dest)) + return + cast_maybe_lowvram_patch(source, dest, offload_stream) + + handle_pin(s, pin, xfer_source, xfer_dest, size=dest_size) for param_key in ("weight", "bias"): - lowvram_fn = getattr(s, param_key + "_lowvram_function", None) - if lowvram_fn is not None: + lowvram_source = getattr(s, param_key + "_lowvram_function", None) + if lowvram_source is not None: ensure_offload_stream(s, cast_buffer_offset, False) - lowvram_fn.prepare(lambda size: get_cast_buffer(size), offload_stream) + lowvram_size = lowvram_source.memory_required() + lowvram_dest = get_cast_buffer(lowvram_size) + lowvram_source.prepare(lowvram_dest, None, copy=False, commit=True) + + pin = comfy.pinned_memory.get_pin(lowvram_source, subset="patches") + handle_pin(lowvram_source, pin, lowvram_source, lowvram_dest, subset="patches", size=lowvram_size) + prefetch["xfer_dest"] = xfer_dest prefetch["cast_dest"] = cast_dest @@ -186,6 +232,23 @@ def cast_modules_with_vbar(comfy_modules, dtype, device, bias_dtype, non_blockin prefetch["needs_cast"] = needs_cast s._prefetch = prefetch + if stream_pin_offset > 0: + if stream_pin_hostbuf.size < stream_pin_offset: + if not comfy.model_management.resize_pin_buffer(stream_pin_hostbuf, stream_pin_offset + STREAM_PIN_BUFFER_HEADROOM): + for xfer_source, _, _, xfer_dest in stream_pin_queue: + cast_maybe_lowvram_patch(xfer_source, xfer_dest, offload_stream) + return offload_stream + stream_pin_tensor = comfy_aimdo.torch.hostbuf_to_tensor(stream_pin_hostbuf) + stream_pin_tensor.untyped_storage()._comfy_hostbuf = stream_pin_hostbuf + for xfer_source, pin_offset, pin_size, xfer_dest in stream_pin_queue: + pin = stream_pin_tensor[pin_offset:pin_offset + pin_size] + if isinstance(xfer_source, list): + comfy.model_management.cast_to_gathered(xfer_source, pin, non_blocking=non_blocking, stream=offload_stream, r2=xfer_dest) + else: + cast_maybe_lowvram_patch(xfer_source, pin, None) + comfy.model_management.cast_to_gathered([ pin ], xfer_dest, non_blocking=non_blocking, stream=offload_stream) + stream_pin_hostbuf._comfy_event = offload_stream.record_event() + return offload_stream @@ -260,7 +323,7 @@ def resolve_cast_module_with_vbar(s, dtype, device, bias_dtype, compute_dtype, w def cast_bias_weight(s, input=None, dtype=None, device=None, bias_dtype=None, offloadable=False, compute_dtype=None, want_requant=False): - # NOTE: offloadable=False is a a legacy and if you are a custom node author reading this please pass + # NOTE: offloadable=False is a legacy mode and if you are a custom node author reading this please pass # offloadable=True and call uncast_bias_weight() after your last usage of the weight/bias. This # will add async-offload support to your cast and improve performance. if input is not None: @@ -562,6 +625,25 @@ class disable_weight_init: else: return super().forward(*args, **kwargs) + class BatchNorm2d(torch.nn.BatchNorm2d, CastWeightBiasOp): + def reset_parameters(self): + return None + + def forward_comfy_cast_weights(self, input): + weight, bias, offload_stream = cast_bias_weight(self, input, offloadable=True) + running_mean = self.running_mean.to(device=input.device, dtype=weight.dtype) if self.running_mean is not None else None + running_var = self.running_var.to(device=input.device, dtype=weight.dtype) if self.running_var is not None else None + x = torch.nn.functional.batch_norm(input, running_mean, running_var, weight, bias, self.training, self.momentum, self.eps) + uncast_bias_weight(self, weight, bias, offload_stream) + return x + + def forward(self, *args, **kwargs): + run_every_op() + if self.comfy_cast_weights or len(self.weight_function) > 0 or len(self.bias_function) > 0: + return self.forward_comfy_cast_weights(*args, **kwargs) + else: + return super().forward(*args, **kwargs) + class LayerNorm(torch.nn.LayerNorm, CastWeightBiasOp): def reset_parameters(self): return None @@ -749,6 +831,9 @@ class manual_cast(disable_weight_init): class Conv3d(disable_weight_init.Conv3d): comfy_cast_weights = True + class BatchNorm2d(disable_weight_init.BatchNorm2d): + comfy_cast_weights = True + class GroupNorm(disable_weight_init.GroupNorm): comfy_cast_weights = True @@ -963,6 +1048,144 @@ class QuantLinearFunc(torch.autograd.Function): return grad_input, grad_weight, grad_bias, None, None, None +# Quantized-weight module helpers + +def _quantized_apply(module, fn, recurse=True): + """Re-wrap Parameters after fn so .to()/.cuda() propagate through QuantizedTensor weights.""" + if recurse: + for child in module.children(): + child._apply(fn) + for key, param in module._parameters.items(): + if param is None: + continue + p = fn(param) + if (not torch.is_inference_mode_enabled()) and p.is_inference(): + p = p.clone() + module.register_parameter(key, torch.nn.Parameter(p, requires_grad=False)) + for key, buf in module._buffers.items(): + if buf is not None: + module._buffers[key] = fn(buf) + return module + + +def _load_quantized_module(module, super_load, state_dict, prefix, local_metadata, strict, + missing_keys, unexpected_keys, error_msgs, load_extra_params=False): + """Shared _load_from_state_dict body for quantized-weight modules. + + Pops weight (+ scales, +/- extras), populates module.weight as a Parameter + or Parameter-wrapped QuantizedTensor, then calls super_load and strips + consumed keys from missing_keys. Reads compute_dtype from factory_kwargs + and disabled formats from module._disabled_formats. + """ + device = module.factory_kwargs["device"] + compute_dtype = module.factory_kwargs["dtype"] + disabled_formats = module._disabled_formats + layer_name = prefix.rstrip('.') + + weight = state_dict.pop(f"{prefix}weight", None) + if weight is None: + logging.warning(f"Missing weight for layer {layer_name}") + module.weight = None + return + manually_loaded_keys = [f"{prefix}weight"] + + def pop_scale(name, dtype=None): + key = f"{prefix}{name}" + v = state_dict.pop(key, None) + if v is not None: + v = v.to(device=device) + if dtype is not None: + v = v.view(dtype=dtype) + manually_loaded_keys.append(key) + return v + + layer_conf = state_dict.pop(f"{prefix}comfy_quant", None) + if layer_conf is not None: + layer_conf = json.loads(layer_conf.numpy().tobytes()) + + if layer_conf is None: + module.weight = torch.nn.Parameter(weight.to(device=device, dtype=compute_dtype), requires_grad=False) + else: + module.quant_format = layer_conf.get("format", None) + module._full_precision_mm_config = layer_conf.get("full_precision_matrix_mult", False) + if not module._full_precision_mm: + module._full_precision_mm = module._full_precision_mm_config + if module.quant_format in disabled_formats: + module._full_precision_mm = True + if module.quant_format is None: + raise ValueError(f"Unknown quantization format for layer {layer_name}") + + qconfig = QUANT_ALGOS[module.quant_format] + module.layout_type = qconfig["comfy_tensor_layout"] + layout_cls = get_layout_class(module.layout_type) + + # Per-format scales; fp8 dtype views handle both legacy uint8-on-disk and native fp8. + if module.quant_format in ("float8_e4m3fn", "float8_e5m2"): + scales = {"scale": pop_scale("weight_scale")} + elif module.quant_format == "mxfp8": + bs = pop_scale("weight_scale", torch.float8_e8m0fnu) + if bs is None: + raise ValueError(f"Missing MXFP8 block scales for layer {layer_name}") + scales = {"scale": bs} + elif module.quant_format == "nvfp4": + ts = pop_scale("weight_scale_2") + bs = pop_scale("weight_scale", torch.float8_e4m3fn) + if ts is None or bs is None: + raise ValueError(f"Missing NVFP4 scales for layer {layer_name}") + scales = {"scale": ts, "block_scale": bs} + else: + raise ValueError(f"Unsupported quantization format: {module.quant_format}") + + params = layout_cls.Params(**scales, orig_dtype=compute_dtype, orig_shape=module._orig_shape) + module.weight = torch.nn.Parameter( + QuantizedTensor(weight.to(device=device, dtype=qconfig["storage_t"]), module.layout_type, params), + requires_grad=False, + ) + + if load_extra_params: + for param_name in qconfig["parameters"]: + if param_name in {"weight_scale", "weight_scale_2"}: + continue + param_key = f"{prefix}{param_name}" + _v = state_dict.pop(param_key, None) + if _v is None: + continue + module.register_parameter(param_name, torch.nn.Parameter(_v.to(device=device), requires_grad=False)) + manually_loaded_keys.append(param_key) + + super_load(state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs) + for key in manually_loaded_keys: + if key in missing_keys: + missing_keys.remove(key) + + +def _quantized_weight_state_dict(module, sd, prefix, extra_quant_conf=None, extra_quant_params=()): + """Shared state_dict body. extra_quant_conf merges into the comfy_quant JSON; + extra_quant_params names attributes written as additional top-level keys.""" + if not hasattr(module, 'weight'): + logging.warning(f"Warning: state dict on uninitialized op {prefix}") + return sd + bias = getattr(module, 'bias', None) + if bias is not None: + sd[f"{prefix}bias"] = bias + if module.weight is None: + return sd + if isinstance(module.weight, QuantizedTensor): + sd.update(module.weight.state_dict(f"{prefix}weight")) + quant_conf = {"format": module.quant_format} + if getattr(module, '_full_precision_mm_config', False): + quant_conf["full_precision_matrix_mult"] = True + if extra_quant_conf: + quant_conf.update(extra_quant_conf) + sd[f"{prefix}comfy_quant"] = torch.tensor(list(json.dumps(quant_conf).encode("utf-8")), dtype=torch.uint8) + for name in extra_quant_params: + value = getattr(module, name, None) + if value is not None: + sd[f"{prefix}{name}"] = value + else: + sd[f"{prefix}weight"] = module.weight + return sd + def mixed_precision_ops(quant_config={}, compute_dtype=torch.bfloat16, full_precision_mm=False, disabled=[]): class MixedPrecisionOps(manual_cast): @@ -972,21 +1195,16 @@ def mixed_precision_ops(quant_config={}, compute_dtype=torch.bfloat16, full_prec _disabled = disabled class Linear(torch.nn.Module, CastWeightBiasOp): - def __init__( - self, - in_features: int, - out_features: int, - bias: bool = True, - device=None, - dtype=None, - ) -> None: + _disabled_formats = disabled + + def __init__(self, in_features: int, out_features: int, bias: bool = True, device=None, dtype=None): super().__init__() self.factory_kwargs = {"device": device, "dtype": MixedPrecisionOps._compute_dtype} - # self.factory_kwargs = {"device": device, "dtype": dtype} self.in_features = in_features self.out_features = out_features + self._orig_shape = (out_features, in_features) if bias: self.bias = torch.nn.Parameter(torch.empty(out_features, **self.factory_kwargs)) else: @@ -999,151 +1217,12 @@ def mixed_precision_ops(quant_config={}, compute_dtype=torch.bfloat16, full_prec def reset_parameters(self): return None - def _load_scale_param(self, state_dict, prefix, param_name, device, manually_loaded_keys, dtype=None): - key = f"{prefix}{param_name}" - value = state_dict.pop(key, None) - if value is not None: - value = value.to(device=device) - if dtype is not None: - value = value.view(dtype=dtype) - manually_loaded_keys.append(key) - return value - - def _load_from_state_dict(self, state_dict, prefix, local_metadata, - strict, missing_keys, unexpected_keys, error_msgs): - - device = self.factory_kwargs["device"] - layer_name = prefix.rstrip('.') - weight_key = f"{prefix}weight" - weight = state_dict.pop(weight_key, None) - if weight is None: - logging.warning(f"Missing weight for layer {layer_name}") - self.weight = None - return - - manually_loaded_keys = [weight_key] - - layer_conf = state_dict.pop(f"{prefix}comfy_quant", None) - if layer_conf is not None: - layer_conf = json.loads(layer_conf.numpy().tobytes()) - - if layer_conf is None: - self.weight = torch.nn.Parameter(weight.to(device=device, dtype=MixedPrecisionOps._compute_dtype), requires_grad=False) - else: - self.quant_format = layer_conf.get("format", None) - self._full_precision_mm_config = layer_conf.get("full_precision_matrix_mult", False) - if not self._full_precision_mm: - self._full_precision_mm = self._full_precision_mm_config - - if self.quant_format in MixedPrecisionOps._disabled: - self._full_precision_mm = True - - if self.quant_format is None: - raise ValueError(f"Unknown quantization format for layer {layer_name}") - - qconfig = QUANT_ALGOS[self.quant_format] - self.layout_type = qconfig["comfy_tensor_layout"] - layout_cls = get_layout_class(self.layout_type) - - # Load format-specific parameters - if self.quant_format in ["float8_e4m3fn", "float8_e5m2"]: - # FP8: single tensor scale - scale = self._load_scale_param(state_dict, prefix, "weight_scale", device, manually_loaded_keys) - - params = layout_cls.Params( - scale=scale, - orig_dtype=MixedPrecisionOps._compute_dtype, - orig_shape=(self.out_features, self.in_features), - ) - - elif self.quant_format == "mxfp8": - # MXFP8: E8M0 block scales stored as uint8 in safetensors - block_scale = self._load_scale_param(state_dict, prefix, "weight_scale", device, manually_loaded_keys, - dtype=torch.uint8) - - if block_scale is None: - raise ValueError(f"Missing MXFP8 block scales for layer {layer_name}") - - block_scale = block_scale.view(torch.float8_e8m0fnu) - - params = layout_cls.Params( - scale=block_scale, - orig_dtype=MixedPrecisionOps._compute_dtype, - orig_shape=(self.out_features, self.in_features), - ) - - elif self.quant_format == "nvfp4": - # NVFP4: tensor_scale (weight_scale_2) + block_scale (weight_scale) - tensor_scale = self._load_scale_param(state_dict, prefix, "weight_scale_2", device, manually_loaded_keys) - block_scale = self._load_scale_param(state_dict, prefix, "weight_scale", device, manually_loaded_keys, - dtype=torch.float8_e4m3fn) - - if tensor_scale is None or block_scale is None: - raise ValueError(f"Missing NVFP4 scales for layer {layer_name}") - - params = layout_cls.Params( - scale=tensor_scale, - block_scale=block_scale, - orig_dtype=MixedPrecisionOps._compute_dtype, - orig_shape=(self.out_features, self.in_features), - ) - else: - raise ValueError(f"Unsupported quantization format: {self.quant_format}") - - self.weight = torch.nn.Parameter( - QuantizedTensor(weight.to(device=device, dtype=qconfig["storage_t"]), self.layout_type, params), - requires_grad=False - ) - - for param_name in qconfig["parameters"]: - if param_name in {"weight_scale", "weight_scale_2"}: - continue # Already handled above - - param_key = f"{prefix}{param_name}" - _v = state_dict.pop(param_key, None) - if _v is None: - continue - self.register_parameter(param_name, torch.nn.Parameter(_v.to(device=device), requires_grad=False)) - manually_loaded_keys.append(param_key) - - super()._load_from_state_dict(state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs) - - for key in manually_loaded_keys: - if key in missing_keys: - missing_keys.remove(key) + def _load_from_state_dict(self, *args): + _load_quantized_module(self, super()._load_from_state_dict, *args, load_extra_params=True) def state_dict(self, *args, destination=None, prefix="", **kwargs): - if destination is not None: - sd = destination - else: - sd = {} - - if not hasattr(self, 'weight'): - logging.warning("Warning: state dict on uninitialized op {}".format(prefix)) - return sd - - if self.bias is not None: - sd["{}bias".format(prefix)] = self.bias - - if self.weight is None: - return sd - - if isinstance(self.weight, QuantizedTensor): - sd_out = self.weight.state_dict("{}weight".format(prefix)) - for k in sd_out: - sd[k] = sd_out[k] - - quant_conf = {"format": self.quant_format} - if self._full_precision_mm_config: - quant_conf["full_precision_matrix_mult"] = True - sd["{}comfy_quant".format(prefix)] = torch.tensor(list(json.dumps(quant_conf).encode('utf-8')), dtype=torch.uint8) - - input_scale = getattr(self, 'input_scale', None) - if input_scale is not None: - sd["{}input_scale".format(prefix)] = input_scale - else: - sd["{}weight".format(prefix)] = self.weight - return sd + sd = destination if destination is not None else {} + return _quantized_weight_state_dict(self, sd, prefix, extra_quant_params=("input_scale",)) def _forward(self, input, weight, bias): return torch.nn.functional.linear(input, weight, bias) @@ -1233,25 +1312,126 @@ def mixed_precision_ops(quant_config={}, compute_dtype=torch.bfloat16, full_prec self.weight = torch.nn.Parameter(weight, requires_grad=False) def _apply(self, fn, recurse=True): # This is to get torch.compile + moving weights to another device working - if recurse: - for module in self.children(): - module._apply(fn) + return _quantized_apply(self, fn, recurse) - for key, param in self._parameters.items(): - if param is None: - continue - p = fn(param) - if (not torch.is_inference_mode_enabled()) and p.is_inference(): - p = p.clone() - self.register_parameter(key, torch.nn.Parameter(p, requires_grad=False)) - for key, buf in self._buffers.items(): - if buf is not None: - self._buffers[key] = fn(buf) - return self + class MoEExperts(torch.nn.Module, CastWeightBiasOp): + """Container for E quantized expert weights, indexed via expert_weight(i). + + The bank lives on self.weight as a single 3D tensor — either a + compute_dtype Parameter or a Parameter wrapping a QuantizedTensor + with leading expert dim. + + State-dict layout matches mixed_precision_ops.Linear with a leading + expert dim: + {prefix}.weight quant data (storage_t), leading dim = E + {prefix}.weight_scale block / per-tensor scale + {prefix}.weight_scale_2 [E] or scalar NVFP4 only + {prefix}.bias [E, out_features] optional, compute_dtype + {prefix}.comfy_quant json -> {{"format": "...", "num_experts": E}} + + Without comfy_quant the weight loads as a plain compute_dtype 3D Parameter [E, out, in]. + """ + + _disabled_formats = disabled + + def __init__(self, num_experts: int, in_features: int, out_features: int, bias: bool = True, device=None, dtype=None): + super().__init__() + self.num_experts = num_experts + self.in_features = in_features + self.out_features = out_features + self._orig_shape = (num_experts, out_features, in_features) + self.factory_kwargs = {"device": device, "dtype": MixedPrecisionOps._compute_dtype} + if bias: + self.bias = torch.nn.Parameter(torch.empty(num_experts, out_features, **self.factory_kwargs)) + else: + self.register_parameter("bias", None) + + # Populated by _load_from_state_dict: + self.weight = None + self.quant_format = None + self.layout_type = None + self._full_precision_mm = MixedPrecisionOps._full_precision_mm + self._full_precision_mm_config = False + self._resident_bank = None + + def reset_parameters(self): + return None + + def _apply(self, fn, recurse=True): + return _quantized_apply(self, fn, recurse) + + def _load_from_state_dict(self, *args): + _load_quantized_module(self, super()._load_from_state_dict, *args, load_extra_params=False) + + def expert_weight(self, i: int): + """Expert i's weight (Tensor or per-expert QuantizedTensor view).""" + if isinstance(self.weight, QuantizedTensor): + return self._expert_qt_from(self.weight, i) + return self.weight[i] + + @contextlib.contextmanager + def bank_resident(self, input): + """Cast the whole bank once; expert_linear inside reuses the cast. + Not re-entrant — do not nest calls on the same instance. + """ + weight, bias, offload_stream = cast_bias_weight(self, input, offloadable=True) + self._resident_bank = (weight, bias) + try: + yield self + finally: + self._resident_bank = None + uncast_bias_weight(self, weight, bias, offload_stream) + + def expert_linear(self, input: torch.Tensor, i: int) -> torch.Tensor: + """Linear against expert i's weight (with optional bias).""" + resident = getattr(self, "_resident_bank", None) + if resident is not None: + weight, bias = resident + return self._expert_linear_impl(input, weight, bias, i) + weight, bias, offload_stream = cast_bias_weight(self, input, offloadable=True) + try: + return self._expert_linear_impl(input, weight, bias, i) + finally: + uncast_bias_weight(self, weight, bias, offload_stream) + + def _expert_linear_impl(self, input, weight, bias, i): + if isinstance(weight, QuantizedTensor): + qw = self._expert_qt_from(weight, i) + else: + qw = weight[i] + b = cast_to_input(bias[i], input, copy=False) if bias is not None else None + + if isinstance(qw, QuantizedTensor): + use_fast = ( + not self._full_precision_mm + and qw.layout_cls.supports_fast_matmul() + and input.dim() == 2 + ) + if use_fast: + qin = QuantizedTensor.from_float(input, self.layout_type) + return torch.nn.functional.linear(qin, qw, b) + out = input @ qw.dequantize().t() + return out + b if b is not None else out + return torch.nn.functional.linear(input, qw, b) + + def _expert_qt_from(self, weight: QuantizedTensor, i: int) -> QuantizedTensor: + """Build a per-expert QuantizedTensor by indexing into a resident bank.""" + params = weight._params + kwargs = { + "scale": params.scale[i] if params.scale.dim() else params.scale, + "orig_dtype": params.orig_dtype, + "orig_shape": (self.out_features, self.in_features), + } + if hasattr(params, "block_scale"): # NVFP4 + kwargs["block_scale"] = params.block_scale[i] + return QuantizedTensor(weight._qdata[i], weight._layout_cls, type(params)(**kwargs)) + + def state_dict(self, *args, destination=None, prefix="", **kwargs): + sd = destination if destination is not None else {} + return _quantized_weight_state_dict(self, sd, prefix, extra_quant_conf={"num_experts": self.num_experts}) class Embedding(manual_cast.Embedding): - def _load_from_state_dict(self, state_dict, prefix, local_metadata, - strict, missing_keys, unexpected_keys, error_msgs): + def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs): weight_key = f"{prefix}weight" layer_conf = state_dict.pop(f"{prefix}comfy_quant", None) if layer_conf is not None: @@ -1259,13 +1439,16 @@ def mixed_precision_ops(quant_config={}, compute_dtype=torch.bfloat16, full_prec # Only fp8 makes sense for embeddings (per-row dequant via index select). # Block-scaled formats (NVFP4, MXFP8) can't do per-row lookup efficiently. - quant_format = layer_conf.get("format", None) if layer_conf is not None else None - if quant_format in ["float8_e4m3fn", "float8_e5m2"] and weight_key in state_dict: + quant_format = layer_conf.get("format") if layer_conf is not None else None + manually_loaded_keys = [] + + if quant_format in ("float8_e4m3fn", "float8_e5m2") and weight_key in state_dict: self.quant_format = quant_format qconfig = QUANT_ALGOS[quant_format] - layout_cls = get_layout_class(qconfig["comfy_tensor_layout"]) + self.layout_type = qconfig["comfy_tensor_layout"] + layout_cls = get_layout_class(self.layout_type) weight = state_dict.pop(weight_key) - manually_loaded_keys = [weight_key] + manually_loaded_keys.append(weight_key) scale_key = f"{prefix}weight_scale" scale = state_dict.pop(scale_key, None) @@ -1281,35 +1464,19 @@ def mixed_precision_ops(quant_config={}, compute_dtype=torch.bfloat16, full_prec self.weight = torch.nn.Parameter( QuantizedTensor(weight.to(dtype=qconfig["storage_t"]), qconfig["comfy_tensor_layout"], params), requires_grad=False) + elif layer_conf is not None: + # Unsupported format — restore the marker so it round-trips; fall through to default load. + state_dict[f"{prefix}comfy_quant"] = torch.tensor( + list(json.dumps(layer_conf).encode('utf-8')), dtype=torch.uint8) - super()._load_from_state_dict(state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs) - for k in manually_loaded_keys: - if k in missing_keys: - missing_keys.remove(k) - else: - if layer_conf is not None: - state_dict[f"{prefix}comfy_quant"] = torch.tensor(list(json.dumps(layer_conf).encode('utf-8')), dtype=torch.uint8) - super()._load_from_state_dict(state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs) + super()._load_from_state_dict(state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs) + for k in manually_loaded_keys: + if k in missing_keys: + missing_keys.remove(k) def state_dict(self, *args, destination=None, prefix="", **kwargs): - if destination is not None: - sd = destination - else: - sd = {} - - if not hasattr(self, 'weight') or self.weight is None: - return sd - - if isinstance(self.weight, QuantizedTensor): - sd_out = self.weight.state_dict("{}weight".format(prefix)) - for k in sd_out: - sd[k] = sd_out[k] - - quant_conf = {"format": self.quant_format} - sd["{}comfy_quant".format(prefix)] = torch.tensor(list(json.dumps(quant_conf).encode('utf-8')), dtype=torch.uint8) - else: - sd["{}weight".format(prefix)] = self.weight - return sd + sd = destination if destination is not None else {} + return _quantized_weight_state_dict(self, sd, prefix) def forward_comfy_cast_weights(self, input, out_dtype=None): weight = self.weight @@ -1353,6 +1520,7 @@ def pick_operations(weight_dtype, compute_dtype, load_device=None, disable_fast_ if not fp8_compute: disabled.add("float8_e4m3fn") disabled.add("float8_e5m2") + logging.info("Native ops: {} {}".format(", ".join(QUANT_ALGOS.keys() - disabled), ", emulated ops: {}".format(", ".join(disabled)) if len(disabled) > 0 else "")) return mixed_precision_ops(model_config.quant_config, compute_dtype, disabled=disabled) if ( diff --git a/comfy/patcher_extension.py b/comfy/patcher_extension.py index 5ee4d5ee5..189ee84ca 100644 --- a/comfy/patcher_extension.py +++ b/comfy/patcher_extension.py @@ -1,8 +1,9 @@ -from __future__ import annotations from typing import Callable class CallbacksMP: ON_CLONE = "on_clone" + ON_DEEPCLONE_MULTIGPU = "on_deepclone_multigpu" + ON_MATCH_MULTIGPU_CLONES = "on_match_multigpu_clones" ON_LOAD = "on_load_after" ON_DETACH = "on_detach_after" ON_CLEANUP = "on_cleanup" diff --git a/comfy/pinned_memory.py b/comfy/pinned_memory.py index 6d3ba367a..0e8f573ba 100644 --- a/comfy/pinned_memory.py +++ b/comfy/pinned_memory.py @@ -2,42 +2,62 @@ import comfy.model_management import comfy.memory_management import comfy_aimdo.host_buffer import comfy_aimdo.torch +import torch from comfy.cli_args import args -def get_pin(module): - return getattr(module, "_pin", None) +def get_pin(module, subset="weights"): + pin = getattr(module, "_pin", None) + if pin is None or module._pin_registered or args.disable_pinned_memory: + return pin -def pin_memory(module): - if module.pin_failed or args.disable_pinned_memory or get_pin(module) is not None: + _, _, stack_split, pinned_size = module._pin_state[subset] + size = pin.nbytes + comfy.model_management.ensure_pin_registerable(size) + + if torch.cuda.cudart().cudaHostRegister(pin.data_ptr(), size, 1) != 0: + comfy.model_management.discard_cuda_async_error() + return pin + + module._pin_registered = True + stack_split[0] = max(stack_split[0], module._pin_stack_index) + comfy.model_management.TOTAL_PINNED_MEMORY += size + pinned_size[0] += size + return pin + +def pin_memory(module, subset="weights", size=None): + pin_state = module._pin_state + if args.disable_pinned_memory: return - size = comfy.memory_management.vram_aligned_size([ module.weight, module.bias ]) + pin = get_pin(module, subset) + if pin is not None or pin_state["failed"]: + return - if comfy.model_management.MAX_PINNED_MEMORY <= 0 or (comfy.model_management.TOTAL_PINNED_MEMORY + size) > comfy.model_management.MAX_PINNED_MEMORY: - module.pin_failed = True + hostbuf, stack, stack_split, pinned_size = pin_state[subset] + if size is None: + size = comfy.memory_management.vram_aligned_size([ module.weight, module.bias ]) + offset = hostbuf.size + registerable_size = size + max(0, hostbuf.size - pinned_size[0]) + + comfy.memory_management.extra_ram_release(comfy.memory_management.RAM_CACHE_HEADROOM) + if (not comfy.model_management.ensure_pin_budget(size) or + not comfy.model_management.ensure_pin_registerable(registerable_size)): + pin_state["failed"] = True return False try: - hostbuf = comfy_aimdo.host_buffer.HostBuffer(size) + hostbuf.extend(size=size) except RuntimeError: - module.pin_failed = True + pin_state["failed"] = True return False - module._pin = comfy_aimdo.torch.hostbuf_to_tensor(hostbuf) - module._pin_hostbuf = hostbuf + module._pin = comfy_aimdo.torch.hostbuf_to_tensor(hostbuf)[offset:offset + size] + module._pin.untyped_storage()._comfy_hostbuf = hostbuf + stack.append((module, offset)) + module._pin_registered = True + module._pin_stack_index = len(stack) - 1 + stack_split[0] = max(stack_split[0], module._pin_stack_index) comfy.model_management.TOTAL_PINNED_MEMORY += size + pinned_size[0] += size return True - -def unpin_memory(module): - if get_pin(module) is None: - return 0 - size = module._pin.numel() * module._pin.element_size() - - comfy.model_management.TOTAL_PINNED_MEMORY -= size - if comfy.model_management.TOTAL_PINNED_MEMORY < 0: - comfy.model_management.TOTAL_PINNED_MEMORY = 0 - - del module._pin - del module._pin_hostbuf - return size diff --git a/comfy/sample.py b/comfy/sample.py index 653829582..2be0cae5f 100644 --- a/comfy/sample.py +++ b/comfy/sample.py @@ -37,11 +37,12 @@ def prepare_noise(latent_image, seed, noise_inds=None): return noises -def fix_empty_latent_channels(model, latent_image, downscale_ratio_spacial=None): +def fix_empty_latent_channels(model, latent_image, downscale_ratio_spacial=None, downscale_ratio_temporal=None): if latent_image.is_nested: return latent_image latent_format = model.get_model_object("latent_format") #Resize the empty latent image so it has the right number of channels - if torch.count_nonzero(latent_image) == 0: + is_empty = torch.count_nonzero(latent_image) == 0 + if is_empty: if latent_format.latent_channels != latent_image.shape[1]: latent_image = comfy.utils.repeat_to_batch_size(latent_image, latent_format.latent_channels, dim=1) if downscale_ratio_spacial is not None: @@ -51,6 +52,13 @@ def fix_empty_latent_channels(model, latent_image, downscale_ratio_spacial=None) if latent_format.latent_dimensions == 3 and latent_image.ndim == 4: latent_image = latent_image.unsqueeze(2) + + if is_empty and downscale_ratio_temporal is not None: + if downscale_ratio_temporal != latent_format.temporal_downscale_ratio: + ratio = downscale_ratio_temporal / latent_format.temporal_downscale_ratio + new_t = max(1, round(latent_image.shape[2] * ratio)) + latent_image = comfy.utils.repeat_to_batch_size(latent_image, new_t, dim=2) + return latent_image def prepare_sampling(model, noise_shape, positive, negative, noise_mask): diff --git a/comfy/sampler_helpers.py b/comfy/sampler_helpers.py index 3782fd2d5..bdce2f2d8 100644 --- a/comfy/sampler_helpers.py +++ b/comfy/sampler_helpers.py @@ -1,16 +1,18 @@ from __future__ import annotations +import torch import uuid import math import collections import comfy.model_management import comfy.conds +import comfy.model_patcher import comfy.utils import comfy.hooks import comfy.patcher_extension from typing import TYPE_CHECKING if TYPE_CHECKING: - from comfy.model_patcher import ModelPatcher from comfy.model_base import BaseModel + from comfy.model_patcher import ModelPatcher from comfy.controlnet import ControlBase def prepare_mask(noise_mask, shape, device): @@ -119,6 +121,47 @@ def cleanup_additional_models(models): if hasattr(m, 'cleanup'): m.cleanup() +def preprocess_multigpu_conds(conds: dict[str, list[dict[str]]], model: ModelPatcher, model_options: dict[str]): + '''If multigpu acceleration required, creates deepclones of ControlNets and GLIGEN per device.''' + multigpu_models: list[ModelPatcher] = model.get_additional_models_with_key("multigpu") + if len(multigpu_models) == 0: + return + extra_devices = [x.load_device for x in multigpu_models] + # handle controlnets + controlnets: set[ControlBase] = set() + for k in conds: + for kk in conds[k]: + if 'control' in kk: + controlnets.add(kk['control']) + if len(controlnets) > 0: + # first, unload all controlnet clones + for cnet in list(controlnets): + cnet_models = cnet.get_models() + for cm in cnet_models: + comfy.model_management.unload_model_and_clones(cm, unload_additional_models=True) + + # next, make sure each controlnet has a deepclone for all relevant devices + for cnet in controlnets: + curr_cnet = cnet + while curr_cnet is not None: + for device in extra_devices: + if device not in curr_cnet.multigpu_clones: + curr_cnet.deepclone_multigpu(device, autoregister=True) + curr_cnet = curr_cnet.previous_controlnet + # since all device clones are now present, recreate the linked list for cloned cnets per device + for cnet in controlnets: + curr_cnet = cnet + while curr_cnet is not None: + prev_cnet = curr_cnet.previous_controlnet + for device in extra_devices: + device_cnet = curr_cnet.get_instance_for_device(device) + prev_device_cnet = None + if prev_cnet is not None: + prev_device_cnet = prev_cnet.get_instance_for_device(device) + device_cnet.set_previous_controlnet(prev_device_cnet) + curr_cnet = prev_cnet + # potentially handle gligen - since not widely used, ignored for now + def estimate_memory(model, noise_shape, conds): cond_shapes = collections.defaultdict(list) cond_shapes_min = {} @@ -143,7 +186,8 @@ def prepare_sampling(model: ModelPatcher, noise_shape, conds, model_options=None return executor.execute(model, noise_shape, conds, model_options=model_options, force_full_load=force_full_load, force_offload=force_offload) def _prepare_sampling(model: ModelPatcher, noise_shape, conds, model_options=None, force_full_load=False, force_offload=False): - real_model: BaseModel = None + model.match_multigpu_clones() + preprocess_multigpu_conds(conds, model, model_options) models, inference_memory = get_additional_models(conds, model.model_dtype()) models += get_additional_models_from_model_options(model_options) models += model.get_nested_additional_models() # TODO: does this require inference_memory update? @@ -155,7 +199,7 @@ def _prepare_sampling(model: ModelPatcher, noise_shape, conds, model_options=Non memory_required += inference_memory minimum_memory_required += inference_memory comfy.model_management.load_models_gpu([model] + models, memory_required=memory_required, minimum_memory_required=minimum_memory_required, force_full_load=force_full_load) - real_model = model.model + real_model: BaseModel = model.model return real_model, conds, models @@ -201,3 +245,18 @@ def prepare_model_patcher(model: ModelPatcher, conds, model_options: dict): comfy.patcher_extension.merge_nested_dicts(to_load_options.setdefault(wc_name, {}), model_options["transformer_options"][wc_name], copy_dict1=False) return to_load_options + +def prepare_model_patcher_multigpu_clones(model_patcher: ModelPatcher, loaded_models: list[ModelPatcher], model_options: dict): + ''' + In case multigpu acceleration is enabled, prep ModelPatchers for each device. + ''' + multigpu_patchers: list[ModelPatcher] = [x for x in loaded_models if x.is_multigpu_base_clone] + if len(multigpu_patchers) > 0: + multigpu_dict: dict[torch.device, ModelPatcher] = {} + multigpu_dict[model_patcher.load_device] = model_patcher + for x in multigpu_patchers: + x.hook_patches = comfy.model_patcher.create_hook_patches_clone(model_patcher.hook_patches, copy_tuples=True) + x.hook_mode = model_patcher.hook_mode # match main model's hook_mode + multigpu_dict[x.load_device] = x + model_options["multigpu_clones"] = multigpu_dict + return multigpu_patchers diff --git a/comfy/samplers.py b/comfy/samplers.py index 0a4d062db..e31277f7b 100755 --- a/comfy/samplers.py +++ b/comfy/samplers.py @@ -1,7 +1,9 @@ from __future__ import annotations + +import comfy.model_management from .k_diffusion import sampling as k_diffusion_sampling from .extra_samplers import uni_pc -from typing import TYPE_CHECKING, Callable, NamedTuple +from typing import TYPE_CHECKING, Callable, NamedTuple, Any if TYPE_CHECKING: from comfy.model_patcher import ModelPatcher from comfy.model_base import BaseModel @@ -16,6 +18,7 @@ import comfy.model_patcher import comfy.patcher_extension import comfy.hooks import comfy.context_windows +import comfy.multigpu import comfy.utils import scipy.stats import numpy @@ -141,7 +144,7 @@ def can_concat_cond(c1, c2): return cond_equal_size(c1.conditioning, c2.conditioning) -def cond_cat(c_list): +def cond_cat(c_list, device=None): temp = {} for x in c_list: for k in x: @@ -153,6 +156,8 @@ def cond_cat(c_list): for k in temp: conds = temp[k] out[k] = conds[0].concat(conds[1:]) + if device is not None and hasattr(out[k], 'to'): + out[k] = out[k].to(device) return out @@ -212,7 +217,12 @@ def _calc_cond_batch_outer(model: BaseModel, conds: list[list[dict]], x_in: torc ) return executor.execute(model, conds, x_in, timestep, model_options) -def _calc_cond_batch(model: BaseModel, conds: list[list[dict]], x_in: torch.Tensor, timestep, model_options): +def _calc_cond_batch(model: BaseModel, conds: list[list[dict]], x_in: torch.Tensor, timestep: torch.Tensor, model_options: dict[str]): + # NOTE: keep in sync with _calc_cond_batch_multigpu below. Shared logic + # (hooked_to_run accumulation, memory-fit batching, per-chunk output + # aggregation) is duplicated there with per-device scheduling layered on top. + if 'multigpu_clones' in model_options: + return _calc_cond_batch_multigpu(model, conds, x_in, timestep, model_options) out_conds = [] out_counts = [] # separate conds by matching hooks @@ -244,7 +254,7 @@ def _calc_cond_batch(model: BaseModel, conds: list[list[dict]], x_in: torch.Tens if has_default_conds: finalize_default_conds(model, hooked_to_run, default_conds, x_in, timestep, model_options) - model.current_patcher.prepare_state(timestep) + model.current_patcher.prepare_state(timestep, model_options) # run every hooked_to_run separately for hooks, to_run in hooked_to_run.items(): @@ -265,7 +275,6 @@ def _calc_cond_batch(model: BaseModel, conds: list[list[dict]], x_in: torch.Tens input_shape = [len(batch_amount) * first_shape[0]] + list(first_shape)[1:] cond_shapes = collections.defaultdict(list) for tt in batch_amount: - cond = {k: v.size() for k, v in to_run[tt][0].conditioning.items()} for k, v in to_run[tt][0].conditioning.items(): cond_shapes[k].append(v.size()) @@ -345,6 +354,239 @@ def _calc_cond_batch(model: BaseModel, conds: list[list[dict]], x_in: torch.Tens return out_conds +def _calc_cond_batch_multigpu(model: BaseModel, conds: list[list[dict]], x_in: torch.Tensor, timestep: torch.Tensor, model_options: dict[str]): + # NOTE: keep in sync with _calc_cond_batch above. Same conds-by-hooks + # accumulation, memory-fit batching, and output aggregation, but adds a + # per-device scheduler, per-device patcher/control lookup, tensor .to(device) + # placement, and MultiGPUThreadPool dispatch around the inner loop. + out_conds = [] + out_counts = [] + # separate conds by matching hooks + hooked_to_run: dict[comfy.hooks.HookGroup,list[tuple[tuple,int]]] = {} + default_conds = [] + has_default_conds = False + + output_device = x_in.device + + for i in range(len(conds)): + out_conds.append(torch.zeros_like(x_in)) + out_counts.append(torch.ones_like(x_in) * 1e-37) + + cond = conds[i] + default_c = [] + if cond is not None: + for x in cond: + if 'default' in x: + default_c.append(x) + has_default_conds = True + continue + p = get_area_and_mult(x, x_in, timestep) + if p is None: + continue + if p.hooks is not None: + model.current_patcher.prepare_hook_patches_current_keyframe(timestep, p.hooks, model_options) + hooked_to_run.setdefault(p.hooks, list()) + hooked_to_run[p.hooks] += [(p, i)] + default_conds.append(default_c) + + if has_default_conds: + finalize_default_conds(model, hooked_to_run, default_conds, x_in, timestep, model_options) + + model.current_patcher.prepare_state(timestep, model_options) + + devices = list(model_options['multigpu_clones'].keys()) + device_batched_hooked_to_run: dict[torch.device, list[tuple[comfy.hooks.HookGroup, tuple]]] = {} + # Track conds currently scheduled per device; single source of truth for capacity checks. + device_load: dict[torch.device, int] = {d: 0 for d in devices} + + total_conds = sum(len(to_run) for to_run in hooked_to_run.values()) + conds_per_device = max(1, math.ceil(total_conds / len(devices))) + + def next_available_device(start: int) -> tuple[int, torch.device]: + """Return (index, device) for the next device with remaining capacity, starting at `start`. + + Scans at most len(devices) positions, so this always terminates. Raises if no device + has remaining capacity, which would indicate a bug in conds_per_device accounting. + """ + for offset in range(len(devices)): + i = (start + offset) % len(devices) + if device_load[devices[i]] < conds_per_device: + return i, devices[i] + raise RuntimeError( + f"MultiGPU scheduler: all {len(devices)} devices at capacity " + f"({conds_per_device}) but conds remain to schedule" + ) + + # run every hooked_to_run separately + index_device = 0 + for hooks, to_run in hooked_to_run.items(): + while len(to_run) > 0: + index_device, current_device = next_available_device(index_device) + remaining_capacity = conds_per_device - device_load[current_device] + + first = to_run[0] + first_shape = first[0][0].shape + # collect candidate indices that can be concatenated with `first`, up to remaining capacity + to_batch_temp = [] + for x in range(len(to_run)): + if can_concat_cond(to_run[x][0], first[0]) and len(to_batch_temp) < remaining_capacity: + to_batch_temp += [x] + + to_batch_temp.reverse() + to_batch = to_batch_temp[:1] + + free_memory = comfy.model_management.get_free_memory(current_device) + for i in range(1, len(to_batch_temp) + 1): + batch_amount = to_batch_temp[:len(to_batch_temp)//i] + input_shape = [len(batch_amount) * first_shape[0]] + list(first_shape)[1:] + cond_shapes = collections.defaultdict(list) + for tt in batch_amount: + for k, v in to_run[tt][0].conditioning.items(): + cond_shapes[k].append(v.size()) + if model.memory_required(input_shape, cond_shapes=cond_shapes) * 1.5 < free_memory: + to_batch = batch_amount + break + + conds_to_batch = [to_run.pop(x) for x in to_batch] + device_load[current_device] += len(conds_to_batch) + device_batched_hooked_to_run.setdefault(current_device, []).append((hooks, conds_to_batch)) + + if device_load[current_device] >= conds_per_device: + index_device += 1 + + class thread_result(NamedTuple): + output: Any + mult: Any + area: Any + batch_chunks: int + cond_or_uncond: Any + error: Exception = None + + def _handle_batch(device: torch.device, batch_tuple: tuple[comfy.hooks.HookGroup, tuple], results: list[thread_result]): + try: + # TODO: non-NVIDIA support -- guard with `if device.type == "cuda":` once + # we extend multigpu QA beyond CUDA. Unconditional call crashes on + # XPU/NPU/MPS/CPU/DirectML backends. + torch.cuda.set_device(device) + model_current: BaseModel = model_options["multigpu_clones"][device].model + # run every hooked_to_run separately + with torch.no_grad(): + for hooks, to_batch in batch_tuple: + input_x = [] + mult = [] + c = [] + cond_or_uncond = [] + uuids = [] + area = [] + control: ControlBase = None + patches = None + for x in to_batch: + o = x + p = o[0] + input_x.append(p.input_x) + mult.append(p.mult) + c.append(p.conditioning) + area.append(p.area) + cond_or_uncond.append(o[1]) + uuids.append(p.uuid) + control = p.control + patches = p.patches + + batch_chunks = len(cond_or_uncond) + input_x = torch.cat(input_x).to(device) + c = cond_cat(c, device=device) + timestep_ = torch.cat([timestep.to(device)] * batch_chunks) + + transformer_options = model_current.current_patcher.apply_hooks(hooks=hooks) + if 'transformer_options' in model_options: + transformer_options = comfy.patcher_extension.merge_nested_dicts(transformer_options, + model_options['transformer_options'], + copy_dict1=False) + + if patches is not None: + transformer_options["patches"] = comfy.patcher_extension.merge_nested_dicts( + transformer_options.get("patches", {}), + patches + ) + + transformer_options["cond_or_uncond"] = cond_or_uncond[:] + transformer_options["uuids"] = uuids[:] + transformer_options["sigmas"] = timestep.to(device) + transformer_options["sample_sigmas"] = transformer_options["sample_sigmas"].to(device) + transformer_options["multigpu_thread_device"] = device + + cast_transformer_options(transformer_options, device=device) + c['transformer_options'] = transformer_options + + if control is not None: + device_control = control.get_instance_for_device(device) + c['control'] = device_control.get_control(input_x, timestep_, c, len(cond_or_uncond), transformer_options) + + if 'model_function_wrapper' in model_options: + output = model_options['model_function_wrapper'](model_current.apply_model, {"input": input_x, "timestep": timestep_, "c": c, "cond_or_uncond": cond_or_uncond}).to(output_device).chunk(batch_chunks) + else: + output = model_current.apply_model(input_x, timestep_, **c).to(output_device).chunk(batch_chunks) + # TODO: non-NVIDIA support -- the `.to(output_device)` copies + # above are async on CUDA, so the main thread's aggregation + # could race with in-flight transfers. CUDA-only QA has not + # surfaced this in practice, but before extending multigpu + # beyond NVIDIA add a `torch.cuda.synchronize(output_device)` + # here (guarded by `output_device.type == "cuda"`). + results.append(thread_result(output, mult, area, batch_chunks, cond_or_uncond)) + except Exception as e: + results.append(thread_result(None, None, None, None, None, error=e)) + raise + + + def _handle_batch_pooled(device, batch_tuple): + worker_results = [] + _handle_batch(device, batch_tuple, worker_results) + return worker_results + + results: list[thread_result] = [] + thread_pool: comfy.multigpu.MultiGPUThreadPool = model_options.get("multigpu_thread_pool") + + # Submit all GPU work to pool threads + pool_devices = [] + for device, batch_tuple in device_batched_hooked_to_run.items(): + if thread_pool is not None: + thread_pool.submit(device, _handle_batch_pooled, device, batch_tuple) + pool_devices.append(device) + else: + # Fallback: no pool, run everything on main thread + _handle_batch(device, batch_tuple, results) + + # Collect results from pool workers + for device in pool_devices: + worker_results, error = thread_pool.get_result(device) + if error is not None: + raise error + results.extend(worker_results) + + for output, mult, area, batch_chunks, cond_or_uncond, error in results: + if error is not None: + raise error + for o in range(batch_chunks): + cond_index = cond_or_uncond[o] + a = area[o] + if a is None: + out_conds[cond_index] += output[o] * mult[o] + out_counts[cond_index] += mult[o] + else: + out_c = out_conds[cond_index] + out_cts = out_counts[cond_index] + dims = len(a) // 2 + for i in range(dims): + out_c = out_c.narrow(i + 2, a[i + dims], a[i]) + out_cts = out_cts.narrow(i + 2, a[i + dims], a[i]) + out_c += output[o] * mult[o] + out_cts += mult[o] + + for i in range(len(out_conds)): + out_conds[i] /= out_counts[i] + + return out_conds + def calc_cond_uncond_batch(model, cond, uncond, x_in, timestep, model_options): #TODO: remove logging.warning("WARNING: The comfy.samplers.calc_cond_uncond_batch function is deprecated please use the calc_cond_batch one instead.") return tuple(calc_cond_batch(model, [cond, uncond], x_in, timestep, model_options)) @@ -643,12 +885,21 @@ def calculate_start_end_timesteps(model, conds): def pre_run_control(model, conds): s = model.model_sampling + # Per-device model lookup so multigpu control clones get the matching + # diffusion_model (e.g. QwenFunControlNet stashes it into extra_args). + device_models: dict = {} + patcher = getattr(model, "current_patcher", None) + if patcher is not None: + for p in patcher.get_additional_models_with_key("multigpu"): + device_models[p.load_device] = p.model for t in range(len(conds)): x = conds[t] percent_to_timestep_function = lambda a: s.percent_to_sigma(a) if 'control' in x: x['control'].pre_run(model, percent_to_timestep_function) + for device, device_cnet in x['control'].multigpu_clones.items(): + device_cnet.pre_run(device_models.get(device, model), percent_to_timestep_function) def apply_empty_x_to_equal_area(conds, uncond, name, uncond_fill_func): cond_cnets = [] @@ -891,7 +1142,9 @@ def cast_to_load_options(model_options: dict[str], device=None, dtype=None): to_load_options = model_options.get("to_load_options", None) if to_load_options is None: return + cast_transformer_options(to_load_options, device, dtype) +def cast_transformer_options(transformer_options: dict[str], device=None, dtype=None): casts = [] if device is not None: casts.append(device) @@ -900,18 +1153,17 @@ def cast_to_load_options(model_options: dict[str], device=None, dtype=None): # if nothing to apply, do nothing if len(casts) == 0: return - # try to call .to on patches - if "patches" in to_load_options: - patches = to_load_options["patches"] + if "patches" in transformer_options: + patches = transformer_options["patches"] for name in patches: patch_list = patches[name] for i in range(len(patch_list)): if hasattr(patch_list[i], "to"): for cast in casts: patch_list[i] = patch_list[i].to(cast) - if "patches_replace" in to_load_options: - patches = to_load_options["patches_replace"] + if "patches_replace" in transformer_options: + patches = transformer_options["patches_replace"] for name in patches: patch_list = patches[name] for k in patch_list: @@ -921,8 +1173,8 @@ def cast_to_load_options(model_options: dict[str], device=None, dtype=None): # try to call .to on any wrappers/callbacks wrappers_and_callbacks = ["wrappers", "callbacks"] for wc_name in wrappers_and_callbacks: - if wc_name in to_load_options: - wc: dict[str, list] = to_load_options[wc_name] + if wc_name in transformer_options: + wc: dict[str, list] = transformer_options[wc_name] for wc_dict in wc.values(): for wc_list in wc_dict.values(): for i in range(len(wc_list)): @@ -930,7 +1182,6 @@ def cast_to_load_options(model_options: dict[str], device=None, dtype=None): for cast in casts: wc_list[i] = wc_list[i].to(cast) - class CFGGuider: def __init__(self, model_patcher: ModelPatcher): self.model_patcher = model_patcher @@ -985,16 +1236,32 @@ class CFGGuider: self.inner_model, self.conds, self.loaded_models = comfy.sampler_helpers.prepare_sampling(self.model_patcher, noise.shape, self.conds, self.model_options) device = self.model_patcher.load_device - noise = noise.to(device=device, dtype=torch.float32) - latent_image = latent_image.to(device=device, dtype=torch.float32) - sigmas = sigmas.to(device) - cast_to_load_options(self.model_options, device=device, dtype=self.model_patcher.model_dtype()) + multigpu_patchers = comfy.sampler_helpers.prepare_model_patcher_multigpu_clones(self.model_patcher, self.loaded_models, self.model_options) - try: - self.model_patcher.pre_run() - output = self.inner_sample(noise, latent_image, device, sampler, sigmas, denoise_mask, callback, disable_pbar, seed, latent_shapes=latent_shapes) - finally: - self.model_patcher.cleanup() + # Create persistent thread pool for all GPU devices (main + extras) + if multigpu_patchers: + extra_devices = [p.load_device for p in multigpu_patchers] + all_devices = [device] + extra_devices + self.model_options["multigpu_thread_pool"] = comfy.multigpu.MultiGPUThreadPool(all_devices) + + with comfy.model_management.cuda_device_context(device): + try: + noise = noise.to(device=device, dtype=torch.float32) + latent_image = latent_image.to(device=device, dtype=torch.float32) + sigmas = sigmas.to(device) + cast_to_load_options(self.model_options, device=device, dtype=self.model_patcher.model_dtype()) + + self.model_patcher.pre_run() + for multigpu_patcher in multigpu_patchers: + multigpu_patcher.pre_run() + output = self.inner_sample(noise, latent_image, device, sampler, sigmas, denoise_mask, callback, disable_pbar, seed, latent_shapes=latent_shapes) + finally: + thread_pool = self.model_options.pop("multigpu_thread_pool", None) + if thread_pool is not None: + thread_pool.shutdown() + self.model_patcher.cleanup() + for multigpu_patcher in multigpu_patchers: + multigpu_patcher.cleanup() comfy.sampler_helpers.cleanup_models(self.conds, self.loaded_models) del self.inner_model diff --git a/comfy/sd.py b/comfy/sd.py index 9fce0e7d0..beb782310 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -1,4 +1,3 @@ -from __future__ import annotations import json import torch from enum import Enum @@ -21,6 +20,7 @@ import comfy.ldm.ace.vae.music_dcae_pipeline import comfy.ldm.cogvideo.vae import comfy.ldm.hunyuan_video.vae import comfy.ldm.mmaudio.vae.autoencoder +import comfy.ldm.audio.vae_sa3 import comfy.pixel_space_convert import comfy.weight_adapter import yaml @@ -66,6 +66,9 @@ import comfy.text_encoders.longcat_image import comfy.text_encoders.qwen35 import comfy.text_encoders.ernie import comfy.text_encoders.gemma4 +import comfy.text_encoders.cogvideo +import comfy.text_encoders.sa3 +import comfy.text_encoders.gpt_oss import comfy.model_patcher import comfy.lora @@ -78,7 +81,7 @@ import comfy.latent_formats import comfy.ldm.flux.redux -def load_lora_for_models(model, clip, lora, strength_model, strength_clip): +def load_lora_for_models(model, clip, lora, strength_model, strength_clip, lora_metadata=None): key_map = {} if model is not None: key_map = comfy.lora.model_lora_keys_unet(model.model, key_map) @@ -90,6 +93,8 @@ def load_lora_for_models(model, clip, lora, strength_model, strength_clip): if model is not None: new_modelpatcher = model.clone() k = new_modelpatcher.add_patches(loaded, strength_model) + if lora_metadata: + new_modelpatcher.set_attachments("lora_metadata", lora_metadata) else: k = () new_modelpatcher = None @@ -97,6 +102,8 @@ def load_lora_for_models(model, clip, lora, strength_model, strength_clip): if clip is not None: new_clip = clip.clone() k1 = new_clip.add_patches(loaded, strength_clip) + if lora_metadata: + new_clip.patcher.set_attachments("lora_metadata", lora_metadata) else: k1 = () new_clip = None @@ -238,7 +245,8 @@ class CLIP: model_management.archive_model_dtypes(self.cond_stage_model) self.tokenizer = tokenizer(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data) - ModelPatcher = comfy.model_patcher.ModelPatcher if disable_dynamic else comfy.model_patcher.CoreModelPatcher + te_disable_dynamic = disable_dynamic or getattr(self.cond_stage_model, "disable_offload", False) + ModelPatcher = comfy.model_patcher.ModelPatcher if te_disable_dynamic else comfy.model_patcher.CoreModelPatcher self.patcher = ModelPatcher(self.cond_stage_model, load_device=load_device, offload_device=offload_device) #Match torch.float32 hardcode upcast in TE implemention self.patcher.set_model_compute_dtype(torch.float32) @@ -327,41 +335,43 @@ class CLIP: self.cond_stage_model.set_clip_options({"projected_pooled": False}) self.load_model(tokens) - self.cond_stage_model.set_clip_options({"execution_device": self.patcher.load_device}) + device = self.patcher.load_device + self.cond_stage_model.set_clip_options({"execution_device": device}) all_hooks.reset() self.patcher.patch_hooks(None) if show_pbar: pbar = ProgressBar(len(scheduled_keyframes)) - for scheduled_opts in scheduled_keyframes: - t_range = scheduled_opts[0] - # don't bother encoding any conds outside of start_percent and end_percent bounds - if "start_percent" in add_dict: - if t_range[1] < add_dict["start_percent"]: - continue - if "end_percent" in add_dict: - if t_range[0] > add_dict["end_percent"]: - continue - hooks_keyframes = scheduled_opts[1] - for hook, keyframe in hooks_keyframes: - hook.hook_keyframe._current_keyframe = keyframe - # apply appropriate hooks with values that match new hook_keyframe - self.patcher.patch_hooks(all_hooks) - # perform encoding as normal - o = self.cond_stage_model.encode_token_weights(tokens) - cond, pooled = o[:2] - pooled_dict = {"pooled_output": pooled} - # add clip_start_percent and clip_end_percent in pooled - pooled_dict["clip_start_percent"] = t_range[0] - pooled_dict["clip_end_percent"] = t_range[1] - # add/update any keys with the provided add_dict - pooled_dict.update(add_dict) - # add hooks stored on clip - self.add_hooks_to_dict(pooled_dict) - all_cond_pooled.append([cond, pooled_dict]) - if show_pbar: - pbar.update(1) - model_management.throw_exception_if_processing_interrupted() + with model_management.cuda_device_context(device): + for scheduled_opts in scheduled_keyframes: + t_range = scheduled_opts[0] + # don't bother encoding any conds outside of start_percent and end_percent bounds + if "start_percent" in add_dict: + if t_range[1] < add_dict["start_percent"]: + continue + if "end_percent" in add_dict: + if t_range[0] > add_dict["end_percent"]: + continue + hooks_keyframes = scheduled_opts[1] + for hook, keyframe in hooks_keyframes: + hook.hook_keyframe._current_keyframe = keyframe + # apply appropriate hooks with values that match new hook_keyframe + self.patcher.patch_hooks(all_hooks) + # perform encoding as normal + o = self.cond_stage_model.encode_token_weights(tokens) + cond, pooled = o[:2] + pooled_dict = {"pooled_output": pooled} + # add clip_start_percent and clip_end_percent in pooled + pooled_dict["clip_start_percent"] = t_range[0] + pooled_dict["clip_end_percent"] = t_range[1] + # add/update any keys with the provided add_dict + pooled_dict.update(add_dict) + # add hooks stored on clip + self.add_hooks_to_dict(pooled_dict) + all_cond_pooled.append([cond, pooled_dict]) + if show_pbar: + pbar.update(1) + model_management.throw_exception_if_processing_interrupted() all_hooks.reset() return all_cond_pooled @@ -375,8 +385,12 @@ class CLIP: self.cond_stage_model.set_clip_options({"projected_pooled": False}) self.load_model(tokens) - self.cond_stage_model.set_clip_options({"execution_device": self.patcher.load_device}) - o = self.cond_stage_model.encode_token_weights(tokens) + device = self.patcher.load_device + self.cond_stage_model.set_clip_options({"execution_device": device}) + + with model_management.cuda_device_context(device): + o = self.cond_stage_model.encode_token_weights(tokens) + cond, pooled = o[:2] if return_dict: out = {"cond": cond, "pooled_output": pooled} @@ -417,6 +431,13 @@ class CLIP: sd_clip[k] = sd_tokenizer[k] return sd_clip + def state_dict_for_saving(self): + sd_clip = self.patcher.model_state_dict_for_saving() + sd_tokenizer = self.tokenizer.state_dict() + for k in sd_tokenizer: + sd_clip[k] = sd_tokenizer[k] + return sd_clip + def load_model(self, tokens={}): memory_used = 0 if hasattr(self.cond_stage_model, "memory_estimation_function"): @@ -431,9 +452,12 @@ class CLIP: self.cond_stage_model.reset_clip_options() self.load_model(tokens) + device = self.patcher.load_device self.cond_stage_model.set_clip_options({"layer": None}) - self.cond_stage_model.set_clip_options({"execution_device": self.patcher.load_device}) - return self.cond_stage_model.generate(tokens, do_sample=do_sample, max_length=max_length, temperature=temperature, top_k=top_k, top_p=top_p, min_p=min_p, repetition_penalty=repetition_penalty, seed=seed, presence_penalty=presence_penalty) + self.cond_stage_model.set_clip_options({"execution_device": device}) + + with model_management.cuda_device_context(device): + return self.cond_stage_model.generate(tokens, do_sample=do_sample, max_length=max_length, temperature=temperature, top_k=top_k, top_p=top_p, min_p=min_p, repetition_penalty=repetition_penalty, seed=seed, presence_penalty=presence_penalty) def decode(self, token_ids, skip_special_tokens=True): return self.tokenizer.decode(token_ids, skip_special_tokens=skip_special_tokens) @@ -775,6 +799,7 @@ class VAE: self.latent_channels = 3 self.latent_dim = 2 self.output_channels = 3 + self.disable_offload = True elif "vocoder.activation_post.downsample.lowpass.filter" in sd: #MMAudio VAE sample_rate = 16000 if sample_rate == 16000: @@ -840,6 +865,34 @@ class VAE: self.working_dtypes = [torch.float32] self.disable_offload = True self.extra_1d_channel = 16 + elif "decoder.layers.3.transformers.0.pre_norm.alpha" in sd: # Stable Audio 3 VAE + if "decoder.layers.3.transformers.11.self_attn.to_out.weight" in sd: + config = {"channels": 256, "transformer_depths": 12, "sinusoidal_blocks": 8, + "sliding_window": [1, 1], "decoder_conv_mapping": False, + "chunk_size": 128, "chunk_midpoint_shift": False} + self.memory_used_encode = lambda shape, dtype: (1500 * shape[2]) * model_management.dtype_size(dtype) + self.memory_used_decode = lambda shape, dtype: (1500 * shape[2] * 4096) * model_management.dtype_size(dtype) + else: + config = {"channels": 128, "transformer_depths": 6, "sinusoidal_blocks": 0, + "sliding_window": None, "decoder_conv_mapping": True, + "chunk_size": 32, "chunk_midpoint_shift": True} + self.memory_used_encode = lambda shape, dtype: (72 * shape[2]) * model_management.dtype_size(dtype) + self.memory_used_decode = lambda shape, dtype: (72 * shape[2] * 4096) * model_management.dtype_size(dtype) + + self.first_stage_model = comfy.ldm.audio.vae_sa3.SA3AudioVAE(**config) + self.latent_channels = 256 + self.output_channels = 2 + self.upscale_ratio = 4096 + self.downscale_ratio = 4096 + self.latent_dim = 1 + self.audio_sample_rate = 44100 + self.process_output = lambda audio: audio + self.process_input = lambda audio: audio + self.working_dtypes = [torch.bfloat16, torch.float16, torch.float32] + #This VAE has Parameters and Buffers the non-dynamic caster cannot handle + #Force cast it for --disable-dynamic-vram users until there is a true core fix. + if not comfy.memory_management.aimdo_enabled: + self.disable_offload = True else: logging.warning("WARNING: No VAE weights detected, VAE not initalized.") self.first_stage_model = None @@ -982,50 +1035,52 @@ class VAE: do_tile = False if self.latent_dim == 2 and samples_in.ndim == 5: samples_in = samples_in[:, :, 0] - try: - memory_used = self.memory_used_decode(samples_in.shape, self.vae_dtype) - model_management.load_models_gpu([self.patcher], memory_required=memory_used, force_full_load=self.disable_offload) - free_memory = self.patcher.get_free_memory(self.device) - batch_number = int(free_memory / memory_used) - batch_number = max(1, batch_number) - # Pre-allocate output for VAEs that support direct buffer writes - preallocated = False - if getattr(self.first_stage_model, 'comfy_has_chunked_io', False): - pixel_samples = torch.empty(self.first_stage_model.decode_output_shape(samples_in.shape), device=self.output_device, dtype=self.vae_output_dtype()) - preallocated = True + with model_management.cuda_device_context(self.device): + try: + memory_used = self.memory_used_decode(samples_in.shape, self.vae_dtype) + model_management.load_models_gpu([self.patcher], memory_required=memory_used, force_full_load=self.disable_offload) + free_memory = self.patcher.get_free_memory(self.device) + batch_number = int(free_memory / memory_used) + batch_number = max(1, batch_number) - for x in range(0, samples_in.shape[0], batch_number): - samples = samples_in[x:x + batch_number].to(device=self.device, dtype=self.vae_dtype) - if preallocated: - self.first_stage_model.decode(samples, output_buffer=pixel_samples[x:x+batch_number], **vae_options) - else: - out = self.first_stage_model.decode(samples, **vae_options).to(device=self.output_device, dtype=self.vae_output_dtype(), copy=True) - if pixel_samples is None: - pixel_samples = torch.empty((samples_in.shape[0],) + tuple(out.shape[1:]), device=self.output_device, dtype=self.vae_output_dtype()) - pixel_samples[x:x+batch_number].copy_(out) - del out - self.process_output(pixel_samples[x:x+batch_number]) - except Exception as e: - model_management.raise_non_oom(e) - logging.warning("Warning: Ran out of memory when regular VAE decoding, retrying with tiled VAE decoding.") - #NOTE: We don't know what tensors were allocated to stack variables at the time of the - #exception and the exception itself refs them all until we get out of this except block. - #So we just set a flag for tiler fallback so that tensor gc can happen once the - #exception is fully off the books. - do_tile = True + # Pre-allocate output for VAEs that support direct buffer writes + preallocated = False + if getattr(self.first_stage_model, 'comfy_has_chunked_io', False): + pixel_samples = torch.empty(self.first_stage_model.decode_output_shape(samples_in.shape), device=self.output_device, dtype=self.vae_output_dtype()) + preallocated = True - if do_tile: - comfy.model_management.soft_empty_cache() - dims = samples_in.ndim - 2 - if dims == 1 or self.extra_1d_channel is not None: - pixel_samples = self.decode_tiled_1d(samples_in) - elif dims == 2: - pixel_samples = self.decode_tiled_(samples_in) - elif dims == 3: - tile = 256 // self.spacial_compression_decode() - overlap = tile // 4 - pixel_samples = self.decode_tiled_3d(samples_in, tile_x=tile, tile_y=tile, overlap=(1, overlap, overlap)) + for x in range(0, samples_in.shape[0], batch_number): + samples = samples_in[x:x + batch_number].to(device=self.device, dtype=self.vae_dtype) + if preallocated: + self.first_stage_model.decode(samples, output_buffer=pixel_samples[x:x+batch_number], **vae_options) + else: + out = self.first_stage_model.decode(samples, **vae_options).to(device=self.output_device, dtype=self.vae_output_dtype(), copy=True) + if pixel_samples is None: + pixel_samples = torch.empty((samples_in.shape[0],) + tuple(out.shape[1:]), device=self.output_device, dtype=self.vae_output_dtype()) + pixel_samples[x:x+batch_number].copy_(out) + del out + self.process_output(pixel_samples[x:x+batch_number]) + except Exception as e: + model_management.raise_non_oom(e) + logging.warning("Warning: Ran out of memory when regular VAE decoding, retrying with tiled VAE decoding.") + #NOTE: We don't know what tensors were allocated to stack variables at the time of the + #exception and the exception itself refs them all until we get out of this except block. + #So we just set a flag for tiler fallback so that tensor gc can happen once the + #exception is fully off the books. + do_tile = True + + if do_tile: + comfy.model_management.soft_empty_cache() + dims = samples_in.ndim - 2 + if dims == 1 or self.extra_1d_channel is not None: + pixel_samples = self.decode_tiled_1d(samples_in) + elif dims == 2: + pixel_samples = self.decode_tiled_(samples_in) + elif dims == 3: + tile = 256 // self.spacial_compression_decode() + overlap = tile // 4 + pixel_samples = self.decode_tiled_3d(samples_in, tile_x=tile, tile_y=tile, overlap=(1, overlap, overlap)) pixel_samples = pixel_samples.to(self.output_device).movedim(1,-1) return pixel_samples @@ -1043,20 +1098,21 @@ class VAE: if overlap is not None: args["overlap"] = overlap - if dims == 1 or self.extra_1d_channel is not None: - args.pop("tile_y") - output = self.decode_tiled_1d(samples, **args) - elif dims == 2: - output = self.decode_tiled_(samples, **args) - elif dims == 3: - if overlap_t is None: - args["overlap"] = (1, overlap, overlap) - else: - args["overlap"] = (max(1, overlap_t), overlap, overlap) - if tile_t is not None: - args["tile_t"] = max(2, tile_t) + with model_management.cuda_device_context(self.device): + if dims == 1 or self.extra_1d_channel is not None: + args.pop("tile_y") + output = self.decode_tiled_1d(samples, **args) + elif dims == 2: + output = self.decode_tiled_(samples, **args) + elif dims == 3: + if overlap_t is None: + args["overlap"] = (1, overlap, overlap) + else: + args["overlap"] = (max(1, overlap_t), overlap, overlap) + if tile_t is not None: + args["tile_t"] = max(2, tile_t) - output = self.decode_tiled_3d(samples, **args) + output = self.decode_tiled_3d(samples, **args) return output.movedim(1, -1) def encode(self, pixel_samples): @@ -1069,44 +1125,46 @@ class VAE: pixel_samples = pixel_samples.movedim(1, 0).unsqueeze(0) else: pixel_samples = pixel_samples.unsqueeze(2) - try: - memory_used = self.memory_used_encode(pixel_samples.shape, self.vae_dtype) - model_management.load_models_gpu([self.patcher], memory_required=memory_used, force_full_load=self.disable_offload) - free_memory = self.patcher.get_free_memory(self.device) - batch_number = int(free_memory / max(1, memory_used)) - batch_number = max(1, batch_number) - samples = None - for x in range(0, pixel_samples.shape[0], batch_number): - pixels_in = self.process_input(pixel_samples[x:x + batch_number]).to(self.vae_dtype) - if getattr(self.first_stage_model, 'comfy_has_chunked_io', False): - out = self.first_stage_model.encode(pixels_in, device=self.device) + + with model_management.cuda_device_context(self.device): + try: + memory_used = self.memory_used_encode(pixel_samples.shape, self.vae_dtype) + model_management.load_models_gpu([self.patcher], memory_required=memory_used, force_full_load=self.disable_offload) + free_memory = self.patcher.get_free_memory(self.device) + batch_number = int(free_memory / max(1, memory_used)) + batch_number = max(1, batch_number) + samples = None + for x in range(0, pixel_samples.shape[0], batch_number): + pixels_in = self.process_input(pixel_samples[x:x + batch_number]).to(self.vae_dtype) + if getattr(self.first_stage_model, 'comfy_has_chunked_io', False): + out = self.first_stage_model.encode(pixels_in, device=self.device) + else: + pixels_in = pixels_in.to(self.device) + out = self.first_stage_model.encode(pixels_in) + out = out.to(self.output_device).to(dtype=self.vae_output_dtype()) + if samples is None: + samples = torch.empty((pixel_samples.shape[0],) + tuple(out.shape[1:]), device=self.output_device, dtype=self.vae_output_dtype()) + samples[x:x + batch_number] = out + + except Exception as e: + model_management.raise_non_oom(e) + logging.warning("Warning: Ran out of memory when regular VAE encoding, retrying with tiled VAE encoding.") + #NOTE: We don't know what tensors were allocated to stack variables at the time of the + #exception and the exception itself refs them all until we get out of this except block. + #So we just set a flag for tiler fallback so that tensor gc can happen once the + #exception is fully off the books. + do_tile = True + + if do_tile: + comfy.model_management.soft_empty_cache() + if self.latent_dim == 3: + tile = 256 + overlap = tile // 4 + samples = self.encode_tiled_3d(pixel_samples, tile_x=tile, tile_y=tile, overlap=(1, overlap, overlap)) + elif self.latent_dim == 1 or self.extra_1d_channel is not None: + samples = self.encode_tiled_1d(pixel_samples) else: - pixels_in = pixels_in.to(self.device) - out = self.first_stage_model.encode(pixels_in) - out = out.to(self.output_device).to(dtype=self.vae_output_dtype()) - if samples is None: - samples = torch.empty((pixel_samples.shape[0],) + tuple(out.shape[1:]), device=self.output_device, dtype=self.vae_output_dtype()) - samples[x:x + batch_number] = out - - except Exception as e: - model_management.raise_non_oom(e) - logging.warning("Warning: Ran out of memory when regular VAE encoding, retrying with tiled VAE encoding.") - #NOTE: We don't know what tensors were allocated to stack variables at the time of the - #exception and the exception itself refs them all until we get out of this except block. - #So we just set a flag for tiler fallback so that tensor gc can happen once the - #exception is fully off the books. - do_tile = True - - if do_tile: - comfy.model_management.soft_empty_cache() - if self.latent_dim == 3: - tile = 256 - overlap = tile // 4 - samples = self.encode_tiled_3d(pixel_samples, tile_x=tile, tile_y=tile, overlap=(1, overlap, overlap)) - elif self.latent_dim == 1 or self.extra_1d_channel is not None: - samples = self.encode_tiled_1d(pixel_samples) - else: - samples = self.encode_tiled_(pixel_samples) + samples = self.encode_tiled_(pixel_samples) return samples @@ -1132,26 +1190,27 @@ class VAE: if overlap is not None: args["overlap"] = overlap - if dims == 1: - args.pop("tile_y") - samples = self.encode_tiled_1d(pixel_samples, **args) - elif dims == 2: - samples = self.encode_tiled_(pixel_samples, **args) - elif dims == 3: - if tile_t is not None: - tile_t_latent = max(2, self.downscale_ratio[0](tile_t)) - else: - tile_t_latent = 9999 - args["tile_t"] = self.upscale_ratio[0](tile_t_latent) + with model_management.cuda_device_context(self.device): + if dims == 1: + args.pop("tile_y") + samples = self.encode_tiled_1d(pixel_samples, **args) + elif dims == 2: + samples = self.encode_tiled_(pixel_samples, **args) + elif dims == 3: + if tile_t is not None: + tile_t_latent = max(2, self.downscale_ratio[0](tile_t)) + else: + tile_t_latent = 9999 + args["tile_t"] = self.upscale_ratio[0](tile_t_latent) - if overlap_t is None: - args["overlap"] = (1, overlap, overlap) - else: - args["overlap"] = (self.upscale_ratio[0](max(1, min(tile_t_latent // 2, self.downscale_ratio[0](overlap_t)))), overlap, overlap) - maximum = pixel_samples.shape[2] - maximum = self.upscale_ratio[0](self.downscale_ratio[0](maximum)) + if overlap_t is None: + args["overlap"] = (1, overlap, overlap) + else: + args["overlap"] = (self.upscale_ratio[0](max(1, min(tile_t_latent // 2, self.downscale_ratio[0](overlap_t)))), overlap, overlap) + maximum = pixel_samples.shape[2] + maximum = self.upscale_ratio[0](self.downscale_ratio[0](maximum)) - samples = self.encode_tiled_3d(pixel_samples[:,:,:maximum], **args) + samples = self.encode_tiled_3d(pixel_samples[:,:,:maximum], **args) return samples @@ -1224,6 +1283,8 @@ class CLIPType(Enum): NEWBIE = 24 FLUX2 = 25 LONGCAT_IMAGE = 26 + COGVIDEOX = 27 + LENS = 28 @@ -1275,6 +1336,8 @@ class TEModel(Enum): GEMMA_4_E4B = 29 GEMMA_4_E2B = 30 GEMMA_4_31B = 31 + T5_GEMMA = 32 + GPT_OSS_20B = 33 def detect_te_model(sd): @@ -1299,6 +1362,8 @@ def detect_te_model(sd): if weight.shape[0] == 384: return TEModel.BYT5_SMALL_GLYPH return TEModel.T5_BASE + if "model.encoder.layers.0.pre_self_attn_layernorm.weight" in sd: + return TEModel.T5_GEMMA if 'model.layers.0.post_feedforward_layernorm.weight' in sd: if 'model.layers.59.self_attn.q_norm.weight' in sd: return TEModel.GEMMA_4_31B @@ -1314,6 +1379,9 @@ def detect_te_model(sd): else: return TEModel.GEMMA_3_4B return TEModel.GEMMA_2_2B + # Must precede the Qwen2.5-7B k_proj.bias=512 check (GPT-OSS also has 8*64=512). + if "layers.0.self_attn.sinks" in sd and "layers.0.mlp.experts.gate_up_proj.weight" in sd: + return TEModel.GPT_OSS_20B if 'model.layers.0.self_attn.k_proj.bias' in sd: weight = sd['model.layers.0.self_attn.k_proj.bias'] if weight.shape[0] == 256: @@ -1428,6 +1496,9 @@ def load_text_encoder_state_dicts(state_dicts=[], embedding_directory=None, clip clip_target.clip = comfy.text_encoders.hidream.hidream_clip(**t5xxl_detect(clip_data), clip_l=False, clip_g=False, t5=True, llama=False, dtype_llama=None) clip_target.tokenizer = comfy.text_encoders.hidream.HiDreamTokenizer + elif clip_type == CLIPType.COGVIDEOX: + clip_target.clip = comfy.text_encoders.cogvideo.cogvideo_te(**t5xxl_detect(clip_data)) + clip_target.tokenizer = comfy.text_encoders.cogvideo.CogVideoXTokenizer else: #CLIPType.MOCHI clip_target.clip = comfy.text_encoders.genmo.mochi_te(**t5xxl_detect(clip_data)) clip_target.tokenizer = comfy.text_encoders.genmo.MochiT5Tokenizer @@ -1445,6 +1516,10 @@ def load_text_encoder_state_dicts(state_dicts=[], embedding_directory=None, clip else: clip_target.clip = comfy.text_encoders.sa_t5.SAT5Model clip_target.tokenizer = comfy.text_encoders.sa_t5.SAT5Tokenizer + elif te_model == TEModel.T5_GEMMA: + clip_target.clip = comfy.text_encoders.sa3.SAT5GemmaModel + clip_target.tokenizer = comfy.text_encoders.sa3.SAT5GemmaTokenizer + tokenizer_data["spiece_model"] = clip_data[0].get("spiece_model", None) elif te_model in (TEModel.GEMMA_4_E4B, TEModel.GEMMA_4_E2B, TEModel.GEMMA_4_31B): variant = {TEModel.GEMMA_4_E4B: comfy.text_encoders.gemma4.Gemma4_E4B, TEModel.GEMMA_4_E2B: comfy.text_encoders.gemma4.Gemma4_E2B, @@ -1489,6 +1564,10 @@ def load_text_encoder_state_dicts(state_dicts=[], embedding_directory=None, clip clip_target.clip = comfy.text_encoders.flux.flux2_te(**llama_detect(clip_data), pruned=te_model == TEModel.MISTRAL3_24B_PRUNED_FLUX2) clip_target.tokenizer = comfy.text_encoders.flux.Flux2Tokenizer tokenizer_data["tekken_model"] = clip_data[0].get("tekken_model", None) + elif te_model == TEModel.GPT_OSS_20B: + clip_target.clip = comfy.text_encoders.gpt_oss.lens_te(**llama_detect(clip_data)) + clip_target.tokenizer = comfy.text_encoders.gpt_oss.LensTokenizer + tokenizer_data["tokenizer_json"] = clip_data[0].get("tokenizer_json", None) elif te_model == TEModel.QWEN3_4B: if clip_type == CLIPType.FLUX or clip_type == CLIPType.FLUX2: clip_target.clip = comfy.text_encoders.flux.klein_te(**llama_detect(clip_data), model_type="qwen3_4b") @@ -1655,12 +1734,52 @@ def load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, o out = load_state_dict_guess_config(sd, output_vae, output_clip, output_clipvision, embedding_directory, output_model, model_options, te_model_options=te_model_options, metadata=metadata, disable_dynamic=disable_dynamic) if out is None: raise RuntimeError("ERROR: Could not detect model type of: {}\n{}".format(ckpt_path, model_detection_error_hint(ckpt_path, sd))) - if output_model and out[0] is not None: - out[0].cached_patcher_init = (load_checkpoint_guess_config_model_only, (ckpt_path, embedding_directory, model_options, te_model_options)) - if output_clip and out[1] is not None: - out[1].patcher.cached_patcher_init = (load_checkpoint_guess_config_clip_only, (ckpt_path, embedding_directory, model_options, te_model_options)) + if out[0] is not None: + out[0].cached_patcher_init = (load_checkpoint_guess_config, (ckpt_path, False, False, False, embedding_directory, output_model, model_options, te_model_options), 0) + # Register reload factories for the CLIP and VAE produced by the same checkpoint so + # ModelPatcher.deepclone_multigpu can spawn per-device copies (Select{CLIP,VAE}Device, + # MultiGPU work-units, etc.) without falling back to copy.deepcopy of an + # already-loaded module. + if out[1] is not None and getattr(out[1], "patcher", None) is not None: + out[1].patcher.cached_patcher_init = (load_checkpoint_clip_patcher, (ckpt_path, embedding_directory, model_options, te_model_options)) + if out[2] is not None and getattr(out[2], "patcher", None) is not None: + out[2].patcher.cached_patcher_init = (load_checkpoint_vae_patcher, (ckpt_path, embedding_directory, model_options, te_model_options)) return out + +def load_checkpoint_clip_patcher(ckpt_path, embedding_directory=None, model_options={}, te_model_options={}, disable_dynamic=False): + """Reload only the CLIP patcher from a checkpoint. Used as the cached_patcher_init + factory for the CLIP returned by load_checkpoint_guess_config.""" + _, clip, _, _ = load_checkpoint_guess_config( + ckpt_path, + output_vae=False, + output_clip=True, + output_clipvision=False, + embedding_directory=embedding_directory, + output_model=False, + model_options=model_options, + te_model_options=te_model_options, + disable_dynamic=disable_dynamic, + ) + return clip.patcher + + +def load_checkpoint_vae_patcher(ckpt_path, embedding_directory=None, model_options={}, te_model_options={}, disable_dynamic=False): + """Reload only the VAE patcher from a checkpoint. Used as the cached_patcher_init + factory for the VAE returned by load_checkpoint_guess_config.""" + _, _, vae, _ = load_checkpoint_guess_config( + ckpt_path, + output_vae=True, + output_clip=False, + output_clipvision=False, + embedding_directory=embedding_directory, + output_model=False, + model_options=model_options, + te_model_options=te_model_options, + disable_dynamic=disable_dynamic, + ) + return vae.patcher + def load_checkpoint_guess_config_model_only(ckpt_path, embedding_directory=None, model_options={}, te_model_options={}, disable_dynamic=False): model, *_ = load_checkpoint_guess_config(ckpt_path, False, False, False, embedding_directory=embedding_directory, @@ -1687,7 +1806,7 @@ def load_state_dict_guess_config(sd, output_vae=True, output_clip=True, output_c diffusion_model_prefix = model_detection.unet_prefix_from_state_dict(sd) parameters = comfy.utils.calculate_parameters(sd, diffusion_model_prefix) weight_dtype = comfy.utils.weight_dtype(sd, diffusion_model_prefix) - load_device = model_management.get_torch_device() + load_device = model_options.get("load_device", model_management.get_torch_device()) custom_operations = model_options.get("custom_operations", None) if custom_operations is None: @@ -1727,13 +1846,15 @@ def load_state_dict_guess_config(sd, output_vae=True, output_clip=True, output_c inital_load_device = model_management.unet_inital_load_device(parameters, unet_dtype) model = model_config.get_model(sd, diffusion_model_prefix, device=inital_load_device) ModelPatcher = comfy.model_patcher.ModelPatcher if disable_dynamic else comfy.model_patcher.CoreModelPatcher - model_patcher = ModelPatcher(model, load_device=load_device, offload_device=model_management.unet_offload_device()) + offload_device = model_options.get("offload_device", model_management.unet_offload_device()) + model_patcher = ModelPatcher(model, load_device=load_device, offload_device=offload_device) model.load_model_weights(sd, diffusion_model_prefix, assign=model_patcher.is_dynamic()) if output_vae: vae_sd = comfy.utils.state_dict_prefix_replace(sd, {k: "" for k in model_config.vae_key_prefix}, filter_keys=True) vae_sd = model_config.process_vae_state_dict(vae_sd) - vae = VAE(sd=vae_sd, metadata=metadata) + vae_device = model_options.get("load_device", None) + vae = VAE(sd=vae_sd, metadata=metadata, device=vae_device) if output_clip: if te_model_options.get("custom_operations", None) is None: @@ -1817,7 +1938,7 @@ def load_diffusion_model_state_dict(sd, model_options={}, metadata=None, disable parameters = comfy.utils.calculate_parameters(sd) weight_dtype = comfy.utils.weight_dtype(sd) - load_device = model_management.get_torch_device() + load_device = model_options.get("load_device", model_management.get_torch_device()) model_config = model_detection.model_config_from_unet(sd, "", metadata=metadata) if model_config is not None: @@ -1842,7 +1963,7 @@ def load_diffusion_model_state_dict(sd, model_options={}, metadata=None, disable else: logging.warning("{} {}".format(diffusers_keys[k], k)) - offload_device = model_management.unet_offload_device() + offload_device = model_options.get("offload_device", model_management.unet_offload_device()) unet_weight_dtype = list(model_config.supported_inference_dtypes) if model_config.quant_config is not None: weight_dtype = None @@ -1884,6 +2005,26 @@ def load_diffusion_model(unet_path, model_options={}, disable_dynamic=False): model.cached_patcher_init = (load_diffusion_model, (unet_path, model_options)) return model + +def load_vae_patcher(vae_path, metadata=None, device=None, disable_dynamic=False): + """Reload a disk-backed VAE from ``vae_path`` and return its patcher. + + Used as the ``cached_patcher_init`` factory on ``VAE.patcher`` so + :meth:`comfy.model_patcher.ModelPatcher.deepclone_multigpu` can produce a + fresh, untainted VAE patcher (no inherited per-device load state, no + in-place quantization fallout) for multigpu work-units and the + SelectVAEDevice node. The optional ``device`` matches the source loader's + VAE initialization path; the deepclone's ``load_device`` still controls + where the cloned patcher is targeted. + """ + if metadata is None: + sd, metadata = comfy.utils.load_torch_file(vae_path, return_metadata=True) + else: + sd = comfy.utils.load_torch_file(vae_path) + vae = VAE(sd=sd, metadata=metadata, device=device) + vae.throw_exception_if_invalid() + return vae.patcher + def load_unet(unet_path, dtype=None): logging.warning("The load_unet function has been deprecated and will be removed please switch to: load_diffusion_model") return load_diffusion_model(unet_path, model_options={"dtype": dtype}) @@ -1897,7 +2038,7 @@ def save_checkpoint(output_path, model, clip=None, vae=None, clip_vision=None, m load_models = [model] if clip is not None: load_models.append(clip.load_model()) - clip_sd = clip.get_sd() + clip_sd = clip.state_dict_for_saving() vae_sd = None if vae is not None: vae_sd = vae.get_sd() diff --git a/comfy/supported_models.py b/comfy/supported_models.py index dff40461f..e451892e9 100644 --- a/comfy/supported_models.py +++ b/comfy/supported_models.py @@ -7,6 +7,7 @@ from . import sdxl_clip import comfy.text_encoders.sd2_clip import comfy.text_encoders.sd3_clip import comfy.text_encoders.sa_t5 +import comfy.text_encoders.sa3 import comfy.text_encoders.aura_t5 import comfy.text_encoders.pixart_t5 import comfy.text_encoders.hydit @@ -28,6 +29,7 @@ import comfy.text_encoders.ace15 import comfy.text_encoders.longcat_image import comfy.text_encoders.ernie import comfy.text_encoders.cogvideo +import comfy.text_encoders.hidream_o1 from . import supported_models_base from . import latent_formats @@ -602,6 +604,29 @@ class StableAudio(supported_models_base.BASE): def clip_target(self, state_dict={}): return supported_models_base.ClipTarget(comfy.text_encoders.sa_t5.SAT5Tokenizer, comfy.text_encoders.sa_t5.SAT5Model) +class StableAudio3(StableAudio): + unet_config = { + "audio_model": "dit1.0", + "global_cond_shared_embed": True, + } + + sampling_settings = { + "multiplier": 1.0, + "shift": 2.0, + } + + latent_format = latent_formats.StableAudio3 + + memory_usage_factor = 7 + + def get_model(self, state_dict, prefix="", device=None): + seconds_total_sd = utils.state_dict_prefix_replace(state_dict, {"conditioner.conditioners.seconds_total.": ""}, filter_keys=True) + padding_embedding = state_dict.get("conditioner.conditioners.prompt.padding_embedding", None) + return model_base.StableAudio3(self, seconds_total_embedder_weights=seconds_total_sd, padding_embedding=padding_embedding, device=device) + + def clip_target(self, state_dict={}): + return supported_models_base.ClipTarget(comfy.text_encoders.sa3.SAT5GemmaTokenizer, comfy.text_encoders.sa3.SAT5GemmaModel) + class AuraFlow(supported_models_base.BASE): unet_config = { "cond_seq_dim": 2048, @@ -804,6 +829,48 @@ class Flux2(Flux): return None + +class Lens(supported_models_base.BASE): + """Microsoft Lens (3.8B dual-stream MMDiT, GPT-OSS-20B text features, Flux2 VAE).""" + + unet_config = { + "image_model": "lens", + } + + sampling_settings = { + "shift": 1.829, # Default mu for 1440x1440 (and any seq_len > 4300 + } + + unet_extra_config = {} + latent_format = latent_formats.Flux2 + + supported_inference_dtypes = [torch.bfloat16, torch.float32] # fp16 causes NaNs + + vae_key_prefix = ["vae."] + text_encoder_key_prefix = ["text_encoders."] + + def __init__(self, unet_config): + super().__init__(unet_config) + + def get_model(self, state_dict, prefix="", device=None): + return model_base.Lens(self, model_type=model_base.ModelType.FLUX, device=device) + + def clip_target(self, state_dict={}): + pref = self.text_encoder_key_prefix[0] + for hint in ("gpt_oss.transformer.", ""): + full_prefix = "{}{}".format(pref, hint) + if "{}layers.0.self_attn.sinks".format(full_prefix) in state_dict: + detect = comfy.text_encoders.hunyuan_video.llama_detect(state_dict, full_prefix) + return supported_models_base.ClipTarget( + comfy.text_encoders.gpt_oss.LensTokenizer, + comfy.text_encoders.gpt_oss.lens_te(**detect), + ) + return supported_models_base.ClipTarget( + comfy.text_encoders.gpt_oss.LensTokenizer, + comfy.text_encoders.gpt_oss.lens_te(), + ) + + class GenmoMochi(supported_models_base.BASE): unet_config = { "image_model": "mochi_preview", @@ -1313,6 +1380,37 @@ class WAN21_SCAIL(WAN21_T2V): out = model_base.WAN21_SCAIL(self, image_to_video=False, device=device) return out +class WAN22_WanDancer(WAN21_T2V): + unet_config = { + "image_model": "wan2.1", + "model_type": "wandancer", + "in_dim": 36, + } + + def __init__(self, unet_config): + super().__init__(unet_config) + self.memory_usage_factor = 1.8 + + def get_model(self, state_dict, prefix="", device=None): + out = model_base.WAN22_WanDancer(self, image_to_video=True, device=device) + return out + + def process_unet_state_dict(self, state_dict): + out_sd = {} + for k in list(state_dict.keys()): + # split music_encoder in_proj into q_proj, k_proj, v_proj + if "music_encoder" in k and "self_attn.in_proj" in k: + suffix = "weight" if k.endswith("weight") else "bias" + tensor = state_dict[k] + d = tensor.shape[0] // 3 + prefix = k.replace(f"in_proj_{suffix}", "") + out_sd[f"{prefix}q_proj.{suffix}"] = tensor[:d] + out_sd[f"{prefix}k_proj.{suffix}"] = tensor[d:2*d] + out_sd[f"{prefix}v_proj.{suffix}"] = tensor[2*d:] + else: + out_sd[k] = state_dict[k] + return out_sd + class Hunyuan3Dv2(supported_models_base.BASE): unet_config = { "image_model": "hunyuan3d2", @@ -1400,6 +1498,50 @@ class HiDream(supported_models_base.BASE): def clip_target(self, state_dict={}): return None # TODO +class HiDreamO1(supported_models_base.BASE): + unet_config = { + "image_model": "hidream_o1", + } + + sampling_settings = { + "shift": 3.0, + "noise_scale": 8.0, + } + + latent_format = latent_formats.HiDreamO1Pixel + memory_usage_factor = 0.033 + # fp16 not supported: LM MLP down_proj activations fp16 overflow, causing NaNs + supported_inference_dtypes = [torch.bfloat16, torch.float32] + + vae_key_prefix = ["vae."] + text_encoder_key_prefix = ["text_encoders."] + + optimizations = {"fp8": False} + + def get_model(self, state_dict, prefix="", device=None): + return model_base.HiDreamO1(self, device=device) + + def process_unet_state_dict(self, state_dict): + # Drop unused Qwen3-VL deepstack merger weights; upstream discards them at inference. + for key in list(state_dict.keys()): + if "visual.deepstack_merger_list" in key: + del state_dict[key] + return state_dict + + def process_vae_state_dict(self, state_dict): + # Pixel-space model: inject sentinel so VAE construction picks PixelspaceConversionVAE. + return {"pixel_space_vae": torch.tensor(1.0)} + + def process_clip_state_dict(self, state_dict): + # Tokenizer-only TE: inject sentinel so load_state_dict_guess_config triggers CLIP init. + return {"_hidream_o1_te_sentinel": torch.zeros(1)} + + def clip_target(self, state_dict={}): + return supported_models_base.ClipTarget( + comfy.text_encoders.hidream_o1.HiDreamO1Tokenizer, + comfy.text_encoders.hidream_o1.HiDreamO1TE, + ) + class Chroma(supported_models_base.BASE): unet_config = { "image_model": "chroma", @@ -1872,6 +2014,14 @@ class CogVideoX_T2V(supported_models_base.BASE): vae_key_prefix = ["vae."] text_encoder_key_prefix = ["text_encoders."] + def __init__(self, unet_config): + # 2b-class (dim=1920, heads=30) uses scale_factor=1.15258426. + # 5b-class (dim=3072, heads=48) — incl. CogVideoX-5b, 1.5-5B, and + # Fun-V1.5 inpainting — uses scale_factor=0.7 per vae/config.json. + if unet_config.get("num_attention_heads", 0) >= 48: + self.latent_format = latent_formats.CogVideoX1_5 + super().__init__(unet_config) + def get_model(self, state_dict, prefix="", device=None): # CogVideoX 1.5 (patch_size_t=2) has different training base dimensions for RoPE if self.unet_config.get("patch_size_t") is not None: @@ -1898,6 +2048,20 @@ class CogVideoX_I2V(CogVideoX_T2V): out = model_base.CogVideoX(self, image_to_video=True, device=device) return out +class CogVideoX_Inpaint(CogVideoX_T2V): + unet_config = { + "image_model": "cogvideox", + "in_channels": 48, + } + + def get_model(self, state_dict, prefix="", device=None): + if self.unet_config.get("patch_size_t") is not None: + self.unet_config.setdefault("sample_height", 96) + self.unet_config.setdefault("sample_width", 170) + self.unet_config.setdefault("sample_frames", 81) + out = model_base.CogVideoX(self, image_to_video=True, device=device) + return out + models = [ LotusD, @@ -1920,6 +2084,7 @@ models = [ SV3D_u, SV3D_p, SD3, + StableAudio3, StableAudio, AuraFlow, PixArtAlpha, @@ -1960,10 +2125,12 @@ models = [ WAN22_Animate, WAN21_FlowRVS, WAN21_SCAIL, + WAN22_WanDancer, Hunyuan3Dv2mini, Hunyuan3Dv2, Hunyuan3Dv2_1, HiDream, + HiDreamO1, Chroma, ChromaRadiance, ACEStep, @@ -1971,6 +2138,7 @@ models = [ Omnigen2, QwenImage, Flux2, + Lens, Kandinsky5Image, Kandinsky5, Anima, @@ -1978,6 +2146,7 @@ models = [ ErnieImage, SAM3, SAM31, + CogVideoX_Inpaint, CogVideoX_I2V, CogVideoX_T2V, SVD_img2vid, diff --git a/comfy/text_encoders/cogvideo.py b/comfy/text_encoders/cogvideo.py index f1e8e3f5d..b97310709 100644 --- a/comfy/text_encoders/cogvideo.py +++ b/comfy/text_encoders/cogvideo.py @@ -1,6 +1,48 @@ import comfy.text_encoders.sd3_clip +from comfy import sd1_clip class CogVideoXT5Tokenizer(comfy.text_encoders.sd3_clip.T5XXLTokenizer): + """Inner T5 tokenizer for CogVideoX. + + CogVideoX was trained with T5 embeddings padded to 226 tokens (not 77 like SD3). + Used both directly by supported_models.CogVideoX_T2V.clip_target (paired with + the raw T5XXLModel) and by the CogVideoXTokenizer outer wrapper below. + """ def __init__(self, embedding_directory=None, tokenizer_data={}): super().__init__(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data, min_length=226) + + +class CogVideoXTokenizer(sd1_clip.SD1Tokenizer): + """Outer tokenizer wrapper for CLIPLoader (type="cogvideox").""" + def __init__(self, embedding_directory=None, tokenizer_data={}): + super().__init__(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data, + clip_name="t5xxl", tokenizer=CogVideoXT5Tokenizer) + + +class CogVideoXT5XXL(sd1_clip.SD1ClipModel): + """Outer T5XXL model wrapper for CLIPLoader (type="cogvideox"). + + Wraps the raw T5XXL model in the SD1ClipModel interface so that CLIP.__init__ + (which reads self.dtypes) works correctly. The inner model is the standard + sd3_clip.T5XXLModel (no attention_mask change needed for CogVideoX). + """ + def __init__(self, device="cpu", dtype=None, model_options={}): + super().__init__(device=device, dtype=dtype, name="t5xxl", + clip_model=comfy.text_encoders.sd3_clip.T5XXLModel, + model_options=model_options) + + +def cogvideo_te(dtype_t5=None, t5_quantization_metadata=None): + """Factory that returns a CogVideoXT5XXL class configured with the detected + T5 dtype and optional quantization metadata, for use in load_text_encoder_state_dicts. + """ + class CogVideoXTEModel_(CogVideoXT5XXL): + def __init__(self, device="cpu", dtype=None, model_options={}): + if t5_quantization_metadata is not None: + model_options = model_options.copy() + model_options["t5xxl_quantization_metadata"] = t5_quantization_metadata + if dtype_t5 is not None: + dtype = dtype_t5 + super().__init__(device=device, dtype=dtype, model_options=model_options) + return CogVideoXTEModel_ diff --git a/comfy/text_encoders/gpt_oss.py b/comfy/text_encoders/gpt_oss.py new file mode 100644 index 000000000..d596ef9a0 --- /dev/null +++ b/comfy/text_encoders/gpt_oss.py @@ -0,0 +1,600 @@ +"""GPT-OSS text encoder for Lens.""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from typing import Any, List, Optional, Sequence + +import torch +import torch.nn as nn +import torch.nn.functional as F + +import comfy.ops +from comfy import sd1_clip +from comfy.ldm.modules.attention import TORCH_HAS_GQA, optimized_attention_for_device +from comfy.text_encoders.llama import RMSNorm, apply_rope + + +@dataclass +class GptOss20BConfig: + vocab_size: int = 201088 + hidden_size: int = 2880 + intermediate_size: int = 2880 + num_hidden_layers: int = 24 + num_attention_heads: int = 64 + num_key_value_heads: int = 8 + head_dim: int = 64 + num_local_experts: int = 32 + num_experts_per_tok: int = 4 + sliding_window: int = 128 + original_max_position_embeddings: int = 4096 + rope_theta: float = 150000.0 + rope_factor: float = 32.0 + rope_beta_fast: float = 32.0 + rope_beta_slow: float = 1.0 + rope_truncate: bool = False + rms_norm_eps: float = 1e-5 + attention_bias: bool = True + layer_types: Optional[List[str]] = None + moe_alpha: float = 1.702 + moe_limit: float = 7.0 + + def __post_init__(self): + if self.layer_types is None: + self.layer_types = [ + "sliding_attention" if (i + 1) % 2 else "full_attention" + for i in range(self.num_hidden_layers) + ] + + +def _yarn_inv_freq(head_dim: int, base: float, factor: float, beta_fast: float, beta_slow: float, + original_max_position_embeddings: int, truncate: bool, device=None) -> tuple[torch.Tensor, float]: + """YARN inv_freq + attention scaling (matches transformers).""" + dim = head_dim + + def find_correction_dim(num_rotations: float) -> float: + return (dim * math.log(original_max_position_embeddings / (num_rotations * 2 * math.pi))) / ( + 2 * math.log(base) + ) + + def find_correction_range() -> tuple[float, float]: + low = find_correction_dim(beta_fast) + high = find_correction_dim(beta_slow) + if truncate: + low = math.floor(low) + high = math.ceil(high) + return max(low, 0), min(high, dim - 1) + + def linear_ramp_factor(min_: float, max_: float, n: int) -> torch.Tensor: + if min_ == max_: + max_ += 0.001 + linear = (torch.arange(n, dtype=torch.float32, device=device) - min_) / (max_ - min_) + return torch.clamp(linear, 0, 1) + + def get_mscale(scale: float) -> float: + if scale <= 1: + return 1.0 + return 0.1 * math.log(scale) + 1.0 + + attention_scaling = get_mscale(factor) + + pos_freqs = base ** (torch.arange(0, dim, 2, dtype=torch.float32, device=device) / dim) + inv_freq_extrapolation = 1.0 / pos_freqs + inv_freq_interpolation = 1.0 / (factor * pos_freqs) + + low, high = find_correction_range() + extrap_factor = 1 - linear_ramp_factor(low, high, dim // 2) + inv_freq = inv_freq_interpolation * (1 - extrap_factor) + inv_freq_extrapolation * extrap_factor + return inv_freq, attention_scaling + + +def _build_freqs_cis(inv_freq: torch.Tensor, attention_scaling: float, position_ids: torch.Tensor, dtype: torch.dtype, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + inv_freq_e = inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1) + pos_e = position_ids[:, None, :].float() + freqs = (inv_freq_e @ pos_e).transpose(1, 2) + emb = torch.cat((freqs, freqs), dim=-1) + cos = (emb.cos() * attention_scaling).to(dtype).unsqueeze(1) + sin = (emb.sin() * attention_scaling).to(dtype).unsqueeze(1) + sin_split = sin.shape[-1] // 2 + return cos, sin[..., :sin_split], -sin[..., sin_split:] + + +def _attention_with_sinks(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, sinks: torch.Tensor, + attention_mask: Optional[torch.Tensor], num_heads: int, num_kv_groups: int) -> torch.Tensor: + """Attention with per-head sinks. + + Sinks add a learned term to each row's softmax denominator but contribute + nothing to the output. We fake this by appending one zero k/v position and + putting the sink logit in the mask at that column. + """ + + if num_kv_groups > 1 and not TORCH_HAS_GQA: + k = k.repeat_interleave(num_kv_groups, dim=1) + v = v.repeat_interleave(num_kv_groups, dim=1) + + B, _, S_q, D = q.shape + H_kv = k.shape[1] + S_kv = k.shape[-2] + + k = torch.cat([k, k.new_zeros(B, H_kv, 1, D)], dim=-2) + v = torch.cat([v, v.new_zeros(B, H_kv, 1, D)], dim=-2) + + sinks_col = sinks.to(q.dtype).view(1, num_heads, 1, 1).expand(B, num_heads, S_q, 1) + if attention_mask is not None: + mask_left = attention_mask[..., :S_kv].expand(B, num_heads, S_q, S_kv) + else: + mask_left = q.new_zeros(B, num_heads, S_q, S_kv) + mask = torch.cat([mask_left, sinks_col], dim=-1) + + op = optimized_attention_for_device(q.device, mask=True, small_input=True) + return op(q, k, v, num_heads, mask=mask, skip_reshape=True, enable_gqa=True) + + +class GptOssAttention(nn.Module): + def __init__(self, config: GptOss20BConfig, layer_idx: int, device=None, dtype=None, ops: Any = None): + super().__init__() + self.layer_idx = layer_idx + self.layer_type = config.layer_types[layer_idx] + self.num_heads = config.num_attention_heads + self.num_kv_heads = config.num_key_value_heads + self.num_kv_groups = self.num_heads // self.num_kv_heads + self.head_dim = config.head_dim + self.hidden_size = config.hidden_size + self.sliding_window = config.sliding_window if self.layer_type == "sliding_attention" else None + + bias = config.attention_bias + self.q_proj = ops.Linear(config.hidden_size, self.num_heads * self.head_dim, bias=bias, device=device, dtype=dtype) + self.k_proj = ops.Linear(config.hidden_size, self.num_kv_heads * self.head_dim, bias=bias, device=device, dtype=dtype) + self.v_proj = ops.Linear(config.hidden_size, self.num_kv_heads * self.head_dim, bias=bias, device=device, dtype=dtype) + self.o_proj = ops.Linear(self.num_heads * self.head_dim, config.hidden_size, bias=bias, device=device, dtype=dtype) + self.sinks = nn.Parameter(torch.empty(self.num_heads, device=device, dtype=dtype)) + + def forward(self, hidden_states: torch.Tensor, attention_mask: Optional[torch.Tensor], freqs_cis) -> torch.Tensor: + B, S, _ = hidden_states.shape + + q = self.q_proj(hidden_states).view(B, S, self.num_heads, self.head_dim).transpose(1, 2) + k = self.k_proj(hidden_states).view(B, S, self.num_kv_heads, self.head_dim).transpose(1, 2) + v = self.v_proj(hidden_states).view(B, S, self.num_kv_heads, self.head_dim).transpose(1, 2) + + q, k = apply_rope(q, k, freqs_cis) + + out = _attention_with_sinks(q, k, v, self.sinks, attention_mask, self.num_heads, self.num_kv_groups) + return self.o_proj(out) + + +# Mixture of Experts + +class GptOssTopKRouter(nn.Module): + def __init__(self, config: GptOss20BConfig, device=None, dtype=None): + super().__init__() + self.top_k = config.num_experts_per_tok + self.num_experts = config.num_local_experts + self.weight = nn.Parameter(torch.empty(config.num_local_experts, config.hidden_size, device=device, dtype=dtype)) + self.bias = nn.Parameter(torch.empty(config.num_local_experts, device=device, dtype=dtype)) + + def forward(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + weight = comfy.ops.cast_to_input(self.weight, hidden_states, copy=False) + bias = comfy.ops.cast_to_input(self.bias, hidden_states, copy=False) + logits = F.linear(hidden_states, weight, bias) + top_vals, top_idx = torch.topk(logits, self.top_k, dim=-1) + # Softmax over top-k slice only + scores = F.softmax(top_vals, dim=-1, dtype=top_vals.dtype) + return scores, top_idx + + +class GptOssExperts(nn.Module): + def __init__(self, config: GptOss20BConfig, device=None, dtype=None, ops: Any = None): + super().__init__() + self.num_experts = config.num_local_experts + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.alpha = config.moe_alpha + self.limit = config.moe_limit + + E = self.num_experts + H = self.hidden_size + I = self.intermediate_size + + self.gate_up_proj = ops.MoEExperts(num_experts=E, in_features=H, out_features=2 * I, bias=True, device=device, dtype=dtype) + self.down_proj = ops.MoEExperts(num_experts=E, in_features=I, out_features=H, bias=True, device=device, dtype=dtype) + + def _apply_gate(self, gate_up: torch.Tensor) -> torch.Tensor: + gate = gate_up[..., ::2] + up = gate_up[..., 1::2] + gate = gate.clamp(max=self.limit) + up = up.clamp(min=-self.limit, max=self.limit) + glu = gate * torch.sigmoid(gate * self.alpha) + return torch.addcmul(glu, up, glu) + + def forward(self, hidden_states: torch.Tensor, router_indices: torch.Tensor, routing_weights: torch.Tensor) -> torch.Tensor: + N = hidden_states.shape[0] + top_k = router_indices.shape[-1] + H = hidden_states.shape[-1] + + per_pair = torch.zeros((N * top_k, H), dtype=hidden_states.dtype, device=hidden_states.device) + + expert_mask = F.one_hot(router_indices, num_classes=self.num_experts).permute(2, 1, 0) + expert_hit = torch.greater(expert_mask.sum(dim=(-1, -2)), 0).nonzero() + + with self.gate_up_proj.bank_resident(hidden_states) as gate_up_bank, \ + self.down_proj.bank_resident(hidden_states) as down_bank: + for ei in expert_hit: + expert_idx = int(ei.item()) + top_k_pos, token_idx = torch.where(expert_mask[expert_idx]) + current = hidden_states[token_idx] + + gate_up = gate_up_bank.expert_linear(current, expert_idx) + gated = self._apply_gate(gate_up) + expert_out = down_bank.expert_linear(gated, expert_idx) + + weighted = expert_out * routing_weights[token_idx, top_k_pos, None] + + flat_idx = token_idx * top_k + top_k_pos + per_pair[flat_idx] = weighted.to(per_pair.dtype) + + return per_pair.view(N, top_k, H).sum(dim=1) + + +class GptOssMLP(nn.Module): + def __init__(self, config: GptOss20BConfig, device=None, dtype=None, ops: Any = None): + super().__init__() + self.router = GptOssTopKRouter(config, device=device, dtype=dtype) + self.experts = GptOssExperts(config, device=device, dtype=dtype, ops=ops) + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + B, S, H = hidden_states.shape + flat = hidden_states.reshape(-1, H) + scores, idx = self.router(flat) + out = self.experts(flat, idx, scores) + return out.reshape(B, S, H) + + +# Decoder layer + model + +class GptOssDecoderLayer(nn.Module): + def __init__(self, config: GptOss20BConfig, layer_idx: int, device=None, dtype=None, ops: Any = None): + super().__init__() + self.self_attn = GptOssAttention(config, layer_idx, device=device, dtype=dtype, ops=ops) + self.mlp = GptOssMLP(config, device=device, dtype=dtype, ops=ops) + self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, device=device, dtype=dtype) + self.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, device=device, dtype=dtype) + self.layer_type = config.layer_types[layer_idx] + + def forward(self, x: torch.Tensor, attention_masks: dict[str, Optional[torch.Tensor]], freqs_cis) -> torch.Tensor: + residual = x + x = self.input_layernorm(x) + x = self.self_attn(x, attention_masks[self.layer_type], freqs_cis) + x = residual + x + + residual = x + x = self.post_attention_layernorm(x) + x = self.mlp(x) + x = residual + x + return x + + +def _make_full_causal_mask(B: int, S: int, key_padding_mask: Optional[torch.Tensor], dtype, device): + neg = torch.finfo(dtype).min + mask = torch.full((S, S), neg, dtype=dtype, device=device).triu_(1) + mask = mask.unsqueeze(0).unsqueeze(0).expand(B, 1, S, S).contiguous() + if key_padding_mask is not None: + kp = key_padding_mask.to(dtype=dtype) + kp = (1.0 - kp).reshape(B, 1, 1, S) * neg + mask = mask + kp + return mask + + +def _make_sliding_causal_mask(B: int, S: int, window: int, key_padding_mask: Optional[torch.Tensor], dtype, device): + neg = torch.finfo(dtype).min + i = torch.arange(S, device=device).view(-1, 1) + j = torch.arange(S, device=device).view(1, -1) + keep = (j <= i) & (j > i - window) + mask = torch.where(keep, torch.zeros((), dtype=dtype, device=device), torch.full((), neg, dtype=dtype, device=device)) + mask = mask.unsqueeze(0).unsqueeze(0).expand(B, 1, S, S).contiguous() + if key_padding_mask is not None: + kp = key_padding_mask.to(dtype=dtype) + kp = (1.0 - kp).reshape(B, 1, 1, S) * neg + mask = mask + kp + return mask + + +class GptOssModel(nn.Module): + """GPT-OSS decoder with multi-layer hidden-state capture + early exit.""" + + def __init__(self, config: GptOss20BConfig, device=None, dtype=None, ops: Any = None): + super().__init__() + self.config = config + self.dtype = dtype + self.embed_tokens = ops.Embedding(config.vocab_size, config.hidden_size, device=device, dtype=dtype) + self.layers = nn.ModuleList( + [ + GptOssDecoderLayer(config, i, device=device, dtype=dtype, ops=ops) + for i in range(config.num_hidden_layers) + ] + ) + self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, device=device, dtype=dtype) + + # Always build on CPU so the buffer survives meta-device construction. + inv_freq, attn_scaling = _yarn_inv_freq( + head_dim=config.head_dim, + base=config.rope_theta, + factor=config.rope_factor, + beta_fast=config.rope_beta_fast, + beta_slow=config.rope_beta_slow, + original_max_position_embeddings=config.original_max_position_embeddings, + truncate=config.rope_truncate, + device=torch.device("cpu"), + ) + self.register_buffer("rope_inv_freq", inv_freq, persistent=False) + self.rope_attention_scaling = float(attn_scaling) + + @property + def num_layers(self) -> int: + return self.config.num_hidden_layers + + def get_input_embeddings(self): + return self.embed_tokens + + def _build_attention_masks(self, B: int, S: int, attention_mask: Optional[torch.Tensor], dtype: torch.dtype, device, + ) -> dict[str, torch.Tensor]: + full = _make_full_causal_mask(B, S, attention_mask, dtype, device) + masks = {"full_attention": full} + if any(t == "sliding_attention" for t in self.config.layer_types): + masks["sliding_attention"] = _make_sliding_causal_mask( + B, S, self.config.sliding_window, attention_mask, dtype, device + ) + return masks + + def forward(self, input_ids: torch.LongTensor, attention_mask: Optional[torch.Tensor] = None, + capture_layers: Optional[Sequence[int]] = None) -> dict[str, Any]: + B, S = input_ids.shape + device = input_ids.device + dtype = self.dtype + + hidden_states = self.embed_tokens(input_ids, out_dtype=dtype) + + position_ids = torch.arange(S, device=device).unsqueeze(0).expand(B, -1) + freqs_cis = _build_freqs_cis(self.rope_inv_freq.to(device=device), self.rope_attention_scaling, position_ids, dtype) + + attn_masks = self._build_attention_masks(B, S, attention_mask, dtype, device) + + capture_layers = list(capture_layers) if capture_layers else None + if capture_layers: + max_layer = max(capture_layers) + wanted = {idx: pos for pos, idx in enumerate(capture_layers)} + captured: List[Optional[torch.Tensor]] = [None] * len(capture_layers) + else: + max_layer = self.config.num_hidden_layers - 1 + wanted = None + captured = None + + for i, layer in enumerate(self.layers): + hidden_states = layer(hidden_states, attn_masks, freqs_cis) + if wanted is not None and i in wanted: + captured[wanted[i]] = hidden_states + if i >= max_layer: + break + + if captured is not None: + return {"hidden_states": captured} + return {"last_hidden_state": self.norm(hidden_states)} + + +# Lens chat-template constants (verbatim from the reference pipeline). +_LENS_CHAT_SYSTEM = ( + "Describe the image by detailing the color, shape, size, texture, " + "quantity, text, spatial relationships of the objects and background." +) +_LENS_CHAT_ASSISTANT_THINKING = "Need to generate one image according to the description." +LENS_TXT_OFFSET = 97 +LENS_SELECTED_LAYERS = (5, 11, 17, 23) +LENS_MAX_TOKENS = 512 + + +# The reference GPT-OSS Harmony template injects today's date here +_LENS_CHAT_DATE = "2026-05-23" + + +def _lens_render_chat(prompt: str) -> str: + """Render the Lens prompt in GPT-OSS Harmony format.""" + return ( + f"<|start|>system<|message|>" + f"You are ChatGPT, a large language model trained by OpenAI.\n" + f"Knowledge cutoff: 2024-06\n" + f"Current date: {_LENS_CHAT_DATE}\n\n" + f"Reasoning: medium\n\n" + f"# Valid channels: analysis, commentary, final. " + f"Channel must be included for every message.<|end|>" + f"<|start|>developer<|message|># Instructions\n\n" + f"{_LENS_CHAT_SYSTEM}\n\n<|end|>" + f"<|start|>user<|message|>{prompt}<|end|>" + f"<|start|>assistant<|channel|>analysis<|message|>" + f"{_LENS_CHAT_ASSISTANT_THINKING}<|end|>" + f"<|start|>assistant<|channel|>final<|message|>" + ) + + +# GPT-OSS-20B fixed token IDs (from the tokenizer's added-tokens table). +_LENS_PAD_TOKEN_ID = 199999 # <|endoftext|> + + +class _GptOssRawTokenizer: + """Raw ``tokenizers.Tokenizer`` wrapper. + + The tokenizer JSON ships as a byte tensor inside the encoder checkpoint + (``tokenizer_json`` key) rather than as a committed file. Extracted + it in ``sd.py`` and passes it here via ``tokenizer_data``. + """ + + def __init__(self, tokenizer_json_bytes=None, **kwargs): + from tokenizers import Tokenizer + if isinstance(tokenizer_json_bytes, torch.Tensor): + tokenizer_json_bytes = bytes(tokenizer_json_bytes.tolist()) + if tokenizer_json_bytes is None: + raise ValueError( + "Lens tokenizer requires the ``tokenizer_json`` byte tensor in the " + "encoder state dict. Re-bundle the encoder via bundle_te.py so it " + "embeds the tokenizer." + ) + self.tokenizer = Tokenizer.from_str(tokenizer_json_bytes.decode("utf-8")) + + @classmethod + def from_pretrained(cls, tokenizer_data, **kwargs): + return cls(tokenizer_json_bytes=tokenizer_data, **kwargs) + + def __call__(self, text): + return {"input_ids": self.tokenizer.encode(text, add_special_tokens=False).ids} + + def get_vocab(self): + return self.tokenizer.get_vocab() + + def convert_tokens_to_ids(self, tokens): + return [self.tokenizer.token_to_id(t) for t in tokens] + + def decode(self, ids, **kwargs): + return self.tokenizer.decode(ids, skip_special_tokens=kwargs.get("skip_special_tokens", False)) + + +class LensGptOssTokenizer(sd1_clip.SDTokenizer): + tokenizer_json_data = None + + def __init__(self, embedding_directory=None, tokenizer_data={}): + tokenizer_json = tokenizer_data.get("tokenizer_json", None) + self.tokenizer_json_data = tokenizer_json + super().__init__( + tokenizer_json, + embedding_directory=embedding_directory, + pad_with_end=False, + embedding_size=2880, + embedding_key="gpt_oss", + tokenizer_class=_GptOssRawTokenizer, + has_start_token=False, + has_end_token=False, + pad_to_max_length=False, + max_length=99999999, + min_length=1, + pad_left=False, + disable_weights=True, + tokenizer_data=tokenizer_data, + ) + self.pad_token_id = _LENS_PAD_TOKEN_ID + + def tokenize_with_weights(self, text: str, return_word_ids=False, **kwargs): + # Empty prompt -> empty list; encode_token_weights returns zeros (uncond). + if not text or not text.strip(): + return [[]] + rendered = _lens_render_chat(text) + ids = self.tokenizer(rendered)["input_ids"] + if len(ids) > LENS_MAX_TOKENS: + ids = ids[:LENS_MAX_TOKENS] + return [[(int(t), 1.0) for t in ids]] + + def state_dict(self): + if self.tokenizer_json_data is not None: + return {"tokenizer_json": self.tokenizer_json_data} + return {} + + +class LensTokenizer(sd1_clip.SD1Tokenizer): + def __init__(self, embedding_directory=None, tokenizer_data={}): + super().__init__( + embedding_directory=embedding_directory, + tokenizer_data=tokenizer_data, + name="gpt_oss", + tokenizer=LensGptOssTokenizer, + ) + + +class LensGptOssClipModel(nn.Module): + """SDClipModel-shaped Lens GPT-OSS encoder (multi-layer feature extractor).""" + + def __init__(self, device="cpu", dtype=None, model_options=None, **kwargs): + super().__init__() + model_options = dict(model_options or {}) + + operations = model_options.get("custom_operations") + if operations is None: + quant_config = model_options.get("quantization_metadata") or {} + operations = comfy.ops.mixed_precision_ops(quant_config, dtype, full_precision_mm=True) + self.operations = operations + + cfg_overrides = model_options.get("gpt_oss_config", {}) + self.config = GptOss20BConfig(**cfg_overrides) + self.selected_layers = tuple(model_options.get("selected_layers", LENS_SELECTED_LAYERS)) + self.txt_offset = int(model_options.get("txt_offset", LENS_TXT_OFFSET)) + + self.transformer = GptOssModel(self.config, device=device, dtype=dtype, ops=operations) + self.num_layers = self.config.num_hidden_layers + self.dtype = dtype + self.execution_device = None + self._pad_token_id = _LENS_PAD_TOKEN_ID + + def set_clip_options(self, options): + self.execution_device = options.get("execution_device", self.execution_device) + + def reset_clip_options(self): + self.execution_device = None + + def _gather_tokens(self, token_weight_pairs): + ids_list = [[int(t[0]) for t in batch] for batch in token_weight_pairs] + pad_id = self._pad_token_id + max_len = max(len(x) for x in ids_list) + device = self.execution_device + ids = torch.full((len(ids_list), max_len), pad_id, dtype=torch.long, device=device) + mask = torch.zeros((len(ids_list), max_len), dtype=torch.long, device=device) + for i, x in enumerate(ids_list): + ids[i, : len(x)] = torch.tensor(x, dtype=torch.long, device=device) + mask[i, : len(x)] = 1 + return ids, mask + + def encode_token_weights(self, token_weight_pairs): + # Empty negative: emit zero-length features + zero mask + if all(len(batch) == 0 for batch in token_weight_pairs): + device = self.execution_device + B = len(token_weight_pairs) + L = len(self.selected_layers) + H = self.config.hidden_size + flat = torch.zeros(B, 0, L * H, dtype=self.dtype, device=device) + mask = torch.zeros(B, 0, dtype=torch.long, device=device) + return flat, None, {"attention_mask": mask, "num_layers_stacked": L} + + input_ids, attn_mask = self._gather_tokens(token_weight_pairs) + out = self.transformer(input_ids, attention_mask=attn_mask, capture_layers=self.selected_layers) + layers = out["hidden_states"] # list of L × [B, S, H] + stacked = torch.stack(layers, dim=2) # [B, S, L, H] + + offset = self.txt_offset + if stacked.shape[1] > offset: + stacked = stacked[:, offset:].contiguous() + mask_trim = attn_mask[:, offset:] + else: + stacked = stacked[:, :0] + mask_trim = attn_mask[:, :0] + + B, S, L, H = stacked.shape + flat = stacked.reshape(B, S, L * H) + extra = {"attention_mask": mask_trim, "num_layers_stacked": L} + return flat, None, extra + + def load_sd(self, sd): + return self.transformer.load_state_dict(sd, strict=False, assign=True) + + +class LensTEModel(sd1_clip.SD1ClipModel): + def __init__(self, device="cpu", dtype=None, model_options=None): + super().__init__(device=device, dtype=dtype, name="gpt_oss", clip_model=LensGptOssClipModel, model_options=model_options or {}) + + +def lens_te(dtype_llama=None, llama_quantization_metadata=None): + class LensTEModel_(LensTEModel): + def __init__(self, device="cpu", dtype=None, model_options=None): + mo = dict(model_options or {}) + if llama_quantization_metadata is not None: + mo["quantization_metadata"] = llama_quantization_metadata + if dtype is None and dtype_llama is not None: + dtype = dtype_llama + super().__init__(device=device, dtype=dtype, model_options=mo) + + return LensTEModel_ diff --git a/comfy/text_encoders/hidream_o1.py b/comfy/text_encoders/hidream_o1.py new file mode 100644 index 000000000..5d287b784 --- /dev/null +++ b/comfy/text_encoders/hidream_o1.py @@ -0,0 +1,119 @@ +"""HiDream-O1-Image tokenizer-only text encoder. + +The real Qwen3-VL backbone runs inside diffusion_model.* every step, so this +module just tokenizes the prompt into text_input_ids and emits them as +conditioning. Position ids / token_types / vinput_mask depend on target H/W +and are built later in model_base.HiDreamO1.extra_conds. +""" + +import os + +import torch +from transformers import Qwen2Tokenizer + +from comfy import sd1_clip + + +# Qwen3-VL special tokens +IM_START_ID = 151644 +IM_END_ID = 151645 +ASSISTANT_ID = 77091 +USER_ID = 872 +NEWLINE_ID = 198 +VISION_START_ID = 151652 +VISION_END_ID = 151653 +IMAGE_TOKEN_ID = 151655 +VIDEO_TOKEN_ID = 151656 +# HiDream-O1-specific tokens +BOI_TOKEN_ID = 151669 +BOR_TOKEN_ID = 151670 +EOR_TOKEN_ID = 151671 +BOT_TOKEN_ID = 151672 +TMS_TOKEN_ID = 151673 + + +class HiDreamO1QwenTokenizer(sd1_clip.SDTokenizer): + def __init__(self, embedding_directory=None, tokenizer_data={}): + tokenizer_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "qwen25_tokenizer" + ) + super().__init__( + tokenizer_path, + pad_with_end=False, + embedding_size=4096, + embedding_key="hidream_o1", + tokenizer_class=Qwen2Tokenizer, + has_start_token=False, + has_end_token=False, + pad_to_max_length=False, + max_length=99999999, + min_length=1, + pad_token=151643, + tokenizer_data=tokenizer_data, + ) + + +class HiDreamO1Tokenizer(sd1_clip.SD1Tokenizer): + """Wraps prompt in the upstream chat template ending with boi/tms markers. + Image tokens get spliced in at sample time once target H/W is known. + """ + + def __init__(self, embedding_directory=None, tokenizer_data={}): + super().__init__( + embedding_directory=embedding_directory, + tokenizer_data=tokenizer_data, + name="hidream_o1", + tokenizer=HiDreamO1QwenTokenizer, + ) + + def tokenize_with_weights(self, text, return_word_ids=False, **kwargs): + text_tokens_dict = super().tokenize_with_weights( + text, return_word_ids=return_word_ids, disable_weights=True, **kwargs + ) + text_tuples = text_tokens_dict["hidream_o1"][0] + text_tuples = [t for t in text_tuples if int(t[0]) != 151643] # strip pad + + # <|im_start|>user\n{text}<|im_end|>\n<|im_start|>assistant\n<|boi|><|tms|> + def tok(tid): + return (tid, 1.0) if not return_word_ids else (tid, 1.0, 0) + + prefix = [tok(IM_START_ID), tok(USER_ID), tok(NEWLINE_ID)] + suffix = [ + tok(IM_END_ID), tok(NEWLINE_ID), + tok(IM_START_ID), tok(ASSISTANT_ID), tok(NEWLINE_ID), + tok(BOI_TOKEN_ID), tok(TMS_TOKEN_ID), + ] + full = prefix + list(text_tuples) + suffix + return {"hidream_o1": [full]} + + +class HiDreamO1TE(torch.nn.Module): + """Passthrough TE: emits int token ids; the Qwen3-VL backbone in diffusion_model does the actual encoding.""" + + def __init__(self, device="cpu", dtype=None, model_options={}): + super().__init__() + self.dtypes = {torch.float32} + self.disable_offload = True # skips dynamic VRAM management for this zero-parameter module + self.device = torch.device("cpu") if device is None else torch.device(device) + + def encode_token_weights(self, token_weight_pairs): + tok_pairs = token_weight_pairs["hidream_o1"][0] + ids = [int(t[0]) for t in tok_pairs] + input_ids = torch.tensor([ids], dtype=torch.long) + # Surrogate keeps the cross_attn slot non-empty for CONDITIONING + # plumbing; the model reads text_input_ids out of `extra` instead. + cross_attn = input_ids.unsqueeze(-1).to(torch.float32) + extra = {"text_input_ids": input_ids} + return cross_attn, None, extra + + def load_sd(self, sd): + return [] + + def get_sd(self): + return {} + + def reset_clip_options(self): + pass + + def set_clip_options(self, options): + pass diff --git a/comfy/text_encoders/llama.py b/comfy/text_encoders/llama.py index a34c41144..5087228ca 100644 --- a/comfy/text_encoders/llama.py +++ b/comfy/text_encoders/llama.py @@ -397,7 +397,7 @@ class RMSNorm(nn.Module): -def precompute_freqs_cis(head_dim, position_ids, theta, rope_scale=None, rope_dims=None, device=None): +def precompute_freqs_cis(head_dim, position_ids, theta, rope_scale=None, rope_dims=None, device=None, interleaved_mrope=False): if not isinstance(theta, list): theta = [theta] @@ -415,16 +415,27 @@ def precompute_freqs_cis(head_dim, position_ids, theta, rope_scale=None, rope_di inv_freq_expanded = inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1) position_ids_expanded = position_ids[:, None, :].float() freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2) - emb = torch.cat((freqs, freqs), dim=-1) - cos = emb.cos() - sin = emb.sin() - if rope_dims is not None and position_ids.shape[0] > 1: - mrope_section = rope_dims * 2 - cos = torch.cat([m[i % 3] for i, m in enumerate(cos.split(mrope_section, dim=-1))], dim=-1).unsqueeze(0) - sin = torch.cat([m[i % 3] for i, m in enumerate(sin.split(mrope_section, dim=-1))], dim=-1).unsqueeze(0) + if rope_dims is not None and position_ids.shape[0] > 1 and interleaved_mrope: + # Qwen3-VL interleaved MRoPE: T-freqs by default, H/W replace every 3rd dim. + freqs_inter = freqs[0].clone() + for axis_idx, offset in ((1, 1), (2, 2)): + length = rope_dims[axis_idx] * 3 + idx = slice(offset, length, 3) + freqs_inter[..., idx] = freqs[axis_idx, ..., idx] + emb = torch.cat((freqs_inter, freqs_inter), dim=-1) + cos = emb.cos().unsqueeze(0) + sin = emb.sin().unsqueeze(0) else: - cos = cos.unsqueeze(1) - sin = sin.unsqueeze(1) + emb = torch.cat((freqs, freqs), dim=-1) + cos = emb.cos() + sin = emb.sin() + if rope_dims is not None and position_ids.shape[0] > 1: + mrope_section = rope_dims * 2 + cos = torch.cat([m[i % 3] for i, m in enumerate(cos.split(mrope_section, dim=-1))], dim=-1).unsqueeze(0) + sin = torch.cat([m[i % 3] for i, m in enumerate(sin.split(mrope_section, dim=-1))], dim=-1).unsqueeze(0) + else: + cos = cos.unsqueeze(1) + sin = sin.unsqueeze(1) sin_split = sin.shape[-1] // 2 out.append((cos, sin[..., : sin_split], -sin[..., sin_split :])) @@ -689,6 +700,7 @@ class Llama2_(nn.Module): self.config.rope_theta, self.config.rope_scale, self.config.rope_dims, + interleaved_mrope=getattr(self.config, "interleaved_mrope", False), device=device) def forward(self, x, attention_mask=None, embeds=None, num_tokens=None, intermediate_output=None, final_layer_norm_intermediate=True, dtype=None, position_ids=None, embeds_info=[], past_key_values=None, input_ids=None): diff --git a/comfy/text_encoders/qwen35.py b/comfy/text_encoders/qwen35.py index d8ed9cd32..416ce9d18 100644 --- a/comfy/text_encoders/qwen35.py +++ b/comfy/text_encoders/qwen35.py @@ -451,9 +451,8 @@ class Qwen35VisionPatchEmbed(nn.Module): self.proj = ops.Conv3d(self.in_channels, self.embed_dim, kernel_size=kernel_size, stride=kernel_size, bias=True, device=device, dtype=dtype) def forward(self, x): - target_dtype = self.proj.weight.dtype x = x.view(-1, self.in_channels, self.temporal_patch_size, self.patch_size, self.patch_size) - return self.proj(x.to(target_dtype)).view(-1, self.embed_dim) + return self.proj(x).view(-1, self.embed_dim) class Qwen35VisionMLP(nn.Module): @@ -651,7 +650,7 @@ class Qwen35VisionModel(nn.Module): x = self.patch_embed(x) pos_embeds = self.fast_pos_embed_interpolate(grid_thw).to(x.device) x = x + pos_embeds - rotary_pos_emb = self.rot_pos_emb(grid_thw) + rotary_pos_emb = self.rot_pos_emb(grid_thw).to(x.device) seq_len = x.shape[0] x = x.reshape(seq_len, -1) rotary_pos_emb = rotary_pos_emb.reshape(seq_len, -1) @@ -761,7 +760,7 @@ class Qwen35ImageTokenizer(sd1_clip.SD1Tokenizer): def tokenize_with_weights(self, text, return_word_ids=False, llama_template=None, images=[], prevent_empty_text=False, thinking=False, **kwargs): image = kwargs.get("image", None) if image is not None and len(images) == 0: - images = [image] + images = [image[i:i + 1] for i in range(image.shape[0])] skip_template = False if text.startswith('<|im_start|>'): @@ -772,13 +771,16 @@ class Qwen35ImageTokenizer(sd1_clip.SD1Tokenizer): if skip_template: llama_text = text else: - if llama_template is None: - if len(images) > 0: - llama_text = self.llama_template_images.format(text) - else: - llama_text = self.llama_template.format(text) + if llama_template is not None: + template = llama_template + elif len(images) == 0: + template = self.llama_template else: - llama_text = llama_template.format(text) + template = self.llama_template_images + if len(images) > 1: + vision_block = "<|vision_start|><|image_pad|><|vision_end|>" + template = template.replace(vision_block, vision_block * len(images), 1) + llama_text = template.format(text) if not thinking: llama_text += "\n\n" diff --git a/comfy/text_encoders/sa3.py b/comfy/text_encoders/sa3.py new file mode 100644 index 000000000..0a1c73ec1 --- /dev/null +++ b/comfy/text_encoders/sa3.py @@ -0,0 +1,207 @@ +import torch +import torch.nn as nn +from comfy import sd1_clip +from comfy.text_encoders.llama import Attention as LlamaAttention, RMSNorm, MLP, precompute_freqs_cis, apply_rope, _make_scaled_embedding +from comfy.text_encoders.spiece_tokenizer import SPieceTokenizer + + +class T5GemmaEncoderConfig: + def __init__(self): + self.vocab_size = 256000 + self.hidden_size = 768 + self.intermediate_size = 2048 + self.num_hidden_layers = 12 + self.num_attention_heads = 12 + self.num_key_value_heads = 12 + self.head_dim = 64 + self.rms_norm_eps = 1e-6 + self.rms_norm_add = False + self.rope_theta = 10000.0 + self.attn_logit_softcapping = 50.0 + self.query_pre_attn_scalar = 64 + self.sliding_window = 4096 + self.mlp_activation = "gelu_pytorch_tanh" + self.layer_types = ["sliding_attention", "full_attention"] * 6 + self.qkv_bias = False + self.q_norm = None + self.k_norm = None + self.rms_norm_add = True + + +class T5GemmaAttention(LlamaAttention): + """Reuses LlamaAttention projection setup; overrides forward for softcap attention. + + T5Gemma applies tanh(QK^T * scale / cap) * cap between the matmul and softmax. + This nonlinearity is incompatible with fused SDPA kernels, so attention is + computed manually. Everything else (projections, RoPE, GQA expansion) is identical + to LlamaAttention so __init__ is inherited unchanged. + """ + + def __init__(self, config, device=None, dtype=None, ops=None): + super().__init__(config, device=device, dtype=dtype, ops=ops) + self.scale = config.query_pre_attn_scalar ** -0.5 + self.softcap = config.attn_logit_softcapping + + def forward(self, hidden_states, attention_mask=None, freqs_cis=None, **kwargs): + B, S, _ = hidden_states.shape + xq = self.q_proj(hidden_states).view(B, S, self.num_heads, self.head_dim).transpose(1, 2) + xk = self.k_proj(hidden_states).view(B, S, self.num_kv_heads, self.head_dim).transpose(1, 2) + xv = self.v_proj(hidden_states).view(B, S, self.num_kv_heads, self.head_dim).transpose(1, 2) + xq, xk = apply_rope(xq, xk, freqs_cis) + xk = xk.repeat_interleave(self.num_heads // self.num_kv_heads, dim=1) + xv = xv.repeat_interleave(self.num_heads // self.num_kv_heads, dim=1) + attn = torch.matmul(xq * self.scale, xk.transpose(-2, -1)) + attn = torch.tanh(attn / self.softcap) * self.softcap + if attention_mask is not None: + attn = attn + attention_mask + attn = torch.nn.functional.softmax(attn.float(), dim=-1).to(xq.dtype) + out = torch.matmul(attn, xv).transpose(1, 2).reshape(B, S, self.inner_size) + return self.o_proj(out), None + + +class T5GemmaBlock(nn.Module): + def __init__(self, config, layer_type, device=None, dtype=None, ops=None): + super().__init__() + self.self_attn = T5GemmaAttention(config, device=device, dtype=dtype, ops=ops) + self.mlp = MLP(config, device=device, dtype=dtype, ops=ops) + # Names match checkpoint keys: model.encoder.layers.X..weight + self.pre_self_attn_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, add=True, device=device, dtype=dtype) + self.post_self_attn_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, add=True, device=device, dtype=dtype) + self.pre_feedforward_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, add=True, device=device, dtype=dtype) + self.post_feedforward_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, add=True, device=device, dtype=dtype) + self.is_sliding = (layer_type == "sliding_attention") + self.sliding_window = config.sliding_window + + def forward(self, x, attention_mask=None, freqs_cis=None): + attn_mask = attention_mask + if self.is_sliding and x.shape[1] > self.sliding_window: + S = x.shape[1] + pos = torch.arange(S, device=x.device) + dist = (pos.unsqueeze(0) - pos.unsqueeze(1)).abs() + sw_mask = torch.zeros(S, S, dtype=x.dtype, device=x.device) + sw_mask.masked_fill_(dist > self.sliding_window, -torch.finfo(x.dtype).max) + sw_mask = sw_mask.unsqueeze(0).unsqueeze(0) + attn_mask = (attention_mask + sw_mask) if attention_mask is not None else sw_mask + residual = x + x = self.pre_self_attn_layernorm(x) + x, _ = self.self_attn(x, attention_mask=attn_mask, freqs_cis=freqs_cis) + x = self.post_self_attn_layernorm(x) + x = residual + x + residual = x + x = self.pre_feedforward_layernorm(x) + x = self.mlp(x) + x = self.post_feedforward_layernorm(x) + x = residual + x + return x + + +class T5GemmaEncoder(nn.Module): + """Encoder stack: embed_tokens, layers, norm. + Keys: embed_tokens.*, layers.X.*, norm.*""" + + def __init__(self, config, device, dtype, ops): + super().__init__() + self.config = config + # Gemma-style scaled embedding: output *= sqrt(hidden_size) + self.embed_tokens = _make_scaled_embedding( + ops, config.vocab_size, config.hidden_size, config.hidden_size ** 0.5, device, dtype) + self.layers = nn.ModuleList([ + T5GemmaBlock(config, config.layer_types[i], device=device, dtype=dtype, ops=ops) + for i in range(config.num_hidden_layers) + ]) + self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, add=True, device=device, dtype=dtype) + + def forward(self, input_ids, attention_mask=None, embeds=None, intermediate_output=None, + final_layer_norm_intermediate=True, dtype=None, num_layers=None): + x = embeds if embeds is not None else self.embed_tokens(input_ids, out_dtype=dtype or torch.float32) + seq_len = x.shape[1] + position_ids = torch.arange(seq_len, device=x.device).unsqueeze(0) + freqs_cis = precompute_freqs_cis(self.config.head_dim, position_ids, self.config.rope_theta, device=x.device) + mask = None + if attention_mask is not None: + mask = 1.0 - attention_mask.to(x.dtype).reshape( + (attention_mask.shape[0], 1, -1, attention_mask.shape[-1]) + ).expand(attention_mask.shape[0], 1, seq_len, attention_mask.shape[-1]) + mask = mask.masked_fill(mask.to(torch.bool), -torch.finfo(x.dtype).max) + intermediate = None + for i, layer in enumerate(self.layers): + x = layer(x, attention_mask=mask, freqs_cis=freqs_cis) + if i == intermediate_output: + intermediate = x.clone() + x = self.norm(x) + if intermediate is not None and final_layer_norm_intermediate: + intermediate = self.norm(intermediate) + return x, intermediate + + +class T5GemmaBody(nn.Module): + """Provides the 'encoder' sub-module. + Keys: encoder.*""" + + def __init__(self, config, device, dtype, ops): + super().__init__() + self.encoder = T5GemmaEncoder(config, device, dtype, ops) + + +class T5GemmaModel(nn.Module): + """Top-level model class passed to SDClipModel as model_class. + Module layout: self.model.encoder.* → matches checkpoint keys model.encoder.*""" + + def __init__(self, config_dict, dtype, device, operations): + super().__init__() + config = T5GemmaEncoderConfig() + self.num_layers = config.num_hidden_layers + self.dtype = dtype + self.model = T5GemmaBody(config, device, dtype, operations) + + def get_input_embeddings(self): + return self.model.encoder.embed_tokens + + def set_input_embeddings(self, embeddings): + self.model.encoder.embed_tokens = embeddings + + def forward(self, input_ids, attention_mask=None, embeds=None, num_tokens=None, + intermediate_output=None, final_layer_norm_intermediate=True, dtype=None, **kwargs): + if intermediate_output is not None and intermediate_output < 0: + intermediate_output = self.num_layers + intermediate_output + return self.model.encoder( + input_ids, attention_mask=attention_mask, embeds=embeds, + intermediate_output=intermediate_output, + final_layer_norm_intermediate=final_layer_norm_intermediate, + dtype=dtype, num_layers=self.num_layers) + + +class T5GemmaSDClipModel(sd1_clip.SDClipModel): + def __init__(self, device="cpu", layer="last", layer_idx=None, dtype=None, model_options={}): + super().__init__(device=device, layer=layer, layer_idx=layer_idx, + textmodel_json_config={}, dtype=dtype, + special_tokens={"pad": 0}, + model_class=T5GemmaModel, + enable_attention_masks=True, zero_out_masked=True, + model_options=model_options) + + +class T5GemmaSDTokenizer(sd1_clip.SDTokenizer): + def __init__(self, embedding_directory=None, tokenizer_data={}): + tokenizer_model = tokenizer_data.get("spiece_model", None) + super().__init__(tokenizer_model, pad_with_end=False, embedding_size=768, + embedding_key="t5gemma", tokenizer_class=SPieceTokenizer, + has_start_token=False, has_end_token=False, pad_to_max_length=False, + max_length=99999999, min_length=1, pad_token=0, + tokenizer_data=tokenizer_data, + tokenizer_args={"add_bos": False, "add_eos": False}) + + def state_dict(self): + return {"spiece_model": self.tokenizer.serialize_model()} + + +class SAT5GemmaTokenizer(sd1_clip.SD1Tokenizer): + def __init__(self, embedding_directory=None, tokenizer_data={}): + super().__init__(embedding_directory=embedding_directory, + tokenizer_data=tokenizer_data, clip_name="t5gemma", tokenizer=T5GemmaSDTokenizer) + + +class SAT5GemmaModel(sd1_clip.SD1ClipModel): + def __init__(self, device="cpu", dtype=None, model_options={}, **kwargs): + super().__init__(device=device, dtype=dtype, model_options=model_options, + name="t5gemma", clip_model=T5GemmaSDClipModel, **kwargs) diff --git a/comfy/utils.py b/comfy/utils.py index 7b7faad3a..49ae12b06 100644 --- a/comfy/utils.py +++ b/comfy/utils.py @@ -86,6 +86,7 @@ def load_safetensors(ckpt): import comfy_aimdo.model_mmap f = open(ckpt, "rb", buffering=0) + file_lock = threading.Lock() model_mmap = comfy_aimdo.model_mmap.ModelMMAP(ckpt) file_size = os.path.getsize(ckpt) mv = memoryview((ctypes.c_uint8 * file_size).from_address(model_mmap.get())) @@ -111,9 +112,8 @@ def load_safetensors(ckpt): storage = tensor.untyped_storage() setattr(storage, "_comfy_tensor_file_slice", - comfy.memory_management.TensorFileSlice(f, threading.get_ident(), data_base_offset + start, end - start)) + comfy.memory_management.TensorFileSlice(f, file_lock, data_base_offset + start, end - start)) setattr(storage, "_comfy_tensor_mmap_refs", (model_mmap, mv)) - setattr(storage, "_comfy_tensor_mmap_touched", False) sd[name] = tensor return sd, header.get("__metadata__", {}), @@ -1020,10 +1020,11 @@ def bislerp(samples, width, height): def lanczos(samples, width, height): #the below API is strict and expects grayscale to be squeezed - samples = samples.squeeze(1) if samples.shape[1] == 1 else samples.movedim(1, -1) + if samples.ndim == 4: + samples = samples.squeeze(1) if samples.shape[1] == 1 else samples.movedim(1, -1) images = [Image.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8)) for image in samples] images = [image.resize((width, height), resample=Image.Resampling.LANCZOS) for image in images] - images = [torch.from_numpy(np.array(image).astype(np.float32) / 255.0).movedim(-1, 0) for image in images] + images = [torch.from_numpy(t).movedim(-1, 0) if (t := np.array(image).astype(np.float32) / 255.0).ndim == 3 else torch.from_numpy(t) for image in images] result = torch.stack(images) return result.to(samples.device, samples.dtype) @@ -1164,12 +1165,18 @@ def tiled_scale_multidim(samples, function, tile=(64, 64), overlap=8, upscale_am o = out o_d = out_div + ps_view = ps + mask_view = mask for d in range(dims): - o = o.narrow(d + 2, upscaled[d], mask.shape[d + 2]) - o_d = o_d.narrow(d + 2, upscaled[d], mask.shape[d + 2]) + l = min(ps_view.shape[d + 2], o.shape[d + 2] - upscaled[d]) + o = o.narrow(d + 2, upscaled[d], l) + o_d = o_d.narrow(d + 2, upscaled[d], l) + if l < ps_view.shape[d + 2]: + ps_view = ps_view.narrow(d + 2, 0, l) + mask_view = mask_view.narrow(d + 2, 0, l) - o.add_(ps * mask) - o_d.add_(mask) + o.add_(ps_view * mask_view) + o_d.add_(mask_view) if pbar is not None: pbar.update(1) @@ -1196,7 +1203,7 @@ def model_trange(*args, **kwargs): pbar.i1_time = time.time() pbar.set_postfix_str(" Model Initialization complete! ") elif pbar._i == 2: - #bring forward the effective start time based the the diff between first and second iteration + #bring forward the effective start time based the diff between first and second iteration #to attempt to remove load overhead from the final step rate estimate. pbar.start_t = pbar.i1_time - (time.time() - pbar.i1_time) pbar.set_postfix_str("") @@ -1390,7 +1397,7 @@ def convert_old_quants(state_dict, model_prefix="", metadata={}): k_out = "{}.weight_scale".format(layer) if layer is not None: - layer_conf = {"format": "float8_e4m3fn"} # TODO: check if anyone did some non e4m3fn scaled checkpoints + layer_conf = {"format": "float8_e4m3fn"} if full_precision_matrix_mult: layer_conf["full_precision_matrix_mult"] = full_precision_matrix_mult layers[layer] = layer_conf @@ -1445,4 +1452,3 @@ def deepcopy_list_dict(obj, memo=None): memo[obj_id] = res return res - diff --git a/comfy/windows.py b/comfy/windows.py deleted file mode 100644 index 213dc481d..000000000 --- a/comfy/windows.py +++ /dev/null @@ -1,52 +0,0 @@ -import ctypes -import logging -import psutil -from ctypes import wintypes - -import comfy_aimdo.control - -psapi = ctypes.WinDLL("psapi") -kernel32 = ctypes.WinDLL("kernel32") - -class PERFORMANCE_INFORMATION(ctypes.Structure): - _fields_ = [ - ("cb", wintypes.DWORD), - ("CommitTotal", ctypes.c_size_t), - ("CommitLimit", ctypes.c_size_t), - ("CommitPeak", ctypes.c_size_t), - ("PhysicalTotal", ctypes.c_size_t), - ("PhysicalAvailable", ctypes.c_size_t), - ("SystemCache", ctypes.c_size_t), - ("KernelTotal", ctypes.c_size_t), - ("KernelPaged", ctypes.c_size_t), - ("KernelNonpaged", ctypes.c_size_t), - ("PageSize", ctypes.c_size_t), - ("HandleCount", wintypes.DWORD), - ("ProcessCount", wintypes.DWORD), - ("ThreadCount", wintypes.DWORD), - ] - -def get_free_ram(): - #Windows is way too conservative and chalks recently used uncommitted model RAM - #as "in-use". So, calculate free RAM for the sake of general use as the greater of: - # - #1: What psutil says - #2: Total Memory - (Committed Memory - VRAM in use) - # - #We have to subtract VRAM in use from the comitted memory as WDDM creates a naked - #commit charge for all VRAM used just incase it wants to page it all out. This just - #isn't realistic so "overcommit" on our calculations by just subtracting it off. - - pi = PERFORMANCE_INFORMATION() - pi.cb = ctypes.sizeof(pi) - - if not psapi.GetPerformanceInfo(ctypes.byref(pi), pi.cb): - logging.warning("WARNING: Failed to query windows performance info. RAM usage may be sub optimal") - return psutil.virtual_memory().available - - committed = pi.CommitTotal * pi.PageSize - total = pi.PhysicalTotal * pi.PageSize - - return max(psutil.virtual_memory().available, - total - (committed - comfy_aimdo.control.get_total_vram_usage())) - diff --git a/comfy_api/latest/__init__.py b/comfy_api/latest/__init__.py index 04973fea0..e0a585b10 100644 --- a/comfy_api/latest/__init__.py +++ b/comfy_api/latest/__init__.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from abc import ABC, abstractmethod from typing import TYPE_CHECKING from comfy_api.internal import ComfyAPIBase diff --git a/comfy_api/latest/_input_impl/video_types.py b/comfy_api/latest/_input_impl/video_types.py index 942278d88..99e67d363 100644 --- a/comfy_api/latest/_input_impl/video_types.py +++ b/comfy_api/latest/_input_impl/video_types.py @@ -1,4 +1,3 @@ -from __future__ import annotations from av.container import InputContainer from av.subtitles.stream import SubtitleStream from fractions import Fraction diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index e50266bc5..5ed968960 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from spandrel import ImageModelDescriptor from comfy.clip_vision import ClipVisionModel from comfy.clip_vision import Output as ClipVisionOutput_ + from comfy.bg_removal_model import BackgroundRemovalModel from comfy.controlnet import ControlNet from comfy.hooks import HookGroup, HookKeyframeGroup from comfy.model_patcher import ModelPatcher @@ -614,6 +615,11 @@ class Model(ComfyTypeIO): if TYPE_CHECKING: Type = ModelPatcher +@comfytype(io_type="BACKGROUND_REMOVAL") +class BackgroundRemoval(ComfyTypeIO): + if TYPE_CHECKING: + Type = BackgroundRemovalModel + @comfytype(io_type="CLIP_VISION") class ClipVision(ComfyTypeIO): if TYPE_CHECKING: @@ -2257,6 +2263,7 @@ __all__ = [ "ModelPatch", "ClipVision", "ClipVisionOutput", + "BackgroundRemoval", "AudioEncoder", "AudioEncoderOutput", "StyleModel", diff --git a/comfy_api/latest/_util/geometry_types.py b/comfy_api/latest/_util/geometry_types.py index b586fceb3..cdde60b10 100644 --- a/comfy_api/latest/_util/geometry_types.py +++ b/comfy_api/latest/_util/geometry_types.py @@ -12,9 +12,24 @@ class VOXEL: class MESH: - def __init__(self, vertices: torch.Tensor, faces: torch.Tensor): - self.vertices = vertices - self.faces = faces + def __init__(self, vertices: torch.Tensor, faces: torch.Tensor, + uvs: torch.Tensor | None = None, + vertex_colors: torch.Tensor | None = None, + texture: torch.Tensor | None = None, + vertex_counts: torch.Tensor | None = None, + face_counts: torch.Tensor | None = None): + + assert (vertex_counts is None) == (face_counts is None), \ + "vertex_counts and face_counts must be provided together (both or neither)" + self.vertices = vertices # vertices: (B, N, 3) + self.faces = faces # faces: (B, M, 3) + self.uvs = uvs # uvs: (B, N, 2) + self.vertex_colors = vertex_colors # vertex_colors: (B, N, 3 or 4) + self.texture = texture # texture: (B, H, W, 3) + # When vertices/faces are zero-padded to a common N/M across the batch (variable-size mesh batch), + # these hold the real per-item lengths (B,). None means rows are uniform and no slicing is needed. + self.vertex_counts = vertex_counts + self.face_counts = face_counts class File3D: diff --git a/comfy_api/latest/_util/video_types.py b/comfy_api/latest/_util/video_types.py index c92477f08..6c9d6a526 100644 --- a/comfy_api/latest/_util/video_types.py +++ b/comfy_api/latest/_util/video_types.py @@ -1,4 +1,3 @@ -from __future__ import annotations from dataclasses import dataclass from enum import Enum from fractions import Fraction diff --git a/comfy_api_nodes/apis/__init__.py b/comfy_api_nodes/apis/__init__.py index 46a583b5e..9c4cfb9b6 100644 --- a/comfy_api_nodes/apis/__init__.py +++ b/comfy_api_nodes/apis/__init__.py @@ -3,7 +3,6 @@ # timestamp: 2025-07-30T08:54:00+00:00 # pylint: disable -from __future__ import annotations from datetime import date, datetime from enum import Enum diff --git a/comfy_api_nodes/apis/anthropic.py b/comfy_api_nodes/apis/anthropic.py new file mode 100644 index 000000000..46a5bb428 --- /dev/null +++ b/comfy_api_nodes/apis/anthropic.py @@ -0,0 +1,98 @@ +from enum import Enum +from typing import Literal + +from pydantic import BaseModel, Field + + +class AnthropicRole(str, Enum): + user = "user" + assistant = "assistant" + + +class AnthropicTextContent(BaseModel): + type: Literal["text"] = "text" + text: str = Field(...) + + +class AnthropicImageSourceBase64(BaseModel): + type: Literal["base64"] = "base64" + media_type: str = Field(..., description="MIME type of the image, e.g. image/png, image/jpeg") + data: str = Field(..., description="Base64-encoded image data") + + +class AnthropicImageSourceUrl(BaseModel): + type: Literal["url"] = "url" + url: str = Field(...) + + +class AnthropicImageContent(BaseModel): + type: Literal["image"] = "image" + source: AnthropicImageSourceBase64 | AnthropicImageSourceUrl = Field(...) + + +class AnthropicMessage(BaseModel): + role: AnthropicRole = Field(...) + content: list[AnthropicTextContent | AnthropicImageContent] = Field(...) + + +class AnthropicThinkingConfig(BaseModel): + type: Literal["enabled", "disabled", "adaptive"] = Field(...) + budget_tokens: int | None = Field( + None, ge=1024, + description="Reasoning budget in tokens. Used when type is 'enabled'. Must be less than max_tokens.", + ) + + +class AnthropicOutputConfig(BaseModel): + """Used with `thinking.type='adaptive'` on models like Opus 4.7.""" + effort: Literal["low", "medium", "high"] | None = Field(None) + + +class AnthropicMessagesRequest(BaseModel): + model: str = Field(...) + messages: list[AnthropicMessage] = Field(...) + max_tokens: int = Field(..., ge=1) + system: str | None = Field(None, description="Top-level system prompt") + temperature: float | None = Field(None, ge=0.0, le=1.0) + top_p: float | None = Field(None, ge=0.0, le=1.0) + top_k: int | None = Field(None, ge=0) + stop_sequences: list[str] | None = Field(None) + thinking: AnthropicThinkingConfig | None = Field(None) + output_config: AnthropicOutputConfig | None = Field(None) + + +class AnthropicResponseTextBlock(BaseModel): + type: Literal["text"] = "text" + text: str = Field(...) + + +class AnthropicResponseThinkingBlock(BaseModel): + type: Literal["thinking"] = "thinking" + thinking: str = Field(...) + + +AnthropicResponseBlock = AnthropicResponseTextBlock | AnthropicResponseThinkingBlock + + +class AnthropicCacheCreationUsage(BaseModel): + ephemeral_5m_input_tokens: int | None = Field(None) + ephemeral_1h_input_tokens: int | None = Field(None) + + +class AnthropicMessagesUsage(BaseModel): + input_tokens: int | None = Field(None) + output_tokens: int | None = Field(None) + cache_creation_input_tokens: int | None = Field(None) + cache_read_input_tokens: int | None = Field(None) + cache_creation: AnthropicCacheCreationUsage | None = Field(None) + + +class AnthropicMessagesResponse(BaseModel): + id: str | None = Field(None) + type: str | None = Field(None) + role: str | None = Field(None) + model: str | None = Field(None) + content: list[AnthropicResponseBlock] | None = Field(None) + stop_reason: str | None = Field(None) + stop_sequence: str | None = Field(None) + usage: AnthropicMessagesUsage | None = Field(None) diff --git a/comfy_api_nodes/apis/bfl.py b/comfy_api_nodes/apis/bfl.py index d8d3557b3..f0665fa09 100644 --- a/comfy_api_nodes/apis/bfl.py +++ b/comfy_api_nodes/apis/bfl.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from enum import Enum from typing import Any, Dict, Optional diff --git a/comfy_api_nodes/apis/bria.py b/comfy_api_nodes/apis/bria.py index 8c496b56c..e08a519a8 100644 --- a/comfy_api_nodes/apis/bria.py +++ b/comfy_api_nodes/apis/bria.py @@ -23,7 +23,7 @@ class BriaEditImageRequest(BaseModel): None, description="Mask image (black and white). Black areas will be preserved, white areas will be edited. " "If omitted, the edit applies to the entire image. " - "The input image and the the input mask must be of the same size.", + "The input image and the input mask must be of the same size.", ) negative_prompt: str | None = Field(None) guidance_scale: float = Field(...) diff --git a/comfy_api_nodes/apis/bytedance.py b/comfy_api_nodes/apis/bytedance.py index c05bd6893..03f4c445b 100644 --- a/comfy_api_nodes/apis/bytedance.py +++ b/comfy_api_nodes/apis/bytedance.py @@ -198,6 +198,62 @@ RECOMMENDED_PRESETS_SEEDREAM_4 = [ ("Custom", None, None), ] +_PRESETS_SEEDREAM_1K = [ + ("(1K) 1024x1024 (1:1)", 1024, 1024), + ("(1K) 864x1152 (3:4)", 864, 1152), + ("(1K) 1152x864 (4:3)", 1152, 864), + ("(1K) 1312x736 (16:9)", 1312, 736), + ("(1K) 736x1312 (9:16)", 736, 1312), + ("(1K) 832x1248 (2:3)", 832, 1248), + ("(1K) 1248x832 (3:2)", 1248, 832), + ("(1K) 1568x672 (21:9)", 1568, 672), +] + +_PRESETS_SEEDREAM_2K = [ + ("(2K) 2048x2048 (1:1)", 2048, 2048), + ("(2K) 1728x2304 (3:4)", 1728, 2304), + ("(2K) 2304x1728 (4:3)", 2304, 1728), + ("(2K) 2848x1600 (16:9)", 2848, 1600), + ("(2K) 1600x2848 (9:16)", 1600, 2848), + ("(2K) 1664x2496 (2:3)", 1664, 2496), + ("(2K) 2496x1664 (3:2)", 2496, 1664), + ("(2K) 3136x1344 (21:9)", 3136, 1344), +] + +_PRESETS_SEEDREAM_3K = [ + ("(3K) 3072x3072 (1:1)", 3072, 3072), + ("(3K) 2592x3456 (3:4)", 2592, 3456), + ("(3K) 3456x2592 (4:3)", 3456, 2592), + ("(3K) 4096x2304 (16:9)", 4096, 2304), + ("(3K) 2304x4096 (9:16)", 2304, 4096), + ("(3K) 2496x3744 (2:3)", 2496, 3744), + ("(3K) 3744x2496 (3:2)", 3744, 2496), + ("(3K) 4704x2016 (21:9)", 4704, 2016), +] + +_PRESETS_SEEDREAM_4K = [ + ("(4K) 4096x4096 (1:1)", 4096, 4096), + ("(4K) 3520x4704 (3:4)", 3520, 4704), + ("(4K) 4704x3520 (4:3)", 4704, 3520), + ("(4K) 5504x3040 (16:9)", 5504, 3040), + ("(4K) 3040x5504 (9:16)", 3040, 5504), + ("(4K) 3328x4992 (2:3)", 3328, 4992), + ("(4K) 4992x3328 (3:2)", 4992, 3328), + ("(4K) 6240x2656 (21:9)", 6240, 2656), +] + +_CUSTOM_PRESET = [("Custom", None, None)] + +RECOMMENDED_PRESETS_SEEDREAM_5_LITE = ( + _PRESETS_SEEDREAM_2K + _PRESETS_SEEDREAM_3K + _PRESETS_SEEDREAM_4K + _CUSTOM_PRESET +) +RECOMMENDED_PRESETS_SEEDREAM_4_5 = ( + _PRESETS_SEEDREAM_2K + _PRESETS_SEEDREAM_4K + _CUSTOM_PRESET +) +RECOMMENDED_PRESETS_SEEDREAM_4_0 = ( + _PRESETS_SEEDREAM_1K + _PRESETS_SEEDREAM_2K + _PRESETS_SEEDREAM_4K + _CUSTOM_PRESET +) + # Seedance 2.0 reference video pixel count limits per model and output resolution. SEEDANCE2_REF_VIDEO_PIXEL_LIMITS = { "dreamina-seedance-2-0-260128": { diff --git a/comfy_api_nodes/apis/bytedance_llm.py b/comfy_api_nodes/apis/bytedance_llm.py new file mode 100644 index 000000000..654c875fc --- /dev/null +++ b/comfy_api_nodes/apis/bytedance_llm.py @@ -0,0 +1,101 @@ +"""Pydantic models for BytePlus ModelArk Responses API. + +See: https://docs.byteplus.com/en/docs/ModelArk/1585128 (request) + https://docs.byteplus.com/en/docs/ModelArk/1783703 (response) +""" + +from typing import Literal + +from pydantic import BaseModel, Field + + +class BytePlusInputText(BaseModel): + type: Literal["input_text"] = "input_text" + text: str = Field(...) + + +class BytePlusInputImage(BaseModel): + type: Literal["input_image"] = "input_image" + image_url: str = Field(..., description="Image URL or `data:image/...;base64,...` payload") + detail: str = Field("auto", description="One of high, low, auto") + + +class BytePlusInputVideo(BaseModel): + type: Literal["input_video"] = "input_video" + video_url: str = Field(..., description="Video URL or `data:video/...;base64,...` payload") + fps: float | None = Field(None, ge=0.2, le=5.0) + + +BytePlusMessageContent = BytePlusInputText | BytePlusInputImage | BytePlusInputVideo + + +class BytePlusInputMessage(BaseModel): + type: Literal["message"] = "message" + role: str = Field(..., description="One of user, system, assistant, developer") + content: list[BytePlusMessageContent] = Field(...) + + +class BytePlusResponseCreateRequest(BaseModel): + model: str = Field(...) + input: list[BytePlusInputMessage] = Field(...) + instructions: str | None = Field(None) + max_output_tokens: int | None = Field(None, ge=1) + temperature: float | None = Field(None, ge=0.0, le=2.0) + store: bool | None = Field(False) + stream: bool | None = Field(False) + + +class BytePlusOutputText(BaseModel): + type: Literal["output_text"] = "output_text" + text: str = Field(...) + + +class BytePlusOutputRefusal(BaseModel): + type: Literal["refusal"] = "refusal" + refusal: str = Field(...) + + +class BytePlusOutputContent(BaseModel): + type: str = Field(...) + text: str | None = Field(None) + refusal: str | None = Field(None) + + +class BytePlusOutputMessage(BaseModel): + type: str = Field(...) + id: str | None = Field(None) + role: str | None = Field(None) + status: str | None = Field(None) + content: list[BytePlusOutputContent] | None = Field(None) + + +class BytePlusInputTokensDetails(BaseModel): + cached_tokens: int | None = Field(None) + + +class BytePlusOutputTokensDetails(BaseModel): + reasoning_tokens: int | None = Field(None) + + +class BytePlusResponseUsage(BaseModel): + input_tokens: int | None = Field(None) + output_tokens: int | None = Field(None) + total_tokens: int | None = Field(None) + input_tokens_details: BytePlusInputTokensDetails | None = Field(None) + output_tokens_details: BytePlusOutputTokensDetails | None = Field(None) + + +class BytePlusResponseError(BaseModel): + code: str = Field(...) + message: str = Field(...) + + +class BytePlusResponseObject(BaseModel): + id: str | None = Field(None) + object: str | None = Field(None) + created_at: int | None = Field(None) + model: str | None = Field(None) + status: str | None = Field(None) + error: BytePlusResponseError | None = Field(None) + output: list[BytePlusOutputMessage] | None = Field(None) + usage: BytePlusResponseUsage | None = Field(None) diff --git a/comfy_api_nodes/apis/openrouter.py b/comfy_api_nodes/apis/openrouter.py new file mode 100644 index 000000000..e30d9bcfb --- /dev/null +++ b/comfy_api_nodes/apis/openrouter.py @@ -0,0 +1,93 @@ +"""Pydantic models for the OpenRouter chat completions API. + +See: https://openrouter.ai/docs/api/api-reference/chat/send-chat-completion-request +""" + +from typing import Literal + +from pydantic import BaseModel, Field + + +class OpenRouterTextContent(BaseModel): + type: Literal["text"] = "text" + text: str = Field(...) + + +class OpenRouterImageUrl(BaseModel): + url: str = Field(...) + + +class OpenRouterImageContent(BaseModel): + type: Literal["image_url"] = "image_url" + image_url: OpenRouterImageUrl = Field(...) + + +class OpenRouterVideoUrl(BaseModel): + url: str = Field(...) + + +class OpenRouterVideoContent(BaseModel): + type: Literal["video_url"] = "video_url" + video_url: OpenRouterVideoUrl = Field(...) + + +OpenRouterContentBlock = OpenRouterTextContent | OpenRouterImageContent | OpenRouterVideoContent + + +class OpenRouterMessage(BaseModel): + role: Literal["system", "user", "assistant"] = Field(...) + content: str | list[OpenRouterContentBlock] = Field(...) + + +class OpenRouterReasoningConfig(BaseModel): + effort: str | None = Field(None) + exclude: bool | None = Field(None, description="If true, model reasons but reasoning is excluded from response.") + + +class OpenRouterWebSearchOptions(BaseModel): + search_context_size: str | None = Field(None) + + +class OpenRouterChatRequest(BaseModel): + model: str = Field(...) + messages: list[OpenRouterMessage] = Field(...) + seed: int | None = Field(None) + reasoning: OpenRouterReasoningConfig | None = Field(None) + web_search_options: OpenRouterWebSearchOptions | None = Field(None) + stream: bool = Field(False) + + +class OpenRouterUsage(BaseModel): + prompt_tokens: int | None = Field(None) + completion_tokens: int | None = Field(None) + total_tokens: int | None = Field(None) + cost: float | None = Field(None, description="Server-side authoritative USD cost of the call.") + + +class OpenRouterResponseMessage(BaseModel): + role: str | None = Field(None) + content: str | None = Field(None) + reasoning: str | None = Field(None) + refusal: str | None = Field(None) + + +class OpenRouterChoice(BaseModel): + index: int | None = Field(None) + message: OpenRouterResponseMessage | None = Field(None) + finish_reason: str | None = Field(None) + + +class OpenRouterError(BaseModel): + code: int | str | None = Field(None) + message: str | None = Field(None) + metadata: dict | None = Field(None) + + +class OpenRouterChatResponse(BaseModel): + id: str | None = Field(None) + model: str | None = Field(None) + object: str | None = Field(None) + provider: str | None = Field(None) + choices: list[OpenRouterChoice] | None = Field(None) + usage: OpenRouterUsage | None = Field(None) + error: OpenRouterError | None = Field(None) diff --git a/comfy_api_nodes/apis/rodin.py b/comfy_api_nodes/apis/rodin.py index fc26a6e73..24524d642 100644 --- a/comfy_api_nodes/apis/rodin.py +++ b/comfy_api_nodes/apis/rodin.py @@ -1,7 +1,5 @@ -from __future__ import annotations - from enum import Enum -from typing import Optional, List + from pydantic import BaseModel, Field @@ -11,44 +9,76 @@ class Rodin3DGenerateRequest(BaseModel): material: str = Field(..., description="The material type.") quality_override: int = Field(..., description="The poly count of the mesh.") mesh_mode: str = Field(..., description="It controls the type of faces of generated models.") - TAPose: Optional[bool] = Field(None, description="") + TAPose: bool | None = Field(None, description="") + + +class Rodin3DGen25Request(BaseModel): + + tier: str = Field(..., description="Gen-2.5 tier (e.g. Gen-2.5-High).") + prompt: str | None = Field(None, description="Required for Text-to-3D; ignored otherwise.") + seed: int | None = Field(None, description="0-65535.") + material: str | None = Field(None, description="PBR | Shaded | All | None.") + geometry_file_format: str | None = Field(None, description="glb | usdz | fbx | obj | stl.") + texture_mode: str | None = Field(None, description="legacy | extreme-low | low | medium | high.") + mesh_mode: str | None = Field(None, description="Raw (triangular) | Quad.") + quality_override: int | None = Field(None, description="Mesh face count override.") + geometry_instruct_mode: str | None = Field(None, description="faithful | creative.") + bbox_condition: list[int] | None = Field(None, description="Bounding box [Width(Y), Height(Z), Length(X)] in cm.") + height: int | None = Field(None, description="Approximate model height in cm.") + TAPose: bool | None = Field(None, description="T/A pose for human-like models.") + hd_texture: bool | None = Field(None, description="Enhanced texture quality.") + texture_delight: bool | None = Field(None, description="Remove baked lighting from textures.") + is_micro: bool | None = Field(None, description="Micro detail (Extreme-High only).") + use_original_alpha: bool | None = Field(None, description="Preserve image transparency.") + preview_render: bool | None = Field(None, description="Generate high-quality preview render.") + addons: list[str] | None = Field(None, description='Optional addons, e.g. ["HighPack"].') + class GenerateJobsData(BaseModel): - uuids: List[str] = Field(..., description="str LIST") + uuids: list[str] = Field(..., description="str LIST") subscription_key: str = Field(..., description="subscription key") + class Rodin3DGenerateResponse(BaseModel): - message: Optional[str] = Field(None, description="Return message.") - prompt: Optional[str] = Field(None, description="Generated Prompt from image.") - submit_time: Optional[str] = Field(None, description="Submit Time") - uuid: Optional[str] = Field(None, description="Task str") - jobs: Optional[GenerateJobsData] = Field(None, description="Details of jobs") + message: str | None = Field(None, description="Return message.") + prompt: str | None = Field(None, description="Generated Prompt from image.") + submit_time: str | None = Field(None, description="Submit Time") + uuid: str | None = Field(None, description="Task str") + jobs: GenerateJobsData | None = Field(None, description="Details of jobs") + class JobStatus(str, Enum): """ Status for jobs """ + Done = "Done" Failed = "Failed" Generating = "Generating" Waiting = "Waiting" + class Rodin3DCheckStatusRequest(BaseModel): subscription_key: str = Field(..., description="subscription from generate endpoint") + class JobItem(BaseModel): uuid: str = Field(..., description="uuid") - status: JobStatus = Field(...,description="Status Currently") + status: JobStatus = Field(..., description="Status Currently") + class Rodin3DCheckStatusResponse(BaseModel): - jobs: List[JobItem] = Field(..., description="Job status List") + jobs: list[JobItem] = Field(..., description="Job status List") + class Rodin3DDownloadRequest(BaseModel): task_uuid: str = Field(..., description="Task str") + class RodinResourceItem(BaseModel): url: str = Field(..., description="Download Url") name: str = Field(..., description="File name with ext") + class Rodin3DDownloadResponse(BaseModel): - list: List[RodinResourceItem] = Field(..., description="Source List") + items: list[RodinResourceItem] = Field(..., alias="list", description="Source List") diff --git a/comfy_api_nodes/apis/stability.py b/comfy_api_nodes/apis/stability.py index 718360187..5b9b5ac7d 100644 --- a/comfy_api_nodes/apis/stability.py +++ b/comfy_api_nodes/apis/stability.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from enum import Enum from typing import Optional diff --git a/comfy_api_nodes/apis/tripo.py b/comfy_api_nodes/apis/tripo.py index ffaaa7dc1..bce6b0e89 100644 --- a/comfy_api_nodes/apis/tripo.py +++ b/comfy_api_nodes/apis/tripo.py @@ -1,10 +1,11 @@ -from __future__ import annotations from enum import Enum -from typing import Optional, List, Dict, Any, Union +from typing import Optional, Any from pydantic import BaseModel, Field, RootModel + class TripoModelVersion(str, Enum): + v3_1_20260211 = 'v3.1-20260211' v3_0_20250812 = 'v3.0-20250812' v2_5_20250123 = 'v2.5-20250123' v2_0_20240919 = 'v2.0-20240919' @@ -142,7 +143,7 @@ class TripoFileEmptyReference(BaseModel): pass class TripoFileReference(RootModel): - root: Union[TripoFileTokenReference, TripoUrlReference, TripoObjectReference, TripoFileEmptyReference] + root: TripoFileTokenReference | TripoUrlReference | TripoObjectReference | TripoFileEmptyReference class TripoGetStsTokenRequest(BaseModel): format: str = Field(..., description='The format of the image') @@ -183,7 +184,7 @@ class TripoImageToModelRequest(BaseModel): class TripoMultiviewToModelRequest(BaseModel): type: TripoTaskType = TripoTaskType.MULTIVIEW_TO_MODEL - files: List[TripoFileReference] = Field(..., description='The file references to convert to a model') + files: list[TripoFileReference] = Field(..., description='The file references to convert to a model') model_version: Optional[TripoModelVersion] = Field(None, description='The model version to use for generation') orthographic_projection: Optional[bool] = Field(False, description='Whether to use orthographic projection') face_limit: Optional[int] = Field(None, description='The number of faces to limit the generation to') @@ -251,27 +252,13 @@ class TripoConvertModelRequest(BaseModel): with_animation: Optional[bool] = Field(None, description='Whether to include animations') pack_uv: Optional[bool] = Field(None, description='Whether to pack the UVs') bake: Optional[bool] = Field(None, description='Whether to bake the model') - part_names: Optional[List[str]] = Field(None, description='The names of the parts to include') + part_names: Optional[list[str]] = Field(None, description='The names of the parts to include') fbx_preset: Optional[TripoFbxPreset] = Field(None, description='The preset for the FBX export') export_vertex_colors: Optional[bool] = Field(None, description='Whether to export the vertex colors') export_orientation: Optional[TripoOrientation] = Field(None, description='The orientation for the export') animate_in_place: Optional[bool] = Field(None, description='Whether to animate in place') -class TripoTaskRequest(RootModel): - root: Union[ - TripoTextToModelRequest, - TripoImageToModelRequest, - TripoMultiviewToModelRequest, - TripoTextureModelRequest, - TripoRefineModelRequest, - TripoAnimatePrerigcheckRequest, - TripoAnimateRigRequest, - TripoAnimateRetargetRequest, - TripoStylizeModelRequest, - TripoConvertModelRequest - ] - class TripoTaskOutput(BaseModel): model: Optional[str] = Field(None, description='URL to the model') base_model: Optional[str] = Field(None, description='URL to the base model') @@ -283,12 +270,13 @@ class TripoTask(BaseModel): task_id: str = Field(..., description='The task ID') type: Optional[str] = Field(None, description='The type of task') status: Optional[TripoTaskStatus] = Field(None, description='The status of the task') - input: Optional[Dict[str, Any]] = Field(None, description='The input parameters for the task') + input: Optional[dict[str, Any]] = Field(None, description='The input parameters for the task') output: Optional[TripoTaskOutput] = Field(None, description='The output of the task') progress: Optional[int] = Field(None, description='The progress of the task', ge=0, le=100) create_time: Optional[int] = Field(None, description='The creation time of the task') running_left_time: Optional[int] = Field(None, description='The estimated time left for the task') queue_position: Optional[int] = Field(None, description='The position in the queue') + consumed_credit: int | None = Field(None) class TripoTaskResponse(BaseModel): code: int = Field(0, description='The response code') @@ -296,7 +284,7 @@ class TripoTaskResponse(BaseModel): class TripoGeneralResponse(BaseModel): code: int = Field(0, description='The response code') - data: Dict[str, str] = Field(..., description='The task ID data') + data: dict[str, str] = Field(..., description='The task ID data') class TripoBalanceData(BaseModel): balance: float = Field(..., description='The account balance') diff --git a/comfy_api_nodes/nodes_anthropic.py b/comfy_api_nodes/nodes_anthropic.py new file mode 100644 index 000000000..42ec5708f --- /dev/null +++ b/comfy_api_nodes/nodes_anthropic.py @@ -0,0 +1,306 @@ +"""API Nodes for Anthropic Claude (Messages API). See: https://docs.anthropic.com/en/api/messages""" + +from typing_extensions import override + +from comfy_api.latest import IO, ComfyExtension, Input +from comfy_api_nodes.apis.anthropic import ( + AnthropicImageContent, + AnthropicImageSourceUrl, + AnthropicMessage, + AnthropicMessagesRequest, + AnthropicMessagesResponse, + AnthropicOutputConfig, + AnthropicResponseTextBlock, + AnthropicRole, + AnthropicTextContent, + AnthropicThinkingConfig, +) +from comfy_api_nodes.util import ( + ApiEndpoint, + get_number_of_images, + sync_op, + upload_images_to_comfyapi, + validate_string, +) + +ANTHROPIC_MESSAGES_ENDPOINT = "/proxy/anthropic/v1/messages" +ANTHROPIC_IMAGE_MAX_PIXELS = 1568 * 1568 +CLAUDE_MAX_IMAGES = 20 + +CLAUDE_MODELS: dict[str, str] = { + "Opus 4.7": "claude-opus-4-7", + "Opus 4.6": "claude-opus-4-6", + "Sonnet 4.6": "claude-sonnet-4-6", + "Sonnet 4.5": "claude-sonnet-4-5-20250929", + "Haiku 4.5": "claude-haiku-4-5-20251001", +} + +_THINKING_UNSUPPORTED = {"Haiku 4.5"} +# Models that use the newer "adaptive" thinking mode (Opus 4.7 requires it; older models keep the explicit budget API). +# Anthropic decides the actual budget when adaptive is used, based on the `output_config.effort` hint. +_ADAPTIVE_THINKING_MODELS = {"Opus 4.7", "Opus 4.6", "Sonnet 4.6"} + +# Budget mode (Sonnet 4.5): effort -> reasoning budget in tokens. Must be < max_tokens. +# Sized so even the "high" budget fits comfortably under the default max_tokens=32768. +_REASONING_BUDGET: dict[str, int] = { + "low": 2048, + "medium": 8192, + "high": 16384, +} +_REASONING_EFFORTS = ["off", "low", "medium", "high"] + + +def _claude_model_inputs(model_label: str): + inputs: list = [ + IO.Int.Input( + "max_tokens", + default=32768, + min=4096, + max=64000, + tooltip="Maximum number of tokens to generate (includes reasoning tokens when enabled).", + advanced=True, + ), + IO.Float.Input( + "temperature", + default=1.0, + min=0.0, + max=1.0, + step=0.01, + tooltip=( + "Controls randomness. 0.0 is deterministic, 1.0 is most random. " + "Ignored for Opus 4.7 and any model when reasoning_effort is set." + ), + advanced=True, + ), + ] + if model_label not in _THINKING_UNSUPPORTED: + inputs.append( + IO.Combo.Input( + "reasoning_effort", + options=_REASONING_EFFORTS, + default="off", + tooltip="Extended thinking effort. 'off' disables reasoning.", + advanced=True, + ) + ) + return inputs + + +def _model_price_per_million(model: str) -> tuple[float, float] | None: + """Return (input_per_1M, output_per_1M) USD for a Claude model, or None if unknown.""" + if "opus-4-7" in model or "opus-4-6" in model or "opus-4-5" in model: + return 5.0, 25.0 + if "sonnet-4" in model: + return 3.0, 15.0 + if "haiku-4-5" in model: + return 1.0, 5.0 + return None + + +def calculate_tokens_price(response: AnthropicMessagesResponse) -> float | None: + """Compute approximate USD price from response usage. Server-side billing is authoritative.""" + if not response.usage or not response.model: + return None + rates = _model_price_per_million(response.model) + if rates is None: + return None + input_rate, output_rate = rates + input_tokens = response.usage.input_tokens or 0 + output_tokens = response.usage.output_tokens or 0 + cache_read = response.usage.cache_read_input_tokens or 0 + cache_5m = 0 + cache_1h = 0 + if response.usage.cache_creation: + cache_5m = response.usage.cache_creation.ephemeral_5m_input_tokens or 0 + cache_1h = response.usage.cache_creation.ephemeral_1h_input_tokens or 0 + total = ( + input_tokens * input_rate + + output_tokens * output_rate + + cache_read * input_rate * 0.1 + + cache_5m * input_rate * 1.25 + + cache_1h * input_rate * 2.0 + ) + return total / 1_000_000.0 + + +def _get_text_from_response(response: AnthropicMessagesResponse) -> str: + if not response.content: + return "" + # Thinking blocks are silently dropped — we never want reasoning in the output. + return "\n".join( + block.text for block in response.content + if isinstance(block, AnthropicResponseTextBlock) and block.text + ) + + +async def _build_image_content_blocks( + cls: type[IO.ComfyNode], + image_tensors: list[Input.Image], +) -> list[AnthropicImageContent]: + urls = await upload_images_to_comfyapi( + cls, + image_tensors, + max_images=CLAUDE_MAX_IMAGES, + total_pixels=ANTHROPIC_IMAGE_MAX_PIXELS, + wait_label="Uploading reference images", + ) + return [AnthropicImageContent(source=AnthropicImageSourceUrl(url=url)) for url in urls] + + +class ClaudeNode(IO.ComfyNode): + """Generate text responses from an Anthropic Claude model.""" + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="ClaudeNode", + display_name="Anthropic Claude", + category="api node/text/Anthropic", + essentials_category="Text Generation", + description="Generate text responses with Anthropic's Claude models. " + "Provide a text prompt and optionally one or more images for multimodal context.", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Text input to the model.", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option(label, _claude_model_inputs(label)) + for label in CLAUDE_MODELS + ], + tooltip="The Claude model used to generate the response.", + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=2147483647, + control_after_generate=True, + tooltip="Seed controls whether the node should re-run; " + "results are non-deterministic regardless of seed.", + ), + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, CLAUDE_MAX_IMAGES + 1)], + min=0, + ), + tooltip=f"Optional image(s) to use as context for the model. Up to {CLAUDE_MAX_IMAGES} images.", + ), + IO.String.Input( + "system_prompt", + multiline=True, + default="", + optional=True, + advanced=True, + tooltip="Foundational instructions that dictate the model's behavior.", + ), + ], + outputs=[IO.String.Output()], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["model"]), + expr=""" + ( + $m := widgets.model; + $contains($m, "opus") ? { + "type": "list_usd", + "usd": [0.005, 0.025], + "format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" } + } + : $contains($m, "sonnet") ? { + "type": "list_usd", + "usd": [0.003, 0.015], + "format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" } + } + : $contains($m, "haiku") ? { + "type": "list_usd", + "usd": [0.001, 0.005], + "format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" } + } + : {"type":"text", "text":"Token-based"} + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int, + images: dict | None = None, + system_prompt: str = "", + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=True, min_length=1) + model_label = model["model"] + max_tokens = model.get("max_tokens", 32768) + reasoning_effort = model.get("reasoning_effort", "off") + thinking_enabled = reasoning_effort not in ("off", None) and model_label not in _THINKING_UNSUPPORTED + + # Anthropic requires temperature to be unset (defaults to 1.0) when thinking is enabled. + # Opus 4.7 also rejects user-supplied temperature. + if thinking_enabled or model_label == "Opus 4.7": + temperature = None + else: + temperature = model.get("temperature", 1.0) + + thinking_cfg: AnthropicThinkingConfig | None = None + output_cfg: AnthropicOutputConfig | None = None + if thinking_enabled: + if model_label in _ADAPTIVE_THINKING_MODELS: + # Adaptive mode - Anthropic chooses the budget based on effort hint + thinking_cfg = AnthropicThinkingConfig(type="adaptive") + output_cfg = AnthropicOutputConfig(effort=reasoning_effort) + else: + # Budget mode (Sonnet 4.5). Leave at least 1024 tokens for the actual response + budget = _REASONING_BUDGET[reasoning_effort] + budget = min(budget, max(1024, max_tokens - 1024)) + thinking_cfg = AnthropicThinkingConfig(type="enabled", budget_tokens=budget) + + image_tensors: list[Input.Image] = [t for t in (images or {}).values() if t is not None] + if sum(get_number_of_images(t) for t in image_tensors) > CLAUDE_MAX_IMAGES: + raise ValueError(f"Up to {CLAUDE_MAX_IMAGES} images are supported per request.") + + content: list[AnthropicTextContent | AnthropicImageContent] = [] + if image_tensors: + content.extend(await _build_image_content_blocks(cls, image_tensors)) + content.append(AnthropicTextContent(text=prompt)) + + response = await sync_op( + cls, + ApiEndpoint(path=ANTHROPIC_MESSAGES_ENDPOINT, method="POST"), + response_model=AnthropicMessagesResponse, + data=AnthropicMessagesRequest( + model=CLAUDE_MODELS[model_label], + max_tokens=max_tokens, + messages=[AnthropicMessage(role=AnthropicRole.user, content=content)], + system=system_prompt or None, + temperature=temperature, + thinking=thinking_cfg, + output_config=output_cfg, + ), + price_extractor=calculate_tokens_price, + ) + return IO.NodeOutput(_get_text_from_response(response) or "Empty response from Claude model.") + + +class AnthropicExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[IO.ComfyNode]]: + return [ClaudeNode] + + +async def comfy_entrypoint() -> AnthropicExtension: + return AnthropicExtension() diff --git a/comfy_api_nodes/nodes_bfl.py b/comfy_api_nodes/nodes_bfl.py index 23590bf24..3f0ce29d8 100644 --- a/comfy_api_nodes/nodes_bfl.py +++ b/comfy_api_nodes/nodes_bfl.py @@ -596,6 +596,7 @@ class Flux2ProImageNode(IO.ComfyNode): depends_on=IO.PriceBadgeDepends(widgets=["width", "height"], inputs=["images"]), expr=cls.PRICE_BADGE_EXPR, ), + is_deprecated=True, ) @classmethod @@ -674,6 +675,175 @@ class Flux2MaxImageNode(Flux2ProImageNode): """ +_FLUX2_MODEL_ENDPOINTS = { + "Flux.2 [pro]": "/proxy/bfl/flux-2-pro/generate", + "Flux.2 [max]": "/proxy/bfl/flux-2-max/generate", +} + + +def _flux2_model_inputs(): + return [ + IO.Int.Input( + "width", + default=1024, + min=256, + max=2048, + step=32, + ), + IO.Int.Input( + "height", + default=768, + min=256, + max=2048, + step=32, + ), + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, 9)], + min=0, + ), + tooltip="Optional reference image(s) for image-to-image generation. Up to 8 images.", + ), + ] + + +class Flux2ImageNode(IO.ComfyNode): + + @classmethod + def define_schema(cls) -> IO.Schema: + return IO.Schema( + node_id="Flux2ImageNode", + display_name="Flux.2 Image", + category="api node/image/BFL", + description="Generate images via Flux.2 [pro] or Flux.2 [max] from a prompt and optional reference images.", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Prompt for the image generation or edit", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option("Flux.2 [pro]", _flux2_model_inputs()), + IO.DynamicCombo.Option("Flux.2 [max]", _flux2_model_inputs()), + ], + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=0xFFFFFFFFFFFFFFFF, + control_after_generate=True, + tooltip="The random seed used for creating the noise.", + ), + ], + outputs=[IO.Image.Output()], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends( + widgets=["model", "model.width", "model.height"], + input_groups=["model.images"], + ), + expr=""" + ( + $isMax := widgets.model = "flux.2 [max]"; + $MP := 1024 * 1024; + $w := $lookup(widgets, "model.width"); + $h := $lookup(widgets, "model.height"); + $outMP := $max([1, $floor((($w * $h) + $MP - 1) / $MP)]); + $outputCost := $isMax + ? (0.07 + 0.03 * ($outMP - 1)) + : (0.03 + 0.015 * ($outMP - 1)); + $refMin := $isMax ? 0.03 : 0.015; + $refMax := $isMax ? 0.24 : 0.12; + $hasRefs := $lookup(inputGroups, "model.images") > 0; + $hasRefs + ? { + "type": "range_usd", + "min_usd": $outputCost + $refMin, + "max_usd": $outputCost + $refMax, + "format": { "approximate": true } + } + : {"type": "usd", "usd": $outputCost} + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int, + ) -> IO.NodeOutput: + model_choice = model["model"] + endpoint = _FLUX2_MODEL_ENDPOINTS[model_choice] + width = model["width"] + height = model["height"] + images_dict = model.get("images") or {} + + image_tensors: list[Input.Image] = [t for t in images_dict.values() if t is not None] + n_images = sum(get_number_of_images(t) for t in image_tensors) + if n_images > 8: + raise ValueError("The current maximum number of supported images is 8.") + + flat_tensors: list[torch.Tensor] = [] + for tensor in image_tensors: + if len(tensor.shape) == 4: + flat_tensors.extend(tensor[i] for i in range(tensor.shape[0])) + else: + flat_tensors.append(tensor) + + reference_images: dict[str, str] = {} + for idx, tensor in enumerate(flat_tensors): + key_name = f"input_image_{idx + 1}" if idx else "input_image" + reference_images[key_name] = tensor_to_base64_string(tensor, total_pixels=2048 * 2048) + + initial_response = await sync_op( + cls, + ApiEndpoint(path=endpoint, method="POST"), + response_model=BFLFluxProGenerateResponse, + data=Flux2ProGenerateRequest( + prompt=prompt, + width=width, + height=height, + seed=seed, + **reference_images, + ), + ) + + def price_extractor(_r: BaseModel) -> float | None: + return None if initial_response.cost is None else initial_response.cost / 100 + + response = await poll_op( + cls, + ApiEndpoint(initial_response.polling_url), + response_model=BFLFluxStatusResponse, + status_extractor=lambda r: r.status, + progress_extractor=lambda r: r.progress, + price_extractor=price_extractor, + completed_statuses=[BFLStatus.ready], + failed_statuses=[ + BFLStatus.request_moderated, + BFLStatus.content_moderated, + BFLStatus.error, + BFLStatus.task_not_found, + ], + queued_statuses=[], + ) + return IO.NodeOutput(await download_url_to_image_tensor(response.result["sample"])) + + class BFLExtension(ComfyExtension): @override async def get_node_list(self) -> list[type[IO.ComfyNode]]: @@ -685,6 +855,7 @@ class BFLExtension(ComfyExtension): FluxProFillNode, Flux2ProImageNode, Flux2MaxImageNode, + Flux2ImageNode, ] diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py index 2f241a775..e08fc0b01 100644 --- a/comfy_api_nodes/nodes_bytedance.py +++ b/comfy_api_nodes/nodes_bytedance.py @@ -10,6 +10,9 @@ from comfy_api.latest import IO, ComfyExtension, Input from comfy_api_nodes.apis.bytedance import ( RECOMMENDED_PRESETS, RECOMMENDED_PRESETS_SEEDREAM_4, + RECOMMENDED_PRESETS_SEEDREAM_4_0, + RECOMMENDED_PRESETS_SEEDREAM_4_5, + RECOMMENDED_PRESETS_SEEDREAM_5_LITE, SEEDANCE2_PRICE_PER_1K_TOKENS, SEEDANCE2_REF_VIDEO_PIXEL_LIMITS, VIDEO_TASKS_EXECUTION_TIME, @@ -40,15 +43,16 @@ from comfy_api_nodes.util import ( ApiEndpoint, download_url_to_image_tensor, download_url_to_video_output, + downscale_video_to_max_pixels, get_number_of_images, image_tensor_pair_to_batch, poll_op, - resize_video_to_pixel_budget, sync_op, upload_audio_to_comfyapi, upload_image_to_comfyapi, upload_images_to_comfyapi, upload_video_to_comfyapi, + upscale_video_to_min_pixels, validate_image_aspect_ratio, validate_image_dimensions, validate_string, @@ -68,6 +72,12 @@ SEEDREAM_MODELS = { "seedream-4-0-250828": "seedream-4-0-250828", } +SEEDREAM_PRESETS = { + "seedream-5-0-260128": RECOMMENDED_PRESETS_SEEDREAM_5_LITE, + "seedream-4-5-251128": RECOMMENDED_PRESETS_SEEDREAM_4_5, + "seedream-4-0-250828": RECOMMENDED_PRESETS_SEEDREAM_4_0, +} + # Long-running tasks endpoints(e.g., video) BYTEPLUS_TASK_ENDPOINT = "/proxy/byteplus/api/v3/contents/generations/tasks" BYTEPLUS_TASK_STATUS_ENDPOINT = "/proxy/byteplus/api/v3/contents/generations/tasks" # + /{task_id} @@ -101,12 +111,13 @@ def _validate_ref_video_pixels(video: Input.Video, model_id: str, resolution: st max_px = limits.get("max") if min_px and pixels < min_px: raise ValueError( - f"Reference video {index} is too small: {w}x{h} = {pixels:,}px. " f"Minimum is {min_px:,}px for this model." + f"Reference video {index} is too small: {w}x{h} = {pixels:,} total pixels. " + f"Minimum for this model is {min_px:,} total pixels." ) if max_px and pixels > max_px: raise ValueError( - f"Reference video {index} is too large: {w}x{h} = {pixels:,}px. " - f"Maximum is {max_px:,}px for this model. Try downscaling the video." + f"Reference video {index} is too large: {w}x{h} = {pixels:,} total pixels. " + f"Maximum for this model is {max_px:,} total pixels. Try downscaling the video." ) @@ -562,6 +573,7 @@ class ByteDanceSeedreamNode(IO.ComfyNode): ) """, ), + is_deprecated=True, ) @classmethod @@ -651,6 +663,226 @@ class ByteDanceSeedreamNode(IO.ComfyNode): return IO.NodeOutput(torch.cat([await download_url_to_image_tensor(i) for i in urls])) +def _seedream_model_inputs(*, max_ref_images: int, presets: list): + return [ + IO.Combo.Input( + "size_preset", + options=[label for label, _, _ in presets], + tooltip="Pick a recommended size. Select Custom to use the width and height below.", + ), + IO.Int.Input( + "width", + default=2048, + min=1024, + max=6240, + step=2, + tooltip="Custom width for image. Value is working only if `size_preset` is set to `Custom`", + ), + IO.Int.Input( + "height", + default=2048, + min=1024, + max=4992, + step=2, + tooltip="Custom height for image. Value is working only if `size_preset` is set to `Custom`", + ), + IO.Int.Input( + "max_images", + default=1, + min=1, + max=max_ref_images, + step=1, + display_mode=IO.NumberDisplay.number, + tooltip="Maximum number of images to generate. With 1, exactly one image is produced. " + "With >1, the model generates between 1 and max_images related images " + "(e.g., story scenes, character variations). " + "Total images (input + generated) cannot exceed 15.", + ), + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, max_ref_images + 1)], + min=0, + ), + tooltip=f"Optional reference image(s) for image-to-image or multi-reference generation. " + f"Up to {max_ref_images} images.", + ), + IO.Boolean.Input( + "fail_on_partial", + default=False, + tooltip="If enabled, abort execution if any requested images are missing or return an error.", + advanced=True, + ), + ] + + +class ByteDanceSeedreamNodeV2(IO.ComfyNode): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="ByteDanceSeedreamNodeV2", + display_name="ByteDance Seedream 4.5 & 5.0", + category="api node/image/ByteDance", + description="Unified text-to-image generation and precise single-sentence editing at up to 4K resolution.", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Text prompt for creating or editing an image.", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option( + "seedream 5.0 lite", + _seedream_model_inputs(max_ref_images=14, presets=RECOMMENDED_PRESETS_SEEDREAM_5_LITE), + ), + IO.DynamicCombo.Option( + "seedream-4-5-251128", + _seedream_model_inputs(max_ref_images=10, presets=RECOMMENDED_PRESETS_SEEDREAM_4_5), + ), + IO.DynamicCombo.Option( + "seedream-4-0-250828", + _seedream_model_inputs(max_ref_images=10, presets=RECOMMENDED_PRESETS_SEEDREAM_4_0), + ), + ], + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=2147483647, + step=1, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + tooltip="Seed to use for generation.", + ), + IO.Boolean.Input( + "watermark", + default=False, + tooltip='Whether to add an "AI generated" watermark to the image.', + advanced=True, + ), + ], + outputs=[ + IO.Image.Output(), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["model"]), + expr=""" + ( + $price := $contains(widgets.model, "5.0 lite") ? 0.035 : + $contains(widgets.model, "4-5") ? 0.04 : 0.03; + { + "type":"usd", + "usd": $price, + "format": { "suffix":" x images/Run", "approximate": true } + } + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int = 0, + watermark: bool = False, + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=True, min_length=1) + model_id = SEEDREAM_MODELS[model["model"]] + presets = SEEDREAM_PRESETS[model_id] + + size_preset = model.get("size_preset", presets[0][0]) + width = model.get("width", 2048) + height = model.get("height", 2048) + max_images = model.get("max_images", 1) + sequential_image_generation = "disabled" if max_images == 1 else "auto" + images_dict = model.get("images") or {} + fail_on_partial = model.get("fail_on_partial", False) + + w = h = None + for label, tw, th in presets: + if label == size_preset: + w, h = tw, th + break + if w is None or h is None: + w, h = width, height + + out_num_pixels = w * h + mp_provided = out_num_pixels / 1_000_000.0 + if ("seedream-4-5" in model_id or "seedream-5-0" in model_id) and out_num_pixels < 3686400: + raise ValueError( + f"Minimum image resolution for the selected model is 3.68MP, but {mp_provided:.2f}MP provided." + ) + if "seedream-4-0" in model_id and out_num_pixels < 921600: + raise ValueError( + f"Minimum image resolution that the selected model can generate is 0.92MP, " + f"but {mp_provided:.2f}MP provided." + ) + if out_num_pixels > 16_777_216: + raise ValueError( + f"Maximum image resolution for the selected model is 16.78MP, but {mp_provided:.2f}MP provided." + ) + + image_tensors: list[Input.Image] = [t for t in images_dict.values() if t is not None] + n_input_images = sum(get_number_of_images(t) for t in image_tensors) + max_num_of_images = 14 if model_id == "seedream-5-0-260128" else 10 + if n_input_images > max_num_of_images: + raise ValueError( + f"Maximum of {max_num_of_images} reference images are supported, but {n_input_images} received." + ) + if sequential_image_generation == "auto" and n_input_images + max_images > 15: + raise ValueError( + "The maximum number of generated images plus the number of reference images cannot exceed 15." + ) + + reference_images_urls: list[str] = [] + if image_tensors: + for tensor in image_tensors: + validate_image_aspect_ratio(tensor, (1, 3), (3, 1)) + reference_images_urls = await upload_images_to_comfyapi( + cls, + image_tensors, + max_images=n_input_images, + mime_type="image/png", + wait_label="Uploading reference images", + ) + + response = await sync_op( + cls, + ApiEndpoint(path=BYTEPLUS_IMAGE_ENDPOINT, method="POST"), + response_model=ImageTaskCreationResponse, + data=Seedream4TaskCreationRequest( + model=model_id, + prompt=prompt, + image=reference_images_urls, + size=f"{w}x{h}", + seed=seed, + sequential_image_generation=sequential_image_generation, + sequential_image_generation_options=Seedream4Options(max_images=max_images), + watermark=watermark, + ), + ) + if len(response.data) == 1: + return IO.NodeOutput(await download_url_to_image_tensor(get_image_url_from_response(response))) + urls = [str(d["url"]) for d in response.data if isinstance(d, dict) and "url" in d] + if fail_on_partial and len(urls) < len(response.data): + raise RuntimeError(f"Only {len(urls)} of {len(response.data)} images were generated before error.") + return IO.NodeOutput(torch.cat([await download_url_to_image_tensor(i) for i in urls])) + + class ByteDanceTextToVideoNode(IO.ComfyNode): @classmethod @@ -1271,7 +1503,7 @@ PRICE_BADGE_VIDEO = IO.PriceBadge( ) -def _seedance2_text_inputs(resolutions: list[str]): +def _seedance2_text_inputs(resolutions: list[str], default_ratio: str = "16:9"): return [ IO.String.Input( "prompt", @@ -1287,6 +1519,7 @@ def _seedance2_text_inputs(resolutions: list[str]): IO.Combo.Input( "ratio", options=["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], + default=default_ratio, tooltip="Aspect ratio of the output video.", ), IO.Int.Input( @@ -1420,8 +1653,14 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): IO.DynamicCombo.Input( "model", options=[ - IO.DynamicCombo.Option("Seedance 2.0", _seedance2_text_inputs(["480p", "720p", "1080p"])), - IO.DynamicCombo.Option("Seedance 2.0 Fast", _seedance2_text_inputs(["480p", "720p"])), + IO.DynamicCombo.Option( + "Seedance 2.0", + _seedance2_text_inputs(["480p", "720p", "1080p"], default_ratio="adaptive"), + ), + IO.DynamicCombo.Option( + "Seedance 2.0 Fast", + _seedance2_text_inputs(["480p", "720p"], default_ratio="adaptive"), + ), ], tooltip="Seedance 2.0 for maximum quality; Seedance 2.0 Fast for speed optimization.", ), @@ -1439,14 +1678,14 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): "first_frame_asset_id", default="", tooltip="Seedance asset_id to use as the first frame. " - "Mutually exclusive with the first_frame image input.", + "Mutually exclusive with the first_frame image input.", optional=True, ), IO.String.Input( "last_frame_asset_id", default="", tooltip="Seedance asset_id to use as the last frame. " - "Mutually exclusive with the last_frame image input.", + "Mutually exclusive with the last_frame image input.", optional=True, ), IO.Int.Input( @@ -1588,9 +1827,9 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode): return IO.NodeOutput(await download_url_to_video_output(response.content.video_url)) -def _seedance2_reference_inputs(resolutions: list[str]): +def _seedance2_reference_inputs(resolutions: list[str], default_ratio: str = "16:9"): return [ - *_seedance2_text_inputs(resolutions), + *_seedance2_text_inputs(resolutions, default_ratio=default_ratio), IO.Autogrow.Input( "reference_images", template=IO.Autogrow.TemplateNames( @@ -1628,11 +1867,20 @@ def _seedance2_reference_inputs(resolutions: list[str]): IO.Boolean.Input( "auto_downscale", default=False, - advanced=True, optional=True, tooltip="Automatically downscale reference videos that exceed the model's pixel budget " "for the selected resolution. Aspect ratio is preserved; videos already within limits are untouched.", ), + IO.Boolean.Input( + "auto_upscale", + default=False, + advanced=True, + optional=True, + tooltip="Automatically upscale reference videos that are below the model's minimum pixel count " + "for the selected resolution. Aspect ratio is preserved; videos already meeting the minimum are " + "untouched. Note: upscaling a low-resolution source does not add real detail and may produce " + "lower-quality generations.", + ), IO.Autogrow.Input( "reference_assets", template=IO.Autogrow.TemplateNames( @@ -1668,8 +1916,14 @@ class ByteDance2ReferenceNode(IO.ComfyNode): IO.DynamicCombo.Input( "model", options=[ - IO.DynamicCombo.Option("Seedance 2.0", _seedance2_reference_inputs(["480p", "720p", "1080p"])), - IO.DynamicCombo.Option("Seedance 2.0 Fast", _seedance2_reference_inputs(["480p", "720p"])), + IO.DynamicCombo.Option( + "Seedance 2.0", + _seedance2_reference_inputs(["480p", "720p", "1080p"], default_ratio="adaptive"), + ), + IO.DynamicCombo.Option( + "Seedance 2.0 Fast", + _seedance2_reference_inputs(["480p", "720p"], default_ratio="adaptive"), + ), ], tooltip="Seedance 2.0 for maximum quality; Seedance 2.0 Fast for speed optimization.", ), @@ -1787,7 +2041,13 @@ class ByteDance2ReferenceNode(IO.ComfyNode): max_px = SEEDANCE2_REF_VIDEO_PIXEL_LIMITS.get(model_id, {}).get(model["resolution"], {}).get("max") if max_px: for key in reference_videos: - reference_videos[key] = resize_video_to_pixel_budget(reference_videos[key], max_px) + reference_videos[key] = downscale_video_to_max_pixels(reference_videos[key], max_px) + + if model.get("auto_upscale") and reference_videos: + min_px = SEEDANCE2_REF_VIDEO_PIXEL_LIMITS.get(model_id, {}).get(model["resolution"], {}).get("min") + if min_px: + for key in reference_videos: + reference_videos[key] = upscale_video_to_min_pixels(reference_videos[key], min_px) total_video_duration = 0.0 for i, key in enumerate(reference_videos, 1): @@ -2092,6 +2352,7 @@ class ByteDanceExtension(ComfyExtension): return [ ByteDanceImageNode, ByteDanceSeedreamNode, + ByteDanceSeedreamNodeV2, ByteDanceTextToVideoNode, ByteDanceImageToVideoNode, ByteDanceFirstLastFrameNode, diff --git a/comfy_api_nodes/nodes_bytedance_llm.py b/comfy_api_nodes/nodes_bytedance_llm.py new file mode 100644 index 000000000..fa7fe370a --- /dev/null +++ b/comfy_api_nodes/nodes_bytedance_llm.py @@ -0,0 +1,271 @@ +"""API Nodes for ByteDance Seed LLM via the BytePlus ModelArk Responses API. + +See: https://docs.byteplus.com/en/docs/ModelArk/1585128 +""" + +from typing_extensions import override + +from comfy_api.latest import IO, ComfyExtension, Input +from comfy_api_nodes.apis.bytedance_llm import ( + BytePlusInputImage, + BytePlusInputMessage, + BytePlusInputText, + BytePlusInputVideo, + BytePlusMessageContent, + BytePlusResponseCreateRequest, + BytePlusResponseObject, +) +from comfy_api_nodes.util import ( + ApiEndpoint, + get_number_of_images, + sync_op, + upload_images_to_comfyapi, + upload_video_to_comfyapi, + validate_string, +) + +BYTEPLUS_RESPONSES_ENDPOINT = "/proxy/byteplus/api/v3/responses" +SEED_MAX_IMAGES = 20 +SEED_MAX_VIDEOS = 4 + +SEED_MODELS: dict[str, str] = { + "Seed 2.0 Pro": "seed-2-0-pro-260328", + "Seed 2.0 Lite": "seed-2-0-lite-260228", + "Seed 2.0 Mini": "seed-2-0-mini-260215", +} + +# USD per 1M tokens: (input, cache_hit_input, output) +_SEED_PRICES_PER_MILLION: dict[str, tuple[float, float, float]] = { + "seed-2-0-pro-260328": (0.50, 0.10, 3.00), + "seed-2-0-lite-260228": (0.25, 0.05, 2.00), + "seed-2-0-mini-260215": (0.10, 0.02, 0.40), +} + + +def _seed_model_inputs(max_images: int = SEED_MAX_IMAGES, max_videos: int = SEED_MAX_VIDEOS): + return [ + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, max_images + 1)], + min=0, + ), + tooltip=f"Optional image(s) to use as context for the model. Up to {max_images} images.", + ), + IO.Autogrow.Input( + "videos", + template=IO.Autogrow.TemplateNames( + IO.Video.Input("video"), + names=[f"video_{i}" for i in range(1, max_videos + 1)], + min=0, + ), + tooltip=f"Optional video(s) to use as context for the model. Up to {max_videos} videos.", + ), + IO.Float.Input( + "temperature", + default=1.0, + min=0.0, + max=2.0, + step=0.01, + tooltip="Controls randomness. 0.0 is deterministic, higher values are more random.", + advanced=True, + ), + ] + + +def _calculate_price(model_id: str, response: BytePlusResponseObject) -> float | None: + """Compute approximate USD price from response usage.""" + if not response.usage: + return None + rates = _SEED_PRICES_PER_MILLION.get(model_id) + if rates is None: + return None + input_rate, cache_hit_rate, output_rate = rates + input_tokens = response.usage.input_tokens or 0 + output_tokens = response.usage.output_tokens or 0 + cached = 0 + if response.usage.input_tokens_details: + cached = response.usage.input_tokens_details.cached_tokens or 0 + fresh_input = max(0, input_tokens - cached) + total = fresh_input * input_rate + cached * cache_hit_rate + output_tokens * output_rate + return total / 1_000_000.0 + + +def _get_text_from_response(response: BytePlusResponseObject) -> str: + """Extract concatenated text from all assistant message output_text blocks.""" + if not response.output: + return "" + chunks: list[str] = [] + for item in response.output: + if item.type != "message" or not item.content: + continue + for block in item.content: + if block.type == "output_text" and block.text: + chunks.append(block.text) + elif block.type == "refusal" and block.refusal: + raise ValueError(f"Model refused to respond: {block.refusal}") + return "\n".join(chunks) + + +async def _build_image_content_blocks( + cls: type[IO.ComfyNode], + image_tensors: list[Input.Image], +) -> list[BytePlusInputImage]: + urls = await upload_images_to_comfyapi( + cls, + image_tensors, + max_images=SEED_MAX_IMAGES, + wait_label="Uploading reference images", + ) + return [BytePlusInputImage(image_url=url) for url in urls] + + +async def _build_video_content_blocks( + cls: type[IO.ComfyNode], + videos: list[Input.Video], +) -> list[BytePlusInputVideo]: + blocks: list[BytePlusInputVideo] = [] + total = len(videos) + for idx, video in enumerate(videos): + label = "Uploading reference video" + if total > 1: + label = f"{label} ({idx + 1}/{total})" + url = await upload_video_to_comfyapi(cls, video, wait_label=label) + blocks.append(BytePlusInputVideo(video_url=url)) + return blocks + + +class ByteDanceSeedNode(IO.ComfyNode): + """Generate text responses from a ByteDance Seed 2.0 model.""" + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="ByteDanceSeedNode", + display_name="ByteDance Seed", + category="api node/text/ByteDance", + essentials_category="Text Generation", + description="Generate text responses with ByteDance's Seed 2.0 models. " + "Provide a text prompt and optionally one or more images or videos for multimodal context.", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Text input to the model.", + ), + IO.DynamicCombo.Input( + "model", + options=[IO.DynamicCombo.Option(label, _seed_model_inputs()) for label in SEED_MODELS], + tooltip="The Seed model used to generate the response.", + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=2147483647, + control_after_generate=True, + tooltip="Seed controls whether the node should re-run; " + "results are non-deterministic regardless of seed.", + ), + IO.String.Input( + "system_prompt", + multiline=True, + default="", + optional=True, + advanced=True, + tooltip="Foundational instructions that dictate the model's behavior.", + ), + ], + outputs=[IO.String.Output()], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["model"]), + expr=""" + ( + $m := widgets.model; + $contains($m, "mini") ? { + "type": "list_usd", + "usd": [0.00025, 0.0009], + "format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" } + } + : $contains($m, "lite") ? { + "type": "list_usd", + "usd": [0.0003, 0.002], + "format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" } + } + : $contains($m, "pro") ? { + "type": "list_usd", + "usd": [0.0005, 0.003], + "format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" } + } + : {"type":"text", "text":"Token-based"} + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int, + system_prompt: str = "", + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=True, min_length=1) + model_label = model["model"] + temperature = model["temperature"] + model_id = SEED_MODELS[model_label] + + image_tensors: list[Input.Image] = [t for t in (model.get("images") or {}).values() if t is not None] + if sum(get_number_of_images(t) for t in image_tensors) > SEED_MAX_IMAGES: + raise ValueError(f"Up to {SEED_MAX_IMAGES} images are supported per request.") + + video_inputs: list[Input.Video] = [v for v in (model.get("videos") or {}).values() if v is not None] + if len(video_inputs) > SEED_MAX_VIDEOS: + raise ValueError(f"Up to {SEED_MAX_VIDEOS} videos are supported per request.") + + content: list[BytePlusMessageContent] = [] + if image_tensors: + content.extend(await _build_image_content_blocks(cls, image_tensors)) + if video_inputs: + content.extend(await _build_video_content_blocks(cls, video_inputs)) + content.append(BytePlusInputText(text=prompt)) + + response = await sync_op( + cls, + ApiEndpoint(path=BYTEPLUS_RESPONSES_ENDPOINT, method="POST"), + response_model=BytePlusResponseObject, + data=BytePlusResponseCreateRequest( + model=model_id, + input=[BytePlusInputMessage(role="user", content=content)], + instructions=system_prompt or None, + temperature=temperature, + store=False, + stream=False, + ), + price_extractor=lambda r: _calculate_price(model_id, r), + ) + if response.error: + raise ValueError(f"Seed API error ({response.error.code}): {response.error.message}") + result = _get_text_from_response(response) + if not result: + raise ValueError("Empty response from Seed model.") + return IO.NodeOutput(result) + + +class ByteDanceLLMExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[IO.ComfyNode]]: + return [ByteDanceSeedNode] + + +async def comfy_entrypoint() -> ByteDanceLLMExtension: + return ByteDanceLLMExtension() diff --git a/comfy_api_nodes/nodes_gemini.py b/comfy_api_nodes/nodes_gemini.py index 2b77a022e..d18c958a8 100644 --- a/comfy_api_nodes/nodes_gemini.py +++ b/comfy_api_nodes/nodes_gemini.py @@ -83,13 +83,16 @@ class GeminiImageModel(str, Enum): async def create_image_parts( cls: type[IO.ComfyNode], - images: Input.Image, + images: Input.Image | list[Input.Image], image_limit: int = 0, ) -> list[GeminiPart]: image_parts: list[GeminiPart] = [] if image_limit < 0: raise ValueError("image_limit must be greater than or equal to 0 when creating Gemini image parts.") - total_images = get_number_of_images(images) + + # Accept either a single (possibly-batched) tensor or a list of them; share URL budget across all. + images_list: list[Input.Image] = images if isinstance(images, list) else [images] + total_images = sum(get_number_of_images(img) for img in images_list) if total_images <= 0: raise ValueError("No images provided to create_image_parts; at least one image is required.") @@ -98,10 +101,18 @@ async def create_image_parts( # Number of images we'll send as URLs (fileData) num_url_images = min(effective_max, 10) # Vertex API max number of image links + upload_kwargs: dict = {"wait_label": "Uploading reference images"} + if effective_max > num_url_images: + # Split path (e.g. 11+ images): suppress per-image counter to avoid a confusing dual-fraction label. + upload_kwargs = { + "wait_label": f"Uploading reference images ({num_url_images}+)", + "show_batch_index": False, + } reference_images_urls = await upload_images_to_comfyapi( cls, - images, + images_list, max_images=num_url_images, + **upload_kwargs, ) for reference_image_url in reference_images_urls: image_parts.append( @@ -112,15 +123,22 @@ async def create_image_parts( ) ) ) - for idx in range(num_url_images, effective_max): - image_parts.append( - GeminiPart( - inlineData=GeminiInlineData( - mimeType=GeminiMimeType.image_png, - data=tensor_to_base64_string(images[idx]), + if effective_max > num_url_images: + flat: list[torch.Tensor] = [] + for tensor in images_list: + if len(tensor.shape) == 4: + flat.extend(tensor[i] for i in range(tensor.shape[0])) + else: + flat.append(tensor) + for idx in range(num_url_images, effective_max): + image_parts.append( + GeminiPart( + inlineData=GeminiInlineData( + mimeType=GeminiMimeType.image_png, + data=tensor_to_base64_string(flat[idx]), + ) ) ) - ) return image_parts @@ -891,10 +909,6 @@ class GeminiNanoBanana2(IO.ComfyNode): "9:16", "16:9", "21:9", - # "1:4", - # "4:1", - # "8:1", - # "1:8", ], default="auto", tooltip="If set to 'auto', matches your input image's aspect ratio; " @@ -902,12 +916,7 @@ class GeminiNanoBanana2(IO.ComfyNode): ), IO.Combo.Input( "resolution", - options=[ - # "512px", - "1K", - "2K", - "4K", - ], + options=["1K", "2K", "4K"], tooltip="Target output resolution. For 2K/4K the native Gemini upscaler is used.", ), IO.Combo.Input( @@ -956,6 +965,7 @@ class GeminiNanoBanana2(IO.ComfyNode): ], is_api_node=True, price_badge=GEMINI_IMAGE_2_PRICE_BADGE, + is_deprecated=True, ) @classmethod @@ -1016,6 +1026,197 @@ class GeminiNanoBanana2(IO.ComfyNode): ) +def _nano_banana_2_v2_model_inputs(): + return [ + IO.Combo.Input( + "aspect_ratio", + options=[ + "auto", + "1:1", + "2:3", + "3:2", + "3:4", + "4:3", + "4:5", + "5:4", + "9:16", + "16:9", + "21:9", + "1:4", + "4:1", + "8:1", + "1:8", + ], + default="auto", + tooltip="If set to 'auto', matches your input image's aspect ratio; " + "if no image is provided, a 16:9 square is usually generated.", + ), + IO.Combo.Input( + "resolution", + options=["1K", "2K", "4K"], + tooltip="Target output resolution. For 2K/4K the native Gemini upscaler is used.", + ), + IO.Combo.Input( + "thinking_level", + options=["MINIMAL", "HIGH"], + ), + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, 15)], + min=0, + ), + tooltip="Optional reference image(s). Up to 14 images total.", + ), + IO.Custom("GEMINI_INPUT_FILES").Input( + "files", + optional=True, + tooltip="Optional file(s) to use as context for the model. " + "Accepts inputs from the Gemini Generate Content Input Files node.", + ), + ] + + +class GeminiNanoBanana2V2(IO.ComfyNode): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="GeminiNanoBanana2V2", + display_name="Nano Banana 2", + category="api node/image/Gemini", + description="Generate or edit images synchronously via Google Vertex API.", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + tooltip="Text prompt describing the image to generate or the edits to apply. " + "Include any constraints, styles, or details the model should follow.", + default="", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option( + "Nano Banana 2 (Gemini 3.1 Flash Image)", + _nano_banana_2_v2_model_inputs(), + ), + ], + ), + IO.Int.Input( + "seed", + default=42, + min=0, + max=0xFFFFFFFFFFFFFFFF, + control_after_generate=True, + tooltip="When the seed is fixed to a specific value, the model makes a best effort to provide " + "the same response for repeated requests. Deterministic output isn't guaranteed. " + "Also, changing the model or parameter settings, such as the temperature, " + "can cause variations in the response even when you use the same seed value. " + "By default, a random seed value is used.", + ), + IO.Combo.Input( + "response_modalities", + options=["IMAGE", "IMAGE+TEXT"], + advanced=True, + ), + IO.String.Input( + "system_prompt", + multiline=True, + default=GEMINI_IMAGE_SYS_PROMPT, + optional=True, + tooltip="Foundational instructions that dictate an AI's behavior.", + advanced=True, + ), + ], + outputs=[ + IO.Image.Output(), + IO.String.Output(), + IO.Image.Output( + display_name="thought_image", + tooltip="First image from the model's thinking process. " + "Only available with thinking_level HIGH and IMAGE+TEXT modality.", + ), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["model", "model.resolution"]), + expr=""" + ( + $r := $lookup(widgets, "model.resolution"); + $prices := {"1k": 0.0696, "2k": 0.1014, "4k": 0.154}; + {"type":"usd","usd": $lookup($prices, $r), "format":{"suffix":"/Image","approximate":true}} + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int, + response_modalities: str, + system_prompt: str = "", + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=True, min_length=1) + model_choice = model["model"] + if model_choice == "Nano Banana 2 (Gemini 3.1 Flash Image)": + model_id = "gemini-3.1-flash-image-preview" + else: + model_id = model_choice + + images = model.get("images") or {} + parts: list[GeminiPart] = [GeminiPart(text=prompt)] + if images: + image_tensors: list[Input.Image] = [t for t in images.values() if t is not None] + if image_tensors: + if sum(get_number_of_images(t) for t in image_tensors) > 14: + raise ValueError("The current maximum number of supported images is 14.") + parts.extend(await create_image_parts(cls, image_tensors)) + files = model.get("files") + if files is not None: + parts.extend(files) + + image_config = GeminiImageConfig(imageSize=model["resolution"]) + if model["aspect_ratio"] != "auto": + image_config.aspectRatio = model["aspect_ratio"] + + gemini_system_prompt = None + if system_prompt: + gemini_system_prompt = GeminiSystemInstructionContent(parts=[GeminiTextPart(text=system_prompt)], role=None) + + response = await sync_op( + cls, + ApiEndpoint(path=f"/proxy/vertexai/gemini/{model_id}", method="POST"), + data=GeminiImageGenerateContentRequest( + contents=[ + GeminiContent(role=GeminiRole.user, parts=parts), + ], + generationConfig=GeminiImageGenerationConfig( + responseModalities=(["IMAGE"] if response_modalities == "IMAGE" else ["TEXT", "IMAGE"]), + imageConfig=image_config, + thinkingConfig=GeminiThinkingConfig(thinkingLevel=model["thinking_level"]), + ), + systemInstruction=gemini_system_prompt, + ), + response_model=GeminiGenerateContentResponse, + price_extractor=calculate_tokens_price, + ) + return IO.NodeOutput( + await get_image_from_response(response), + get_text_from_response(response), + await get_image_from_response(response, thought=True), + ) + + class GeminiExtension(ComfyExtension): @override async def get_node_list(self) -> list[type[IO.ComfyNode]]: @@ -1024,6 +1225,7 @@ class GeminiExtension(ComfyExtension): GeminiImage, GeminiImage2, GeminiNanoBanana2, + GeminiNanoBanana2V2, GeminiInputFiles, ] diff --git a/comfy_api_nodes/nodes_grok.py b/comfy_api_nodes/nodes_grok.py index f42d84616..a103f24ee 100644 --- a/comfy_api_nodes/nodes_grok.py +++ b/comfy_api_nodes/nodes_grok.py @@ -54,7 +54,12 @@ class GrokImageNode(IO.ComfyNode): inputs=[ IO.Combo.Input( "model", - options=["grok-imagine-image-pro", "grok-imagine-image", "grok-imagine-image-beta"], + options=[ + "grok-imagine-image-quality", + "grok-imagine-image-pro", + "grok-imagine-image", + "grok-imagine-image-beta", + ], ), IO.String.Input( "prompt", @@ -111,10 +116,12 @@ class GrokImageNode(IO.ComfyNode): ], is_api_node=True, price_badge=IO.PriceBadge( - depends_on=IO.PriceBadgeDepends(widgets=["model", "number_of_images"]), + depends_on=IO.PriceBadgeDepends(widgets=["model", "number_of_images", "resolution"]), expr=""" ( - $rate := $contains(widgets.model, "pro") ? 0.07 : 0.02; + $rate := widgets.model = "grok-imagine-image-quality" + ? (widgets.resolution = "1k" ? 0.05 : 0.07) + : ($contains(widgets.model, "pro") ? 0.07 : 0.02); {"type":"usd","usd": $rate * widgets.number_of_images} ) """, @@ -155,6 +162,61 @@ class GrokImageNode(IO.ComfyNode): ) +_GROK_IMAGE_EDIT_ASPECT_RATIO_OPTIONS = [ + "auto", + "1:1", + "2:3", + "3:2", + "3:4", + "4:3", + "9:16", + "16:9", + "9:19.5", + "19.5:9", + "9:20", + "20:9", + "1:2", + "2:1", +] + + +def _grok_image_edit_model_inputs(*, max_ref_images: int, with_aspect_ratio: bool): + inputs = [ + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, max_ref_images + 1)], + min=1, + ), + tooltip=( + "Reference image to edit." + if max_ref_images == 1 + else f"Reference image(s) to edit. Up to {max_ref_images} images." + ), + ), + IO.Combo.Input("resolution", options=["1K", "2K"]), + IO.Int.Input( + "number_of_images", + default=1, + min=1, + max=10, + step=1, + tooltip="Number of edited images to generate", + display_mode=IO.NumberDisplay.number, + ), + ] + if with_aspect_ratio: + inputs.append( + IO.Combo.Input( + "aspect_ratio", + options=_GROK_IMAGE_EDIT_ASPECT_RATIO_OPTIONS, + tooltip="Only allowed when multiple images are connected.", + ) + ) + return inputs + + class GrokImageEditNode(IO.ComfyNode): @classmethod @@ -167,7 +229,12 @@ class GrokImageEditNode(IO.ComfyNode): inputs=[ IO.Combo.Input( "model", - options=["grok-imagine-image-pro", "grok-imagine-image", "grok-imagine-image-beta"], + options=[ + "grok-imagine-image-quality", + "grok-imagine-image-pro", + "grok-imagine-image", + "grok-imagine-image-beta", + ], ), IO.Image.Input("image", display_name="images"), IO.String.Input( @@ -228,14 +295,23 @@ class GrokImageEditNode(IO.ComfyNode): ], is_api_node=True, price_badge=IO.PriceBadge( - depends_on=IO.PriceBadgeDepends(widgets=["model", "number_of_images"]), + depends_on=IO.PriceBadgeDepends(widgets=["model", "number_of_images", "resolution"]), expr=""" ( - $rate := $contains(widgets.model, "pro") ? 0.07 : 0.02; - {"type":"usd","usd": 0.002 + $rate * widgets.number_of_images} + $isQualityModel := widgets.model = "grok-imagine-image-quality"; + $isPro := $contains(widgets.model, "pro"); + $rate := $isQualityModel + ? (widgets.resolution = "1k" ? 0.05 : 0.07) + : ($isPro ? 0.07 : 0.02); + $base := $isQualityModel ? 0.01 : 0.002; + $output := $rate * widgets.number_of_images; + $isPro + ? {"type":"usd","usd": $base + $output} + : {"type":"range_usd","min_usd": $base + $output, "max_usd": 3 * $base + $output} ) """, ), + is_deprecated=True, ) @classmethod @@ -283,6 +359,143 @@ class GrokImageEditNode(IO.ComfyNode): ) +class GrokImageEditNodeV2(IO.ComfyNode): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="GrokImageEditNodeV2", + display_name="Grok Image Edit", + category="api node/image/Grok", + description="Modify an existing image based on a text prompt", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="The text prompt used to generate the image", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option( + "grok-imagine-image-quality", + _grok_image_edit_model_inputs(max_ref_images=3, with_aspect_ratio=True), + ), + IO.DynamicCombo.Option( + "grok-imagine-image-pro", + _grok_image_edit_model_inputs(max_ref_images=1, with_aspect_ratio=False), + ), + IO.DynamicCombo.Option( + "grok-imagine-image", + _grok_image_edit_model_inputs(max_ref_images=3, with_aspect_ratio=True), + ), + ], + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=2147483647, + step=1, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + tooltip="Seed to determine if node should re-run; " + "actual results are nondeterministic regardless of seed.", + ), + ], + outputs=[ + IO.Image.Output(), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends( + widgets=["model", "model.resolution", "model.number_of_images"], + ), + expr=""" + ( + $isQualityModel := widgets.model = "grok-imagine-image-quality"; + $isPro := $contains(widgets.model, "pro"); + $res := $lookup(widgets, "model.resolution"); + $n := $lookup(widgets, "model.number_of_images"); + $rate := $isQualityModel + ? ($res = "1k" ? 0.05 : 0.07) + : ($isPro ? 0.07 : 0.02); + $base := $isQualityModel ? 0.01 : 0.002; + $output := $rate * $n; + $isPro + ? {"type":"usd","usd": $base + $output} + : {"type":"range_usd","min_usd": $base + $output, "max_usd": 3 * $base + $output} + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int, + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=True, min_length=1) + model_id = model["model"] + resolution = model["resolution"] + number_of_images = model["number_of_images"] + images_dict = model.get("images") or {} + aspect_ratio = model.get("aspect_ratio", "auto") + + image_tensors: list[Input.Image] = [t for t in images_dict.values() if t is not None] + n_images = sum(get_number_of_images(t) for t in image_tensors) + if n_images < 1: + raise ValueError("At least one image is required for editing.") + if model_id == "grok-imagine-image-pro" and n_images > 1: + raise ValueError("The pro model supports only 1 input image.") + if model_id != "grok-imagine-image-pro" and n_images > 3: + raise ValueError("A maximum of 3 input images is supported.") + if aspect_ratio != "auto" and n_images == 1: + raise ValueError( + "Custom aspect ratio is only allowed when multiple images are connected to the image input." + ) + + flat_tensors: list[torch.Tensor] = [] + for tensor in image_tensors: + if len(tensor.shape) == 4: + flat_tensors.extend(tensor[i] for i in range(tensor.shape[0])) + else: + flat_tensors.append(tensor) + + response = await sync_op( + cls, + ApiEndpoint(path="/proxy/xai/v1/images/edits", method="POST"), + data=ImageEditRequest( + model=model_id, + images=[ + InputUrlObject(url=f"data:image/png;base64,{tensor_to_base64_string(i)}") for i in flat_tensors + ], + prompt=prompt, + resolution=resolution.lower(), + n=number_of_images, + seed=seed, + aspect_ratio=None if aspect_ratio == "auto" else aspect_ratio, + ), + response_model=ImageGenerationResponse, + price_extractor=_extract_grok_price, + ) + if len(response.data) == 1: + return IO.NodeOutput(await download_url_to_image_tensor(response.data[0].url)) + return IO.NodeOutput( + torch.cat( + [await download_url_to_image_tensor(i) for i in [str(d.url) for d in response.data if d.url]], + ) + ) + + class GrokVideoNode(IO.ComfyNode): @classmethod @@ -717,6 +930,7 @@ class GrokExtension(ComfyExtension): return [ GrokImageNode, GrokImageEditNode, + GrokImageEditNodeV2, GrokVideoNode, GrokVideoReferenceNode, GrokVideoEditNode, diff --git a/comfy_api_nodes/nodes_kling.py b/comfy_api_nodes/nodes_kling.py index efd58fac3..7586f1816 100644 --- a/comfy_api_nodes/nodes_kling.py +++ b/comfy_api_nodes/nodes_kling.py @@ -2787,11 +2787,15 @@ class MotionControl(IO.ComfyNode): ], is_api_node=True, price_badge=IO.PriceBadge( - depends_on=IO.PriceBadgeDepends(widgets=["mode"]), + depends_on=IO.PriceBadgeDepends(widgets=["mode", "model"]), expr=""" ( - $prices := {"std": 0.07, "pro": 0.112}; - {"type":"usd","usd": $lookup($prices, widgets.mode), "format":{"suffix":"/second"}} + $prices := { + "kling-v3": {"std": 0.126, "pro": 0.168}, + "kling-v2-6": {"std": 0.07, "pro": 0.112} + }; + $modelPrices := $lookup($prices, widgets.model); + {"type":"usd","usd": $lookup($modelPrices, widgets.mode), "format":{"suffix":"/second"}} ) """, ), diff --git a/comfy_api_nodes/nodes_openai.py b/comfy_api_nodes/nodes_openai.py index daed495da..a5a188634 100644 --- a/comfy_api_nodes/nodes_openai.py +++ b/comfy_api_nodes/nodes_openai.py @@ -27,6 +27,7 @@ from comfy_api_nodes.util import ( ApiEndpoint, download_url_to_bytesio, downscale_image_tensor, + get_number_of_images, poll_op, sync_op, tensor_to_base64_string, @@ -372,6 +373,7 @@ class OpenAIGPTImage1(IO.ComfyNode): display_name="OpenAI GPT Image 2", category="api node/image/OpenAI", description="Generates images synchronously via OpenAI's GPT Image endpoint.", + is_deprecated=True, inputs=[ IO.String.Input( "prompt", @@ -640,6 +642,316 @@ class OpenAIGPTImage1(IO.ComfyNode): return IO.NodeOutput(await validate_and_cast_response(response)) +def _gpt_image_shared_inputs(): + """Inputs shared by all GPT Image models (quality + reference images + mask).""" + return [ + IO.Combo.Input( + "quality", + default="low", + options=["low", "medium", "high"], + tooltip="Image quality, affects cost and generation time.", + ), + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, 17)], + min=0, + ), + tooltip="Optional reference image(s) for image editing. Up to 16 images.", + ), + IO.Mask.Input( + "mask", + optional=True, + tooltip="Optional mask for inpainting (white areas will be replaced). " + "Requires exactly one reference image.", + ), + ] + + +def _gpt_image_legacy_model_inputs(): + """Per-model widget set for legacy gpt-image-1 / gpt-image-1.5 (4 base sizes, transparent bg allowed).""" + return [ + IO.Combo.Input( + "size", + default="auto", + options=["auto", "1024x1024", "1024x1536", "1536x1024"], + tooltip="Image size.", + ), + IO.Combo.Input( + "background", + default="auto", + options=["auto", "opaque", "transparent"], + tooltip="Return image with or without background.", + ), + *_gpt_image_shared_inputs(), + ] + + +class OpenAIGPTImageNodeV2(IO.ComfyNode): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="OpenAIGPTImageNodeV2", + display_name="OpenAI GPT Image 2", + category="api node/image/OpenAI", + description="Generates images via OpenAI's GPT Image endpoint.", + inputs=[ + IO.String.Input( + "prompt", + default="", + multiline=True, + tooltip="Text prompt for GPT Image", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option( + "gpt-image-2", + [ + IO.Combo.Input( + "size", + default="auto", + options=[ + "auto", + "1024x1024", + "1024x1536", + "1536x1024", + "2048x2048", + "2048x1152", + "1152x2048", + "3840x2160", + "2160x3840", + "Custom", + ], + tooltip="Image size. Select 'Custom' to use the custom width and height.", + ), + IO.Int.Input( + "custom_width", + default=1024, + min=1024, + max=3840, + step=16, + tooltip="Used only when `size` is 'Custom'. Must be a multiple of 16.", + ), + IO.Int.Input( + "custom_height", + default=1024, + min=1024, + max=3840, + step=16, + tooltip="Used only when `size` is 'Custom'. Must be a multiple of 16.", + ), + IO.Combo.Input( + "background", + default="auto", + options=["auto", "opaque"], + tooltip="Return image with or without background.", + ), + *_gpt_image_shared_inputs(), + ], + ), + IO.DynamicCombo.Option("gpt-image-1.5", _gpt_image_legacy_model_inputs()), + IO.DynamicCombo.Option("gpt-image-1", _gpt_image_legacy_model_inputs()), + ], + ), + IO.Int.Input( + "n", + default=1, + min=1, + max=8, + step=1, + tooltip="How many images to generate", + display_mode=IO.NumberDisplay.number, + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=2147483647, + step=1, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + tooltip="not implemented yet in backend", + ), + ], + outputs=[IO.Image.Output()], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["model", "model.quality", "n"]), + expr=""" + ( + $ranges := { + "gpt-image-1": { + "low": [0.011, 0.02], + "medium": [0.042, 0.07], + "high": [0.167, 0.25] + }, + "gpt-image-1.5": { + "low": [0.009, 0.02], + "medium": [0.034, 0.062], + "high": [0.133, 0.22] + }, + "gpt-image-2": { + "low": [0.0048, 0.019], + "medium": [0.041, 0.168], + "high": [0.165, 0.67] + } + }; + $range := $lookup($lookup($ranges, widgets.model), $lookup(widgets, "model.quality")); + $nRaw := widgets.n; + $n := ($nRaw != null and $nRaw != 0) ? $nRaw : 1; + ($n = 1) + ? {"type":"range_usd","min_usd": $range[0], "max_usd": $range[1], "format": {"approximate": true}} + : { + "type":"range_usd", + "min_usd": $range[0] * $n, + "max_usd": $range[1] * $n, + "format": { "suffix": "/Run", "approximate": true } + } + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + n: int, + seed: int, + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=False) + + model_id = model["model"] + size = model["size"] + background = model["background"] + quality = model["quality"] + custom_width = model.get("custom_width", 1024) + custom_height = model.get("custom_height", 1024) + + images_dict = model.get("images") or {} + image_tensors: list[Input.Image] = [t for t in images_dict.values() if t is not None] + n_images = sum(get_number_of_images(t) for t in image_tensors) + mask = model.get("mask") + + if mask is not None and n_images == 0: + raise ValueError("Cannot use a mask without an input image") + + if size == "Custom": + if custom_width % 16 != 0 or custom_height % 16 != 0: + raise ValueError( + f"Custom width and height must be multiples of 16, got {custom_width}x{custom_height}" + ) + if max(custom_width, custom_height) > 3840: + raise ValueError( + f"Custom resolution max edge must be <= 3840, got {custom_width}x{custom_height}" + ) + ratio = max(custom_width, custom_height) / min(custom_width, custom_height) + if ratio > 3: + raise ValueError( + f"Custom resolution aspect ratio must not exceed 3:1, got {custom_width}x{custom_height}" + ) + total_pixels = custom_width * custom_height + if not 655_360 <= total_pixels <= 8_294_400: + raise ValueError( + f"Custom resolution total pixels must be between 655,360 and 8,294,400, got {total_pixels}" + ) + size = f"{custom_width}x{custom_height}" + + if model_id == "gpt-image-1": + price_extractor = calculate_tokens_price_image_1 + elif model_id == "gpt-image-1.5": + price_extractor = calculate_tokens_price_image_1_5 + elif model_id == "gpt-image-2": + price_extractor = calculate_tokens_price_image_2_0 + else: + raise ValueError(f"Unknown model: {model_id}") + + if image_tensors: + flat: list[torch.Tensor] = [] + for tensor in image_tensors: + if len(tensor.shape) == 4: + flat.extend(tensor[i : i + 1] for i in range(tensor.shape[0])) + else: + flat.append(tensor.unsqueeze(0)) + + files = [] + for i, single_image in enumerate(flat): + scaled_image = downscale_image_tensor(single_image, total_pixels=2048 * 2048).squeeze() + image_np = (scaled_image.numpy() * 255).astype(np.uint8) + img = Image.fromarray(image_np) + img_byte_arr = BytesIO() + img.save(img_byte_arr, format="PNG") + img_byte_arr.seek(0) + + if len(flat) == 1: + files.append(("image", (f"image_{i}.png", img_byte_arr, "image/png"))) + else: + files.append(("image[]", (f"image_{i}.png", img_byte_arr, "image/png"))) + + if mask is not None: + if len(flat) != 1: + raise Exception("Cannot use a mask with multiple image") + ref_image = flat[0] + if mask.shape[1:] != ref_image.shape[1:-1]: + raise Exception("Mask and Image must be the same size") + _, height, width = mask.shape + rgba_mask = torch.zeros(height, width, 4, device="cpu") + rgba_mask[:, :, 3] = 1 - mask.squeeze().cpu() + scaled_mask = downscale_image_tensor( + rgba_mask.unsqueeze(0), total_pixels=2048 * 2048 + ).squeeze() + mask_np = (scaled_mask.numpy() * 255).astype(np.uint8) + mask_img = Image.fromarray(mask_np) + mask_img_byte_arr = BytesIO() + mask_img.save(mask_img_byte_arr, format="PNG") + mask_img_byte_arr.seek(0) + files.append(("mask", ("mask.png", mask_img_byte_arr, "image/png"))) + + response = await sync_op( + cls, + ApiEndpoint(path="/proxy/openai/images/edits", method="POST"), + response_model=OpenAIImageGenerationResponse, + data=OpenAIImageEditRequest( + model=model_id, + prompt=prompt, + quality=quality, + background=background, + n=n, + size=size, + moderation="low", + ), + content_type="multipart/form-data", + files=files, + price_extractor=price_extractor, + ) + else: + response = await sync_op( + cls, + ApiEndpoint(path="/proxy/openai/images/generations", method="POST"), + response_model=OpenAIImageGenerationResponse, + data=OpenAIImageGenerationRequest( + model=model_id, + prompt=prompt, + quality=quality, + background=background, + n=n, + size=size, + moderation="low", + ), + price_extractor=price_extractor, + ) + return IO.NodeOutput(await validate_and_cast_response(response)) + + class OpenAIChatNode(IO.ComfyNode): """ Node to generate text responses from an OpenAI model. @@ -999,6 +1311,7 @@ class OpenAIExtension(ComfyExtension): OpenAIDalle2, OpenAIDalle3, OpenAIGPTImage1, + OpenAIGPTImageNodeV2, OpenAIChatNode, OpenAIInputFiles, OpenAIChatConfig, diff --git a/comfy_api_nodes/nodes_openrouter.py b/comfy_api_nodes/nodes_openrouter.py new file mode 100644 index 000000000..031301870 --- /dev/null +++ b/comfy_api_nodes/nodes_openrouter.py @@ -0,0 +1,374 @@ +"""API Nodes for OpenRouter LLM chat completions.""" + +from dataclasses import dataclass +from typing import Literal + +from typing_extensions import override + +from comfy_api.latest import IO, ComfyExtension, Input +from comfy_api_nodes.apis.openrouter import ( + OpenRouterChatRequest, + OpenRouterChatResponse, + OpenRouterContentBlock, + OpenRouterImageContent, + OpenRouterImageUrl, + OpenRouterMessage, + OpenRouterReasoningConfig, + OpenRouterTextContent, + OpenRouterVideoContent, + OpenRouterVideoUrl, + OpenRouterWebSearchOptions, +) +from comfy_api_nodes.util import ( + ApiEndpoint, + get_number_of_images, + sync_op, + upload_images_to_comfyapi, + upload_video_to_comfyapi, + validate_string, +) + +OPENROUTER_CHAT_ENDPOINT = "/proxy/openrouter/api/v1/chat/completions" + + +Profile = Literal["standard", "reasoning", "frontier_reasoning", "perplexity", "perplexity_reasoning"] + + +@dataclass(frozen=True) +class _ModelSpec: + slug: str # exact OpenRouter model id + profile: Profile + price_in: float # USD per token (prompt) + price_out: float # USD per token (completion) + max_images: int = 0 # 0 = no image input; otherwise max URL-passed images supported + max_videos: int = 0 # 0 = no video input; otherwise max URL-passed videos supported + + +MODELS: list[_ModelSpec] = [ + _ModelSpec("anthropic/claude-opus-4.7", "frontier_reasoning", 0.000005, 0.000025, max_images=20), + _ModelSpec("openai/gpt-5.5-pro", "frontier_reasoning", 0.00003, 0.00018, max_images=20), + _ModelSpec("openai/gpt-5.5", "frontier_reasoning", 0.000005, 0.00003, max_images=20), + _ModelSpec("google/gemini-3.5-flash", "reasoning", 0.0000015, 0.000009, max_images=20, max_videos=4), + _ModelSpec("x-ai/grok-4.20", "reasoning", 0.00000125, 0.0000025, max_images=20), + _ModelSpec("x-ai/grok-4.3", "reasoning", 0.00000125, 0.0000025, max_images=20), + _ModelSpec("deepseek/deepseek-v4-pro", "reasoning", 0.000000435, 0.00000087), + _ModelSpec("deepseek/deepseek-v4-flash", "reasoning", 0.000000112, 0.000000224), + _ModelSpec("deepseek/deepseek-v3.2", "reasoning", 0.000000252, 0.000000378), + _ModelSpec("qwen/qwen3.6-max-preview", "reasoning", 0.00000104, 0.00000624), + _ModelSpec("qwen/qwen3.6-plus", "reasoning", 0.000000325, 0.00000195, max_images=10, max_videos=4), + _ModelSpec("qwen/qwen3.6-flash", "reasoning", 0.0000001875, 0.000001125, max_images=10, max_videos=4), + _ModelSpec("mistralai/mistral-large-2512", "standard", 0.0000005, 0.0000015, max_images=8), + _ModelSpec("mistralai/mistral-medium-3-5", "reasoning", 0.0000015, 0.0000075, max_images=8), + _ModelSpec("z-ai/glm-4.6", "reasoning", 0.00000043, 0.00000174), + _ModelSpec("z-ai/glm-5", "reasoning", 0.0000006, 0.00000192), + _ModelSpec("moonshotai/kimi-k2.6", "reasoning", 0.00000073, 0.00000349, max_images=10), + _ModelSpec("moonshotai/kimi-k2-thinking", "reasoning", 0.0000006, 0.0000025), + _ModelSpec("perplexity/sonar-pro", "perplexity", 0.000003, 0.000015), + _ModelSpec("perplexity/sonar-reasoning-pro", "perplexity_reasoning", 0.000002, 0.000008), + _ModelSpec("perplexity/sonar-deep-research", "perplexity_reasoning", 0.000002, 0.000008), +] + +_MODELS_BY_SLUG: dict[str, _ModelSpec] = {m.slug: m for m in MODELS} +_REASONING_EFFORTS = ["off", "low", "medium", "high"] +_SEARCH_CONTEXT_SIZES = ["low", "medium", "high"] + + +def _reasoning_extra_inputs() -> list: + return [ + IO.Combo.Input( + "reasoning_effort", + options=_REASONING_EFFORTS, + default="off", + tooltip="Reasoning effort. 'off' disables reasoning entirely.", + advanced=True, + ), + ] + + +def _perplexity_extra_inputs() -> list: + return [ + IO.Combo.Input( + "search_context_size", + options=_SEARCH_CONTEXT_SIZES, + default="medium", + tooltip="How much web search context to retrieve. Larger = more grounded but slower/pricier.", + advanced=True, + ), + ] + + +def _profile_inputs(profile: Profile) -> list: + if profile == "standard": + return [] + if profile in ("reasoning", "frontier_reasoning"): + return _reasoning_extra_inputs() + if profile == "perplexity": + return _perplexity_extra_inputs() + if profile == "perplexity_reasoning": + return _perplexity_extra_inputs() + _reasoning_extra_inputs() + raise ValueError(f"Unknown profile: {profile}") + + +def _media_inputs(spec: _ModelSpec) -> list: + extras: list = [] + if spec.max_images > 0: + extras.append( + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + names=[f"image_{i}" for i in range(1, spec.max_images + 1)], + min=0, + ), + tooltip=f"Optional reference image(s) — up to {spec.max_images}. Sent as URLs.", + ) + ) + if spec.max_videos > 0: + extras.append( + IO.Autogrow.Input( + "videos", + template=IO.Autogrow.TemplateNames( + IO.Video.Input("video"), + names=[f"video_{i}" for i in range(1, spec.max_videos + 1)], + min=0, + ), + tooltip=f"Optional reference video(s) — up to {spec.max_videos}. Sent as URLs.", + ) + ) + return extras + + +def _inputs_for_model(spec: _ModelSpec) -> list: + return _profile_inputs(spec.profile) + _media_inputs(spec) + + +def _build_model_options() -> list[IO.DynamicCombo.Option]: + return [IO.DynamicCombo.Option(spec.slug, _inputs_for_model(spec)) for spec in MODELS] + + +def _calculate_price(response: OpenRouterChatResponse) -> float | None: + if response.usage and response.usage.cost is not None: + return float(response.usage.cost) + return None + + +def _price_badge_jsonata() -> str: + rates_pairs = [] + for spec in MODELS: + prompt_per_1k = spec.price_in * 1000 + completion_per_1k = spec.price_out * 1000 + rates_pairs.append(f' "{spec.slug}": [{prompt_per_1k:.8g}, {completion_per_1k:.8g}]') + rates_block = ",\n".join(rates_pairs) + return ( + "(\n" + " $rates := {\n" + f"{rates_block}\n" + " };\n" + " $r := $lookup($rates, widgets.model);\n" + " $r ? {\n" + ' "type": "list_usd",\n' + ' "usd": $r,\n' + ' "format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" }\n' + ' } : {"type": "text", "text": "Token-based"}\n' + ")" + ) + + +async def _build_image_blocks( + cls: type[IO.ComfyNode], spec: _ModelSpec, images: list[Input.Image] +) -> list[OpenRouterImageContent]: + urls = await upload_images_to_comfyapi( + cls, + images, + max_images=spec.max_images, + total_pixels=2048 * 2048, + mime_type="image/png", + wait_label="Uploading reference images", + ) + return [OpenRouterImageContent(image_url=OpenRouterImageUrl(url=url)) for url in urls] + + +async def _build_video_blocks(cls: type[IO.ComfyNode], videos: list[Input.Video]) -> list[OpenRouterVideoContent]: + blocks: list[OpenRouterVideoContent] = [] + total = len(videos) + for idx, video in enumerate(videos): + label = "Uploading reference video" + if total > 1: + label = f"{label} ({idx + 1}/{total})" + url = await upload_video_to_comfyapi(cls, video, wait_label=label) + blocks.append(OpenRouterVideoContent(video_url=OpenRouterVideoUrl(url=url))) + return blocks + + +def _user_message(prompt: str, media_blocks: list[OpenRouterContentBlock]) -> OpenRouterMessage: + if not media_blocks: + return OpenRouterMessage(role="user", content=prompt) + blocks: list[OpenRouterContentBlock] = list(media_blocks) + blocks.append(OpenRouterTextContent(text=prompt)) + return OpenRouterMessage(role="user", content=blocks) + + +def _build_messages( + system_prompt: str, prompt: str, media_blocks: list[OpenRouterContentBlock] +) -> list[OpenRouterMessage]: + messages: list[OpenRouterMessage] = [] + if system_prompt: + messages.append(OpenRouterMessage(role="system", content=system_prompt)) + messages.append(_user_message(prompt, media_blocks)) + return messages + + +def _build_request( + slug: str, + system_prompt: str, + prompt: str, + media_blocks: list[OpenRouterContentBlock], + *, + seed: int, + reasoning_effort: str | None, + search_context_size: str | None, +) -> OpenRouterChatRequest: + reasoning_cfg: OpenRouterReasoningConfig | None = None + if reasoning_effort and reasoning_effort != "off": + # exclude=True asks providers to reason internally but not return the trace + reasoning_cfg = OpenRouterReasoningConfig(effort=reasoning_effort, exclude=True) + web_search_cfg: OpenRouterWebSearchOptions | None = None + if search_context_size: + web_search_cfg = OpenRouterWebSearchOptions(search_context_size=search_context_size) + return OpenRouterChatRequest( + model=slug, + messages=_build_messages(system_prompt, prompt, media_blocks), + seed=seed if seed > 0 else None, + reasoning=reasoning_cfg, + web_search_options=web_search_cfg, + ) + + +def _extract_text(response: OpenRouterChatResponse) -> str: + if response.error: + code = response.error.code if response.error.code is not None else "unknown" + raise ValueError(f"OpenRouter error ({code}): {response.error.message or 'no message'}") + if not response.choices: + raise ValueError("Empty response from OpenRouter (no choices).") + message = response.choices[0].message + if not message: + raise ValueError("Empty response from OpenRouter (no message).") + if message.refusal: + raise ValueError(f"Model refused to respond: {message.refusal}") + return message.content or "" + + +class OpenRouterLLMNode(IO.ComfyNode): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="OpenRouterLLMNode", + display_name="OpenRouter LLM", + category="api node/text/OpenRouter", + essentials_category="Text Generation", + description=( + "Generate text responses through OpenRouter. Routes to a curated set of popular " + "models from xAI, DeepSeek, Qwen, Mistral, Z.AI (GLM), Moonshot (Kimi), and " + "Perplexity Sonar." + ), + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Text input to the model.", + ), + IO.DynamicCombo.Input( + "model", + options=_build_model_options(), + tooltip="The OpenRouter model used to generate the response.", + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=2147483647, + control_after_generate=True, + tooltip="Seed for sampling. Set to 0 to omit. Most models treat this as a hint only.", + ), + IO.String.Input( + "system_prompt", + multiline=True, + default="", + optional=True, + advanced=True, + tooltip="Foundational instructions that dictate the model's behavior.", + ), + ], + outputs=[IO.String.Output()], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["model"]), + expr=_price_badge_jsonata(), + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int, + system_prompt: str = "", + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=True, min_length=1) + slug: str = model["model"] + spec = _MODELS_BY_SLUG.get(slug) + if spec is None: + raise ValueError(f"Unknown OpenRouter model: {slug}") + + reasoning_effort: str | None = model.get("reasoning_effort") + search_context_size: str | None = model.get("search_context_size") + + image_tensors: list[Input.Image] = [t for t in (model.get("images") or {}).values() if t is not None] + if image_tensors and sum(get_number_of_images(t) for t in image_tensors) > spec.max_images: + raise ValueError(f"Up to {spec.max_images} images are supported for {slug}.") + video_inputs: list[Input.Video] = [v for v in (model.get("videos") or {}).values() if v is not None] + if video_inputs and len(video_inputs) > spec.max_videos: + raise ValueError(f"Up to {spec.max_videos} videos are supported for {slug}.") + + media_blocks: list[OpenRouterContentBlock] = [] + if image_tensors: + media_blocks.extend(await _build_image_blocks(cls, spec, image_tensors)) + if video_inputs: + media_blocks.extend(await _build_video_blocks(cls, video_inputs)) + + request = _build_request( + slug, + system_prompt, + prompt, + media_blocks, + seed=seed, + reasoning_effort=reasoning_effort, + search_context_size=search_context_size, + ) + + response = await sync_op( + cls, + ApiEndpoint(path=OPENROUTER_CHAT_ENDPOINT, method="POST"), + response_model=OpenRouterChatResponse, + data=request, + price_extractor=_calculate_price, + ) + return IO.NodeOutput(_extract_text(response)) + + +class OpenRouterExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[IO.ComfyNode]]: + return [OpenRouterLLMNode] + + +async def comfy_entrypoint() -> OpenRouterExtension: + return OpenRouterExtension() diff --git a/comfy_api_nodes/nodes_quiver.py b/comfy_api_nodes/nodes_quiver.py index 28862e368..3269c0afe 100644 --- a/comfy_api_nodes/nodes_quiver.py +++ b/comfy_api_nodes/nodes_quiver.py @@ -143,7 +143,7 @@ class QuiverTextToSVGNode(IO.ComfyNode): if reference_images: references = [] for key in reference_images: - url = await upload_image_to_comfyapi(cls, reference_images[key]) + url = await upload_image_to_comfyapi(cls, reference_images[key], mime_type="image/png") references.append(QuiverImageObject(url=url)) if len(references) > 4: raise ValueError("Maximum 4 reference images are allowed.") @@ -252,7 +252,7 @@ class QuiverImageToSVGNode(IO.ComfyNode): model: dict, seed: int, ) -> IO.NodeOutput: - image_url = await upload_image_to_comfyapi(cls, image) + image_url = await upload_image_to_comfyapi(cls, image, mime_type="image/png") response = await sync_op( cls, diff --git a/comfy_api_nodes/nodes_rodin.py b/comfy_api_nodes/nodes_rodin.py index 2b829b8db..2df5a3e13 100644 --- a/comfy_api_nodes/nodes_rodin.py +++ b/comfy_api_nodes/nodes_rodin.py @@ -5,32 +5,37 @@ Rodin API docs: https://developer.hyper3d.ai/ """ -from inspect import cleandoc -import folder_paths as comfy_paths -import os import logging import math +import os +from inspect import cleandoc from io import BytesIO -from typing_extensions import override +from typing import Any + +import aiohttp from PIL import Image +from typing_extensions import override + +import folder_paths as comfy_paths +from comfy_api.latest import IO, ComfyExtension, Types from comfy_api_nodes.apis.rodin import ( - Rodin3DGenerateRequest, - Rodin3DGenerateResponse, + JobStatus, Rodin3DCheckStatusRequest, Rodin3DCheckStatusResponse, Rodin3DDownloadRequest, Rodin3DDownloadResponse, - JobStatus, + Rodin3DGen25Request, + Rodin3DGenerateRequest, + Rodin3DGenerateResponse, ) from comfy_api_nodes.util import ( - sync_op, - poll_op, ApiEndpoint, download_url_to_bytesio, download_url_to_file_3d, + poll_op, + sync_op, + validate_string, ) -from comfy_api.latest import ComfyExtension, IO, Types - COMMON_PARAMETERS = [ IO.Int.Input( @@ -51,40 +56,30 @@ COMMON_PARAMETERS = [ ] -def get_quality_mode(poly_count): - polycount = poly_count.split("-") - poly = polycount[1] - count = polycount[0] - if poly == "Triangle": - mesh_mode = "Raw" - elif poly == "Quad": - mesh_mode = "Quad" - else: - mesh_mode = "Quad" - - if count == "4K": - quality_override = 4000 - elif count == "8K": - quality_override = 8000 - elif count == "18K": - quality_override = 18000 - elif count == "50K": - quality_override = 50000 - elif count == "2K": - quality_override = 2000 - elif count == "20K": - quality_override = 20000 - elif count == "150K": - quality_override = 150000 - elif count == "500K": - quality_override = 500000 - else: - quality_override = 18000 - - return mesh_mode, quality_override +_QUALITY_MESH_OPTIONS: dict[str, tuple[str, int]] = { + "4K-Quad": ("Quad", 4000), + "8K-Quad": ("Quad", 8000), + "18K-Quad": ("Quad", 18000), + "50K-Quad": ("Quad", 50000), + "200K-Quad": ("Quad", 200000), + "2K-Triangle": ("Raw", 2000), + "20K-Triangle": ("Raw", 20000), + "150K-Triangle": ("Raw", 150000), + "200K-Triangle": ("Raw", 200000), + "500K-Triangle": ("Raw", 500000), + "1M-Triangle": ("Raw", 1000000), +} -def tensor_to_filelike(tensor, max_pixels: int = 2048*2048): +def get_quality_mode(poly_count: str) -> tuple[str, int]: + """Map a polygon-count preset like '18K-Quad' to (mesh_mode, quality_override). + + Falls back to ('Quad', 18000) for unknown labels; legacy parity. + """ + return _QUALITY_MESH_OPTIONS.get(poly_count, ("Quad", 18000)) + + +def tensor_to_filelike(tensor, max_pixels: int = 2048 * 2048): """ Converts a PyTorch tensor to a file-like object. @@ -96,8 +91,8 @@ def tensor_to_filelike(tensor, max_pixels: int = 2048*2048): - io.BytesIO: A file-like object containing the image data. """ array = tensor.cpu().numpy() - array = (array * 255).astype('uint8') - image = Image.fromarray(array, 'RGB') + array = (array * 255).astype("uint8") + image = Image.fromarray(array, "RGB") original_width, original_height = image.size original_pixels = original_width * original_height @@ -112,7 +107,7 @@ def tensor_to_filelike(tensor, max_pixels: int = 2048*2048): image = image.resize((new_width, new_height), Image.Resampling.LANCZOS) img_byte_arr = BytesIO() - image.save(img_byte_arr, format='PNG') # PNG is used for lossless compression + image.save(img_byte_arr, format="PNG") # PNG is used for lossless compression img_byte_arr.seek(0) return img_byte_arr @@ -145,11 +140,9 @@ async def create_generate_task( TAPose=ta_pose, ), files=[ - ( - "images", - open(image, "rb") if isinstance(image, str) else tensor_to_filelike(image) - ) - for image in images if image is not None + ("images", open(image, "rb") if isinstance(image, str) else tensor_to_filelike(image)) + for image in images + if image is not None ], content_type="multipart/form-data", ) @@ -177,6 +170,7 @@ def check_rodin_status(response: Rodin3DCheckStatusResponse) -> str: return "DONE" return "Generating" + def extract_progress(response: Rodin3DCheckStatusResponse) -> int | None: if not response.jobs: return None @@ -214,7 +208,7 @@ async def download_files(url_list, task_uuid: str) -> tuple[str | None, Types.Fi model_file_path = None file_3d = None - for i in url_list.list: + for i in url_list.items: file_path = os.path.join(save_path, i.name) if i.name.lower().endswith(".glb"): model_file_path = os.path.join(result_folder_name, i.name) @@ -489,7 +483,16 @@ class Rodin3D_Gen2(IO.ComfyNode): IO.Combo.Input("Material_Type", options=["PBR", "Shaded"], default="PBR", optional=True), IO.Combo.Input( "Polygon_count", - options=["4K-Quad", "8K-Quad", "18K-Quad", "50K-Quad", "2K-Triangle", "20K-Triangle", "150K-Triangle", "500K-Triangle"], + options=[ + "4K-Quad", + "8K-Quad", + "18K-Quad", + "50K-Quad", + "2K-Triangle", + "20K-Triangle", + "150K-Triangle", + "500K-Triangle", + ], default="500K-Triangle", optional=True, ), @@ -542,6 +545,566 @@ class Rodin3D_Gen2(IO.ComfyNode): return IO.NodeOutput(model_path, file_3d) +def _rodin_multipart_parser(data: dict[str, Any]) -> aiohttp.FormData: + """Convert a Rodin request dict to an aiohttp form, fixing bool/list serialization. + + Booleans --> "true"/"false". Lists --> one field per element. + """ + form = aiohttp.FormData(default_to_multipart=True) + for key, value in data.items(): + if value is None: + continue + if isinstance(value, bool): + form.add_field(key, "true" if value else "false") + elif isinstance(value, list): + for item in value: + form.add_field(key, str(item)) + elif isinstance(value, (bytes, bytearray)): + form.add_field(key, value) + else: + form.add_field(key, str(value)) + return form + + +async def _create_gen25_task( + cls: type[IO.ComfyNode], + request: Rodin3DGen25Request, + images: list | None, +) -> tuple[str, str]: + """Submit a Gen-2.5 generate job; returns (task_uuid, subscription_key).""" + + if images is not None and len(images) > 5: + raise ValueError("Rodin Gen-2.5 supports at most 5 input images.") + + files = None + if images: + files = [ + ( + "images", + open(image, "rb") if isinstance(image, str) else tensor_to_filelike(image), + ) + for image in images + if image is not None + ] + + response = await sync_op( + cls, + ApiEndpoint(path="/proxy/rodin/api/v2/rodin", method="POST"), + response_model=Rodin3DGenerateResponse, + data=request, + files=files, + content_type="multipart/form-data", + multipart_parser=_rodin_multipart_parser, + ) + + if not response.uuid or not response.jobs or not response.jobs.subscription_key: + raise RuntimeError(f"Rodin Gen-2.5 submit failed: message={response.message!r}") + return response.uuid, response.jobs.subscription_key + + +_PREVIEWABLE_3D_EXTS = {".glb", ".obj", ".fbx", ".stl", ".gltf"} + + +async def _download_gen25_files( + download_list: Rodin3DDownloadResponse, + task_uuid: str, + geometry_file_format: str, +) -> Types.File3D | None: + """Download every file in the list; return the File3D matching the chosen format.""" + + folder_name = f"Rodin3D_Gen25_{task_uuid}" + save_dir = os.path.join(comfy_paths.get_output_directory(), folder_name) + os.makedirs(save_dir, exist_ok=True) + + target_ext = f".{geometry_file_format.lower().lstrip('.')}" + file_3d: Types.File3D | None = None + + for item in download_list.items: + file_path = os.path.join(save_dir, item.name) + ext = os.path.splitext(item.name.lower())[1] + # Prefer the file matching the user's chosen format; fall back below. + if file_3d is None and ext == target_ext and ext in _PREVIEWABLE_3D_EXTS: + file_3d = await download_url_to_file_3d(item.url, target_ext.lstrip(".")) + with open(file_path, "wb") as f: + f.write(file_3d.get_bytes()) + continue + await download_url_to_bytesio(item.url, file_path) + + # If the chosen format wasn't found, surface any model file we did get. + if file_3d is None: + for item in download_list.items: + ext = os.path.splitext(item.name.lower())[1] + if ext in _PREVIEWABLE_3D_EXTS: + file_3d = await download_url_to_file_3d(item.url, ext.lstrip(".")) + break + return file_3d + + +_MODE_REGULAR = "Regular" +_MODE_FAST = "Fast" +_MODE_EXTREME_HIGH = "Extreme-High" + +_REGULAR_POLY_OPTIONS = [ + "Default", + "4K-Quad", + "8K-Quad", + "18K-Quad", + "50K-Quad", + "2K-Triangle", + "20K-Triangle", + "150K-Triangle", + "500K-Triangle", + "1M-Triangle", +] + +_TEXTURE_MODE_OPTIONS = ["Default", "legacy", "extreme-low", "low", "medium", "high"] +_GEOMETRY_FORMAT_OPTIONS = ["glb", "fbx", "obj", "stl"] +_MATERIAL_OPTIONS = ["PBR", "Shaded", "All", "None"] + + +def _build_mode_input(name: str = "mode") -> IO.DynamicCombo.Input: + return IO.DynamicCombo.Input( + name, + options=[ + IO.DynamicCombo.Option( + _MODE_REGULAR, + [ + IO.Combo.Input( + "tier", + options=["Gen-2.5-Low", "Gen-2.5-Medium", "Gen-2.5-High"], + default="Gen-2.5-High", + tooltip="Quality tier. Higher tiers produce higher-fidelity geometry.", + ), + IO.Combo.Input( + "polygon_count", + options=_REGULAR_POLY_OPTIONS, + default="Default", + tooltip="Preset face count. 'Default' uses the server's default for the selected tier.", + ), + IO.Boolean.Input( + "creative", + default=False, + tooltip="Creative mode (Medium/High only). Enhances generative robustness.", + ), + ], + ), + IO.DynamicCombo.Option( + _MODE_FAST, + [ + IO.Combo.Input( + "tier", + options=[ + "Gen-2.5-Extreme-Low", + "Gen-2.5-Low", + "Gen-2.5-Medium", + "Gen-2.5-High", + ], + default="Gen-2.5-Low", + ), + IO.Int.Input( + "mesh_faces", + default=20000, + min=1000, + max=20000, + display_mode=IO.NumberDisplay.number, + tooltip="Mesh face count (1K-20K in Fast mode).", + ), + ], + ), + IO.DynamicCombo.Option( + _MODE_EXTREME_HIGH, + [ + IO.Combo.Input("mesh_mode", options=["Raw", "Quad"], default="Raw"), + IO.Int.Input( + "mesh_faces", + default=1000000, + min=20000, + max=2000000, + display_mode=IO.NumberDisplay.number, + tooltip=( + "Mesh face count. Raw mode: 20K-2M. " + "Quad mode: keep under 200K (upstream may reject higher values)." + ), + ), + IO.Boolean.Input( + "is_micro", + default=False, + tooltip="Enable micro detail (Extreme-High only).", + ), + IO.Boolean.Input( + "creative", + default=False, + tooltip="Creative mode. Enhances generative robustness.", + ), + ], + ), + ], + tooltip=( + "Generation mode. Regular = balanced. Fast = 1K-20K faces for rapid prototyping. " + "Extreme-High = 20K-2M faces with optional micro details." + ), + ) + + +def _build_common_inputs(*, include_image_only: bool) -> list: + inputs: list = [ + IO.Combo.Input("material", options=_MATERIAL_OPTIONS, default="Shaded"), + IO.Combo.Input("geometry_file_format", options=_GEOMETRY_FORMAT_OPTIONS, default="glb"), + IO.Combo.Input( + "texture_mode", + options=_TEXTURE_MODE_OPTIONS, + default="Default", + optional=True, + tooltip="Texture quality preset. 'Default' uses the server's default for the selected tier.", + ), + IO.Int.Input( + "seed", + default=0, + min=0, + max=65535, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + optional=True, + ), + IO.Boolean.Input( + "TAPose", default=False, optional=True, advanced=True, tooltip="T/A pose for human-like models." + ), + IO.Boolean.Input( + "hd_texture", default=False, optional=True, advanced=True, tooltip="High-quality texture enhancement." + ), + IO.Boolean.Input( + "texture_delight", + default=False, + optional=True, + advanced=True, + tooltip="Remove baked lighting from textures.", + ), + ] + if include_image_only: + inputs.append( + IO.Boolean.Input( + "use_original_alpha", + default=False, + optional=True, + advanced=True, + tooltip="Preserve image transparency.", + ) + ) + inputs.extend( + [ + IO.Boolean.Input( + "addon_highpack", + default=False, + optional=True, + advanced=True, + tooltip="HighPack addon: 4K textures and ~16x faces in Quad mode.", + ), + IO.Int.Input( + "bbox_width", + default=0, + min=0, + max=300, + display_mode=IO.NumberDisplay.number, + optional=True, + advanced=True, + tooltip="Bounding-box width (Y axis). Set to 0 with the others to skip bbox.", + ), + IO.Int.Input( + "bbox_height", + default=0, + min=0, + max=300, + display_mode=IO.NumberDisplay.number, + optional=True, + advanced=True, + tooltip="Bounding-box height (Z axis).", + ), + IO.Int.Input( + "bbox_length", + default=0, + min=0, + max=300, + display_mode=IO.NumberDisplay.number, + optional=True, + advanced=True, + tooltip="Bounding-box length (X axis).", + ), + IO.Int.Input( + "height_cm", + default=0, + min=0, + max=10000, + display_mode=IO.NumberDisplay.number, + optional=True, + advanced=True, + tooltip="Approximate model height in centimeters (0 to skip).", + ), + ] + ) + return inputs + + +_PRICE_EXPR = """ +( + $baseCredits := widgets.mode = "extreme-high" ? 1.0 : 0.5; + $addonCredits := widgets.addon_highpack ? 1.0 : 0.0; + $total := ($baseCredits * 1.5) + ($addonCredits * 0.8); + {"type":"usd","usd": $total} +) +""" + + +def _resolve_mode_params(mode_input: dict) -> dict: + """Translate the DynamicCombo `mode` payload into Gen-2.5 request fields. + + Returns a dict with: tier, quality_override, mesh_mode, geometry_instruct_mode, is_micro. + Missing keys mean "do not send" (so we don't override server defaults). + """ + selected = mode_input["mode"] + out: dict = {} + + if selected == _MODE_REGULAR: + out["tier"] = mode_input["tier"] + polygon = mode_input.get("polygon_count", "Default") + if polygon != "Default": + mesh_mode, faces = get_quality_mode(polygon) + out["mesh_mode"] = mesh_mode + out["quality_override"] = faces + if mode_input.get("creative"): + out["geometry_instruct_mode"] = "creative" + + elif selected == _MODE_FAST: + out["tier"] = mode_input["tier"] + out["mesh_mode"] = "Raw" + out["quality_override"] = int(mode_input["mesh_faces"]) + + elif selected == _MODE_EXTREME_HIGH: + out["tier"] = "Gen-2.5-Extreme-High" + out["mesh_mode"] = mode_input["mesh_mode"] + out["quality_override"] = int(mode_input["mesh_faces"]) + if mode_input.get("is_micro"): + out["is_micro"] = True + if mode_input.get("creative"): + out["geometry_instruct_mode"] = "creative" + return out + + +def _build_request( + *, + mode_input: dict, + material: str, + geometry_file_format: str, + texture_mode: str, + seed: int, + TAPose: bool, + hd_texture: bool, + texture_delight: bool, + addon_highpack: bool, + bbox_width: int, + bbox_height: int, + bbox_length: int, + height_cm: int, + prompt: str | None = None, + use_original_alpha: bool = False, +) -> Rodin3DGen25Request: + mode_params = _resolve_mode_params(mode_input) + + bbox = None + if bbox_width and bbox_height and bbox_length: + bbox = [bbox_width, bbox_height, bbox_length] + + return Rodin3DGen25Request( + tier=mode_params["tier"], + prompt=prompt or None, + seed=seed, + material=material, + geometry_file_format=geometry_file_format, + texture_mode=None if texture_mode == "Default" else texture_mode, + mesh_mode=mode_params.get("mesh_mode"), + quality_override=mode_params.get("quality_override"), + geometry_instruct_mode=mode_params.get("geometry_instruct_mode"), + bbox_condition=bbox, + height=height_cm or None, + TAPose=TAPose or None, + hd_texture=hd_texture or None, + texture_delight=texture_delight or None, + is_micro=mode_params.get("is_micro"), + use_original_alpha=use_original_alpha or None, + addons=["HighPack"] if addon_highpack else None, + ) + + +class Rodin3D_Gen25_Image(IO.ComfyNode): + + @classmethod + def define_schema(cls) -> IO.Schema: + return IO.Schema( + node_id="Rodin3D_Gen25_Image", + display_name="Rodin 3D Gen-2.5 - Image to 3D", + category="api node/3d/Rodin", + description=( + "Generate a 3D model from 1-5 reference images via Rodin Gen-2.5. " + "Pick a mode (Fast / Regular / Extreme-High) to tune quality vs. cost." + ), + inputs=[ + IO.Autogrow.Input( + "images", + template=IO.Autogrow.TemplatePrefix(IO.Image.Input("image"), prefix="image", min=1, max=5), + tooltip="1-5 images. The first image is used for materials when multi-view.", + ), + _build_mode_input(), + *_build_common_inputs(include_image_only=True), + ], + outputs=[IO.File3DAny.Output(display_name="model_file")], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["mode", "addon_highpack"]), + expr=_PRICE_EXPR, + ), + ) + + @classmethod + async def execute( + cls, + images: IO.Autogrow.Type, + mode: dict, + material: str, + geometry_file_format: str, + texture_mode: str, + seed: int, + TAPose: bool, + hd_texture: bool, + texture_delight: bool, + use_original_alpha: bool, + addon_highpack: bool, + bbox_width: int, + bbox_height: int, + bbox_length: int, + height_cm: int, + ) -> IO.NodeOutput: + image_tensors = [img for img in images.values() if img is not None] + if not image_tensors: + raise ValueError("Rodin Gen-2.5 Image-to-3D requires at least one image.") + + # Flatten multi-image tensors into individual frames; the API accepts each as a separate part. + flat_images: list = [] + for tensor in image_tensors: + if hasattr(tensor, "shape") and len(tensor.shape) == 4: + for i in range(tensor.shape[0]): + flat_images.append(tensor[i]) + else: + flat_images.append(tensor) + + if len(flat_images) > 5: + raise ValueError(f"Rodin Gen-2.5 accepts at most 5 images; received {len(flat_images)}.") + + request = _build_request( + mode_input=mode, + material=material, + geometry_file_format=geometry_file_format, + texture_mode=texture_mode, + seed=seed, + TAPose=TAPose, + hd_texture=hd_texture, + texture_delight=texture_delight, + addon_highpack=addon_highpack, + bbox_width=bbox_width, + bbox_height=bbox_height, + bbox_length=bbox_length, + height_cm=height_cm, + prompt=None, + use_original_alpha=use_original_alpha, + ) + + task_uuid, subscription_key = await _create_gen25_task(cls, request, flat_images) + await poll_for_task_status(subscription_key, cls) + download_list = await get_rodin_download_list(task_uuid, cls) + file_3d = await _download_gen25_files(download_list, task_uuid, geometry_file_format) + return IO.NodeOutput(file_3d) + + +class Rodin3D_Gen25_Text(IO.ComfyNode): + + @classmethod + def define_schema(cls) -> IO.Schema: + return IO.Schema( + node_id="Rodin3D_Gen25_Text", + display_name="Rodin 3D Gen-2.5 - Text to 3D", + category="api node/3d/Rodin", + description=( + "Generate a 3D model from a text prompt via Rodin Gen-2.5. " + "Pick a mode (Fast / Regular / Extreme-High) to tune quality vs. cost." + ), + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Text prompt for the 3D model.", + ), + _build_mode_input(), + *_build_common_inputs(include_image_only=False), + ], + outputs=[IO.File3DAny.Output(display_name="model_file")], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends(widgets=["mode", "addon_highpack"]), + expr=_PRICE_EXPR, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + mode: dict, + material: str, + geometry_file_format: str, + texture_mode: str, + seed: int, + TAPose: bool, + hd_texture: bool, + texture_delight: bool, + addon_highpack: bool, + bbox_width: int, + bbox_height: int, + bbox_length: int, + height_cm: int, + ) -> IO.NodeOutput: + validate_string(prompt, field_name="prompt", min_length=1, max_length=2500) + request = _build_request( + mode_input=mode, + material=material, + geometry_file_format=geometry_file_format, + texture_mode=texture_mode, + seed=seed, + TAPose=TAPose, + hd_texture=hd_texture, + texture_delight=texture_delight, + addon_highpack=addon_highpack, + bbox_width=bbox_width, + bbox_height=bbox_height, + bbox_length=bbox_length, + height_cm=height_cm, + prompt=prompt, + ) + task_uuid, subscription_key = await _create_gen25_task(cls, request, images=None) + await poll_for_task_status(subscription_key, cls) + download_list = await get_rodin_download_list(task_uuid, cls) + file_3d = await _download_gen25_files(download_list, task_uuid, geometry_file_format) + return IO.NodeOutput(file_3d) + + class Rodin3DExtension(ComfyExtension): @override async def get_node_list(self) -> list[type[IO.ComfyNode]]: @@ -551,6 +1114,8 @@ class Rodin3DExtension(ComfyExtension): Rodin3D_Smooth, Rodin3D_Sketch, Rodin3D_Gen2, + Rodin3D_Gen25_Image, + Rodin3D_Gen25_Text, ] diff --git a/comfy_api_nodes/nodes_tripo.py b/comfy_api_nodes/nodes_tripo.py index 9f4298dce..d6501dee4 100644 --- a/comfy_api_nodes/nodes_tripo.py +++ b/comfy_api_nodes/nodes_tripo.py @@ -60,6 +60,7 @@ async def poll_until_finished( ], status_extractor=lambda x: x.data.status, progress_extractor=lambda x: x.data.progress, + price_extractor=lambda x: x.data.consumed_credit * 0.01 if x.data.consumed_credit else None, estimated_duration=average_duration, ) if response_poll.data.status == TripoTaskStatus.SUCCESS: @@ -113,7 +114,6 @@ class TripoTextToModelNode(IO.ComfyNode): depends_on=IO.PriceBadgeDepends( widgets=[ "model_version", - "style", "texture", "pbr", "quad", @@ -124,20 +124,17 @@ class TripoTextToModelNode(IO.ComfyNode): expr=""" ( $isV14 := $contains(widgets.model_version,"v1.4"); - $style := widgets.style; - $hasStyle := ($style != "" and $style != "none"); + $isV3OrLater := $contains(widgets.model_version,"v3."); $withTexture := widgets.texture or widgets.pbr; $isHdTexture := (widgets.texture_quality = "detailed"); $isDetailedGeometry := (widgets.geometry_quality = "detailed"); - $baseCredits := - $isV14 ? 20 : ($withTexture ? 20 : 10); - $credits := - $baseCredits - + ($hasStyle ? 5 : 0) + $credits := $isV14 ? 20 : ( + ($withTexture ? 20 : 10) + (widgets.quad ? 5 : 0) + ($isHdTexture ? 10 : 0) - + ($isDetailedGeometry ? 20 : 0); - {"type":"usd","usd": $round($credits * 0.01, 2)} + + (($isDetailedGeometry and $isV3OrLater) ? 20 : 0) + ); + {"type":"usd","usd": $round($credits * 0.01, 2), "format": {"approximate": true}} ) """, ), @@ -239,7 +236,6 @@ class TripoImageToModelNode(IO.ComfyNode): depends_on=IO.PriceBadgeDepends( widgets=[ "model_version", - "style", "texture", "pbr", "quad", @@ -250,20 +246,17 @@ class TripoImageToModelNode(IO.ComfyNode): expr=""" ( $isV14 := $contains(widgets.model_version,"v1.4"); - $style := widgets.style; - $hasStyle := ($style != "" and $style != "none"); + $isV3OrLater := $contains(widgets.model_version,"v3."); $withTexture := widgets.texture or widgets.pbr; $isHdTexture := (widgets.texture_quality = "detailed"); $isDetailedGeometry := (widgets.geometry_quality = "detailed"); - $baseCredits := - $isV14 ? 30 : ($withTexture ? 30 : 20); - $credits := - $baseCredits - + ($hasStyle ? 5 : 0) + $credits := $isV14 ? 30 : ( + ($withTexture ? 30 : 20) + (widgets.quad ? 5 : 0) + ($isHdTexture ? 10 : 0) - + ($isDetailedGeometry ? 20 : 0); - {"type":"usd","usd": $round($credits * 0.01, 2)} + + (($isDetailedGeometry and $isV3OrLater) ? 20 : 0) + ); + {"type":"usd","usd": $round($credits * 0.01, 2), "format": {"approximate": true}} ) """, ), @@ -358,7 +351,7 @@ class TripoMultiviewToModelNode(IO.ComfyNode): "texture_alignment", default="original_image", options=["original_image", "geometry"], optional=True, advanced=True ), IO.Int.Input("face_limit", default=-1, min=-1, max=500000, optional=True, advanced=True), - IO.Boolean.Input("quad", default=False, optional=True, advanced=True), + IO.Boolean.Input("quad", default=False, optional=True, advanced=True, tooltip="This parameter is deprecated and does nothing."), IO.Combo.Input("geometry_quality", default="standard", options=["standard", "detailed"], optional=True, advanced=True), ], outputs=[ @@ -379,7 +372,6 @@ class TripoMultiviewToModelNode(IO.ComfyNode): "model_version", "texture", "pbr", - "quad", "texture_quality", "geometry_quality", ], @@ -387,17 +379,16 @@ class TripoMultiviewToModelNode(IO.ComfyNode): expr=""" ( $isV14 := $contains(widgets.model_version,"v1.4"); + $isV3OrLater := $contains(widgets.model_version,"v3."); $withTexture := widgets.texture or widgets.pbr; $isHdTexture := (widgets.texture_quality = "detailed"); $isDetailedGeometry := (widgets.geometry_quality = "detailed"); - $baseCredits := - $isV14 ? 30 : ($withTexture ? 30 : 20); - $credits := - $baseCredits - + (widgets.quad ? 5 : 0) + $credits := $isV14 ? 30 : ( + ($withTexture ? 30 : 20) + ($isHdTexture ? 10 : 0) - + ($isDetailedGeometry ? 20 : 0); - {"type":"usd","usd": $round($credits * 0.01, 2)} + + (($isDetailedGeometry and $isV3OrLater) ? 20 : 0) + ); + {"type":"usd","usd": $round($credits * 0.01, 2), "format": {"approximate": true}} ) """, ), @@ -457,7 +448,7 @@ class TripoMultiviewToModelNode(IO.ComfyNode): geometry_quality=geometry_quality, texture_alignment=texture_alignment, face_limit=face_limit if face_limit != -1 else None, - quad=quad, + quad=None, ), ) return await poll_until_finished(cls, response, average_duration=80) @@ -498,7 +489,7 @@ class TripoTextureNode(IO.ComfyNode): expr=""" ( $tq := widgets.texture_quality; - {"type":"usd","usd": ($contains($tq,"detailed") ? 0.2 : 0.1)} + {"type":"usd","usd": ($contains($tq,"detailed") ? 0.2 : 0.1), "format": {"approximate": true}} ) """, ), @@ -555,7 +546,7 @@ class TripoRefineNode(IO.ComfyNode): is_api_node=True, is_output_node=True, price_badge=IO.PriceBadge( - expr="""{"type":"usd","usd":0.3}""", + expr="""{"type":"usd","usd":0.3, "format": {"approximate": true}}""", ), ) @@ -592,7 +583,7 @@ class TripoRigNode(IO.ComfyNode): is_api_node=True, is_output_node=True, price_badge=IO.PriceBadge( - expr="""{"type":"usd","usd":0.25}""", + expr="""{"type":"usd","usd":0.25, "format": {"approximate": true}}""", ), ) @@ -652,7 +643,7 @@ class TripoRetargetNode(IO.ComfyNode): is_api_node=True, is_output_node=True, price_badge=IO.PriceBadge( - expr="""{"type":"usd","usd":0.1}""", + expr="""{"type":"usd","usd":0.1, "format": {"approximate": true}}""", ), ) @@ -761,19 +752,10 @@ class TripoConversionNode(IO.ComfyNode): "face_limit", "texture_size", "texture_format", - "force_symmetry", "flatten_bottom", "flatten_bottom_threshold", "pivot_to_center_bottom", "scale_factor", - "with_animation", - "pack_uv", - "bake", - "part_names", - "fbx_preset", - "export_vertex_colors", - "export_orientation", - "animate_in_place", ], ), expr=""" @@ -783,28 +765,16 @@ class TripoConversionNode(IO.ComfyNode): $flatThresh := (widgets.flatten_bottom_threshold != null) ? widgets.flatten_bottom_threshold : 0; $scale := (widgets.scale_factor != null) ? widgets.scale_factor : 1; $texFmt := (widgets.texture_format != "" ? widgets.texture_format : "jpeg"); - $part := widgets.part_names; - $fbx := (widgets.fbx_preset != "" ? widgets.fbx_preset : "blender"); - $orient := (widgets.export_orientation != "" ? widgets.export_orientation : "default"); $advanced := widgets.quad or - widgets.force_symmetry or widgets.flatten_bottom or widgets.pivot_to_center_bottom or - widgets.with_animation or - widgets.pack_uv or - widgets.bake or - widgets.export_vertex_colors or - widgets.animate_in_place or ($face != -1) or ($texSize != 4096) or ($flatThresh != 0) or ($scale != 1) or - ($texFmt != "jpeg") or - ($part != "") or - ($fbx != "blender") or - ($orient != "default"); - {"type":"usd","usd": ($advanced ? 0.1 : 0.05)} + ($texFmt != "jpeg"); + {"type":"usd","usd": ($advanced ? 0.1 : 0.05), "format": {"approximate": true}} ) """, ), diff --git a/comfy_api_nodes/util/__init__.py b/comfy_api_nodes/util/__init__.py index f3584aba9..25cb88869 100644 --- a/comfy_api_nodes/util/__init__.py +++ b/comfy_api_nodes/util/__init__.py @@ -16,16 +16,17 @@ from .conversions import ( convert_mask_to_image, downscale_image_tensor, downscale_image_tensor_by_max_side, + downscale_video_to_max_pixels, image_tensor_pair_to_batch, pil_to_bytesio, resize_mask_to_image, - resize_video_to_pixel_budget, tensor_to_base64_string, tensor_to_bytesio, tensor_to_pil, text_filepath_to_base64_string, text_filepath_to_data_uri, trim_video, + upscale_video_to_min_pixels, video_to_base64_string, ) from .download_helpers import ( @@ -88,16 +89,17 @@ __all__ = [ "convert_mask_to_image", "downscale_image_tensor", "downscale_image_tensor_by_max_side", + "downscale_video_to_max_pixels", "image_tensor_pair_to_batch", "pil_to_bytesio", "resize_mask_to_image", - "resize_video_to_pixel_budget", "tensor_to_base64_string", "tensor_to_bytesio", "tensor_to_pil", "text_filepath_to_base64_string", "text_filepath_to_data_uri", "trim_video", + "upscale_video_to_min_pixels", "video_to_base64_string", # Validation utilities "get_image_dimensions", diff --git a/comfy_api_nodes/util/client.py b/comfy_api_nodes/util/client.py index 8e1ba91ba..052301c33 100644 --- a/comfy_api_nodes/util/client.py +++ b/comfy_api_nodes/util/client.py @@ -488,10 +488,30 @@ async def _diagnose_connectivity() -> dict[str, bool]: "api_accessible": False, } timeout = aiohttp.ClientTimeout(total=5.0) + + # Probe Google and Baidu in parallel: Google is blocked by the GFW in mainland China, so a Baidu probe is required + # to correctly detect that Chinese users with working internet do have working internet. + internet_probe_urls = ("https://www.google.com", "https://www.baidu.com") + async with aiohttp.ClientSession(timeout=timeout) as session: - with contextlib.suppress(ClientError, OSError): - async with session.get("https://www.google.com") as resp: - results["internet_accessible"] = resp.status < 500 + async def _probe(url: str) -> bool: + try: + async with session.get(url) as resp: + return resp.status < 500 + except (ClientError, OSError, asyncio.TimeoutError): + return False + + probe_tasks = [asyncio.create_task(_probe(u)) for u in internet_probe_urls] + try: + for fut in asyncio.as_completed(probe_tasks): + if await fut: + results["internet_accessible"] = True + break + finally: + for t in probe_tasks: + if not t.done(): + t.cancel() + await asyncio.gather(*probe_tasks, return_exceptions=True) if not results["internet_accessible"]: return results diff --git a/comfy_api_nodes/util/conversions.py b/comfy_api_nodes/util/conversions.py index be5d5719b..5738df57f 100644 --- a/comfy_api_nodes/util/conversions.py +++ b/comfy_api_nodes/util/conversions.py @@ -415,14 +415,48 @@ def trim_video(video: Input.Video, duration_sec: float) -> Input.Video: raise RuntimeError(f"Failed to trim video: {str(e)}") from e -def resize_video_to_pixel_budget(video: Input.Video, total_pixels: int) -> Input.Video: - """Downscale a video to fit within ``total_pixels`` (w * h), preserving aspect ratio. +def downscale_video_to_max_pixels(video: Input.Video, max_pixels: int) -> Input.Video: + """Downscale a video to fit within ``max_pixels`` (w * h), preserving aspect ratio. Returns the original video object untouched when it already fits. Preserves frame rate, duration, and audio. Aspect ratio is preserved up to a fraction of a percent (even-dim rounding). """ src_w, src_h = video.get_dimensions() - scale_dims = _compute_downscale_dims(src_w, src_h, total_pixels) + scale_dims = _compute_downscale_dims(src_w, src_h, max_pixels) + if scale_dims is None: + return video + return _apply_video_scale(video, scale_dims) + + +def _compute_upscale_dims(src_w: int, src_h: int, total_pixels: int) -> tuple[int, int] | None: + """Return upscaled (w, h) with even dims meeting at least ``total_pixels``, or None if already large enough. + + Source aspect ratio is preserved; output may drift by a fraction of a percent because both dimensions + are rounded up to even values (many codecs require divisible-by-2). The result is guaranteed to be at + least ``total_pixels``. + """ + pixels = src_w * src_h + if pixels >= total_pixels: + return None + scale = math.sqrt(total_pixels / pixels) + new_w = math.ceil(src_w * scale) + new_h = math.ceil(src_h * scale) + if new_w % 2: + new_w += 1 + if new_h % 2: + new_h += 1 + return new_w, new_h + + +def upscale_video_to_min_pixels(video: Input.Video, min_pixels: int) -> Input.Video: + """Upscale a video to meet at least ``min_pixels`` (w * h), preserving aspect ratio. + + Returns the original video object untouched when it already meets the minimum. Preserves frame rate, + duration, and audio. Aspect ratio is preserved up to a fraction of a percent (even-dim rounding). + Note: upscaling a low-resolution source does not add real detail; downstream model quality may suffer. + """ + src_w, src_h = video.get_dimensions() + scale_dims = _compute_upscale_dims(src_w, src_h, min_pixels) if scale_dims is None: return video return _apply_video_scale(video, scale_dims) diff --git a/comfy_execution/graph.py b/comfy_execution/graph.py index c47f3c79b..479ee8a53 100644 --- a/comfy_execution/graph.py +++ b/comfy_execution/graph.py @@ -1,4 +1,3 @@ -from __future__ import annotations from typing import Type, Literal import nodes diff --git a/comfy_execution/progress.py b/comfy_execution/progress.py index f951a3350..731b8dc66 100644 --- a/comfy_execution/progress.py +++ b/comfy_execution/progress.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import TypedDict, Dict, Optional, Tuple from typing_extensions import override from PIL import Image diff --git a/comfy_execution/validation.py b/comfy_execution/validation.py index e73624bd1..ae9a2376c 100644 --- a/comfy_execution/validation.py +++ b/comfy_execution/validation.py @@ -1,4 +1,3 @@ -from __future__ import annotations from comfy_api.latest import IO diff --git a/comfy_extras/mediapipe/face_geometry.py b/comfy_extras/mediapipe/face_geometry.py new file mode 100644 index 000000000..4f3813430 --- /dev/null +++ b/comfy_extras/mediapipe/face_geometry.py @@ -0,0 +1,110 @@ +"""Pure-numpy port of MediaPipe's face_geometry (FACE_LANDMARK_PIPELINE mode) ++ weighted Procrustes solver. Computes the 4x4 facial transformation matrix. +""" + + +import math +import numpy as np + + +def _solve_weighted_orthogonal_problem(src: np.ndarray, tgt: np.ndarray, weights: np.ndarray) -> np.ndarray: + """Weighted orthogonal Procrustes (similarity). Returns 4x4 M with + `target ≈ M @ homogeneous(source)` in the weighted LS sense. fp64 for + SVD stability. Port of procrustes_solver.cc.""" + sqrt_w = np.sqrt(weights.astype(np.float64)) + w_total = float((sqrt_w ** 2).sum()) + ws = src.astype(np.float64) * sqrt_w + wt = tgt.astype(np.float64) * sqrt_w + + c_w = (ws @ sqrt_w) / w_total + centered = ws - np.outer(c_w, sqrt_w) + U, _S, Vt = np.linalg.svd(wt @ centered.T, full_matrices=True) + # Disallow reflection: flip the least-significant axis when det(U)·det(V)<0. + post, pre = U.copy(), Vt.T.copy() + if np.linalg.det(post) * np.linalg.det(pre) < 0: + post[:, 2] *= -1.0 + R = post @ pre.T + + denom = float((centered * ws).sum()) + if denom < 1e-12: + raise ValueError("Procrustes denominator collapsed (degenerate source).") + scale = float((R @ centered * wt).sum()) / denom + translation = ((wt - scale * (R @ ws)) @ sqrt_w) / w_total + + M = np.eye(4, dtype=np.float64) + M[:3, :3] = scale * R + M[:3, 3] = translation + return M + + +def _estimate_scale(canonical: np.ndarray, runtime: np.ndarray, weights: np.ndarray) -> float: + """scale = ‖first column of M[:3]‖ per geometry_pipeline.cc::EstimateScale.""" + return float(np.linalg.norm(_solve_weighted_orthogonal_problem(canonical, runtime, weights)[:3, 0])) + + +def solve_facial_transformation_matrix( + landmarks_normalized: np.ndarray, + canonical_vertices: np.ndarray, + procrustes_indices: np.ndarray, + procrustes_weights: np.ndarray, + image_width: int, + image_height: int, + # face_geometry_calculator_options.pbtxt defaults + vertical_fov_degrees: float = 63.0, + near: float = 1.0, +) -> np.ndarray: + """4x4 facial transformation matrix via two-pass scale recovery + `landmarks_normalized` is (N, 3) in MediaPipe normalized convention: x, y + in [0,1] with TOP-LEFT origin, z in width-scaled units. + """ + + h_near = 2.0 * near * math.tan(0.5 * math.radians(vertical_fov_degrees)) + w_near = image_width * h_near / image_height + + sub = procrustes_indices.astype(np.int64) + screen = landmarks_normalized[sub].T.astype(np.float64).copy() + canon = canonical_vertices[sub].T.astype(np.float64).copy() + weights = procrustes_weights.astype(np.float64) + + # ProjectXY (TOP_LEFT y-flip, then scale all 3 axes; z uses x-scale). + screen[1] = 1.0 - screen[1] + screen[0] = screen[0] * w_near - 0.5 * w_near + screen[1] = screen[1] * h_near - 0.5 * h_near + screen[2] = screen[2] * w_near + depth_offset = float(screen[2].mean()) + + def _unproject(s: np.ndarray, scale: float) -> np.ndarray: + s = s.copy() + s[2] = (s[2] - depth_offset + near) / scale + s[0] *= s[2] / near + s[1] *= s[2] / near + s[2] *= -1.0 + return s + + first = screen.copy() + first[2] *= -1.0 + s1 = _estimate_scale(canon, first, weights) # 1st pass: Procrustes on projected XY + s2 = _estimate_scale(canon, _unproject(screen, s1), weights) # 2nd pass: rescale z by s1, un-project XY + return _solve_weighted_orthogonal_problem(canon, _unproject(screen, s1 * s2), weights).astype(np.float32) + + +def transformation_matrix_from_detection(face_dict: dict, image_width: int, image_height: int, canonical_data: dict) -> np.ndarray: + """Adapt a FaceLandmarker face dict to MP's normalized convention and solve. + FaceMesh emits (x, y, z) in 192-canonical units; MP's geometry expects + z_norm = z_canonical * scale_x / image_width""" + + lmks_xy, lmks_3d = face_dict["landmarks_xy"], face_dict["landmarks_3d"] + aug = np.concatenate([lmks_3d[:, :2].astype(np.float64), np.ones((lmks_xy.shape[0], 1))], axis=1) + M, *_ = np.linalg.lstsq(aug, lmks_xy.astype(np.float64), rcond=None) + scale_x = float(np.linalg.norm(M[0])) + z_scale = scale_x / image_width if scale_x > 1e-6 else 1.0 / image_width + + normalized = np.empty((lmks_xy.shape[0], 3), dtype=np.float32) + normalized[:, 0] = lmks_xy[:, 0] / image_width + normalized[:, 1] = lmks_xy[:, 1] / image_height + normalized[:, 2] = lmks_3d[:, 2] * z_scale + return solve_facial_transformation_matrix( + normalized, canonical_data["canonical_vertices"], + canonical_data["procrustes_indices"], canonical_data["procrustes_weights"], + image_width=image_width, image_height=image_height, + ) diff --git a/comfy_extras/mediapipe/face_landmarker.py b/comfy_extras/mediapipe/face_landmarker.py new file mode 100644 index 000000000..e6b463c4c --- /dev/null +++ b/comfy_extras/mediapipe/face_landmarker.py @@ -0,0 +1,681 @@ +"""Pure-PyTorch port of MediaPipe's face_landmarker_v2_with_blendshapes.task: +BlazeFace detector → FaceMesh v2 → ARKit-52 blendshapes.""" + + +import math +from functools import lru_cache +from typing import List, Tuple + +import numpy as np +import torch +import torch.nn.functional as F +from scipy.special import expit +from torch import Tensor, nn + + +# Values below must stay verbatim with the published face_landmarker_v2 graph + +# face_blendshapes_graph.cc::kLandmarksSubsetIdxs +_BS_INPUT_INDICES: Tuple[int, ...] = ( + 0, 1, 4, 5, 6, 7, 8, 10, 13, 14, 17, 21, 33, 37, 39, 40, 46, 52, 53, 54, + 55, 58, 61, 63, 65, 66, 67, 70, 78, 80, 81, 82, 84, 87, 88, 91, 93, 95, + 103, 105, 107, 109, 127, 132, 133, 136, 144, 145, 146, 148, 149, 150, 152, + 153, 154, 155, 157, 158, 159, 160, 161, 162, 163, 168, 172, 173, 176, 178, + 181, 185, 191, 195, 197, 234, 246, 249, 251, 263, 267, 269, 270, 276, 282, + 283, 284, 285, 288, 291, 293, 295, 296, 297, 300, 308, 310, 311, 312, 314, + 317, 318, 321, 323, 324, 332, 334, 336, 338, 356, 361, 362, 365, 373, 374, + 375, 377, 378, 379, 380, 381, 382, 384, 385, 386, 387, 388, 389, 390, 397, + 398, 400, 402, 405, 409, 415, 454, 466, 468, 469, 470, 471, 472, 473, 474, + 475, 476, 477, +) + +# face_blendshapes_graph.cc::kCategoryNames +BLENDSHAPE_NAMES: Tuple[str, ...] = ( + "_neutral", "browDownLeft", "browDownRight", "browInnerUp", "browOuterUpLeft", + "browOuterUpRight", "cheekPuff", "cheekSquintLeft", "cheekSquintRight", + "eyeBlinkLeft", "eyeBlinkRight", "eyeLookDownLeft", "eyeLookDownRight", + "eyeLookInLeft", "eyeLookInRight", "eyeLookOutLeft", "eyeLookOutRight", + "eyeLookUpLeft", "eyeLookUpRight", "eyeSquintLeft", "eyeSquintRight", + "eyeWideLeft", "eyeWideRight", "jawForward", "jawLeft", "jawOpen", + "jawRight", "mouthClose", "mouthDimpleLeft", "mouthDimpleRight", + "mouthFrownLeft", "mouthFrownRight", "mouthFunnel", "mouthLeft", + "mouthLowerDownLeft", "mouthLowerDownRight", "mouthPressLeft", + "mouthPressRight", "mouthPucker", "mouthRight", "mouthRollLower", + "mouthRollUpper", "mouthShrugLower", "mouthShrugUpper", "mouthSmileLeft", + "mouthSmileRight", "mouthStretchLeft", "mouthStretchRight", + "mouthUpperUpLeft", "mouthUpperUpRight", "noseSneerLeft", "noseSneerRight", +) + +# face_detection.pbtxt — short-range BlazeFace. +_BF_NUM_LAYERS = 4 +_BF_INPUT_SIZE = 128 +_BF_STRIDES = (8, 16, 16, 16) +_BF_ANCHOR_OFFSET_X = 0.5 +_BF_ANCHOR_OFFSET_Y = 0.5 +_BF_ASPECT_RATIOS = (1.0,) +_BF_INTERP_SCALE_AR = 1.0 +_BF_BOX_SCALE = 128.0 +_BF_KP_OFFSET = 4 +_BF_SCORE_CLIP = 100.0 +_BF_MIN_SCORE = 0.5 + +# face_detection_full_range.pbtxt — 48x48 grid at stride 4, 1 anchor/cell. +_BF_FR_INPUT_SIZE = 192 +_BF_FR_GRID = 48 +_BF_FR_NUM_ANCHORS = _BF_FR_GRID * _BF_FR_GRID +_BF_FR_BOX_SCALE = 192.0 +_BF_FR_SCORE_CLIP = 100.0 + +_FM_INPUT_SIZE = 192 + +# Face ROI: 1.5xbbox rect warped anisotropically into 192x192. +_FACE_LEFT_EYE_KP = 0 +_FACE_RIGHT_EYE_KP = 1 +_FACE_ROI_SCALE_X = 1.5 +_FACE_ROI_SCALE_Y = 1.5 +_FACE_ROI_TARGET_ANGLE = 0.0 + + +def _tf_same_pad(x: Tensor, kernel: int, stride: int) -> Tensor: + """TF SAME pad (asymmetric on stride-2; PyTorch's symmetric pad undershoots by 1 px).""" + H, W = x.shape[-2], x.shape[-1] + pad_h = max(((H + stride - 1) // stride - 1) * stride + kernel - H, 0) + pad_w = max(((W + stride - 1) // stride - 1) * stride + kernel - W, 0) + if pad_h == 0 and pad_w == 0: + return x + return F.pad(x, (pad_w // 2, pad_w - pad_w // 2, pad_h // 2, pad_h - pad_h // 2)) + + +# BlazeFace short-range: stem 5x5/s2 → 16 BlazeBlocks → parallel heads at +# 16²x88 (2 anchors/cell) and 8²x96 (6/cell) = 896 anchors. (in, out, stride): +_BLAZEFACE_BLOCKS = [ + (24, 24, 1), (24, 28, 1), (28, 32, 2), (32, 36, 1), + (36, 42, 1), (42, 48, 2), (48, 56, 1), (56, 64, 1), + (64, 72, 1), (72, 80, 1), (80, 88, 1), (88, 96, 2), + (96, 96, 1), (96, 96, 1), (96, 96, 1), (96, 96, 1), +] + + +class BlazeFaceBlock(nn.Module): + """DW 3x3 + PW + residual. Residual max-pools on stride>1, channel-pads on out_ch>in_ch.""" + + def __init__(self, in_ch: int, out_ch: int, stride: int, device=None, dtype=None, operations=None): + super().__init__() + ops = operations if operations is not None else nn + self.in_ch, self.out_ch, self.stride = in_ch, out_ch, stride + self.depthwise = ops.Conv2d(in_ch, in_ch, 3, stride=stride, padding=0, groups=in_ch, bias=True, device=device, dtype=dtype) + self.pointwise = ops.Conv2d(in_ch, out_ch, 1, padding=0, bias=True, device=device, dtype=dtype) + + def forward(self, x: Tensor) -> Tensor: + residual = F.max_pool2d(x, 2, 2) if self.stride > 1 else x + if self.out_ch > self.in_ch: + residual = F.pad(residual, (0, 0, 0, 0, 0, self.out_ch - self.in_ch)) + x = _tf_same_pad(x, 3, self.stride) if self.stride > 1 else F.pad(x, (1, 1, 1, 1)) + return F.relu(self.pointwise(self.depthwise(x)) + residual) + + +class BlazeFace(nn.Module): + """Short-range BlazeFace: (B, 3, 128, 128) in [-1, 1] → 896 anchors x 17.""" + + def __init__(self, device=None, dtype=None, operations=None): + super().__init__() + ops = operations if operations is not None else nn + kw = dict(device=device, dtype=dtype) + self.stem = ops.Conv2d(3, 24, 5, stride=2, padding=0, bias=True, **kw) + self.blocks = nn.ModuleList(BlazeFaceBlock(i, o, s, device=device, dtype=dtype, operations=operations) + for (i, o, s) in _BLAZEFACE_BLOCKS) + # 16²x2 + 8²x6 = 512 + 384 = 896 anchors. + self.cls_16 = ops.Conv2d(88, 2, 1, padding=0, bias=True, **kw) + self.cls_8 = ops.Conv2d(96, 6, 1, padding=0, bias=True, **kw) + self.reg_16 = ops.Conv2d(88, 32, 1, padding=0, bias=True, **kw) + self.reg_8 = ops.Conv2d(96, 96, 1, padding=0, bias=True, **kw) + + def forward(self, image_chw_normalized: Tensor) -> tuple[Tensor, Tensor]: + x = F.relu(self.stem(_tf_same_pad(image_chw_normalized, 5, 2))) + # 16x16 tap is block-10 output (before the 88→96 stride-2 in block 11). + for i in range(11): + x = self.blocks[i](x) + feat_16 = x + for i in range(11, 16): + x = self.blocks[i](x) + feat_8 = x + + def flat(t, a, k): # NHWC flatten → (B, H*W*A, K) + B, _, H, W = t.shape + return t.permute(0, 2, 3, 1).reshape(B, H * W * a, k) + + cls = torch.cat([flat(self.cls_16(feat_16), 2, 1), flat(self.cls_8(feat_8), 6, 1)], dim=1) + reg = torch.cat([flat(self.reg_16(feat_16), 2, 16), flat(self.reg_8(feat_8), 6, 16)], dim=1) + return reg, cls + + +# BlazeFace full-range (face_detection_full_range_sparse.tflite): MobileNetV2-ish +# backbone + top-down FPN, 192² input → 2304 anchors at the 48x48 grid. +class FRBlock(nn.Module): + """Double inverted residual: DW → PW(mid) → DW → PW(out) [+ residual]. + + Per source tflite: dw* have no fused activation, pw1 is always ReLU, pw2 + is ReLU only when no residual (else ReLU fuses into the ADD). + """ + + def __init__(self, in_ch: int, mid_ch: int, out_ch: int, stride: int, device=None, dtype=None, operations=None): + super().__init__() + ops = operations if operations is not None else nn + kw = dict(device=device, dtype=dtype) + self.has_residual = (in_ch == out_ch and stride == 1) + self.dw1 = ops.Conv2d(in_ch, in_ch, 3, stride=stride, padding=0, groups=in_ch, bias=True, **kw) + self.pw1 = ops.Conv2d(in_ch, mid_ch, 1, padding=0, bias=True, **kw) + self.dw2 = ops.Conv2d(mid_ch, mid_ch, 3, stride=1, padding=0, groups=mid_ch, bias=True, **kw) + self.pw2 = ops.Conv2d(mid_ch, out_ch, 1, padding=0, bias=True, **kw) + + def forward(self, x: Tensor) -> Tensor: + residual = x if self.has_residual else None + x = F.relu(self.pw1(self.dw1(F.pad(x, (1, 1, 1, 1))))) + x = self.pw2(self.dw2(F.pad(x, (1, 1, 1, 1)))) + return F.relu(x + residual) if residual is not None else F.relu(x) + + +# (in_ch, mid_ch, out_ch, stride). Stages downsample 96²x32 → 48²x64 → 24²x128 +# → 12²x192 → 6²x384. Lateral taps at indices 4, 7, 10 (see _FR_LATERAL_*). +_FR_BACKBONE_BLOCKS = [ + (32, 8, 32, 1), (32, 8, 32, 1), # 96²x32 + (32, 16, 64, 2), (64, 16, 64, 1), (64, 16, 64, 1), # 48²x64 — tap[0] + (64, 32, 128, 2), (128, 32, 128, 1), (128, 32, 128, 1), # 24²x128 — tap[1] + (128, 48, 192, 2), (192, 48, 192, 1), (192, 48, 192, 1), # 12²x192 — tap[2] + (192, 96, 384, 2), (384, 96, 384, 1), (384, 96, 384, 1), (384, 96, 384, 1), # 6²x384 +] +_FR_LATERAL_TAP_INDICES = (4, 7, 10) +_FR_LATERAL_CHANNELS = ((64, 48), (128, 64), (192, 96)) # (in, out) per side-conv + +# Decoder blocks per FPN level (after upsample-and-merge with the lateral). +_FR_DECODER_BLOCKS = [ + [(96, 48, 96, 1), (96, 48, 96, 1)], # 12²x96 + [(64, 32, 64, 1), (64, 32, 64, 1)], # 24²x64 + [(48, 24, 48, 1)], # 48²x48 — feeds the heads +] + + +def _dcr_depth_to_space(t: Tensor, r: int, c_out: int) -> Tensor: + """TF DEPTH_TO_SPACE in DCR layout (input channels = (i, j, c_out)). + pixel_shuffle uses CRD which permutes output channels for c_out > 1.""" + B_, _, H_, W_ = t.shape + t = t.reshape(B_, r, r, c_out, H_, W_) + t = t.permute(0, 3, 4, 1, 5, 2).contiguous() + return t.reshape(B_, c_out, H_ * r, W_ * r) + + +class BlazeFaceFullRange(nn.Module): + """Full-range face detector: (B, 3, 192, 192) in [-1, 1] → 2304 anchors x 17 values.""" + + def __init__(self, device=None, dtype=None, operations=None): + super().__init__() + ops = operations if operations is not None else nn + kw = dict(device=device, dtype=dtype) + mk_block = lambda i, m, o, s: FRBlock(i, m, o, s, device=device, dtype=dtype, operations=operations) + self.stem = ops.Conv2d(3, 32, 3, stride=2, padding=0, bias=True, **kw) + self.backbone = nn.ModuleList(mk_block(i, m, o, s) for (i, m, o, s) in _FR_BACKBONE_BLOCKS) + self.lateral_convs = nn.ModuleList(ops.Conv2d(i, o, 1, padding=0, bias=True, **kw) for (i, o) in _FR_LATERAL_CHANNELS) + self.top_conv = ops.Conv2d(384, 96, 1, padding=0, bias=True, **kw) + self.decoder_levels = nn.ModuleList( + nn.ModuleList(mk_block(i, m, o, s) for (i, m, o, s) in lvl) for lvl in _FR_DECODER_BLOCKS + ) + # 96→64 before 12→24, 64→48 before 24→48. + self.decoder_reduce_convs = nn.ModuleList([ + ops.Conv2d(96, 64, 1, padding=0, bias=True, **kw), + ops.Conv2d(64, 48, 1, padding=0, bias=True, **kw), + ]) + # Heads mix 2x2-cell info via DW-stride-2 + depth_to_space block_size=2. + self.cls_conv = ops.Conv2d(48, 4, 1, padding=0, bias=True, **kw) + self.cls_dw = ops.Conv2d(4, 4, 3, stride=2, padding=0, groups=4, bias=True, **kw) + self.reg_conv = ops.Conv2d(48, 64, 1, padding=0, bias=True, **kw) + self.reg_dw = ops.Conv2d(64, 64, 3, stride=2, padding=0, groups=64, bias=True, **kw) + + def forward(self, image_chw_normalized: Tensor) -> tuple[Tensor, Tensor]: + # Symmetric pad-1 throughout (full-range tflite uses explicit TF PAD, not SAME). + x = F.relu(self.stem(F.pad(image_chw_normalized, (1, 1, 1, 1)))) + tap_set = set(_FR_LATERAL_TAP_INDICES) + laterals: list[Tensor] = [] + for i, blk in enumerate(self.backbone): + x = blk(x) + if i in tap_set: + laterals.append(x) + + # top_conv / lateral_convs / decoder_reduce_convs all have fused ReLU in the tflite. + p = F.relu(self.top_conv(x)) + laterals_rev = list(reversed(laterals)) + lateral_convs_rev = list(reversed(self.lateral_convs)) + for level in range(len(self.decoder_levels)): + lateral = laterals_rev[level] + p = F.interpolate(p, size=lateral.shape[-2:], mode="bilinear", align_corners=False) + p = p + F.relu(lateral_convs_rev[level](lateral)) + for blk in self.decoder_levels[level]: + p = blk(p) + if level < len(self.decoder_reduce_convs): + p = F.relu(self.decoder_reduce_convs[level](p)) + + c = self.cls_dw(F.pad(self.cls_conv(p), (1, 1, 1, 1))) + c = _dcr_depth_to_space(c, r=2, c_out=1) + r = self.reg_dw(F.pad(self.reg_conv(p), (1, 1, 1, 1))) + r = _dcr_depth_to_space(r, r=2, c_out=16) + B = c.shape[0] + cls_out = c.permute(0, 2, 3, 1).reshape(B, _BF_FR_NUM_ANCHORS, 1) + reg_out = r.permute(0, 2, 3, 1).reshape(B, _BF_FR_NUM_ANCHORS, 16) + return reg_out, cls_out + + +@lru_cache(maxsize=1) +def _blazeface_full_range_anchors() -> np.ndarray: + """2304 anchors over 48x48; anchor_w=anchor_h=1 (fixed_anchor_size).""" + feat = _BF_FR_GRID + yy, xx = np.meshgrid(np.arange(feat, dtype=np.float32), np.arange(feat, dtype=np.float32), indexing="ij") + cx, cy, ones = (xx + 0.5) / feat, (yy + 0.5) / feat, np.ones_like(xx) + return np.stack([cx, cy, ones, ones], axis=-1).reshape(_BF_FR_NUM_ANCHORS, 4) + + +def _decode_blazeface_full_range(regressors: np.ndarray, classificators: np.ndarray, + score_thresh: float = _BF_MIN_SCORE) -> np.ndarray: + """Same decode as short-range with 2304-anchor grid and box_scale=192.""" + scores = expit(np.clip(classificators[:, 0], -_BF_FR_SCORE_CLIP, _BF_FR_SCORE_CLIP)) + keep = scores >= score_thresh + if not keep.any(): + return np.empty((0, 17), dtype=np.float32) + r = regressors[keep] / _BF_FR_BOX_SCALE + a = _blazeface_full_range_anchors()[keep] + cxs, cys, aws, ahs = a[:, 0:1], a[:, 1:2], a[:, 2:3], a[:, 3:4] + xc, yc = r[:, 0:1] * aws + cxs, r[:, 1:2] * ahs + cys + w, h = r[:, 2:3] * aws, r[:, 3:4] * ahs + out = np.empty((r.shape[0], 17), dtype=np.float32) + out[:, 0:1], out[:, 1:2], out[:, 2:3], out[:, 3:4] = xc - w / 2, yc - h / 2, xc + w / 2, yc + h / 2 + out[:, 4:16:2] = r[:, _BF_KP_OFFSET::2] * aws + cxs + out[:, 5:16:2] = r[:, _BF_KP_OFFSET + 1::2] * ahs + cys + out[:, 16] = scores[keep] + return out + + +# FaceMesh (face_landmarks_detector.tflite): PReLU variant of BlazeBlock, +# 17 blocks, heads for 478x3 landmarks + presence. +_FACEMESH_BLOCKS = [ # (in_ch, out_ch, stride) + (16, 16, 1), (16, 16, 1), (16, 32, 2), (32, 32, 1), (32, 32, 1), (32, 64, 2), + (64, 64, 1), (64, 64, 1), (64, 128, 2), (128, 128, 1), (128, 128, 1), (128, 128, 2), + (128, 128, 1), (128, 128, 1), (128, 128, 2), (128, 128, 1), (128, 128, 1), +] + + +class FaceMeshBlock(nn.Module): + """PReLU BlazeBlock: PReLU between DW and PW, and after the residual add.""" + + def __init__(self, in_ch: int, out_ch: int, stride: int, device=None, dtype=None, operations=None): + super().__init__() + ops = operations if operations is not None else nn + kw = dict(device=device, dtype=dtype) + self.in_ch, self.out_ch, self.stride = in_ch, out_ch, stride + self.depthwise = ops.Conv2d(in_ch, in_ch, 3, stride=stride, padding=0, groups=in_ch, bias=True, **kw) + self.prelu_dwise = nn.PReLU(num_parameters=in_ch, **kw) + self.pointwise = ops.Conv2d(in_ch, out_ch, 1, padding=0, bias=True, **kw) + self.prelu_out = nn.PReLU(num_parameters=out_ch, **kw) + + def forward(self, x: Tensor) -> Tensor: + residual = F.max_pool2d(x, 2, 2) if self.stride > 1 else x + if self.out_ch > self.in_ch: + residual = F.pad(residual, (0, 0, 0, 0, 0, self.out_ch - self.in_ch)) + x = _tf_same_pad(x, 3, self.stride) if self.stride > 1 else F.pad(x, (1, 1, 1, 1)) + return self.prelu_out(self.pointwise(self.prelu_dwise(self.depthwise(x))) + residual) + + +class FaceMesh(nn.Module): + NUM_LANDMARKS = 478 + + def __init__(self, device=None, dtype=None, operations=None): + super().__init__() + ops = operations if operations is not None else nn + kw = dict(device=device, dtype=dtype) + self.stem = ops.Conv2d(3, 16, 3, stride=2, padding=0, bias=True, **kw) + self.prelu_stem = nn.PReLU(num_parameters=16, **kw) + self.blocks = nn.ModuleList(FaceMeshBlock(i, o, s, device=device, dtype=dtype, operations=operations) + for (i, o, s) in _FACEMESH_BLOCKS) + self.head_reduce = ops.Conv2d(128, 8, 1, padding=0, bias=True, **kw) + self.prelu_head_reduce = nn.PReLU(num_parameters=8, **kw) + self.head_block = FaceMeshBlock(8, 8, 1, device=device, dtype=dtype, operations=operations) + self.head_presence = ops.Conv2d(8, 1, 3, padding=0, bias=True, **kw) + self.head_landmarks = ops.Conv2d(8, self.NUM_LANDMARKS * 3, 3, padding=0, bias=True, **kw) + + def forward(self, face_chw_normalized: Tensor) -> tuple[Tensor, Tensor]: + """(B, 3, 192, 192) in [0, 1] → ((B, 478, 3) landmarks in 192-canonical, (B,) presence).""" + x = self.prelu_stem(self.stem(_tf_same_pad(face_chw_normalized, 3, 2))) + for blk in self.blocks: + x = blk(x) + x = self.prelu_head_reduce(self.head_reduce(x)) + x = self.head_block(x) + B = x.shape[0] + presence = self.head_presence(x).reshape(B) + lmks = self.head_landmarks(x).reshape(B, self.NUM_LANDMARKS, 3) + return lmks, presence + + +# FaceBlendshapes (MLP-Mixer "GhumMarkerPoserMlpMixerGeneral"): +# 146x2 → token-reduce 146→96 → embed 2→64 → +cls token → 4x mixer → cls→52. +_BS_NUM_INPUT_LANDMARKS = 146 +_BS_NUM_TOKENS_REDUCED = 96 +_BS_NUM_TOKENS = 97 # +1 cls +_BS_TOKEN_DIM = 64 +_BS_TOKEN_MIX_HIDDEN = 384 +_BS_CHANNEL_MIX_HIDDEN = 256 +_BS_NUM_BLENDSHAPES = 52 +_BS_LN_EPS = 1e-6 + + +class MlpMixerBlock(nn.Module): + """MLP-Mixer block: token-mixing MLP (over tokens) → channel-mixing MLP (over dim). + Both pre-LN, both residual. LN has no beta (bias=False) to match MP.""" + + def __init__(self, num_tokens: int, token_dim: int, token_hidden: int, channel_hidden: int, + device=None, dtype=None, operations=None): + super().__init__() + ops = operations if operations is not None else nn + kw = dict(device=device, dtype=dtype) + # bias=False → no LN beta (matches MP). + self.ln1 = ops.LayerNorm(token_dim, eps=_BS_LN_EPS, bias=False, **kw) + self.ln2 = ops.LayerNorm(token_dim, eps=_BS_LN_EPS, bias=False, **kw) + self.token_mlp1 = ops.Linear(num_tokens, token_hidden, bias=True, **kw) + self.token_mlp2 = ops.Linear(token_hidden, num_tokens, bias=True, **kw) + self.channel_mlp1 = ops.Linear(token_dim, channel_hidden, bias=True, **kw) + self.channel_mlp2 = ops.Linear(channel_hidden, token_dim, bias=True, **kw) + + def forward(self, x: Tensor) -> Tensor: + y = self.ln1(x).transpose(1, 2) + x = x + self.token_mlp2(F.relu(self.token_mlp1(y))).transpose(1, 2) + return x + self.channel_mlp2(F.relu(self.channel_mlp1(self.ln2(x)))) + + +class FaceBlendshapes(nn.Module): + def __init__(self, device=None, dtype=None, operations=None): + super().__init__() + ops = operations if operations is not None else nn + kw = dict(device=device, dtype=dtype) + self.token_reduce = ops.Linear(_BS_NUM_INPUT_LANDMARKS, _BS_NUM_TOKENS_REDUCED, bias=True, **kw) + self.token_embed = ops.Linear(2, _BS_TOKEN_DIM, bias=True, **kw) + self.cls_token = nn.Parameter(torch.zeros(1, 1, _BS_TOKEN_DIM, **kw)) + self.blocks = nn.ModuleList( + MlpMixerBlock(_BS_NUM_TOKENS, _BS_TOKEN_DIM, _BS_TOKEN_MIX_HIDDEN, _BS_CHANNEL_MIX_HIDDEN, + device=device, dtype=dtype, operations=operations) for _ in range(4) + ) + self.head = ops.Linear(_BS_TOKEN_DIM, _BS_NUM_BLENDSHAPES, bias=True, **kw) + + @staticmethod + def _input_normalize(landmarks_2d: Tensor) -> Tensor: + # Centroid-subtract → L2 scale → x0.5. The 0.5 is baked into training. + centroid = landmarks_2d.mean(dim=1, keepdim=True) + x = landmarks_2d - centroid + mag = torch.sqrt((x * x).sum(dim=-1, keepdim=True)) + scale = mag.mean(dim=1, keepdim=True) + return (x / scale.clamp(min=1e-12)) * 0.5 + + def forward(self, landmarks_2d: Tensor) -> Tensor: + """(B, 146, 2) → (B, 52) in [0, 1]. Input units don't matter (centroid + L2 normalize).""" + x = self._input_normalize(landmarks_2d) + x = self.token_reduce(x.transpose(1, 2)).transpose(1, 2) + x = self.token_embed(x) + cls = self.cls_token.expand(x.shape[0], -1, -1) + x = torch.cat([cls, x], dim=1) + for blk in self.blocks: + x = blk(x) + return torch.sigmoid(self.head(x[:, 0])) + + +@lru_cache(maxsize=1) +def _blazeface_anchors() -> np.ndarray: + """896 anchors per SsdAnchorsCalculator (fixed_anchor_size → anchor_w=anchor_h=1).""" + per_ar = len(_BF_ASPECT_RATIOS) + (1 if _BF_INTERP_SCALE_AR > 0 else 0) + layer_anchors: List[np.ndarray] = [] + layer = 0 + while layer < _BF_NUM_LAYERS: + stride = _BF_STRIDES[layer] + last = layer + while last < _BF_NUM_LAYERS and _BF_STRIDES[last] == stride: + last += 1 + per_cell = per_ar * (last - layer) + feat = (_BF_INPUT_SIZE + stride - 1) // stride + yy, xx = np.meshgrid(np.arange(feat, dtype=np.float32), np.arange(feat, dtype=np.float32), indexing="ij") + cx, cy, ones = (xx + _BF_ANCHOR_OFFSET_X) / feat, (yy + _BF_ANCHOR_OFFSET_Y) / feat, np.ones_like(xx) + cell = np.stack([cx, cy, ones, ones], axis=-1).reshape(-1, 4) + layer_anchors.append(np.repeat(cell, per_cell, axis=0)) + layer = last + out = np.concatenate(layer_anchors, axis=0) + assert out.shape == (896, 4), out.shape + return out + + +def _decode_blazeface(regressors: np.ndarray, classificators: np.ndarray, + score_thresh: float = _BF_MIN_SCORE) -> np.ndarray: + """Decode (regs (896,16), cls (896,1)) → (N, 17) = [xyxy, kp0x..kp5y, score] in [0, 1].""" + scores = expit(np.clip(classificators[:, 0], -_BF_SCORE_CLIP, _BF_SCORE_CLIP)) + keep = scores >= score_thresh + if not keep.any(): + return np.empty((0, 17), dtype=np.float32) + r = regressors[keep] / _BF_BOX_SCALE + a = _blazeface_anchors()[keep] # (N, 4) cx, cy, 1, 1 + cxs, cys, aws, ahs = a[:, 0:1], a[:, 1:2], a[:, 2:3], a[:, 3:4] + xc, yc = r[:, 0:1] * aws + cxs, r[:, 1:2] * ahs + cys + w, h = r[:, 2:3] * aws, r[:, 3:4] * ahs + out = np.empty((r.shape[0], 17), dtype=np.float32) + out[:, 0:1], out[:, 1:2], out[:, 2:3], out[:, 3:4] = xc - w / 2, yc - h / 2, xc + w / 2, yc + h / 2 + out[:, 4:16:2] = r[:, _BF_KP_OFFSET::2] * aws + cxs + out[:, 5:16:2] = r[:, _BF_KP_OFFSET + 1::2] * ahs + cys + out[:, 16] = scores[keep] + return out + + +def _weighted_nms(detections: np.ndarray, iou_thresh: float = 0.5) -> np.ndarray: + """MP weighted NMS — kept boxes are score-weighted averages of overlapping detections.""" + if detections.shape[0] == 0: + return detections + dets = detections[np.argsort(-detections[:, 16])] + N = dets.shape[0] + areas = np.clip(dets[:, 2] - dets[:, 0], 0, None) * np.clip(dets[:, 3] - dets[:, 1], 0, None) + kept: List[np.ndarray] = [] + used = np.zeros(N, dtype=bool) + for i in range(N): + if used[i]: + continue + ax1, ay1, ax2, ay2 = dets[i, 0:4] + merge_idx = [i] + for j in range(i + 1, N): + if used[j]: + continue + bx1, by1, bx2, by2 = dets[j, 0:4] + iw = max(0.0, min(ax2, bx2) - max(ax1, bx1)) + ih = max(0.0, min(ay2, by2) - max(ay1, by1)) + inter = iw * ih + union = areas[i] + areas[j] - inter + if union > 0 and inter / union > iou_thresh: # strict > matches MP + merge_idx.append(j) + used[j] = True + used[i] = True + cluster = dets[merge_idx] + ws = cluster[:, 16:17] + ws_sum = ws.sum() + merged = np.copy(cluster[0]) + if ws_sum > 0: + merged[:16] = (cluster[:, :16] * ws).sum(axis=0) / ws_sum + kept.append(merged) + return np.stack(kept, axis=0) if kept else np.empty((0, 17), dtype=np.float32) + + +def _detection_to_face_rect(detection: np.ndarray, image_w: int, image_h: int) -> Tuple[float, float, float, float, float]: + """Detection (normalized) → rotated 1.5xbbox ROI in image pixels (anisotropic).""" + xmin, ymin, xmax, ymax = detection[0:4] + lx = detection[4 + _FACE_LEFT_EYE_KP * 2 + 0] * image_w + ly = detection[4 + _FACE_LEFT_EYE_KP * 2 + 1] * image_h + rx = detection[4 + _FACE_RIGHT_EYE_KP * 2 + 0] * image_w + ry = detection[4 + _FACE_RIGHT_EYE_KP * 2 + 1] * image_h + # Image-y-down convention: angle = target - atan2(-dy, dx). + angle = _FACE_ROI_TARGET_ANGLE - math.atan2(ly - ry, rx - lx) + return (float((xmin + xmax) * 0.5 * image_w), + float((ymin + ymax) * 0.5 * image_h), + float((xmax - xmin) * image_w * _FACE_ROI_SCALE_X), + float((ymax - ymin) * image_h * _FACE_ROI_SCALE_Y), + float(angle)) + + +def _sample_warp(image_chw: Tensor, src_x: Tensor, src_y: Tensor, padding_mode: str) -> Tensor: + """Bilinear-sample image_chw at corner-aligned (src_x, src_y).""" + H, W = int(image_chw.shape[-2]), int(image_chw.shape[-1]) + grid = torch.stack([(2.0 * src_x + 1.0) / W - 1.0, + (2.0 * src_y + 1.0) / H - 1.0], dim=-1).unsqueeze(0) + return F.grid_sample(image_chw.unsqueeze(0), grid, mode="bilinear", + align_corners=False, padding_mode=padding_mode).squeeze(0) + + +def _warp_face_crop(image_chw: Tensor, cx: float, cy: float, width: float, height: float, + angle: float, output_size: int = _FM_INPUT_SIZE) -> Tensor: + """Rotated rect → output_size² with BORDER_REPLICATE. image_chw must be in [0, 1].""" + s_x, s_y = width / output_size, height / output_size + cos_a, sin_a = math.cos(angle), math.sin(angle) + arange = torch.arange(output_size, dtype=image_chw.dtype, device=image_chw.device) - output_size * 0.5 + v_grid, u_grid = torch.meshgrid(arange, arange, indexing="ij") + src_x = cx + u_grid * s_x * cos_a - v_grid * s_y * sin_a + src_y = cy + u_grid * s_x * sin_a + v_grid * s_y * cos_a + return _sample_warp(image_chw, src_x, src_y, "border") + + +def _blazeface_input_warp(image_chw_raw: Tensor, target: int = _BF_INPUT_SIZE) -> Tuple[Tensor, float, float, float]: + """Centered max(W,H) square → target² with BORDER_ZERO + [-1, 1] norm. + + Sub-pixel grid_sample matters; integer-pad-then-resize drifts the bbox ~5%. + Returns (warped, sub_rect_cx, sub_rect_cy, sub_rect_size) — the triplet maps + tensor-normalized [0,1] detections back to image pixels. + """ + H, W = int(image_chw_raw.shape[1]), int(image_chw_raw.shape[2]) + sub_rect_size = float(max(W, H)) + sub_rect_cx, sub_rect_cy = W * 0.5, H * 0.5 + s = sub_rect_size / target + arange = torch.arange(target, dtype=image_chw_raw.dtype, device=image_chw_raw.device) - target * 0.5 + v_grid, u_grid = torch.meshgrid(arange, arange, indexing="ij") + out = _sample_warp(image_chw_raw, sub_rect_cx + u_grid * s, sub_rect_cy + v_grid * s, "zeros") + return (out / 127.5) - 1.0, sub_rect_cx, sub_rect_cy, sub_rect_size + + +class FaceLandmarker(nn.Module): + """BlazeFace → FaceMesh v2 → blendshapes. `detector_variant` selects 'short' + (128², ≤2m) or 'full' (192² FPN, ≤5m). State dict uses inner-module prefixes + `detector.*` / `mesh.*` / `blendshapes.*`; the outer FaceLandmarkerModel + wrapper rewrites `detector_{variant}.*` keys to `detector.*` before loading. + """ + + def __init__(self, device=None, dtype=None, operations=None, detector_variant: str = "short"): + super().__init__() + det_cls = {"short": BlazeFace, "full": BlazeFaceFullRange}.get(detector_variant) + + self.detector_variant = detector_variant + self.detector = det_cls(device=device, dtype=dtype, operations=operations) + self.mesh = FaceMesh(device=device, dtype=dtype, operations=operations) + self.blendshapes = FaceBlendshapes(device=device, dtype=dtype, operations=operations) + self.register_buffer("_bs_idx", torch.tensor(_BS_INPUT_INDICES, dtype=torch.long), persistent=False) + + def run_detector_batch(self, images_rgb_uint8: List[np.ndarray], + score_thresh: float = _BF_MIN_SCORE, + iou_thresh: float = 0.5): + """Batched detector pass. Returns (img_raws, sub_rects, sizes, per_frame_decoded) + where per_frame_decoded[b] is (N, 17) in tensor-normalized [0,1] coords.""" + if not images_rgb_uint8: + return [], [], [], [] + device, dtype = self.detector.stem.weight.device, self.detector.stem.weight.dtype + det_input_size, decode_fn = ((_BF_FR_INPUT_SIZE, _decode_blazeface_full_range) + if self.detector_variant == "full" + else (_BF_INPUT_SIZE, _decode_blazeface)) + + # Same-size frames: stack once and transfer once. Variable size falls back + # to per-image (only triggers for SAM3DBody's head crops). + sizes = [tuple(img.shape[:2]) for img in images_rgb_uint8] + if len(set(sizes)) == 1: + batch_chw = torch.from_numpy(np.stack(images_rgb_uint8, axis=0)).to(device, dtype).movedim(-1, -3).contiguous() + img_raws = [batch_chw[bi] for bi in range(batch_chw.shape[0])] + else: + img_raws = [torch.from_numpy(img).to(device, dtype).movedim(-1, -3).contiguous() for img in images_rgb_uint8] + + warps = [_blazeface_input_warp(img_raw, det_input_size) for img_raw in img_raws] + det_crops = [w[0] for w in warps] + sub_rects = [(w[1], w[2], w[3]) for w in warps] + + regs_b, cls_b = self.detector(torch.stack(det_crops, dim=0)) + regs_np, cls_np = regs_b.float().cpu().numpy(), cls_b.float().cpu().numpy() + per_frame = [] + for b in range(len(images_rgb_uint8)): + decoded = decode_fn(regs_np[b], cls_np[b], score_thresh=score_thresh) + per_frame.append(_weighted_nms(decoded, iou_thresh=iou_thresh) if decoded.shape[0] > 0 else decoded) + return img_raws, sub_rects, sizes, per_frame + + def detect_batch(self, images_rgb_uint8: List[np.ndarray], num_faces: int = 1, + score_thresh: float = _BF_MIN_SCORE) -> List[List[dict]]: + """Full pipeline batched across `images_rgb_uint8`. Returns one face-dict + list per image (empty if nothing detected). Face dict: + bbox_xyxy (4,) image pixels, blendshapes {52} ∈ [0,1], + landmarks_xy (478, 2) image pixels, landmarks_3d (478, 3) in + 192-canonical (pre-transformation) units, presence float (raw logit). + """ + img_raws, sub_rects, sizes, per_frame_dets = self.run_detector_batch( + images_rgb_uint8, score_thresh=score_thresh, + ) + # tensor-normalized → image-normalized [0,1] for _detection_to_face_rect. + for b, decoded in enumerate(per_frame_dets): + if decoded.shape[0] == 0: + continue + cx, cy, size = sub_rects[b] + H, W = sizes[b] + sx0, sy0 = cx - size * 0.5, cy - size * 0.5 + decoded[:, 0:16:2] = (sx0 + size * decoded[:, 0:16:2]) / W + decoded[:, 1:16:2] = (sy0 + size * decoded[:, 1:16:2]) / H + if num_faces > 0: + per_frame_dets[b] = decoded[: int(num_faces)] + + # Collect every detected face across all frames into one mesh input. + face_params: List[Tuple[int, float, float, float, float, float, float]] = [] + mesh_crops: List[Tensor] = [] + for b, dets in enumerate(per_frame_dets): + if dets.shape[0] == 0: + continue + H, W = sizes[b] + img_for_mesh = img_raws[b] / 255.0 + for det in dets: + cx, cy, w, h, angle = _detection_to_face_rect(det, W, H) + mesh_crops.append(_warp_face_crop(img_for_mesh, cx, cy, w, h, angle, _FM_INPUT_SIZE)) + face_params.append((b, float(det[16]), cx, cy, w, h, angle)) + + results: List[List[dict]] = [[] for _ in range(len(images_rgb_uint8))] + if not mesh_crops: + return results + + lmks_canon_b, presence_b = self.mesh(torch.stack(mesh_crops, dim=0)) + bs_out_b = self.blendshapes(lmks_canon_b[:, self._bs_idx, :2]) + + # Batched canonical→image affine + params_t = torch.tensor( + [(cx, cy, w, h, math.cos(a), math.sin(a)) for (_b, _s, cx, cy, w, h, a) in face_params], + device=lmks_canon_b.device, dtype=lmks_canon_b.dtype, + ) + cxs, cys, ws, hs, cos_a, sin_a = params_t.unbind(dim=1) + inv = 1.0 / _FM_INPUT_SIZE + u = lmks_canon_b[..., 0] - _FM_INPUT_SIZE * 0.5 + v = lmks_canon_b[..., 1] - _FM_INPUT_SIZE * 0.5 + lmks_xy_t = torch.stack([ + cxs[:, None] + u * (ws * inv * cos_a)[:, None] - v * (hs * inv * sin_a)[:, None], + cys[:, None] + u * (ws * inv * sin_a)[:, None] + v * (hs * inv * cos_a)[:, None], + ], dim=-1) + + lmks_xy_np = lmks_xy_t.float().cpu().numpy() + lmks_canon_np = lmks_canon_b.float().cpu().numpy() + presence_np = presence_b.float().cpu().numpy() + bs_np = bs_out_b.float().cpu().numpy() + + for i, (b, score, *_) in enumerate(face_params): + lmks_xy = lmks_xy_np[i] + mn, mx = lmks_xy.min(0), lmks_xy.max(0) + results[b].append({ + "bbox_xyxy": np.array([mn[0], mn[1], mx[0], mx[1]], dtype=np.float32), + "blendshapes": dict(zip(BLENDSHAPE_NAMES, bs_np[i].tolist())), + "landmarks_xy": lmks_xy, + "landmarks_3d": lmks_canon_np[i], + "presence": float(presence_np[i]), + "score": score, + }) + return results diff --git a/comfy_extras/nodes_ace.py b/comfy_extras/nodes_ace.py index 1602add84..247d9ae8a 100644 --- a/comfy_extras/nodes_ace.py +++ b/comfy_extras/nodes_ace.py @@ -42,7 +42,7 @@ class TextEncodeAceStepAudio15(IO.ComfyNode): IO.Int.Input("bpm", default=120, min=10, max=300), IO.Float.Input("duration", default=120.0, min=0.0, max=2000.0, step=0.1), IO.Combo.Input("timesignature", options=['2', '3', '4', '6']), - IO.Combo.Input("language", options=["en", "ja", "zh", "es", "de", "fr", "pt", "ru", "it", "nl", "pl", "tr", "vi", "cs", "fa", "id", "ko", "uk", "hu", "ar", "sv", "ro", "el"]), + IO.Combo.Input("language", options=['ar', 'az', 'bg', 'bn', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'fa', 'fi', 'fr', 'he', 'hi', 'hr', 'ht', 'hu', 'id', 'is', 'it', 'ja', 'ko', 'la', 'lt', 'ms', 'ne', 'nl', 'no', 'pa', 'pl', 'pt', 'ro', 'ru', 'sa', 'sk', 'sr', 'sv', 'sw', 'ta', 'te', 'th', 'tl', 'tr', 'uk', 'ur', 'vi', 'yue', 'zh', 'unknown'], default='en'), IO.Combo.Input("keyscale", options=[f"{root} {quality}" for quality in ["major", "minor"] for root in ["C", "C#", "Db", "D", "D#", "Eb", "E", "F", "F#", "Gb", "G", "G#", "Ab", "A", "A#", "Bb", "B"]]), IO.Boolean.Input("generate_audio_codes", default=True, tooltip="Enable the LLM that generates audio codes. This can be slow but will increase the quality of the generated audio. Turn this off if you are giving the model an audio reference.", advanced=True), IO.Float.Input("cfg_scale", default=2.0, min=0.0, max=100.0, step=0.1, advanced=True), @@ -104,7 +104,7 @@ class EmptyAceStep15LatentAudio(IO.ComfyNode): def execute(cls, seconds, batch_size) -> IO.NodeOutput: length = round((seconds * 48000 / 1920)) latent = torch.zeros([batch_size, 64, length], device=comfy.model_management.intermediate_device(), dtype=comfy.model_management.intermediate_dtype()) - return IO.NodeOutput({"samples": latent, "type": "audio"}) + return IO.NodeOutput({"samples": latent, "type": "audio", "downscale_ratio_temporal": 1764}) class ReferenceAudio(IO.ComfyNode): @classmethod diff --git a/comfy_extras/nodes_advanced_samplers.py b/comfy_extras/nodes_advanced_samplers.py index 7f716cd76..20717ca38 100644 --- a/comfy_extras/nodes_advanced_samplers.py +++ b/comfy_extras/nodes_advanced_samplers.py @@ -45,7 +45,7 @@ class SamplerLCMUpscale(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="SamplerLCMUpscale", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Float.Input("scale_ratio", default=1.0, min=0.1, max=20.0, step=0.01, advanced=True), io.Int.Input("scale_steps", default=-1, min=-1, max=1000, step=1, advanced=True), @@ -86,13 +86,44 @@ def sample_euler_pp(model, x, sigmas, extra_args=None, callback=None, disable=No return x +class SamplerLCM(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="SamplerLCM", + category="sampling/samplers", + description=("LCM sampler with tunable per-step noise. s_noise is a multiplier on the model's training noise scale"), + inputs=[ + io.Float.Input("s_noise", default=1.0, min=0.0, max=64.0, step=0.01, + tooltip="Per-step noise multiplier at the first step (1.0 = match training)."), + io.Float.Input("s_noise_end", default=1.0, min=0.0, max=64.0, step=0.01, + tooltip="Per-step noise multiplier at the last step. Set equal to s_noise for a constant schedule."), + io.Float.Input("noise_clip_std", default=0.0, min=0.0, max=10.0, step=0.01, + tooltip="Clamp per-step noise to +/- N*std. 0 disables."), + ], + outputs=[io.Sampler.Output()], + ) + + @classmethod + def execute(cls, s_noise, s_noise_end, noise_clip_std) -> io.NodeOutput: + sampler = comfy.samplers.ksampler( + "lcm", + { + "s_noise": float(s_noise), + "s_noise_end": float(s_noise_end), + "noise_clip_std": float(noise_clip_std), + }, + ) + return io.NodeOutput(sampler) + + class SamplerEulerCFGpp(io.ComfyNode): @classmethod def define_schema(cls) -> io.Schema: return io.Schema( node_id="SamplerEulerCFGpp", display_name="SamplerEulerCFG++", - category="_for_testing", # "sampling/custom_sampling/samplers" + category="experimental", # "sampling/samplers" inputs=[ io.Combo.Input("version", options=["regular", "alternative"], advanced=True), ], @@ -114,6 +145,7 @@ class AdvancedSamplersExtension(ComfyExtension): async def get_node_list(self) -> list[type[io.ComfyNode]]: return [ SamplerLCMUpscale, + SamplerLCM, SamplerEulerCFGpp, ] diff --git a/comfy_extras/nodes_align_your_steps.py b/comfy_extras/nodes_align_your_steps.py index 4fc511d2c..307f41337 100644 --- a/comfy_extras/nodes_align_your_steps.py +++ b/comfy_extras/nodes_align_your_steps.py @@ -29,7 +29,7 @@ class AlignYourStepsScheduler(io.ComfyNode): return io.Schema( node_id="AlignYourStepsScheduler", search_aliases=["AYS scheduler"], - category="sampling/custom_sampling/schedulers", + category="sampling/schedulers", inputs=[ io.Combo.Input("model_type", options=["SD1", "SDXL", "SVD"]), io.Int.Input("steps", default=10, min=1, max=10000), diff --git a/comfy_extras/nodes_ar_video.py b/comfy_extras/nodes_ar_video.py index 09ee886fd..1a15facfa 100644 --- a/comfy_extras/nodes_ar_video.py +++ b/comfy_extras/nodes_ar_video.py @@ -2,6 +2,7 @@ ComfyUI nodes for autoregressive video generation (Causal Forcing, Self-Forcing, etc.). - EmptyARVideoLatent: create 5D [B, C, T, H, W] video latent tensors - SamplerARVideo: SAMPLER for the block-by-block autoregressive denoising loop + - ARVideoI2V: image-to-video conditioning for AR models (seeds KV cache with start image) """ import torch @@ -9,6 +10,7 @@ from typing_extensions import override import comfy.model_management import comfy.samplers +import comfy.utils from comfy_api.latest import ComfyExtension, io @@ -51,7 +53,7 @@ class SamplerARVideo(io.ComfyNode): return io.Schema( node_id="SamplerARVideo", display_name="Sampler AR Video", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Int.Input( "num_frame_per_block", @@ -71,12 +73,62 @@ class SamplerARVideo(io.ComfyNode): return io.NodeOutput(comfy.samplers.ksampler("ar_video", extra_options)) +class ARVideoI2V(io.ComfyNode): + """Image-to-video setup for AR video models (Causal Forcing, Self-Forcing). + + VAE-encodes the start image and stores it in the model's transformer_options + so that sample_ar_video can seed the KV cache before denoising. + Uses the same T2V model checkpoint -- no separate I2V architecture needed. + """ + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="ARVideoI2V", + category="conditioning/video_models", + inputs=[ + io.Model.Input("model"), + io.Vae.Input("vae"), + io.Image.Input("start_image"), + io.Int.Input("width", default=832, min=16, max=8192, step=16), + io.Int.Input("height", default=480, min=16, max=8192, step=16), + io.Int.Input("length", default=81, min=1, max=1024, step=4), + io.Int.Input("batch_size", default=1, min=1, max=64), + ], + outputs=[ + io.Model.Output(display_name="MODEL"), + io.Latent.Output(display_name="LATENT"), + ], + ) + + @classmethod + def execute(cls, model, vae, start_image, width, height, length, batch_size) -> io.NodeOutput: + start_image = comfy.utils.common_upscale( + start_image[:1].movedim(-1, 1), width, height, "bilinear", "center" + ).movedim(1, -1) + + initial_latent = vae.encode(start_image[:, :, :, :3]) + + m = model.clone() + to = m.model_options.setdefault("transformer_options", {}) + ar_cfg = to.setdefault("ar_config", {}) + ar_cfg["initial_latent"] = initial_latent + + lat_t = ((length - 1) // 4) + 1 + latent = torch.zeros( + [batch_size, 16, lat_t, height // 8, width // 8], + device=comfy.model_management.intermediate_device(), + ) + return io.NodeOutput(m, {"samples": latent}) + + class ARVideoExtension(ComfyExtension): @override async def get_node_list(self) -> list[type[io.ComfyNode]]: return [ EmptyARVideoLatent, SamplerARVideo, + ARVideoI2V, ] diff --git a/comfy_extras/nodes_attention_multiply.py b/comfy_extras/nodes_attention_multiply.py index 060a5c9be..f4ee6a689 100644 --- a/comfy_extras/nodes_attention_multiply.py +++ b/comfy_extras/nodes_attention_multiply.py @@ -25,7 +25,7 @@ class UNetSelfAttentionMultiply(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="UNetSelfAttentionMultiply", - category="_for_testing/attention_experiments", + category="experimental/attention_experiments", inputs=[ io.Model.Input("model"), io.Float.Input("q", default=1.0, min=0.0, max=10.0, step=0.01, advanced=True), @@ -48,7 +48,7 @@ class UNetCrossAttentionMultiply(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="UNetCrossAttentionMultiply", - category="_for_testing/attention_experiments", + category="experimental/attention_experiments", inputs=[ io.Model.Input("model"), io.Float.Input("q", default=1.0, min=0.0, max=10.0, step=0.01, advanced=True), @@ -72,7 +72,7 @@ class CLIPAttentionMultiply(io.ComfyNode): return io.Schema( node_id="CLIPAttentionMultiply", search_aliases=["clip attention scale", "text encoder attention"], - category="_for_testing/attention_experiments", + category="experimental/attention_experiments", inputs=[ io.Clip.Input("clip"), io.Float.Input("q", default=1.0, min=0.0, max=10.0, step=0.01, advanced=True), @@ -106,7 +106,7 @@ class UNetTemporalAttentionMultiply(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="UNetTemporalAttentionMultiply", - category="_for_testing/attention_experiments", + category="experimental/attention_experiments", inputs=[ io.Model.Input("model"), io.Float.Input("self_structural", default=1.0, min=0.0, max=10.0, step=0.01, advanced=True), diff --git a/comfy_extras/nodes_audio.py b/comfy_extras/nodes_audio.py index 5f514716f..f09a8a874 100644 --- a/comfy_extras/nodes_audio.py +++ b/comfy_extras/nodes_audio.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import av import torchaudio import torch @@ -33,7 +31,7 @@ class EmptyLatentAudio(IO.ComfyNode): def execute(cls, seconds, batch_size) -> IO.NodeOutput: length = round((seconds * 44100 / 2048) / 2) * 2 latent = torch.zeros([batch_size, 64, length], device=comfy.model_management.intermediate_device()) - return IO.NodeOutput({"samples":latent, "type": "audio"}) + return IO.NodeOutput({"samples": latent, "type": "audio", "downscale_ratio_temporal": 2048}) generate = execute # TODO: remove @@ -82,6 +80,8 @@ class VAEEncodeAudio(IO.ComfyNode): @classmethod def execute(cls, vae, audio) -> IO.NodeOutput: + if audio is None: + raise ValueError("VAEEncodeAudio: input audio is None (source video may have no audio track).") sample_rate = audio["sample_rate"] vae_sample_rate = getattr(vae, "audio_sample_rate", 44100) if vae_sample_rate != sample_rate: @@ -171,6 +171,8 @@ class SaveAudio(IO.ComfyNode): @classmethod def execute(cls, audio, filename_prefix="ComfyUI", format="flac") -> IO.NodeOutput: + if audio is None: + raise ValueError("SaveAudio: input audio is None (source video may have no audio track).") return IO.NodeOutput( ui=UI.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=format) ) @@ -198,6 +200,8 @@ class SaveAudioMP3(IO.ComfyNode): @classmethod def execute(cls, audio, filename_prefix="ComfyUI", format="mp3", quality="128k") -> IO.NodeOutput: + if audio is None: + raise ValueError("SaveAudioMP3: input audio is None (source video may have no audio track).") return IO.NodeOutput( ui=UI.AudioSaveHelper.get_save_audio_ui( audio, filename_prefix=filename_prefix, cls=cls, format=format, quality=quality @@ -226,6 +230,8 @@ class SaveAudioOpus(IO.ComfyNode): @classmethod def execute(cls, audio, filename_prefix="ComfyUI", format="opus", quality="V3") -> IO.NodeOutput: + if audio is None: + raise ValueError("SaveAudioOpus: input audio is None (source video may have no audio track).") return IO.NodeOutput( ui=UI.AudioSaveHelper.get_save_audio_ui( audio, filename_prefix=filename_prefix, cls=cls, format=format, quality=quality @@ -252,6 +258,8 @@ class PreviewAudio(IO.ComfyNode): @classmethod def execute(cls, audio) -> IO.NodeOutput: + if audio is None: + raise ValueError("PreviewAudio: input audio is None (source video may have no audio track).") return IO.NodeOutput(ui=UI.PreviewAudio(audio, cls=cls)) save_flac = execute # TODO: remove @@ -297,6 +305,7 @@ class LoadAudio(IO.ComfyNode): @classmethod def define_schema(cls): input_dir = folder_paths.get_input_directory() + os.makedirs(input_dir, exist_ok=True) files = folder_paths.filter_files_content_types(os.listdir(input_dir), ["audio", "video"]) return IO.Schema( node_id="LoadAudio", @@ -391,21 +400,26 @@ class TrimAudioDuration(IO.ComfyNode): @classmethod def execute(cls, audio, start_index, duration) -> IO.NodeOutput: + if audio is None: + return IO.NodeOutput(None) waveform = audio["waveform"] sample_rate = audio["sample_rate"] audio_length = waveform.shape[-1] + if audio_length == 0: + return IO.NodeOutput(audio) + if start_index < 0: start_frame = audio_length + int(round(start_index * sample_rate)) else: start_frame = int(round(start_index * sample_rate)) - start_frame = max(0, min(start_frame, audio_length - 1)) + start_frame = max(0, min(start_frame, audio_length)) end_frame = start_frame + int(round(duration * sample_rate)) end_frame = max(0, min(end_frame, audio_length)) if start_frame >= end_frame: - raise ValueError("AudioTrim: Start time must be less than end time and be within the audio length.") + raise ValueError("TrimAudioDuration: Start time must be less than end time and be within the audio length.") return IO.NodeOutput({"waveform": waveform[..., start_frame:end_frame], "sample_rate": sample_rate}) @@ -432,11 +446,13 @@ class SplitAudioChannels(IO.ComfyNode): @classmethod def execute(cls, audio) -> IO.NodeOutput: + if audio is None: + return IO.NodeOutput(None, None) waveform = audio["waveform"] sample_rate = audio["sample_rate"] if waveform.shape[1] != 2: - raise ValueError("AudioSplit: Input audio has only one channel.") + raise ValueError(f"AudioSplit: Input audio must be stereo (2 channels), got {waveform.shape[1]} channel(s).") left_channel = waveform[..., 0:1, :] right_channel = waveform[..., 1:2, :] @@ -464,6 +480,12 @@ class JoinAudioChannels(IO.ComfyNode): @classmethod def execute(cls, audio_left, audio_right) -> IO.NodeOutput: + if audio_left is None and audio_right is None: + return IO.NodeOutput(None) + if audio_left is None: + return IO.NodeOutput(audio_right) + if audio_right is None: + return IO.NodeOutput(audio_left) waveform_left = audio_left["waveform"] sample_rate_left = audio_left["sample_rate"] waveform_right = audio_right["waveform"] @@ -519,7 +541,7 @@ class AudioConcat(IO.ComfyNode): return IO.Schema( node_id="AudioConcat", search_aliases=["join audio", "combine audio", "append audio"], - display_name="Audio Concat", + display_name="Concatenate Audio", description="Concatenates the audio1 to audio2 in the specified direction.", category="audio", inputs=[ @@ -537,6 +559,12 @@ class AudioConcat(IO.ComfyNode): @classmethod def execute(cls, audio1, audio2, direction) -> IO.NodeOutput: + if audio1 is None and audio2 is None: + return IO.NodeOutput(None) + if audio1 is None: + return IO.NodeOutput(audio2) + if audio2 is None: + return IO.NodeOutput(audio1) waveform_1 = audio1["waveform"] waveform_2 = audio2["waveform"] sample_rate_1 = audio1["sample_rate"] @@ -567,7 +595,7 @@ class AudioMerge(IO.ComfyNode): return IO.Schema( node_id="AudioMerge", search_aliases=["mix audio", "overlay audio", "layer audio"], - display_name="Audio Merge", + display_name="Merge Audio", description="Combine two audio tracks by overlaying their waveforms.", category="audio", inputs=[ @@ -584,6 +612,12 @@ class AudioMerge(IO.ComfyNode): @classmethod def execute(cls, audio1, audio2, merge_method) -> IO.NodeOutput: + if audio1 is None and audio2 is None: + return IO.NodeOutput(None) + if audio1 is None: + return IO.NodeOutput(audio2) + if audio2 is None: + return IO.NodeOutput(audio1) waveform_1 = audio1["waveform"] waveform_2 = audio2["waveform"] sample_rate_1 = audio1["sample_rate"] @@ -594,6 +628,9 @@ class AudioMerge(IO.ComfyNode): length_1 = waveform_1.shape[-1] length_2 = waveform_2.shape[-1] + if length_1 == 0 or length_2 == 0: + return IO.NodeOutput({"waveform": waveform_1, "sample_rate": output_sample_rate}) + if length_2 > length_1: logging.info(f"AudioMerge: Trimming audio2 from {length_2} to {length_1} samples to match audio1 length.") waveform_2 = waveform_2[..., :length_1] @@ -628,8 +665,9 @@ class AudioAdjustVolume(IO.ComfyNode): return IO.Schema( node_id="AudioAdjustVolume", search_aliases=["audio gain", "loudness", "audio level"], - display_name="Audio Adjust Volume", + display_name="Adjust Audio Volume", category="audio", + description="Adjust the volume of the audio by a specified amount in decibels (dB).", inputs=[ IO.Audio.Input("audio"), IO.Int.Input( @@ -645,6 +683,8 @@ class AudioAdjustVolume(IO.ComfyNode): @classmethod def execute(cls, audio, volume) -> IO.NodeOutput: + if audio is None: + return IO.NodeOutput(None) if volume == 0: return IO.NodeOutput(audio) waveform = audio["waveform"] @@ -728,8 +768,14 @@ class AudioEqualizer3Band(IO.ComfyNode): @classmethod def execute(cls, audio, low_gain_dB, low_freq, mid_gain_dB, mid_freq, mid_q, high_gain_dB, high_freq) -> IO.NodeOutput: + if audio is None: + return IO.NodeOutput(None) waveform = audio["waveform"] sample_rate = audio["sample_rate"] + + if waveform.shape[-1] == 0: + return IO.NodeOutput(audio) + eq_waveform = waveform.clone() # 1. Apply Low Shelf (Bass) diff --git a/comfy_extras/nodes_audio_encoder.py b/comfy_extras/nodes_audio_encoder.py index 13aacd41a..6a85da89b 100644 --- a/comfy_extras/nodes_audio_encoder.py +++ b/comfy_extras/nodes_audio_encoder.py @@ -10,6 +10,7 @@ class AudioEncoderLoader(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="AudioEncoderLoader", + display_name="Load Audio Encoder", category="loaders", inputs=[ io.Combo.Input( diff --git a/comfy_extras/nodes_bg_removal.py b/comfy_extras/nodes_bg_removal.py new file mode 100644 index 000000000..793fd802b --- /dev/null +++ b/comfy_extras/nodes_bg_removal.py @@ -0,0 +1,61 @@ +import folder_paths +from typing_extensions import override +from comfy_api.latest import ComfyExtension, IO +from comfy.bg_removal_model import load + + +class LoadBackgroundRemovalModel(IO.ComfyNode): + @classmethod + def define_schema(cls): + files = folder_paths.get_filename_list("background_removal") + return IO.Schema( + node_id="LoadBackgroundRemovalModel", + display_name="Load Background Removal Model", + category="loaders", + inputs=[ + IO.Combo.Input("bg_removal_name", options=sorted(files), tooltip="The model used to remove backgrounds from images"), + ], + outputs=[ + IO.BackgroundRemoval.Output("bg_model") + ] + ) + @classmethod + def execute(cls, bg_removal_name): + path = folder_paths.get_full_path_or_raise("background_removal", bg_removal_name) + bg = load(path) + if bg is None: + raise RuntimeError("ERROR: background model file is invalid and does not contain a valid background removal model.") + return IO.NodeOutput(bg) + +class RemoveBackground(IO.ComfyNode): + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="RemoveBackground", + display_name="Remove Background", + category="image/background removal", + description="Generates a foreground mask to remove the background from an image using a background removal model.", + inputs=[ + IO.Image.Input("image", tooltip="Input image to remove the background from"), + IO.BackgroundRemoval.Input("bg_removal_model", tooltip="Background removal model used to generate the mask") + ], + outputs=[ + IO.Mask.Output("mask", tooltip="Generated foreground mask") + ] + ) + @classmethod + def execute(cls, image, bg_removal_model): + mask = bg_removal_model.encode_image(image) + return IO.NodeOutput(mask) + +class BackgroundRemovalExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[IO.ComfyNode]]: + return [ + LoadBackgroundRemovalModel, + RemoveBackground + ] + + +async def comfy_entrypoint() -> BackgroundRemovalExtension: + return BackgroundRemovalExtension() diff --git a/comfy_extras/nodes_camera_trajectory.py b/comfy_extras/nodes_camera_trajectory.py index e7efa29ba..34b78e81b 100644 --- a/comfy_extras/nodes_camera_trajectory.py +++ b/comfy_extras/nodes_camera_trajectory.py @@ -153,7 +153,7 @@ class WanCameraEmbedding(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="WanCameraEmbedding", - category="camera", + category="conditioning/video_models", inputs=[ io.Combo.Input( "camera_pose", diff --git a/comfy_extras/nodes_canny.py b/comfy_extras/nodes_canny.py index 648b4279d..462f6fea0 100644 --- a/comfy_extras/nodes_canny.py +++ b/comfy_extras/nodes_canny.py @@ -11,9 +11,9 @@ class Canny(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="Canny", - display_name="Canny", + display_name="Detect Edges (Canny)", search_aliases=["edge detection", "outline", "contour detection", "line art"], - category="image/preprocessors", + category="image/filters", essentials_category="Image Tools", inputs=[ io.Image.Input("image"), diff --git a/comfy_extras/nodes_cfg.py b/comfy_extras/nodes_cfg.py index 4ebb4b51e..b585c560f 100644 --- a/comfy_extras/nodes_cfg.py +++ b/comfy_extras/nodes_cfg.py @@ -57,24 +57,55 @@ class CFGNorm(io.ComfyNode): inputs=[ io.Model.Input("model"), io.Float.Input("strength", default=1.0, min=0.0, max=100.0, step=0.01), + io.Boolean.Input( + "pre_cfg", + default=False, + optional=True, + tooltip=( + "If true, rescale the combined noise BEFORE the sampler's CFG combine, " + "without clamping (can amplify). Matches the norm-scaled CFG used by " + "models like Lens. Default false keeps the original post-CFG x0-space " + "attenuate-only behavior." + ), + ), ], outputs=[io.Model.Output(display_name="patched_model")], is_experimental=True, ) @classmethod - def execute(cls, model, strength) -> io.NodeOutput: + def execute(cls, model, strength, pre_cfg=False) -> io.NodeOutput: m = model.clone() - def cfg_norm(args): - cond_p = args['cond_denoised'] - pred_text_ = args["denoised"] + if pre_cfg: + def cfg_norm_pre(args): + cond = args["cond"] + uncond = args["uncond"] + cond_scale = args["cond_scale"] + comb = uncond + cond_scale * (cond - uncond) + cond_norm = torch.linalg.vector_norm(cond, dim=1, keepdim=True) + comb_norm = torch.linalg.vector_norm(comb, dim=1, keepdim=True) + rescale = torch.where( + comb_norm > 0, + cond_norm / comb_norm.clamp_min(1e-12), + torch.ones_like(comb_norm), + ) + rescaled = comb * rescale + # strength blends back toward standard linear CFG (1.0 = full rescale). + if strength != 1.0: + rescaled = strength * rescaled + (1.0 - strength) * comb + return rescaled + m.set_model_sampler_cfg_function(cfg_norm_pre) + else: + def cfg_norm(args): + cond_p = args['cond_denoised'] + pred_text_ = args["denoised"] - norm_full_cond = torch.norm(cond_p, dim=1, keepdim=True) - norm_pred_text = torch.norm(pred_text_, dim=1, keepdim=True) - scale = (norm_full_cond / (norm_pred_text + 1e-8)).clamp(min=0.0, max=1.0) - return pred_text_ * scale * strength + norm_full_cond = torch.norm(cond_p, dim=1, keepdim=True) + norm_pred_text = torch.norm(pred_text_, dim=1, keepdim=True) + scale = (norm_full_cond / (norm_pred_text + 1e-8)).clamp(min=0.0, max=1.0) + return pred_text_ * scale * strength - m.set_model_sampler_post_cfg_function(cfg_norm) + m.set_model_sampler_post_cfg_function(cfg_norm) return io.NodeOutput(m) diff --git a/comfy_extras/nodes_compositing.py b/comfy_extras/nodes_compositing.py index 5b4423734..8fcbe720e 100644 --- a/comfy_extras/nodes_compositing.py +++ b/comfy_extras/nodes_compositing.py @@ -111,7 +111,7 @@ class PorterDuffImageComposite(io.ComfyNode): node_id="PorterDuffImageComposite", search_aliases=["alpha composite", "blend modes", "layer blend", "transparency blend"], display_name="Porter-Duff Image Composite", - category="mask/compositing", + category="image/compositing", inputs=[ io.Image.Input("source"), io.Mask.Input("source_alpha"), @@ -168,7 +168,7 @@ class SplitImageWithAlpha(io.ComfyNode): node_id="SplitImageWithAlpha", search_aliases=["extract alpha", "separate transparency", "remove alpha"], display_name="Split Image with Alpha", - category="mask/compositing", + category="image/compositing", inputs=[ io.Image.Input("image"), ], @@ -192,7 +192,7 @@ class JoinImageWithAlpha(io.ComfyNode): node_id="JoinImageWithAlpha", search_aliases=["add transparency", "apply alpha", "composite alpha", "RGBA"], display_name="Join Image with Alpha", - category="mask/compositing", + category="image/compositing", inputs=[ io.Image.Input("image"), io.Mask.Input("alpha"), @@ -203,7 +203,7 @@ class JoinImageWithAlpha(io.ComfyNode): @classmethod def execute(cls, image: torch.Tensor, alpha: torch.Tensor) -> io.NodeOutput: batch_size = max(len(image), len(alpha)) - alpha = 1.0 - resize_mask(alpha, image.shape[1:]) + alpha = 1.0 - resize_mask(alpha.to(image), image.shape[1:]) alpha = comfy.utils.repeat_to_batch_size(alpha, batch_size) image = comfy.utils.repeat_to_batch_size(image, batch_size) return io.NodeOutput(torch.cat((image[..., :3], alpha.unsqueeze(-1)), dim=-1)) diff --git a/comfy_extras/nodes_cond.py b/comfy_extras/nodes_cond.py index 86426a780..b745a43af 100644 --- a/comfy_extras/nodes_cond.py +++ b/comfy_extras/nodes_cond.py @@ -8,7 +8,7 @@ class CLIPTextEncodeControlnet(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="CLIPTextEncodeControlnet", - category="_for_testing/conditioning", + category="experimental/conditioning", inputs=[ io.Clip.Input("clip"), io.Conditioning.Input("conditioning"), @@ -35,7 +35,7 @@ class T5TokenizerOptions(io.ComfyNode): def define_schema(cls) -> io.Schema: return io.Schema( node_id="T5TokenizerOptions", - category="_for_testing/conditioning", + category="experimental/conditioning", inputs=[ io.Clip.Input("clip"), io.Int.Input("min_padding", default=0, min=0, max=10000, step=1, advanced=True), diff --git a/comfy_extras/nodes_context_windows.py b/comfy_extras/nodes_context_windows.py index f8dbc85ee..2ad5bd65b 100644 --- a/comfy_extras/nodes_context_windows.py +++ b/comfy_extras/nodes_context_windows.py @@ -1,4 +1,3 @@ -from __future__ import annotations from comfy_api.latest import ComfyExtension, io import comfy.context_windows import nodes @@ -10,7 +9,7 @@ class ContextWindowsManualNode(io.ComfyNode): return io.Schema( node_id="ContextWindowsManual", display_name="Context Windows (Manual)", - category="context", + category="model_patches", description="Manually set context windows.", inputs=[ io.Model.Input("model", tooltip="The model to apply context windows to during sampling."), diff --git a/comfy_extras/nodes_curve.py b/comfy_extras/nodes_curve.py index 9803e8034..099453131 100644 --- a/comfy_extras/nodes_curve.py +++ b/comfy_extras/nodes_curve.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import numpy as np from comfy_api.latest import ComfyExtension, io diff --git a/comfy_extras/nodes_custom_sampler.py b/comfy_extras/nodes_custom_sampler.py index 1e957c09b..10b56b91c 100644 --- a/comfy_extras/nodes_custom_sampler.py +++ b/comfy_extras/nodes_custom_sampler.py @@ -17,7 +17,7 @@ class BasicScheduler(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="BasicScheduler", - category="sampling/custom_sampling/schedulers", + category="sampling/schedulers", inputs=[ io.Model.Input("model"), io.Combo.Input("scheduler", options=comfy.samplers.SCHEDULER_NAMES), @@ -47,7 +47,7 @@ class KarrasScheduler(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="KarrasScheduler", - category="sampling/custom_sampling/schedulers", + category="sampling/schedulers", inputs=[ io.Int.Input("steps", default=20, min=1, max=10000), io.Float.Input("sigma_max", default=14.614642, min=0.0, max=5000.0, step=0.01, round=False, advanced=True), @@ -69,7 +69,7 @@ class ExponentialScheduler(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="ExponentialScheduler", - category="sampling/custom_sampling/schedulers", + category="sampling/schedulers", inputs=[ io.Int.Input("steps", default=20, min=1, max=10000), io.Float.Input("sigma_max", default=14.614642, min=0.0, max=5000.0, step=0.01, round=False, advanced=True), @@ -90,7 +90,7 @@ class PolyexponentialScheduler(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="PolyexponentialScheduler", - category="sampling/custom_sampling/schedulers", + category="sampling/schedulers", inputs=[ io.Int.Input("steps", default=20, min=1, max=10000), io.Float.Input("sigma_max", default=14.614642, min=0.0, max=5000.0, step=0.01, round=False, advanced=True), @@ -112,7 +112,7 @@ class LaplaceScheduler(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="LaplaceScheduler", - category="sampling/custom_sampling/schedulers", + category="sampling/schedulers", inputs=[ io.Int.Input("steps", default=20, min=1, max=10000), io.Float.Input("sigma_max", default=14.614642, min=0.0, max=5000.0, step=0.01, round=False, advanced=True), @@ -136,7 +136,7 @@ class SDTurboScheduler(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SDTurboScheduler", - category="sampling/custom_sampling/schedulers", + category="sampling/schedulers", inputs=[ io.Model.Input("model"), io.Int.Input("steps", default=1, min=1, max=10), @@ -160,7 +160,7 @@ class BetaSamplingScheduler(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="BetaSamplingScheduler", - category="sampling/custom_sampling/schedulers", + category="sampling/schedulers", inputs=[ io.Model.Input("model"), io.Int.Input("steps", default=20, min=1, max=10000), @@ -182,7 +182,7 @@ class VPScheduler(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="VPScheduler", - category="sampling/custom_sampling/schedulers", + category="sampling/schedulers", inputs=[ io.Int.Input("steps", default=20, min=1, max=10000), io.Float.Input("beta_d", default=19.9, min=0.0, max=5000.0, step=0.01, round=False, advanced=True), #TODO: fix default values @@ -204,7 +204,7 @@ class SplitSigmas(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SplitSigmas", - category="sampling/custom_sampling/sigmas", + category="sampling/sigmas", inputs=[ io.Sigmas.Input("sigmas"), io.Int.Input("step", default=0, min=0, max=10000), @@ -228,7 +228,7 @@ class SplitSigmasDenoise(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SplitSigmasDenoise", - category="sampling/custom_sampling/sigmas", + category="sampling/sigmas", inputs=[ io.Sigmas.Input("sigmas"), io.Float.Input("denoise", default=1.0, min=0.0, max=1.0, step=0.01), @@ -254,7 +254,7 @@ class FlipSigmas(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="FlipSigmas", - category="sampling/custom_sampling/sigmas", + category="sampling/sigmas", inputs=[io.Sigmas.Input("sigmas")], outputs=[io.Sigmas.Output()] ) @@ -276,7 +276,7 @@ class SetFirstSigma(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SetFirstSigma", - category="sampling/custom_sampling/sigmas", + category="sampling/sigmas", inputs=[ io.Sigmas.Input("sigmas"), io.Float.Input("sigma", default=136.0, min=0.0, max=20000.0, step=0.001, round=False), @@ -298,7 +298,7 @@ class ExtendIntermediateSigmas(io.ComfyNode): return io.Schema( node_id="ExtendIntermediateSigmas", search_aliases=["interpolate sigmas"], - category="sampling/custom_sampling/sigmas", + category="sampling/sigmas", inputs=[ io.Sigmas.Input("sigmas"), io.Int.Input("steps", default=2, min=1, max=100), @@ -351,7 +351,7 @@ class SamplingPercentToSigma(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SamplingPercentToSigma", - category="sampling/custom_sampling/sigmas", + category="sampling/sigmas", inputs=[ io.Model.Input("model"), io.Float.Input("sampling_percent", default=0.0, min=0.0, max=1.0, step=0.0001), @@ -379,7 +379,7 @@ class KSamplerSelect(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="KSamplerSelect", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[io.Combo.Input("sampler_name", options=comfy.samplers.SAMPLER_NAMES)], outputs=[io.Sampler.Output()] ) @@ -396,7 +396,7 @@ class SamplerDPMPP_3M_SDE(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SamplerDPMPP_3M_SDE", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Float.Input("eta", default=1.0, min=0.0, max=100.0, step=0.01, round=False, advanced=True), io.Float.Input("s_noise", default=1.0, min=0.0, max=100.0, step=0.01, round=False, advanced=True), @@ -421,7 +421,7 @@ class SamplerDPMPP_2M_SDE(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SamplerDPMPP_2M_SDE", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Combo.Input("solver_type", options=['midpoint', 'heun']), io.Float.Input("eta", default=1.0, min=0.0, max=100.0, step=0.01, round=False, advanced=True), @@ -448,7 +448,7 @@ class SamplerDPMPP_SDE(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SamplerDPMPP_SDE", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Float.Input("eta", default=1.0, min=0.0, max=100.0, step=0.01, round=False, advanced=True), io.Float.Input("s_noise", default=1.0, min=0.0, max=100.0, step=0.01, round=False, advanced=True), @@ -474,7 +474,7 @@ class SamplerDPMPP_2S_Ancestral(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SamplerDPMPP_2S_Ancestral", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Float.Input("eta", default=1.0, min=0.0, max=100.0, step=0.01, round=False), io.Float.Input("s_noise", default=1.0, min=0.0, max=100.0, step=0.01, round=False), @@ -494,7 +494,7 @@ class SamplerEulerAncestral(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SamplerEulerAncestral", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Float.Input("eta", default=1.0, min=0.0, max=100.0, step=0.01, round=False, advanced=True), io.Float.Input("s_noise", default=1.0, min=0.0, max=100.0, step=0.01, round=False, advanced=True), @@ -515,7 +515,7 @@ class SamplerEulerAncestralCFGPP(io.ComfyNode): return io.Schema( node_id="SamplerEulerAncestralCFGPP", display_name="SamplerEulerAncestralCFG++", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Float.Input("eta", default=1.0, min=0.0, max=1.0, step=0.01, round=False), io.Float.Input("s_noise", default=1.0, min=0.0, max=10.0, step=0.01, round=False), @@ -537,7 +537,7 @@ class SamplerLMS(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SamplerLMS", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[io.Int.Input("order", default=4, min=1, max=100, advanced=True)], outputs=[io.Sampler.Output()] ) @@ -554,7 +554,7 @@ class SamplerDPMAdaptative(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SamplerDPMAdaptative", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Int.Input("order", default=3, min=2, max=3, advanced=True), io.Float.Input("rtol", default=0.05, min=0.0, max=100.0, step=0.01, round=False, advanced=True), @@ -585,7 +585,7 @@ class SamplerER_SDE(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SamplerER_SDE", - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Combo.Input("solver_type", options=["ER-SDE", "Reverse-time SDE", "ODE"]), io.Int.Input("max_stage", default=3, min=1, max=3, advanced=True), @@ -623,7 +623,7 @@ class SamplerSASolver(io.ComfyNode): return io.Schema( node_id="SamplerSASolver", search_aliases=["sde"], - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Model.Input("model"), io.Float.Input("eta", default=1.0, min=0.0, max=10.0, step=0.01, round=False, advanced=True), @@ -668,7 +668,7 @@ class SamplerSEEDS2(io.ComfyNode): return io.Schema( node_id="SamplerSEEDS2", search_aliases=["sde", "exp heun"], - category="sampling/custom_sampling/samplers", + category="sampling/samplers", inputs=[ io.Combo.Input("solver_type", options=["phi_1", "phi_2"]), io.Float.Input("eta", default=1.0, min=0.0, max=100.0, step=0.01, round=False, tooltip="Stochastic strength", advanced=True), @@ -750,7 +750,7 @@ class SamplerCustom(io.ComfyNode): latent = latent_image latent_image = latent["samples"] latent = latent.copy() - latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image, latent.get("downscale_ratio_spacial", None)) + latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image, latent.get("downscale_ratio_spacial", None), latent.get("downscale_ratio_temporal", None)) latent["samples"] = latent_image if not add_noise: @@ -770,6 +770,7 @@ class SamplerCustom(io.ComfyNode): out = latent.copy() out.pop("downscale_ratio_spacial", None) + out.pop("downscale_ratio_temporal", None) out["samples"] = samples if "x0" in x0_output: x0_out = model.model.process_latent_out(x0_output["x0"].cpu()) @@ -793,7 +794,8 @@ class BasicGuider(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="BasicGuider", - category="sampling/custom_sampling/guiders", + display_name="Basic Guider", + category="sampling/guiders", inputs=[ io.Model.Input("model"), io.Conditioning.Input("conditioning"), @@ -814,7 +816,8 @@ class CFGGuider(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="CFGGuider", - category="sampling/custom_sampling/guiders", + display_name="CFG Guider", + category="sampling/guiders", inputs=[ io.Model.Input("model"), io.Conditioning.Input("positive"), @@ -868,7 +871,8 @@ class DualCFGGuider(io.ComfyNode): return io.Schema( node_id="DualCFGGuider", search_aliases=["dual prompt guidance"], - category="sampling/custom_sampling/guiders", + display_name="Dual CFG Guider", + category="sampling/guiders", inputs=[ io.Model.Input("model"), io.Conditioning.Input("cond1"), @@ -896,7 +900,7 @@ class DisableNoise(io.ComfyNode): return io.Schema( node_id="DisableNoise", search_aliases=["zero noise"], - category="sampling/custom_sampling/noise", + category="sampling/noise", inputs=[], outputs=[io.Noise.Output()] ) @@ -913,7 +917,7 @@ class RandomNoise(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="RandomNoise", - category="sampling/custom_sampling/noise", + category="sampling/noise", inputs=[io.Int.Input("noise_seed", default=0, min=0, max=0xffffffffffffffff, control_after_generate=True)], outputs=[io.Noise.Output()] ) @@ -949,7 +953,7 @@ class SamplerCustomAdvanced(io.ComfyNode): latent = latent_image latent_image = latent["samples"] latent = latent.copy() - latent_image = comfy.sample.fix_empty_latent_channels(guider.model_patcher, latent_image, latent.get("downscale_ratio_spacial", None)) + latent_image = comfy.sample.fix_empty_latent_channels(guider.model_patcher, latent_image, latent.get("downscale_ratio_spacial", None), latent.get("downscale_ratio_temporal", None)) latent["samples"] = latent_image noise_mask = None @@ -965,6 +969,7 @@ class SamplerCustomAdvanced(io.ComfyNode): out = latent.copy() out.pop("downscale_ratio_spacial", None) + out.pop("downscale_ratio_temporal", None) out["samples"] = samples if "x0" in x0_output: x0_out = guider.model_patcher.model.process_latent_out(x0_output["x0"].cpu()) @@ -984,7 +989,7 @@ class AddNoise(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="AddNoise", - category="_for_testing/custom_sampling/noise", + category="experimental/custom_sampling/noise", is_experimental=True, inputs=[ io.Model.Input("model"), @@ -1034,7 +1039,7 @@ class ManualSigmas(io.ComfyNode): return io.Schema( node_id="ManualSigmas", search_aliases=["custom noise schedule", "define sigmas"], - category="_for_testing/custom_sampling", + category="experimental/custom_sampling", is_experimental=True, inputs=[ io.String.Input("sigmas", default="1, 0.5", multiline=False) diff --git a/comfy_extras/nodes_dataset.py b/comfy_extras/nodes_dataset.py index 98ed25d7e..22f5ff203 100644 --- a/comfy_extras/nodes_dataset.py +++ b/comfy_extras/nodes_dataset.py @@ -47,8 +47,10 @@ class LoadImageDataSetFromFolderNode(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="LoadImageDataSetFromFolder", - display_name="Load Image Dataset from Folder", - category="dataset", + search_aliases=["load folder", "load from folder", "load dataset", "load images", "import dataset"], + display_name="Load Image (from Folder)", + category="image", + description="Load a dataset of images from a specified folder and return a list of images. Supported formats: PNG, JPG, JPEG, WEBP.", is_experimental=True, inputs=[ io.Combo.Input( @@ -84,14 +86,16 @@ class LoadImageTextDataSetFromFolderNode(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="LoadImageTextDataSetFromFolder", - display_name="Load Image and Text Dataset from Folder", - category="dataset", + search_aliases=["load folder", "load from folder", "load dataset", "load images", "import dataset"], + display_name="Load Image-Text (from Folder)", + category="image", + description="Load a dataset of pairs of images and text captions from a specified folder and return them as a list. Supported formats: PNG, JPG, JPEG, WEBP.", is_experimental=True, inputs=[ io.Combo.Input( "folder", options=folder_paths.get_input_subfolders(), - tooltip="The folder to load images from.", + tooltip="The folder to load images and text captions from.", ) ], outputs=[ @@ -206,8 +210,10 @@ class SaveImageDataSetToFolderNode(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SaveImageDataSetToFolder", - display_name="Save Image Dataset to Folder", - category="dataset", + search_aliases=["save folder", "save to folder", "save dataset", "save images", "export dataset"], + display_name="Save Image (to Folder) (DEPRECATED)", + category="image", + description="Save a dataset of images to a specified folder. Supported formats: PNG.", is_experimental=True, is_output_node=True, is_input_list=True, # Receive images as list @@ -226,6 +232,7 @@ class SaveImageDataSetToFolderNode(io.ComfyNode): ), ], outputs=[], + is_deprecated=True, # This node is redundant and superseded by existing Save Image nodes where the target folder can be specified in the filename_prefix ) @classmethod @@ -246,14 +253,20 @@ class SaveImageTextDataSetToFolderNode(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SaveImageTextDataSetToFolder", - display_name="Save Image and Text Dataset to Folder", - category="dataset", + search_aliases=["save folder", "save to folder", "save dataset", "save images", "save text", "export dataset"], + display_name="Save Image-Text (to Folder)", + category="image", + description="Save a dataset of pairs of images and text captions to a specified folder. Images are saved as PNG files and captions are saved as TXT files with the same filename_prefix.", is_experimental=True, is_output_node=True, is_input_list=True, # Receive both images and texts as lists inputs=[ io.Image.Input("images", tooltip="List of images to save."), - io.String.Input("texts", tooltip="List of text captions to save."), + io.String.Input("texts", + optional=True, + force_input=True, + tooltip="List of text captions to save." + ), io.String.Input( "folder_name", default="dataset", @@ -270,7 +283,7 @@ class SaveImageTextDataSetToFolderNode(io.ComfyNode): ) @classmethod - def execute(cls, images, texts, folder_name, filename_prefix): + def execute(cls, images, folder_name, filename_prefix, texts=None): # Extract scalar values folder_name = folder_name[0] filename_prefix = filename_prefix[0] @@ -279,11 +292,12 @@ class SaveImageTextDataSetToFolderNode(io.ComfyNode): saved_files = save_images_to_folder(images, output_dir, filename_prefix) # Save captions - for idx, (filename, caption) in enumerate(zip(saved_files, texts)): - caption_filename = filename.replace(".png", ".txt") - caption_path = os.path.join(output_dir, caption_filename) - with open(caption_path, "w", encoding="utf-8") as f: - f.write(caption) + if texts: + for idx, (filename, caption) in enumerate(zip(saved_files, texts)): + caption_filename = filename.replace(".png", ".txt") + caption_path = os.path.join(output_dir, caption_filename) + with open(caption_path, "w", encoding="utf-8") as f: + f.write(caption) logging.info(f"Saved {len(saved_files)} images and captions to {output_dir}.") return io.NodeOutput() @@ -314,11 +328,13 @@ class ImageProcessingNode(io.ComfyNode): Child classes should set: node_id: Unique node identifier (required) + search_aliases: List of search aliases (optional) display_name: Display name (optional, defaults to node_id) description: Node description (optional) extra_inputs: List of additional io.Input objects beyond "images" (optional) is_group_process: None (auto-detect), True (group), or False (individual) (optional) is_output_list: True (list output) or False (single output) (optional, default True) + is_deprecated: True if the node is deprecated (optional, default False) Child classes must implement ONE of: _process(cls, image, **kwargs) -> tensor (for single-item processing) @@ -326,12 +342,13 @@ class ImageProcessingNode(io.ComfyNode): """ node_id = None + search_aliases = [] display_name = None description = None extra_inputs = [] is_group_process = None # None = auto-detect, True/False = explicit is_output_list = None # None = auto-detect based on processing mode - + is_deprecated = False @classmethod def _detect_processing_mode(cls): """Detect whether this node uses group or individual processing. @@ -402,8 +419,10 @@ class ImageProcessingNode(io.ComfyNode): return io.Schema( node_id=cls.node_id, + search_aliases=cls.search_aliases, display_name=cls.display_name or cls.node_id, - category="dataset/image", + category=cls.category, + description=cls.description, is_experimental=True, is_input_list=is_group, # True for group, False for individual inputs=inputs, @@ -472,11 +491,13 @@ class TextProcessingNode(io.ComfyNode): Child classes should set: node_id: Unique node identifier (required) + search_aliases: List of search aliases (optional) display_name: Display name (optional, defaults to node_id) description: Node description (optional) extra_inputs: List of additional io.Input objects beyond "texts" (optional) is_group_process: None (auto-detect), True (group), or False (individual) (optional) is_output_list: True (list output) or False (single output) (optional, default True) + is_deprecated: True if the node is deprecated (optional, default False) Child classes must implement ONE of: _process(cls, text, **kwargs) -> str (for single-item processing) @@ -484,12 +505,13 @@ class TextProcessingNode(io.ComfyNode): """ node_id = None + search_aliases = [] display_name = None description = None extra_inputs = [] is_group_process = None # None = auto-detect, True/False = explicit is_output_list = None # None = auto-detect based on processing mode - + is_deprecated = False @classmethod def _detect_processing_mode(cls): """Detect whether this node uses group or individual processing. @@ -627,15 +649,17 @@ class TextProcessingNode(io.ComfyNode): class ResizeImagesByShorterEdgeNode(ImageProcessingNode): node_id = "ResizeImagesByShorterEdge" - display_name = "Resize Images by Shorter Edge" - description = "Resize images so that the shorter edge matches the specified length while preserving aspect ratio." + display_name = "Resize Images by Shorter Edge (DEPRECATED)" + category = "image/transform" + description = "Resize images so that the shorter edge matches the specified dimension while preserving aspect ratio." + is_deprecated = True # This node is superseded by Resize Image/Mask with resize_type = scale shorter dimension extra_inputs = [ io.Int.Input( "shorter_edge", default=512, min=1, max=8192, - tooltip="Target length for the shorter edge.", + tooltip="Target dimension for the shorter edge.", ), ] @@ -655,15 +679,17 @@ class ResizeImagesByShorterEdgeNode(ImageProcessingNode): class ResizeImagesByLongerEdgeNode(ImageProcessingNode): node_id = "ResizeImagesByLongerEdge" - display_name = "Resize Images by Longer Edge" - description = "Resize images so that the longer edge matches the specified length while preserving aspect ratio." + display_name = "Resize Images by Longer Edge (DEPRECATED)" + category = "image/transform" + description = "Resize images so that the longer edge matches the specified dimension while preserving aspect ratio." + is_deprecated = True # This node is superseded by Resize Image/Mask with resize_type = scale longer dimension extra_inputs = [ io.Int.Input( "longer_edge", default=1024, min=1, max=8192, - tooltip="Target length for the longer edge.", + tooltip="Target dimension for the longer edge.", ), ] @@ -686,8 +712,10 @@ class ResizeImagesByLongerEdgeNode(ImageProcessingNode): class CenterCropImagesNode(ImageProcessingNode): node_id = "CenterCropImages" - display_name = "Center Crop Images" - description = "Center crop all images to the specified dimensions." + search_aliases=["crop", "cut", "trim"] + display_name="Crop Image (Center)" + category="image/transform" + description = "Center crop an image to the specified dimensions." extra_inputs = [ io.Int.Input("width", default=512, min=1, max=8192, tooltip="Crop width."), io.Int.Input("height", default=512, min=1, max=8192, tooltip="Crop height."), @@ -706,10 +734,11 @@ class CenterCropImagesNode(ImageProcessingNode): class RandomCropImagesNode(ImageProcessingNode): node_id = "RandomCropImages" - display_name = "Random Crop Images" - description = ( - "Randomly crop all images to the specified dimensions (for data augmentation)." - ) + search_aliases=["crop", "cut", "trim"] + display_name = "Crop Image (Random)" + category="image/transform" + description = "Randomly crop an image to the specified dimensions." + extra_inputs = [ io.Int.Input("width", default=512, min=1, max=8192, tooltip="Crop width."), io.Int.Input("height", default=512, min=1, max=8192, tooltip="Crop height."), @@ -734,7 +763,9 @@ class RandomCropImagesNode(ImageProcessingNode): class NormalizeImagesNode(ImageProcessingNode): node_id = "NormalizeImages" - display_name = "Normalize Images" + search_aliases=["normalize", "normalize colors"] + display_name = "Normalize Image Colors" + category = "image/color" description = "Normalize images using mean and standard deviation." extra_inputs = [ io.Float.Input( @@ -762,8 +793,10 @@ class NormalizeImagesNode(ImageProcessingNode): class AdjustBrightnessNode(ImageProcessingNode): node_id = "AdjustBrightness" + search_aliases=["brightness"] display_name = "Adjust Brightness" - description = "Adjust brightness of all images." + category="image/adjustments" + description = "Adjust the brightness of an image." extra_inputs = [ io.Float.Input( "factor", @@ -781,8 +814,10 @@ class AdjustBrightnessNode(ImageProcessingNode): class AdjustContrastNode(ImageProcessingNode): node_id = "AdjustContrast" + search_aliases=["contrast"] display_name = "Adjust Contrast" - description = "Adjust contrast of all images." + category="image/adjustments" + description = "Adjust the contrast of an image." extra_inputs = [ io.Float.Input( "factor", @@ -800,8 +835,10 @@ class AdjustContrastNode(ImageProcessingNode): class ShuffleDatasetNode(ImageProcessingNode): node_id = "ShuffleDataset" - display_name = "Shuffle Image Dataset" - description = "Randomly shuffle the order of images in the dataset." + search_aliases=["shuffle", "randomize", "mix"] + display_name = "Shuffle Images List" + category = "image/batch" + description = "Randomly shuffle the order of images in a list." is_group_process = True # Requires full list to shuffle extra_inputs = [ io.Int.Input( @@ -823,13 +860,15 @@ class ShuffleImageTextDatasetNode(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="ShuffleImageTextDataset", - display_name="Shuffle Image-Text Dataset", - category="dataset/image", + search_aliases=["shuffle", "randomize", "mix"], + display_name = "Shuffle Pairs of Image-Text", + category = "image/batch", + description = "Randomly shuffle the order of pairs of image-text in a list.", is_experimental=True, is_input_list=True, inputs=[ io.Image.Input("images", tooltip="List of images to shuffle."), - io.String.Input("texts", tooltip="List of texts to shuffle."), + io.String.Input("texts", tooltip="List of texts to shuffle.", force_input=True), io.Int.Input( "seed", default=0, @@ -865,8 +904,11 @@ class ShuffleImageTextDatasetNode(io.ComfyNode): class TextToLowercaseNode(TextProcessingNode): node_id = "TextToLowercase" - display_name = "Text to Lowercase" - description = "Convert all texts to lowercase." + search_aliases=["lowercase"] + display_name = "Convert Text to Lowercase (DEPRECATED)" + category = "text" + description = "Convert text to lowercase." + is_deprecated = True # This node is superseded by the Convert Text Case node @classmethod def _process(cls, text): @@ -875,8 +917,11 @@ class TextToLowercaseNode(TextProcessingNode): class TextToUppercaseNode(TextProcessingNode): node_id = "TextToUppercase" - display_name = "Text to Uppercase" - description = "Convert all texts to uppercase." + search_aliases=["uppercase"] + display_name = "Convert Text to Uppercase (DEPRECATED)" + category = "text" + description = "Convert text to uppercase." + is_deprecated = True # This node is superseded by the Convert Text Case node @classmethod def _process(cls, text): @@ -885,8 +930,10 @@ class TextToUppercaseNode(TextProcessingNode): class TruncateTextNode(TextProcessingNode): node_id = "TruncateText" + search_aliases=["truncate", "cut", "shorten"] display_name = "Truncate Text" - description = "Truncate all texts to a maximum length." + category = "text" + description = "Truncate text to a maximum length." extra_inputs = [ io.Int.Input( "max_length", default=77, min=1, max=10000, tooltip="Maximum text length." @@ -900,8 +947,10 @@ class TruncateTextNode(TextProcessingNode): class AddTextPrefixNode(TextProcessingNode): node_id = "AddTextPrefix" - display_name = "Add Text Prefix" + display_name = "Add Text Prefix (DEPRECATED)" + category = "text" description = "Add a prefix to all texts." + is_deprecated = True # This node is superseded by the Concatenate Text node extra_inputs = [ io.String.Input("prefix", default="", tooltip="Prefix to add."), ] @@ -913,8 +962,10 @@ class AddTextPrefixNode(TextProcessingNode): class AddTextSuffixNode(TextProcessingNode): node_id = "AddTextSuffix" - display_name = "Add Text Suffix" + display_name = "Add Text Suffix (DEPRECATED)" + category = "text" description = "Add a suffix to all texts." + is_deprecated = True # This node is superseded by the Concatenate Text node extra_inputs = [ io.String.Input("suffix", default="", tooltip="Suffix to add."), ] @@ -926,8 +977,10 @@ class AddTextSuffixNode(TextProcessingNode): class ReplaceTextNode(TextProcessingNode): node_id = "ReplaceText" - display_name = "Replace Text" + display_name = "Replace Text (DEPRECATED)" + category = "text" description = "Replace text in all texts." + is_deprecated = True # This node is superseded by the other Replace Text node extra_inputs = [ io.String.Input("find", default="", tooltip="Text to find."), io.String.Input("replace", default="", tooltip="Text to replace with."), @@ -940,8 +993,10 @@ class ReplaceTextNode(TextProcessingNode): class StripWhitespaceNode(TextProcessingNode): node_id = "StripWhitespace" - display_name = "Strip Whitespace" + display_name = "Strip Whitespace (DEPRECATED)" + category = "text" description = "Strip leading and trailing whitespace from all texts." + is_deprecated = True # This node is superseded by the Trim Text node @classmethod def _process(cls, text): @@ -952,11 +1007,13 @@ class StripWhitespaceNode(TextProcessingNode): class ImageDeduplicationNode(ImageProcessingNode): - """Remove duplicate or very similar images from the dataset using perceptual hashing.""" + """Remove duplicate or very similar images from a list using perceptual hashing.""" node_id = "ImageDeduplication" - display_name = "Image Deduplication" - description = "Remove duplicate or very similar images from the dataset." + search_aliases=["deduplicate", "remove duplicates", "similarity filter"] + display_name = "Deduplicate Images" + category = "image/batch" + description = "Remove duplicate or very similar images from a list." is_group_process = True # Requires full list to compare images extra_inputs = [ io.Float.Input( @@ -1026,7 +1083,9 @@ class ImageGridNode(ImageProcessingNode): """Combine multiple images into a single grid/collage.""" node_id = "ImageGrid" - display_name = "Image Grid" + search_aliases=["grid", "collage", "combine"] + display_name = "Make Image Grid" + category="image/batch" description = "Arrange multiple images into a grid layout." is_group_process = True # Requires full list to create grid is_output_list = False # Outputs single grid image @@ -1102,9 +1161,12 @@ class MergeImageListsNode(ImageProcessingNode): """Merge multiple image lists into a single list.""" node_id = "MergeImageLists" - display_name = "Merge Image Lists" + search_aliases=["list", "merge list", "make list"] + display_name = "Merge Image Lists (DEPRECATED)" + category = "image/batch" description = "Concatenate multiple image lists into one." is_group_process = True # Receives images as list + is_deprecated = True # This node is superseded by the Create List node @classmethod def _group_process(cls, images): @@ -1119,9 +1181,11 @@ class MergeTextListsNode(TextProcessingNode): """Merge multiple text lists into a single list.""" node_id = "MergeTextLists" - display_name = "Merge Text Lists" + display_name = "Merge Text Lists (DEPRECATED)" + category = "text" description = "Concatenate multiple text lists into one." is_group_process = True # Receives texts as list + is_deprecated = True # This node is superseded by the Create List node @classmethod def _group_process(cls, texts): @@ -1142,8 +1206,10 @@ class ResolutionBucket(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="ResolutionBucket", + search_aliases=["bucket by resolution", "group by resolution", "batch by resolution"], display_name="Resolution Bucket", - category="dataset", + category="training", + description="Group latents and conditionings into buckets", is_experimental=True, is_input_list=True, inputs=[ @@ -1236,7 +1302,8 @@ class MakeTrainingDataset(io.ComfyNode): node_id="MakeTrainingDataset", search_aliases=["encode dataset"], display_name="Make Training Dataset", - category="dataset", + category="training", + description="Encode images with VAE and texts with CLIP to create a training dataset of latents and conditionings.", is_experimental=True, is_input_list=True, # images and texts as lists inputs=[ @@ -1251,6 +1318,7 @@ class MakeTrainingDataset(io.ComfyNode): "texts", optional=True, tooltip="List of text captions. Can be length n (matching images), 1 (repeated for all), or omitted (uses empty string).", + force_input=True ), ], outputs=[ @@ -1320,9 +1388,10 @@ class SaveTrainingDataset(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="SaveTrainingDataset", - search_aliases=["export training data"], + search_aliases=["export dataset", "save dataset"], display_name="Save Training Dataset", - category="dataset", + category="training", + description="Save encoded training dataset (latents + conditioning) to disk for efficient loading during training.", is_experimental=True, is_output_node=True, is_input_list=True, # Receive lists @@ -1424,7 +1493,8 @@ class LoadTrainingDataset(io.ComfyNode): node_id="LoadTrainingDataset", search_aliases=["import dataset", "training data"], display_name="Load Training Dataset", - category="dataset", + category="training", + description="Load encoded training dataset (latents + conditioning) from disk for use in training.", is_experimental=True, inputs=[ io.String.Input( diff --git a/comfy_extras/nodes_differential_diffusion.py b/comfy_extras/nodes_differential_diffusion.py index 34ffb9a89..4fa61ad0e 100644 --- a/comfy_extras/nodes_differential_diffusion.py +++ b/comfy_extras/nodes_differential_diffusion.py @@ -13,7 +13,7 @@ class DifferentialDiffusion(io.ComfyNode): node_id="DifferentialDiffusion", search_aliases=["inpaint gradient", "variable denoise strength"], display_name="Differential Diffusion", - category="_for_testing", + category="experimental", inputs=[ io.Model.Input("model"), io.Float.Input( diff --git a/comfy_extras/nodes_flux.py b/comfy_extras/nodes_flux.py index 3a23c7d04..997f21c09 100644 --- a/comfy_extras/nodes_flux.py +++ b/comfy_extras/nodes_flux.py @@ -102,7 +102,7 @@ class FluxDisableGuidance(io.ComfyNode): append = execute # TODO: remove -PREFERED_KONTEXT_RESOLUTIONS = [ +PREFERRED_KONTEXT_RESOLUTIONS = [ (672, 1568), (688, 1504), (720, 1456), @@ -143,7 +143,7 @@ class FluxKontextImageScale(io.ComfyNode): width = image.shape[2] height = image.shape[1] aspect_ratio = width / height - _, width, height = min((abs(aspect_ratio - w / h), w, h) for w, h in PREFERED_KONTEXT_RESOLUTIONS) + _, width, height = min((abs(aspect_ratio - w / h), w, h) for w, h in PREFERRED_KONTEXT_RESOLUTIONS) image = comfy.utils.common_upscale(image.movedim(-1, 1), width, height, "lanczos", "center").movedim(1, -1) return io.NodeOutput(image) @@ -215,7 +215,7 @@ class Flux2Scheduler(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="Flux2Scheduler", - category="sampling/custom_sampling/schedulers", + category="sampling/schedulers", inputs=[ io.Int.Input("steps", default=20, min=1, max=4096), io.Int.Input("width", default=1024, min=16, max=nodes.MAX_RESOLUTION, step=1), @@ -263,7 +263,7 @@ class FluxKVCache(io.ComfyNode): node_id="FluxKVCache", display_name="Flux KV Cache", description="Enables KV Cache optimization for reference images on Flux family models.", - category="", + category="experimental", is_experimental=True, inputs=[ io.Model.Input("model", tooltip="The model to use KV Cache on."), diff --git a/comfy_extras/nodes_fresca.py b/comfy_extras/nodes_fresca.py index eab4f303f..173f42154 100644 --- a/comfy_extras/nodes_fresca.py +++ b/comfy_extras/nodes_fresca.py @@ -60,7 +60,7 @@ class FreSca(io.ComfyNode): node_id="FreSca", search_aliases=["frequency guidance"], display_name="FreSca", - category="_for_testing", + category="experimental", description="Applies frequency-dependent scaling to the guidance", inputs=[ io.Model.Input("model"), diff --git a/comfy_extras/nodes_gits.py b/comfy_extras/nodes_gits.py index d48483862..0b7666524 100644 --- a/comfy_extras/nodes_gits.py +++ b/comfy_extras/nodes_gits.py @@ -340,7 +340,7 @@ class GITSScheduler(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="GITSScheduler", - category="sampling/custom_sampling/schedulers", + category="sampling/schedulers", inputs=[ io.Float.Input("coeff", default=1.20, min=0.80, max=1.50, step=0.05, advanced=True), io.Int.Input("steps", default=10, min=2, max=1000), diff --git a/comfy_extras/nodes_hidream_o1.py b/comfy_extras/nodes_hidream_o1.py new file mode 100644 index 000000000..f393745f6 --- /dev/null +++ b/comfy_extras/nodes_hidream_o1.py @@ -0,0 +1,256 @@ +from typing_extensions import override + +import torch + +import comfy.model_management +import comfy.patcher_extension +import node_helpers +from comfy_api.latest import ComfyExtension, io + + +class EmptyHiDreamO1LatentImage(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="EmptyHiDreamO1LatentImage", + display_name="Empty HiDream-O1 Latent Image", + category="latent/image", + description=( + "Empty pixel-space latent for HiDream-O1-Image. The model was " + "trained at ~4 megapixels; lower resolutions go off-distribution " + "and quality regresses noticeably. Trained resolutions: " + "2048x2048, 2304x1728, 1728x2304, 2560x1440, 1440x2560, " + "2496x1664, 1664x2496, 3104x1312, 1312x3104, 2304x1792, 1792x2304." + ), + inputs=[ + io.Int.Input(id="width", default=2048, min=64, max=4096, step=32), + io.Int.Input(id="height", default=2048, min=64, max=4096, step=32), + io.Int.Input(id="batch_size", default=1, min=1, max=64), + ], + outputs=[io.Latent().Output()], + ) + + @classmethod + def execute(cls, *, width: int, height: int, batch_size: int = 1) -> io.NodeOutput: + latent = torch.zeros( + (batch_size, 3, height, width), + device=comfy.model_management.intermediate_device(), + ) + return io.NodeOutput({"samples": latent}) + + +class HiDreamO1ReferenceImages(io.ComfyNode): + """Attach reference images to both positive and negative conditioning.""" + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="HiDreamO1ReferenceImages", + display_name="HiDream-O1 Reference Images", + category="conditioning/image", + description=( + "Attach 1-10 reference images to conditioning, one for edit instruction" + "or multiple for subject-driven personalization." + ), + inputs=[ + io.Conditioning.Input(id="positive"), + io.Conditioning.Input(id="negative"), + io.Autogrow.Input( + "images", + template=io.Autogrow.TemplateNames( + io.Image.Input("image"), + names=[f"image_{i}" for i in range(1, 11)], + min=1, + ), + tooltip=("Reference images. 1 image = instruction edit; 2-10 images = multi reference." + ), + ), + ], + outputs=[ + io.Conditioning.Output(display_name="positive"), + io.Conditioning.Output(display_name="negative"), + ], + ) + + @classmethod + def execute(cls, *, positive, negative, images: io.Autogrow.Type) -> io.NodeOutput: + refs = [images[f"image_{i}"] for i in range(1, 11) if f"image_{i}" in images] + positive = node_helpers.conditioning_set_values(positive, {"reference_latents": refs}, append=True) + negative = node_helpers.conditioning_set_values(negative, {"reference_latents": refs}, append=True) + return io.NodeOutput(positive, negative) + + +class HiDreamO1PatchSeamSmoothing(io.ComfyNode): + PATCH_SIZE = 32 + EDGE_FEATHER = 4 + + # Shift presets per (pattern, N). 8-pass = 4-quadrant + 4 quarter-patch offsets. + SHIFTS_BY_PATTERN = { + ("single_shift", 2): [(0, 0), (16, 16)], + ("single_shift", 4): [(0, 0), (16, 0), (0, 16), (16, 16)], + ("single_shift", 8): [(0, 0), (16, 0), (0, 16), (16, 16), + (8, 8), (24, 8), (8, 24), (24, 24)], + ("symmetric", 2): [(-8, -8), (8, 8)], + ("symmetric", 4): [(-8, -8), (8, -8), (-8, 8), (8, 8)], + ("symmetric", 8): [(-12, -12), (4, -12), (-12, 4), (4, 4), + (-4, -4), (12, -4), (-4, 12), (12, 12)], + } + RAMP_LEVELS = { + "2": [2], + "4": [4], + "ramp_2_4": [2, 4], + "ramp_2_4_8": [2, 4, 8], + } + + @staticmethod + def _hann_tile(cy: int, cx: int, size: int = 32) -> torch.Tensor: + """size x size Hann tile peaking at (cy, cx) within a patch.""" + half = size // 2 + yy = torch.arange(size).view(size, 1) + xx = torch.arange(size).view(1, size) + dy = ((yy - cy + half) % size) - half + dx = ((xx - cx + half) % size) - half + return 0.25 * (1 + torch.cos(torch.pi * dy / half)) * (1 + torch.cos(torch.pi * dx / half)) + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="HiDreamO1PatchSeamSmoothing", + display_name="HiDream-O1 Patch Seam Smoothing", + category="advanced/model", + is_experimental=True, + description=( + "Average the model output across multiple shifted patch-grid " + "positions during the late portion of sampling. Cancels seams." + ), + inputs=[ + io.Model.Input(id="model"), + io.Float.Input(id="start_percent", default=0.8, min=0.0, max=1.0, step=0.01, + tooltip="Sampling progress (0=start, 1=end) at which the blend turns ON.", + ), + io.Float.Input(id="end_percent", default=1.0, min=0.0, max=1.0, step=0.01, + tooltip="Sampling progress at which the blend turns OFF.", + ), + io.Combo.Input( + id="pattern", + options=["single_shift", "symmetric"], + default="single_shift", + tooltip="Shift layout. single_shift: one pass at the natural patch grid + others offset. symmetric: all passes off-grid, shifts split around origin.", + ), + io.Combo.Input( + id="passes", + options=["2", "4", "ramp_2_4", "ramp_2_4_8"], + default="2", + tooltip="Number of passes per gated step. 2/4 = fixed. ramp_*: pass count increases as sampling approaches end (more smoothing where seams are most visible).", + ), + io.Combo.Input( + id="blend", + options=["average", "window", "median"], + default="average", + tooltip="average: equal-weight mean. window: Hann-windowed weighting favoring each pass away from its patch boundaries. median: per-pixel median, rejects wraparound-outlier passes.", + ), + io.Float.Input(id="strength", default=1.0, min=0.0, max=1.0, step=0.01, + tooltip="Interpolation between the natural-grid pred (0) and the averaged result (1).", + ), + ], + outputs=[io.Model.Output()], + ) + + @classmethod + def execute(cls, *, model, start_percent: float, end_percent: float, pattern: str, passes: str, blend: str, strength: float) -> io.NodeOutput: + if strength <= 0.0 or end_percent <= start_percent: + return io.NodeOutput(model) + + P = cls.PATCH_SIZE + half = P // 2 + shift_levels = [cls.SHIFTS_BY_PATTERN[(pattern, n)] for n in cls.RAMP_LEVELS[passes]] + + if blend == "window": + window_tile_levels = [ + torch.stack([cls._hann_tile((half - sy) % P, (half - sx) % P, P) for sy, sx in lst], dim=0) + for lst in shift_levels + ] + else: + window_tile_levels = [None] * len(shift_levels) + + m = model.clone() + model_sampling = m.get_model_object("model_sampling") + multiplier = float(model_sampling.multiplier) + start_t = float(model_sampling.percent_to_sigma(start_percent)) * multiplier + end_t = float(model_sampling.percent_to_sigma(end_percent)) * multiplier + + edge_ramp_cache: dict = {} + + def get_edge_ramp(H: int, W: int, device, dtype) -> torch.Tensor: + key = (H, W, device, dtype) + cached = edge_ramp_cache.get(key) + if cached is not None: + return cached + feather = cls.EDGE_FEATHER + ys = torch.minimum(torch.arange(H, device=device, dtype=torch.float32), + (H - 1) - torch.arange(H, device=device, dtype=torch.float32)) + xs = torch.minimum(torch.arange(W, device=device, dtype=torch.float32), + (W - 1) - torch.arange(W, device=device, dtype=torch.float32)) + y_mask = ((ys - P) / feather).clamp(0, 1) + x_mask = ((xs - P) / feather).clamp(0, 1) + ramp = (y_mask[:, None] * x_mask[None, :]).to(dtype) + edge_ramp_cache[key] = ramp + return ramp + + def smoothing_wrapper(executor, *args, **kwargs): + x = args[0] + t = float(args[1][0]) + pred = executor(*args, **kwargs) + if not (end_t <= t <= start_t): + return pred + # Pick shift-level by sigma phase across the gated range. + if len(shift_levels) == 1: + level_idx = 0 + else: + phase = (start_t - t) / max(start_t - end_t, 1e-8) + level_idx = min(int(phase * len(shift_levels)), len(shift_levels) - 1) + shifts = shift_levels[level_idx] + window_tiles = window_tile_levels[level_idx] + + preds = [] + for sy, sx in shifts: + if sy == 0 and sx == 0: + preds.append(pred) + continue + x_rolled = torch.roll(x, shifts=(sy, sx), dims=(-2, -1)) + pred_rolled = executor(x_rolled, *args[1:], **kwargs) + preds.append(torch.roll(pred_rolled, shifts=(-sy, -sx), dims=(-2, -1))) + stacked = torch.stack(preds, dim=0) # (N, B, C, H, W) + _, _, _, H, W = stacked.shape + if blend == "window": + N = stacked.shape[0] + tiles = window_tiles.to(device=stacked.device, dtype=stacked.dtype) + w = tiles.repeat(1, H // P, W // P)[:, :H, :W] + sum_w = w.sum(dim=0, keepdim=True) + w = torch.where(sum_w < 1e-3, torch.full_like(w, 1.0 / N), w / sum_w.clamp(min=1e-8)) + avg = (stacked * w[:, None, None, :, :]).sum(dim=0) + elif blend == "median": + avg = torch.median(stacked, dim=0).values + else: + avg = stacked.mean(dim=0) + + # Mask out the P-px wraparound contamination strip at each edge. + mask = get_edge_ramp(H, W, pred.device, pred.dtype) + return pred * (1.0 - mask * strength) + avg * (mask * strength) + + m.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.DIFFUSION_MODEL, "hidream_o1_patch_seam_smoothing", smoothing_wrapper) + return io.NodeOutput(m) + + +class HiDreamO1Extension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [ + EmptyHiDreamO1LatentImage, + HiDreamO1ReferenceImages, + HiDreamO1PatchSeamSmoothing, + ] + + +async def comfy_entrypoint() -> HiDreamO1Extension: + return HiDreamO1Extension() diff --git a/comfy_extras/nodes_hunyuan.py b/comfy_extras/nodes_hunyuan.py index 4ea93a499..9e4873be5 100644 --- a/comfy_extras/nodes_hunyuan.py +++ b/comfy_extras/nodes_hunyuan.py @@ -131,6 +131,8 @@ class HunyuanVideo15SuperResolution(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="HunyuanVideo15SuperResolution", + display_name="Hunyuan Video 1.5 Super Resolution", + category="conditioning/video_models", inputs=[ io.Conditioning.Input("positive"), io.Conditioning.Input("negative"), @@ -381,6 +383,8 @@ class HunyuanRefinerLatent(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="HunyuanRefinerLatent", + display_name="Hunyuan Latent Refiner", + category="conditioning/video_models", inputs=[ io.Conditioning.Input("positive"), io.Conditioning.Input("negative"), diff --git a/comfy_extras/nodes_hunyuan3d.py b/comfy_extras/nodes_hunyuan3d.py index fa55ead59..bcd3f9198 100644 --- a/comfy_extras/nodes_hunyuan3d.py +++ b/comfy_extras/nodes_hunyuan3d.py @@ -1,12 +1,7 @@ import torch -import os -import json -import struct -import numpy as np from comfy.ldm.modules.diffusionmodules.mmdit import get_1d_sincos_pos_embed_from_grid_torch -import folder_paths import comfy.model_management -from comfy.cli_args import args +from comfy_extras.nodes_save_3d import pack_variable_mesh_batch from typing_extensions import override from comfy_api.latest import ComfyExtension, IO, Types from comfy_api.latest._util import MESH, VOXEL # only for backward compatibility if someone import it from this file (will be removed later) # noqa @@ -40,7 +35,7 @@ class Hunyuan3Dv2Conditioning(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="Hunyuan3Dv2Conditioning", - category="conditioning/video_models", + category="conditioning/3d_models", inputs=[ IO.ClipVisionOutput.Input("clip_vision_output"), ], @@ -65,7 +60,7 @@ class Hunyuan3Dv2ConditioningMultiView(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="Hunyuan3Dv2ConditioningMultiView", - category="conditioning/video_models", + category="conditioning/3d_models", inputs=[ IO.ClipVisionOutput.Input("front", optional=True), IO.ClipVisionOutput.Input("left", optional=True), @@ -424,14 +419,17 @@ class VoxelToMeshBasic(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="VoxelToMeshBasic", + display_name="Voxel to Mesh (Basic) (DEPRECATED)", category="3d", + description="Converts a voxel grid to a mesh.", + is_deprecated=True, # This node is superseded by the Voxel To Mesh node inputs=[ IO.Voxel.Input("voxel"), IO.Float.Input("threshold", default=0.6, min=-1.0, max=1.0, step=0.01), ], outputs=[ IO.Mesh.Output(), - ] + ], ) @classmethod @@ -443,7 +441,9 @@ class VoxelToMeshBasic(IO.ComfyNode): vertices.append(v) faces.append(f) - return IO.NodeOutput(Types.MESH(torch.stack(vertices), torch.stack(faces))) + if vertices and all(v.shape == vertices[0].shape for v in vertices) and all(f.shape == faces[0].shape for f in faces): + return IO.NodeOutput(Types.MESH(torch.stack(vertices), torch.stack(faces))) + return IO.NodeOutput(pack_variable_mesh_batch(vertices, faces)) decode = execute # TODO: remove @@ -453,10 +453,12 @@ class VoxelToMesh(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="VoxelToMesh", + display_name="Voxel to Mesh", category="3d", + description="Converts a voxel grid to a mesh.", inputs=[ IO.Voxel.Input("voxel"), - IO.Combo.Input("algorithm", options=["surface net", "basic"], advanced=True), + IO.Combo.Input("algorithm", options=["surface net", "basic"]), IO.Float.Input("threshold", default=0.6, min=-1.0, max=1.0, step=0.01), ], outputs=[ @@ -479,206 +481,13 @@ class VoxelToMesh(IO.ComfyNode): vertices.append(v) faces.append(f) - return IO.NodeOutput(Types.MESH(torch.stack(vertices), torch.stack(faces))) + if vertices and all(v.shape == vertices[0].shape for v in vertices) and all(f.shape == faces[0].shape for f in faces): + return IO.NodeOutput(Types.MESH(torch.stack(vertices), torch.stack(faces))) + return IO.NodeOutput(pack_variable_mesh_batch(vertices, faces)) decode = execute # TODO: remove -def save_glb(vertices, faces, filepath, metadata=None): - """ - Save PyTorch tensor vertices and faces as a GLB file without external dependencies. - - Parameters: - vertices: torch.Tensor of shape (N, 3) - The vertex coordinates - faces: torch.Tensor of shape (M, 3) - The face indices (triangle faces) - filepath: str - Output filepath (should end with .glb) - """ - - # Convert tensors to numpy arrays - vertices_np = vertices.cpu().numpy().astype(np.float32) - faces_np = faces.cpu().numpy().astype(np.uint32) - - vertices_buffer = vertices_np.tobytes() - indices_buffer = faces_np.tobytes() - - def pad_to_4_bytes(buffer): - padding_length = (4 - (len(buffer) % 4)) % 4 - return buffer + b'\x00' * padding_length - - vertices_buffer_padded = pad_to_4_bytes(vertices_buffer) - indices_buffer_padded = pad_to_4_bytes(indices_buffer) - - buffer_data = vertices_buffer_padded + indices_buffer_padded - - vertices_byte_length = len(vertices_buffer) - vertices_byte_offset = 0 - indices_byte_length = len(indices_buffer) - indices_byte_offset = len(vertices_buffer_padded) - - gltf = { - "asset": {"version": "2.0", "generator": "ComfyUI"}, - "buffers": [ - { - "byteLength": len(buffer_data) - } - ], - "bufferViews": [ - { - "buffer": 0, - "byteOffset": vertices_byte_offset, - "byteLength": vertices_byte_length, - "target": 34962 # ARRAY_BUFFER - }, - { - "buffer": 0, - "byteOffset": indices_byte_offset, - "byteLength": indices_byte_length, - "target": 34963 # ELEMENT_ARRAY_BUFFER - } - ], - "accessors": [ - { - "bufferView": 0, - "byteOffset": 0, - "componentType": 5126, # FLOAT - "count": len(vertices_np), - "type": "VEC3", - "max": vertices_np.max(axis=0).tolist(), - "min": vertices_np.min(axis=0).tolist() - }, - { - "bufferView": 1, - "byteOffset": 0, - "componentType": 5125, # UNSIGNED_INT - "count": faces_np.size, - "type": "SCALAR" - } - ], - "meshes": [ - { - "primitives": [ - { - "attributes": { - "POSITION": 0 - }, - "indices": 1, - "mode": 4 # TRIANGLES - } - ] - } - ], - "nodes": [ - { - "mesh": 0 - } - ], - "scenes": [ - { - "nodes": [0] - } - ], - "scene": 0 - } - - if metadata is not None: - gltf["asset"]["extras"] = metadata - - # Convert the JSON to bytes - gltf_json = json.dumps(gltf).encode('utf8') - - def pad_json_to_4_bytes(buffer): - padding_length = (4 - (len(buffer) % 4)) % 4 - return buffer + b' ' * padding_length - - gltf_json_padded = pad_json_to_4_bytes(gltf_json) - - # Create the GLB header - # Magic glTF - glb_header = struct.pack('<4sII', b'glTF', 2, 12 + 8 + len(gltf_json_padded) + 8 + len(buffer_data)) - - # Create JSON chunk header (chunk type 0) - json_chunk_header = struct.pack('