diff --git a/.ci/windows_amd_base_files/run_amd_gpu_disable_smart_memory.bat b/.ci/windows_amd_base_files/run_amd_gpu_enable_dynamic_vram.bat
similarity index 66%
rename from .ci/windows_amd_base_files/run_amd_gpu_disable_smart_memory.bat
rename to .ci/windows_amd_base_files/run_amd_gpu_enable_dynamic_vram.bat
index cece0aeb2..94ad31942 100755
--- a/.ci/windows_amd_base_files/run_amd_gpu_disable_smart_memory.bat
+++ b/.ci/windows_amd_base_files/run_amd_gpu_enable_dynamic_vram.bat
@@ -1,2 +1,2 @@
-.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build --disable-smart-memory
+.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build --enable-dynamic-vram
pause
diff --git a/.github/workflows/openapi-lint.yml b/.github/workflows/openapi-lint.yml
new file mode 100644
index 000000000..be949de2a
--- /dev/null
+++ b/.github/workflows/openapi-lint.yml
@@ -0,0 +1,31 @@
+name: OpenAPI Lint
+
+on:
+ pull_request:
+ paths:
+ - 'openapi.yaml'
+ - '.spectral.yaml'
+ - '.github/workflows/openapi-lint.yml'
+
+permissions:
+ contents: read
+
+jobs:
+ spectral:
+ name: Run Spectral
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Install Spectral
+ run: npm install -g @stoplight/spectral-cli@6
+
+ - name: Lint openapi.yaml
+ run: spectral lint openapi.yaml --ruleset .spectral.yaml --fail-severity=error
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/.github/workflows/tag-dispatch-cloud.yml b/.github/workflows/tag-dispatch-cloud.yml
new file mode 100644
index 000000000..53a0e91d6
--- /dev/null
+++ b/.github/workflows/tag-dispatch-cloud.yml
@@ -0,0 +1,45 @@
+name: Tag Dispatch to Cloud
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ dispatch-cloud:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Send repository dispatch to cloud
+ env:
+ DISPATCH_TOKEN: ${{ secrets.CLOUD_REPO_DISPATCH_TOKEN }}
+ RELEASE_TAG: ${{ github.ref_name }}
+ run: |
+ set -euo pipefail
+
+ if [ -z "${DISPATCH_TOKEN:-}" ]; then
+ echo "::error::CLOUD_REPO_DISPATCH_TOKEN is required but not set."
+ exit 1
+ fi
+
+ RELEASE_URL="https://github.com/${{ github.repository }}/releases/tag/${RELEASE_TAG}"
+
+ PAYLOAD="$(jq -n \
+ --arg release_tag "$RELEASE_TAG" \
+ --arg release_url "$RELEASE_URL" \
+ '{
+ event_type: "comfyui_tag_pushed",
+ client_payload: {
+ release_tag: $release_tag,
+ release_url: $release_url
+ }
+ }')"
+
+ curl -fsSL \
+ -X POST \
+ -H "Accept: application/vnd.github+json" \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer ${DISPATCH_TOKEN}" \
+ https://api.github.com/repos/Comfy-Org/cloud/dispatches \
+ -d "$PAYLOAD"
+
+ echo "✅ Dispatched ComfyUI tag ${RELEASE_TAG} to Comfy-Org/cloud"
diff --git a/.gitignore b/.gitignore
index 2700ad5c2..fc426eda4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,6 @@ venv*/
*.log
web_custom_versions/
.DS_Store
-openapi.yaml
filtered-openapi.yaml
uv.lock
+.comfy_environment
diff --git a/.spectral.yaml b/.spectral.yaml
new file mode 100644
index 000000000..a4b137628
--- /dev/null
+++ b/.spectral.yaml
@@ -0,0 +1,100 @@
+extends:
+ - spectral:oas
+
+# Severity levels: error, warn, info, hint, off
+# Rules from the built-in "spectral:oas" ruleset are active by default.
+# Below we tune severity and add custom rules for our conventions.
+#
+# This ruleset mirrors Comfy-Org/cloud/.spectral.yaml so specs across the
+# organization are linted against a single consistent standard.
+
+rules:
+ # -----------------------------------------------------------------------
+ # Built-in rule severity overrides
+ # -----------------------------------------------------------------------
+ operation-operationId: error
+ operation-description: warn
+ operation-tag-defined: error
+ info-contact: off
+ info-description: warn
+ no-eval-in-markdown: error
+ no-$ref-siblings: error
+
+ # -----------------------------------------------------------------------
+ # Custom rules: naming conventions
+ # -----------------------------------------------------------------------
+
+ # Property names should be snake_case
+ property-name-snake-case:
+ description: Property names must be snake_case
+ severity: warn
+ given: "$.components.schemas.*.properties[*]~"
+ then:
+ function: pattern
+ functionOptions:
+ match: "^[a-z][a-z0-9]*(_[a-z0-9]+)*$"
+
+ # Operation IDs should be camelCase
+ operation-id-camel-case:
+ description: Operation IDs must be camelCase
+ severity: warn
+ given: "$.paths.*.*.operationId"
+ then:
+ function: pattern
+ functionOptions:
+ match: "^[a-z][a-zA-Z0-9]*$"
+
+ # -----------------------------------------------------------------------
+ # Custom rules: response conventions
+ # -----------------------------------------------------------------------
+
+ # Error responses (4xx, 5xx) should use a consistent shape
+ error-response-schema:
+ description: Error responses should reference a standard error schema
+ severity: hint
+ given: "$.paths.*.*.responses[?(@property >= '400' && @property < '600')].content['application/json'].schema"
+ then:
+ field: "$ref"
+ function: truthy
+
+ # All 2xx responses with JSON body should have a schema
+ response-schema-defined:
+ description: Success responses with JSON content should define a schema
+ severity: warn
+ given: "$.paths.*.*.responses[?(@property >= '200' && @property < '300')].content['application/json']"
+ then:
+ field: schema
+ function: truthy
+
+ # -----------------------------------------------------------------------
+ # Custom rules: best practices
+ # -----------------------------------------------------------------------
+
+ # Path parameters must have a description
+ path-param-description:
+ description: Path parameters should have a description
+ severity: warn
+ given:
+ - "$.paths.*.parameters[?(@.in == 'path')]"
+ - "$.paths.*.*.parameters[?(@.in == 'path')]"
+ then:
+ field: description
+ function: truthy
+
+ # Schemas should have a description
+ schema-description:
+ description: Component schemas should have a description
+ severity: hint
+ given: "$.components.schemas.*"
+ 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 4d5448636..946dbf946 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,2 +1,2 @@
# Admins
-* @comfyanonymous @kosinkadink @guill
+* @comfyanonymous @kosinkadink @guill @alexisrolland @rattus128 @kijai
diff --git a/README.md b/README.md
index f05311421..0eecd8a4b 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# ComfyUI
-**The most powerful and modular visual AI engine and application.**
+**The most powerful and modular AI engine for content creation.**
[![Website][website-shield]][website-url]
@@ -31,10 +31,16 @@
[github-downloads-latest-shield]: https://img.shields.io/github/downloads/comfyanonymous/ComfyUI/latest/total?style=flat&label=downloads%40latest
[github-downloads-link]: https://github.com/comfyanonymous/ComfyUI/releases
-
+
+
-ComfyUI lets you design and execute advanced stable diffusion pipelines using a graph/nodes/flowchart based interface. Available on Windows, Linux, and macOS.
+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](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.
## Get Started
@@ -77,6 +83,7 @@ See what ComfyUI can do with the [newer template workflows](https://comfy.org/wo
- [Hunyuan Image 2.1](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_image/)
- [Flux 2](https://comfyanonymous.github.io/ComfyUI_examples/flux2/)
- [Z Image](https://comfyanonymous.github.io/ComfyUI_examples/z_image/)
+ - Ernie Image
- Image Editing Models
- [Omnigen 2](https://comfyanonymous.github.io/ComfyUI_examples/omnigen/)
- [Flux Kontext](https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-kontext-image-editing-model)
@@ -126,7 +133,7 @@ Workflow examples can be found on the [Examples page](https://comfyanonymous.git
ComfyUI follows a weekly release cycle targeting Monday but this regularly changes because of model releases or large changes to the codebase. There are three interconnected repositories:
1. **[ComfyUI Core](https://github.com/comfyanonymous/ComfyUI)**
- - Releases a new stable version (e.g., v0.7.0) roughly every week.
+ - Releases a new major stable version (e.g., v0.7.0) roughly every 2 weeks.
- Starting from v0.4.0 patch versions will be used for fixes backported onto the current stable release.
- Minor versions will be used for releases off the master branch.
- Patch versions may still be used for releases on the master branch in cases where a backport would not make sense.
@@ -193,13 +200,15 @@ If you have trouble extracting it, right click the file -> properties -> unblock
The portable above currently comes with python 3.13 and pytorch cuda 13.0. Update your Nvidia drivers if it doesn't start.
-#### Alternative Downloads:
+#### All Official Portable Downloads:
[Portable for AMD GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_amd.7z)
-[Experimental portable for Intel GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_intel.7z)
+[Portable for Intel GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_intel.7z)
-[Portable with pytorch cuda 12.6 and python 3.12](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu126.7z) (Supports Nvidia 10 series and older GPUs).
+[Portable for Nvidia GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia.7z) (supports 20 series and above).
+
+[Portable for Nvidia GPUs with pytorch cuda 12.6 and python 3.12](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu126.7z) (Supports Nvidia 10 series and older GPUs).
#### How do I share models between another UI and ComfyUI?
@@ -420,6 +429,8 @@ 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.
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/frontend_management.py b/app/frontend_management.py
index f753ef0de..d0596b276 100644
--- a/app/frontend_management.py
+++ b/app/frontend_management.py
@@ -27,7 +27,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 +38,54 @@ 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:
+
+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
+ 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:
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))}.
+Installed {pkg["name"]} version {installed_str} is lower than the recommended version {required_str}.
-{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}")
+ logging.info("{} version: {}".format(pkg["name"], installed_str))
REQUEST_TIMEOUT = 10 # seconds
@@ -201,6 +215,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 +360,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 +422,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/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..0517b3344 100644
--- a/app/user_manager.py
+++ b/app/user_manager.py
@@ -28,8 +28,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/.glsl/Glow_30.frag b/blueprints/.glsl/Glow_30.frag
index 0ee152628..f3c85a212 100644
--- a/blueprints/.glsl/Glow_30.frag
+++ b/blueprints/.glsl/Glow_30.frag
@@ -2,7 +2,6 @@
precision mediump float;
uniform sampler2D u_image0;
-uniform vec2 u_resolution;
uniform int u_int0; // Blend mode
uniform int u_int1; // Color tint
uniform float u_float0; // Intensity
@@ -75,7 +74,7 @@ void main() {
float t0 = threshold - 0.15;
float t1 = threshold + 0.15;
- vec2 texelSize = 1.0 / u_resolution;
+ vec2 texelSize = 1.0 / vec2(textureSize(u_image0, 0));
float radius2 = radius * radius;
float sampleScale = clamp(radius * 0.75, 0.35, 1.0);
diff --git a/blueprints/.glsl/Image_Blur_1.frag b/blueprints/.glsl/Image_Blur_1.frag
index 83238111d..1819e1695 100644
--- a/blueprints/.glsl/Image_Blur_1.frag
+++ b/blueprints/.glsl/Image_Blur_1.frag
@@ -12,7 +12,6 @@ const int RADIAL_SAMPLES = 12;
const float RADIAL_STRENGTH = 0.0003;
uniform sampler2D u_image0;
-uniform vec2 u_resolution;
uniform int u_int0; // Blur type (BLUR_GAUSSIAN, BLUR_BOX, BLUR_RADIAL)
uniform float u_float0; // Blur radius/amount
uniform int u_pass; // Pass index (0 = horizontal, 1 = vertical)
@@ -25,7 +24,7 @@ float gaussian(float x, float sigma) {
}
void main() {
- vec2 texelSize = 1.0 / u_resolution;
+ vec2 texelSize = 1.0 / vec2(textureSize(u_image0, 0));
float radius = max(u_float0, 0.0);
// Radial (angular) blur - single pass, doesn't use separable
diff --git a/blueprints/.glsl/Sharpen_23.frag b/blueprints/.glsl/Sharpen_23.frag
index c03f94b66..e7463a329 100644
--- a/blueprints/.glsl/Sharpen_23.frag
+++ b/blueprints/.glsl/Sharpen_23.frag
@@ -2,14 +2,13 @@
precision highp float;
uniform sampler2D u_image0;
-uniform vec2 u_resolution;
uniform float u_float0; // strength [0.0 – 2.0] typical: 0.3–1.0
in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;
void main() {
- vec2 texel = 1.0 / u_resolution;
+ vec2 texel = 1.0 / vec2(textureSize(u_image0, 0));
// Sample center and neighbors
vec4 center = texture(u_image0, v_texCoord);
diff --git a/blueprints/.glsl/Unsharp_Mask_26.frag b/blueprints/.glsl/Unsharp_Mask_26.frag
index f5990cb4a..d968c9c03 100644
--- a/blueprints/.glsl/Unsharp_Mask_26.frag
+++ b/blueprints/.glsl/Unsharp_Mask_26.frag
@@ -2,7 +2,6 @@
precision highp float;
uniform sampler2D u_image0;
-uniform vec2 u_resolution;
uniform float u_float0; // amount [0.0 - 3.0] typical: 0.5-1.5
uniform float u_float1; // radius [0.5 - 10.0] blur radius in pixels
uniform float u_float2; // threshold [0.0 - 0.1] min difference to sharpen
@@ -19,7 +18,7 @@ float getLuminance(vec3 color) {
}
void main() {
- vec2 texel = 1.0 / u_resolution;
+ vec2 texel = 1.0 / vec2(textureSize(u_image0, 0));
float radius = max(u_float1, 0.5);
float amount = u_float0;
float threshold = u_float2;
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..14deb64cc 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/Canny to image",
+ "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..a9682c8a4 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/Canny to video",
+ "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..fbec95a97
--- /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/ControlNet",
+ "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
new file mode 100644
index 000000000..99b89b608
--- /dev/null
+++ b/blueprints/Crop Images 2x2.json
@@ -0,0 +1,1621 @@
+{
+ "revision": 0,
+ "last_node_id": 139,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 135,
+ "type": "3b5ed000-6ab3-4458-91f7-8d6d366b0b40",
+ "pos": [
+ -2479.9999801712506,
+ 2019.9999372732784
+ ],
+ "size": [
+ 230,
+ 170
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "label": "top_left",
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "bottom_left",
+ "localized_name": "IMAGE_1",
+ "name": "IMAGE_1",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "top_right",
+ "localized_name": "IMAGE_2",
+ "name": "IMAGE_2",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "bottom_right",
+ "localized_name": "IMAGE_3",
+ "name": "IMAGE_3",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "images",
+ "name": "IMAGE_4",
+ "type": "IMAGE",
+ "links": []
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [],
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1"
+ },
+ "widgets_values": [],
+ "title": "Crop Images 2x2"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "3b5ed000-6ab3-4458-91f7-8d6d366b0b40",
+ "version": 1,
+ "state": {
+ "lastGroupId": 3,
+ "lastNodeId": 142,
+ "lastLinkId": 245,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Crop Images 2x2",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -10,
+ 1570,
+ 120,
+ 60
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 2919.9998608196274,
+ 1435,
+ 120,
+ 140
+ ]
+ },
+ "inputs": [
+ {
+ "id": "741854dd-bfb1-4700-ba8c-3b9dea59d021",
+ "name": "image",
+ "type": "IMAGE",
+ "linkIds": [
+ 2,
+ 11,
+ 13,
+ 30,
+ 32
+ ],
+ "localized_name": "image",
+ "pos": [
+ 90,
+ 1590
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "0eaca6d4-679a-433e-9703-bfa6dceacb18",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "linkIds": [
+ 41
+ ],
+ "localized_name": "IMAGE",
+ "label": "top_left",
+ "pos": [
+ 2939.9998608196274,
+ 1455
+ ]
+ },
+ {
+ "id": "fff5a1ad-3a74-4c87-938c-ee0fff55f840",
+ "name": "IMAGE_1",
+ "type": "IMAGE",
+ "linkIds": [
+ 42
+ ],
+ "localized_name": "IMAGE_1",
+ "label": "bottom_left",
+ "pos": [
+ 2939.9998608196274,
+ 1475
+ ]
+ },
+ {
+ "id": "08f40978-fb25-4d98-b716-b61e43b16043",
+ "name": "IMAGE_2",
+ "type": "IMAGE",
+ "linkIds": [
+ 43
+ ],
+ "localized_name": "IMAGE_2",
+ "label": "top_right",
+ "pos": [
+ 2939.9998608196274,
+ 1495
+ ]
+ },
+ {
+ "id": "17b9416f-3369-43c1-b62f-3e31fc2a7e32",
+ "name": "IMAGE_3",
+ "type": "IMAGE",
+ "linkIds": [
+ 44
+ ],
+ "localized_name": "IMAGE_3",
+ "label": "bottom_right",
+ "pos": [
+ 2939.9998608196274,
+ 1515
+ ]
+ },
+ {
+ "id": "430e2f3b-c617-4549-9daf-3ebf5be423a3",
+ "name": "IMAGE_4",
+ "type": "IMAGE",
+ "linkIds": [
+ 240
+ ],
+ "label": "images",
+ "pos": [
+ 2939.9998608196274,
+ 1535
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 7,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 740,
+ 1390
+ ],
+ "size": [
+ 370,
+ 190
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 3
+ },
+ {
+ "label": "b",
+ "localized_name": "values.b",
+ "name": "values.b",
+ "shape": 7,
+ "type": "FLOAT,INT",
+ "link": 4
+ },
+ {
+ "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": [
+ 7,
+ 14,
+ 28,
+ 40,
+ 242
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "max(1, int(a/b))"
+ ]
+ },
+ {
+ "id": 8,
+ "type": "GetImageSize",
+ "pos": [
+ 390,
+ 1450
+ ],
+ "size": [
+ 230,
+ 120
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 2
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "links": [
+ 3,
+ 241
+ ]
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "links": [
+ 5,
+ 245
+ ]
+ },
+ {
+ "localized_name": "batch_size",
+ "name": "batch_size",
+ "type": "INT",
+ "links": null
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "GetImageSize"
+ }
+ },
+ {
+ "id": 9,
+ "type": "PrimitiveInt",
+ "pos": [
+ 390,
+ 1650
+ ],
+ "size": [
+ 230,
+ 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": [
+ 4,
+ 6
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveInt"
+ },
+ "widgets_values": [
+ 2,
+ "fixed"
+ ]
+ },
+ {
+ "id": 10,
+ "type": "ImageCropV2",
+ "pos": [
+ 1710,
+ 430
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 11
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 9
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 41,
+ 236
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 12,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1370,
+ 570
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "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": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 7
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 8
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 9
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 13,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 750,
+ 1650
+ ],
+ "size": [
+ 370,
+ 190
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 5
+ },
+ {
+ "label": "b",
+ "localized_name": "values.b",
+ "name": "values.b",
+ "shape": 7,
+ "type": "FLOAT,INT",
+ "link": 6
+ },
+ {
+ "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": [
+ 8,
+ 23,
+ 27,
+ 39,
+ 246
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "max(1, int(a/b))"
+ ]
+ },
+ {
+ "id": 138,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 1170,
+ 1210
+ ],
+ "size": [
+ 420,
+ 190
+ ],
+ "flags": {},
+ "order": 13,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 241
+ },
+ {
+ "label": "b",
+ "localized_name": "values.b",
+ "name": "values.b",
+ "shape": 7,
+ "type": "FLOAT,INT",
+ "link": 242
+ },
+ {
+ "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": [
+ 243,
+ 244
+ ]
+ }
+ ],
+ "title": "Math Expression (Right Width)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "max(1, a - b)"
+ ]
+ },
+ {
+ "id": 139,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 1170,
+ 1860
+ ],
+ "size": [
+ 420,
+ 190
+ ],
+ "flags": {},
+ "order": 14,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 245
+ },
+ {
+ "label": "b",
+ "localized_name": "values.b",
+ "name": "values.b",
+ "shape": 7,
+ "type": "FLOAT,INT",
+ "link": 246
+ },
+ {
+ "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": [
+ 247,
+ 248
+ ]
+ }
+ ],
+ "title": "Math Expression (Bottom Height)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "max(1, a - b)"
+ ]
+ },
+ {
+ "id": 15,
+ "type": "ImageCropV2",
+ "pos": [
+ 1740,
+ 1600
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 13
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 12
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 42,
+ 238
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 16,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1350,
+ 1780
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "x",
+ "name": "x",
+ "type": "INT",
+ "widget": {
+ "name": "x"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "y",
+ "name": "y",
+ "type": "INT",
+ "widget": {
+ "name": "y"
+ },
+ "link": 23
+ },
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 14
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 247
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 12
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 25,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1350,
+ 1200
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "x",
+ "name": "x",
+ "type": "INT",
+ "widget": {
+ "name": "x"
+ },
+ "link": 28
+ },
+ {
+ "localized_name": "y",
+ "name": "y",
+ "type": "INT",
+ "widget": {
+ "name": "y"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 243
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 27
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 29
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 6,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 26,
+ "type": "ImageCropV2",
+ "pos": [
+ 1720,
+ 1050
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 30
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 29
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 43,
+ 237
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 30,
+ "type": "ImageCropV2",
+ "pos": [
+ 1740,
+ 2130
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 32
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 35
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 44,
+ 239
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 32,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1370,
+ 2280
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "x",
+ "name": "x",
+ "type": "INT",
+ "widget": {
+ "name": "x"
+ },
+ "link": 40
+ },
+ {
+ "localized_name": "y",
+ "name": "y",
+ "type": "INT",
+ "widget": {
+ "name": "y"
+ },
+ "link": 39
+ },
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 244
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 248
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 35
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 6,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 137,
+ "type": "BatchImagesNode",
+ "pos": [
+ 2520,
+ 1540
+ ],
+ "size": [
+ 230,
+ 170
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "image0",
+ "localized_name": "images.image0",
+ "name": "images.image0",
+ "type": "IMAGE",
+ "link": 236
+ },
+ {
+ "label": "image1",
+ "localized_name": "images.image1",
+ "name": "images.image1",
+ "type": "IMAGE",
+ "link": 237
+ },
+ {
+ "label": "image2",
+ "localized_name": "images.image2",
+ "name": "images.image2",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 238
+ },
+ {
+ "label": "image3",
+ "localized_name": "images.image3",
+ "name": "images.image3",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 239
+ },
+ {
+ "label": "image4",
+ "localized_name": "images.image4",
+ "name": "images.image4",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 240
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "BatchImagesNode"
+ }
+ }
+ ],
+ "groups": [
+ {
+ "id": 1,
+ "title": "Crop Images 2x2",
+ "bounding": [
+ 380,
+ 360,
+ 1710,
+ 2270
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 3,
+ "origin_id": 8,
+ "origin_slot": 0,
+ "target_id": 7,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 4,
+ "origin_id": 9,
+ "origin_slot": 0,
+ "target_id": 7,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 9,
+ "origin_id": 12,
+ "origin_slot": 0,
+ "target_id": 10,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 7,
+ "origin_id": 7,
+ "origin_slot": 1,
+ "target_id": 12,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 8,
+ "origin_id": 13,
+ "origin_slot": 1,
+ "target_id": 12,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 5,
+ "origin_id": 8,
+ "origin_slot": 1,
+ "target_id": 13,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 6,
+ "origin_id": 9,
+ "origin_slot": 0,
+ "target_id": 13,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 12,
+ "origin_id": 16,
+ "origin_slot": 0,
+ "target_id": 15,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 23,
+ "origin_id": 13,
+ "origin_slot": 1,
+ "target_id": 16,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 14,
+ "origin_id": 7,
+ "origin_slot": 1,
+ "target_id": 16,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 247,
+ "origin_id": 139,
+ "origin_slot": 1,
+ "target_id": 16,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 28,
+ "origin_id": 7,
+ "origin_slot": 1,
+ "target_id": 25,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 243,
+ "origin_id": 138,
+ "origin_slot": 1,
+ "target_id": 25,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 27,
+ "origin_id": 13,
+ "origin_slot": 1,
+ "target_id": 25,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 29,
+ "origin_id": 25,
+ "origin_slot": 0,
+ "target_id": 26,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 35,
+ "origin_id": 32,
+ "origin_slot": 0,
+ "target_id": 30,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 40,
+ "origin_id": 7,
+ "origin_slot": 1,
+ "target_id": 32,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 39,
+ "origin_id": 13,
+ "origin_slot": 1,
+ "target_id": 32,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 244,
+ "origin_id": 138,
+ "origin_slot": 1,
+ "target_id": 32,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 248,
+ "origin_id": 139,
+ "origin_slot": 1,
+ "target_id": 32,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 241,
+ "origin_id": 8,
+ "origin_slot": 0,
+ "target_id": 138,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 242,
+ "origin_id": 7,
+ "origin_slot": 1,
+ "target_id": 138,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 245,
+ "origin_id": 8,
+ "origin_slot": 1,
+ "target_id": 139,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 246,
+ "origin_id": 13,
+ "origin_slot": 1,
+ "target_id": 139,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 2,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 8,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 11,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 10,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 13,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 15,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 30,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 26,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 32,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 30,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 41,
+ "origin_id": 10,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 42,
+ "origin_id": 15,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 1,
+ "type": "IMAGE"
+ },
+ {
+ "id": 43,
+ "origin_id": 26,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 2,
+ "type": "IMAGE"
+ },
+ {
+ "id": 44,
+ "origin_id": 30,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 3,
+ "type": "IMAGE"
+ },
+ {
+ "id": 236,
+ "origin_id": 10,
+ "origin_slot": 0,
+ "target_id": 137,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 237,
+ "origin_id": 26,
+ "origin_slot": 0,
+ "target_id": 137,
+ "target_slot": 1,
+ "type": "IMAGE"
+ },
+ {
+ "id": 238,
+ "origin_id": 15,
+ "origin_slot": 0,
+ "target_id": 137,
+ "target_slot": 2,
+ "type": "IMAGE"
+ },
+ {
+ "id": 239,
+ "origin_id": 30,
+ "origin_slot": 0,
+ "target_id": 137,
+ "target_slot": 3,
+ "type": "IMAGE"
+ },
+ {
+ "id": 240,
+ "origin_id": 137,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 4,
+ "type": "IMAGE"
+ }
+ ],
+ "extra": {},
+ "category": "Image Tools/Crop",
+ "description": "Splits an image into a 2×2 grid of four equal tiles."
+ }
+ ]
+ },
+ "extra": {
+ "ue_links": [],
+ "links_added_by_ue": []
+ }
+}
\ No newline at end of file
diff --git a/blueprints/Crop Images 3x3.json b/blueprints/Crop Images 3x3.json
new file mode 100644
index 000000000..6ac636da4
--- /dev/null
+++ b/blueprints/Crop Images 3x3.json
@@ -0,0 +1,2958 @@
+{
+ "revision": 0,
+ "last_node_id": 141,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 134,
+ "type": "7fd47bca-ff89-476c-a98d-ca6f7cf756fe",
+ "pos": [
+ -2620,
+ 1620
+ ],
+ "size": [
+ 230,
+ 290
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "label": "top_left",
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "top_center",
+ "name": "IMAGE_1",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "top_right",
+ "name": "IMAGE_2",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "middle_left",
+ "name": "IMAGE_3",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "middle_center",
+ "name": "IMAGE_4",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "middle_right",
+ "name": "IMAGE_5",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "bottom_left",
+ "name": "IMAGE_6",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "bottom_center",
+ "name": "IMAGE_7",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "bottom_right",
+ "name": "IMAGE_8",
+ "type": "IMAGE",
+ "links": []
+ },
+ {
+ "label": "images",
+ "name": "IMAGE_9",
+ "type": "IMAGE",
+ "links": []
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [],
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1"
+ },
+ "widgets_values": [],
+ "title": "Crop Images 3x3"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "7fd47bca-ff89-476c-a98d-ca6f7cf756fe",
+ "version": 1,
+ "state": {
+ "lastGroupId": 3,
+ "lastNodeId": 142,
+ "lastLinkId": 245,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Crop Images 3x3",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -710,
+ 5440,
+ 120,
+ 60
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 3430,
+ 5270,
+ 121.720703125,
+ 240
+ ]
+ },
+ "inputs": [
+ {
+ "id": "e54e8e8b-6ce6-4f80-a38f-87a77d990efc",
+ "name": "image",
+ "type": "IMAGE",
+ "linkIds": [
+ 74,
+ 75,
+ 82,
+ 91,
+ 94,
+ 117,
+ 129,
+ 137,
+ 148,
+ 157
+ ],
+ "localized_name": "image",
+ "pos": [
+ -610,
+ 5460
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "3dd8abe2-a7da-4052-a556-9ae157ff3cf4",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "linkIds": [
+ 101
+ ],
+ "localized_name": "IMAGE",
+ "label": "top_left",
+ "pos": [
+ 3450,
+ 5290
+ ]
+ },
+ {
+ "id": "aa220733-759b-474e-9d29-634a3a23c5da",
+ "name": "IMAGE_1",
+ "type": "IMAGE",
+ "linkIds": [
+ 192
+ ],
+ "label": "top_center",
+ "pos": [
+ 3450,
+ 5310
+ ]
+ },
+ {
+ "id": "f1911df1-d50c-4bf8-9623-5e581d2a8902",
+ "name": "IMAGE_2",
+ "type": "IMAGE",
+ "linkIds": [
+ 193
+ ],
+ "label": "top_right",
+ "pos": [
+ 3450,
+ 5330
+ ]
+ },
+ {
+ "id": "71ebb807-e7e9-438f-990d-511e0745d10d",
+ "name": "IMAGE_3",
+ "type": "IMAGE",
+ "linkIds": [
+ 194
+ ],
+ "label": "middle_left",
+ "pos": [
+ 3450,
+ 5350
+ ]
+ },
+ {
+ "id": "4fb9c99c-3340-4de5-ba2d-51a653aab0b3",
+ "name": "IMAGE_4",
+ "type": "IMAGE",
+ "linkIds": [
+ 195
+ ],
+ "label": "middle_center",
+ "pos": [
+ 3450,
+ 5370
+ ]
+ },
+ {
+ "id": "398643e8-e349-4d59-9c68-6403b7a2772d",
+ "name": "IMAGE_5",
+ "type": "IMAGE",
+ "linkIds": [
+ 196
+ ],
+ "label": "middle_right",
+ "pos": [
+ 3450,
+ 5390
+ ]
+ },
+ {
+ "id": "5b11949c-f4cc-4525-86ae-690e30d3dada",
+ "name": "IMAGE_6",
+ "type": "IMAGE",
+ "linkIds": [
+ 197
+ ],
+ "label": "bottom_left",
+ "pos": [
+ 3450,
+ 5410
+ ]
+ },
+ {
+ "id": "82c69fd9-de36-4c8f-8311-a9e49159640b",
+ "name": "IMAGE_7",
+ "type": "IMAGE",
+ "linkIds": [
+ 198
+ ],
+ "label": "bottom_center",
+ "pos": [
+ 3450,
+ 5430
+ ]
+ },
+ {
+ "id": "aef678db-20aa-47d4-be8a-978065f078c6",
+ "name": "IMAGE_8",
+ "type": "IMAGE",
+ "linkIds": [
+ 199
+ ],
+ "label": "bottom_right",
+ "pos": [
+ 3450,
+ 5450
+ ]
+ },
+ {
+ "id": "77574277-edde-439c-8720-7daa849f4f27",
+ "name": "IMAGE_9",
+ "type": "IMAGE",
+ "linkIds": [
+ 226
+ ],
+ "label": "images",
+ "pos": [
+ 3450,
+ 5470
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 50,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 770,
+ 5310
+ ],
+ "size": [
+ 370,
+ 190
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 73
+ },
+ {
+ "label": "b",
+ "localized_name": "values.b",
+ "name": "values.b",
+ "shape": 7,
+ "type": "FLOAT,INT",
+ "link": 108
+ },
+ {
+ "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": [
+ 77,
+ 85,
+ 89,
+ 97,
+ 99,
+ 127,
+ 142,
+ 146,
+ 152,
+ 300
+ ]
+ }
+ ],
+ "title": "Math Expression (Width)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "max(1, int(a/b))"
+ ]
+ },
+ {
+ "id": 51,
+ "type": "GetImageSize",
+ "pos": [
+ 440,
+ 5390
+ ],
+ "size": [
+ 230,
+ 120
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 74
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "links": [
+ 73,
+ 300
+ ]
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "links": [
+ 79,
+ 305
+ ]
+ },
+ {
+ "localized_name": "batch_size",
+ "name": "batch_size",
+ "type": "INT",
+ "links": null
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "GetImageSize"
+ }
+ },
+ {
+ "id": 52,
+ "type": "PrimitiveInt",
+ "pos": [
+ 440,
+ 5590
+ ],
+ "size": [
+ 230,
+ 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": [
+ 80,
+ 108
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveInt"
+ },
+ "widgets_values": [
+ 3,
+ "fixed"
+ ]
+ },
+ {
+ "id": 53,
+ "type": "ImageCropV2",
+ "pos": [
+ 2080,
+ 3020
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 75
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 76
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 101,
+ 227
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 54,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1740,
+ 3160
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "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": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 77
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 78
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 76
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 55,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 780,
+ 5570
+ ],
+ "size": [
+ 370,
+ 190
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 79
+ },
+ {
+ "label": "b",
+ "localized_name": "values.b",
+ "name": "values.b",
+ "shape": 7,
+ "type": "FLOAT,INT",
+ "link": 80
+ },
+ {
+ "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": [
+ 78,
+ 84,
+ 86,
+ 88,
+ 90,
+ 98,
+ 100,
+ 121,
+ 123,
+ 126,
+ 161
+ ]
+ }
+ ],
+ "title": "Math Expression(Height)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "max(1, int(a/b))"
+ ]
+ },
+ {
+ "id": 57,
+ "type": "ImageCropV2",
+ "pos": [
+ 2080,
+ 4700
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 82
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 83
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 194,
+ 230
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 58,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1740,
+ 4830
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "x",
+ "name": "x",
+ "type": "INT",
+ "widget": {
+ "name": "x"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "y",
+ "name": "y",
+ "type": "INT",
+ "widget": {
+ "name": "y"
+ },
+ "link": 84
+ },
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 85
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 86
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 83
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 60,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1740,
+ 3700
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "x",
+ "name": "x",
+ "type": "INT",
+ "widget": {
+ "name": "x"
+ },
+ "link": 88
+ },
+ {
+ "localized_name": "y",
+ "name": "y",
+ "type": "INT",
+ "widget": {
+ "name": "y"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 89
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 90
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 92
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 6,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 61,
+ "type": "ImageCropV2",
+ "pos": [
+ 2100,
+ 3570
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 91
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 92
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 192,
+ 228
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 63,
+ "type": "ImageCropV2",
+ "pos": [
+ 2080,
+ 5310
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 94
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 95
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 195,
+ 231
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 65,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1750,
+ 5330
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "x",
+ "name": "x",
+ "type": "INT",
+ "widget": {
+ "name": "x"
+ },
+ "link": 97
+ },
+ {
+ "localized_name": "y",
+ "name": "y",
+ "type": "INT",
+ "widget": {
+ "name": "y"
+ },
+ "link": 98
+ },
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 99
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 100
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 95
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 6,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 71,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 780,
+ 6090
+ ],
+ "size": [
+ 400,
+ 190
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 126
+ },
+ {
+ "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": [
+ 136,
+ 147,
+ 156,
+ 306
+ ]
+ }
+ ],
+ "title": "Math Expression(height)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "2 * a"
+ ]
+ },
+ {
+ "id": 75,
+ "type": "ImageCropV2",
+ "pos": [
+ 2100,
+ 5900
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 13,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 117
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 118
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 196,
+ 232
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 77,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1750,
+ 5970
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 14,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "x",
+ "name": "x",
+ "type": "INT",
+ "widget": {
+ "name": "x"
+ },
+ "link": 128
+ },
+ {
+ "localized_name": "y",
+ "name": "y",
+ "type": "INT",
+ "widget": {
+ "name": "y"
+ },
+ "link": 121
+ },
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 302
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 123
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 118
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 6,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 78,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 780,
+ 5820
+ ],
+ "size": [
+ 400,
+ 200
+ ],
+ "flags": {},
+ "order": 15,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 127
+ },
+ {
+ "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": [
+ 128,
+ 132,
+ 163,
+ 301
+ ]
+ }
+ ],
+ "title": "Math Expression(width)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "2 * a"
+ ]
+ },
+ {
+ "id": 140,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 1240,
+ 5640
+ ],
+ "size": [
+ 420,
+ 190
+ ],
+ "flags": {},
+ "order": 24,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 300
+ },
+ {
+ "label": "b",
+ "localized_name": "values.b",
+ "name": "values.b",
+ "shape": 7,
+ "type": "FLOAT,INT",
+ "link": 301
+ },
+ {
+ "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": [
+ 302,
+ 303,
+ 304
+ ]
+ }
+ ],
+ "title": "Math Expression (Right Width)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "max(1, a - b)"
+ ]
+ },
+ {
+ "id": 141,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 1230,
+ 6340
+ ],
+ "size": [
+ 420,
+ 190
+ ],
+ "flags": {},
+ "order": 25,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 305
+ },
+ {
+ "label": "b",
+ "localized_name": "values.b",
+ "name": "values.b",
+ "shape": 7,
+ "type": "FLOAT,INT",
+ "link": 306
+ },
+ {
+ "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": [
+ 307,
+ 308,
+ 309
+ ]
+ }
+ ],
+ "title": "Math Expression (Bottom Height)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "max(1, a - b)"
+ ]
+ },
+ {
+ "id": 79,
+ "type": "ImageCropV2",
+ "pos": [
+ 2120,
+ 7580
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 16,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 129
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 130
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 199,
+ 235
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 81,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1720,
+ 7620
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 17,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "x",
+ "name": "x",
+ "type": "INT",
+ "widget": {
+ "name": "x"
+ },
+ "link": 132
+ },
+ {
+ "localized_name": "y",
+ "name": "y",
+ "type": "INT",
+ "widget": {
+ "name": "y"
+ },
+ "link": 136
+ },
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 303
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 307
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 130
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 6,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 82,
+ "type": "ImageCropV2",
+ "pos": [
+ 2120,
+ 7040
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 18,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 137
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 138
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 198,
+ 234
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 84,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1720,
+ 7080
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 19,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "x",
+ "name": "x",
+ "type": "INT",
+ "widget": {
+ "name": "x"
+ },
+ "link": 146
+ },
+ {
+ "localized_name": "y",
+ "name": "y",
+ "type": "INT",
+ "widget": {
+ "name": "y"
+ },
+ "link": 147
+ },
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 142
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 308
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 138
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 6,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 85,
+ "type": "ImageCropV2",
+ "pos": [
+ 2110,
+ 6480
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 20,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 148
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 149
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 197,
+ 233
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 86,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1670,
+ 6570
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 21,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "x",
+ "name": "x",
+ "type": "INT",
+ "widget": {
+ "name": "x"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "y",
+ "name": "y",
+ "type": "INT",
+ "widget": {
+ "name": "y"
+ },
+ "link": 156
+ },
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 152
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 309
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 149
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 88,
+ "type": "ImageCropV2",
+ "pos": [
+ 2060,
+ 4140
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 22,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 157
+ },
+ {
+ "localized_name": "crop_region",
+ "name": "crop_region",
+ "type": "BOUNDING_BOX",
+ "widget": {
+ "name": "crop_region"
+ },
+ "link": 158
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 193,
+ 229
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ImageCropV2"
+ },
+ "widgets_values": [
+ {
+ "x": 0,
+ "y": 0,
+ "width": 512,
+ "height": 512
+ },
+ 0,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 89,
+ "type": "PrimitiveBoundingBox",
+ "pos": [
+ 1720,
+ 4150
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 23,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "x",
+ "name": "x",
+ "type": "INT",
+ "widget": {
+ "name": "x"
+ },
+ "link": 163
+ },
+ {
+ "localized_name": "y",
+ "name": "y",
+ "type": "INT",
+ "widget": {
+ "name": "y"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 304
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 161
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOUNDING_BOX",
+ "name": "BOUNDING_BOX",
+ "type": "BOUNDING_BOX",
+ "links": [
+ 158
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "PrimitiveBoundingBox"
+ },
+ "widgets_values": [
+ 6,
+ 0,
+ 512,
+ 512
+ ]
+ },
+ {
+ "id": 136,
+ "type": "BatchImagesNode",
+ "pos": [
+ 3170,
+ 5640
+ ],
+ "size": [
+ 230,
+ 290
+ ],
+ "flags": {},
+ "order": 24,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "image0",
+ "localized_name": "images.image0",
+ "name": "images.image0",
+ "type": "IMAGE",
+ "link": 227
+ },
+ {
+ "label": "image1",
+ "localized_name": "images.image1",
+ "name": "images.image1",
+ "type": "IMAGE",
+ "link": 228
+ },
+ {
+ "label": "image2",
+ "localized_name": "images.image2",
+ "name": "images.image2",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 229
+ },
+ {
+ "label": "image3",
+ "localized_name": "images.image3",
+ "name": "images.image3",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 230
+ },
+ {
+ "label": "image4",
+ "localized_name": "images.image4",
+ "name": "images.image4",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 231
+ },
+ {
+ "label": "image5",
+ "localized_name": "images.image5",
+ "name": "images.image5",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 232
+ },
+ {
+ "label": "image6",
+ "localized_name": "images.image6",
+ "name": "images.image6",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 233
+ },
+ {
+ "label": "image7",
+ "localized_name": "images.image7",
+ "name": "images.image7",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 234
+ },
+ {
+ "label": "image8",
+ "localized_name": "images.image8",
+ "name": "images.image8",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 235
+ },
+ {
+ "label": "image9",
+ "localized_name": "images.image9",
+ "name": "images.image9",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 226
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "BatchImagesNode"
+ }
+ }
+ ],
+ "groups": [
+ {
+ "id": 3,
+ "title": "Crop Images 3x3",
+ "bounding": [
+ 100,
+ 2700,
+ 2640,
+ 5480
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 73,
+ "origin_id": 51,
+ "origin_slot": 0,
+ "target_id": 50,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 108,
+ "origin_id": 52,
+ "origin_slot": 0,
+ "target_id": 50,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 76,
+ "origin_id": 54,
+ "origin_slot": 0,
+ "target_id": 53,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 77,
+ "origin_id": 50,
+ "origin_slot": 1,
+ "target_id": 54,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 78,
+ "origin_id": 55,
+ "origin_slot": 1,
+ "target_id": 54,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 79,
+ "origin_id": 51,
+ "origin_slot": 1,
+ "target_id": 55,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 80,
+ "origin_id": 52,
+ "origin_slot": 0,
+ "target_id": 55,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 83,
+ "origin_id": 58,
+ "origin_slot": 0,
+ "target_id": 57,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 84,
+ "origin_id": 55,
+ "origin_slot": 1,
+ "target_id": 58,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 85,
+ "origin_id": 50,
+ "origin_slot": 1,
+ "target_id": 58,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 86,
+ "origin_id": 55,
+ "origin_slot": 1,
+ "target_id": 58,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 88,
+ "origin_id": 50,
+ "origin_slot": 1,
+ "target_id": 60,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 89,
+ "origin_id": 50,
+ "origin_slot": 1,
+ "target_id": 60,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 90,
+ "origin_id": 55,
+ "origin_slot": 1,
+ "target_id": 60,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 92,
+ "origin_id": 60,
+ "origin_slot": 0,
+ "target_id": 61,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 95,
+ "origin_id": 65,
+ "origin_slot": 0,
+ "target_id": 63,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 97,
+ "origin_id": 50,
+ "origin_slot": 1,
+ "target_id": 65,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 98,
+ "origin_id": 55,
+ "origin_slot": 1,
+ "target_id": 65,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 99,
+ "origin_id": 50,
+ "origin_slot": 1,
+ "target_id": 65,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 100,
+ "origin_id": 55,
+ "origin_slot": 1,
+ "target_id": 65,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 126,
+ "origin_id": 55,
+ "origin_slot": 1,
+ "target_id": 71,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 118,
+ "origin_id": 77,
+ "origin_slot": 0,
+ "target_id": 75,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 128,
+ "origin_id": 78,
+ "origin_slot": 1,
+ "target_id": 77,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 121,
+ "origin_id": 55,
+ "origin_slot": 1,
+ "target_id": 77,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 302,
+ "origin_id": 140,
+ "origin_slot": 1,
+ "target_id": 77,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 123,
+ "origin_id": 55,
+ "origin_slot": 1,
+ "target_id": 77,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 127,
+ "origin_id": 50,
+ "origin_slot": 1,
+ "target_id": 78,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 130,
+ "origin_id": 81,
+ "origin_slot": 0,
+ "target_id": 79,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 132,
+ "origin_id": 78,
+ "origin_slot": 1,
+ "target_id": 81,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 136,
+ "origin_id": 71,
+ "origin_slot": 1,
+ "target_id": 81,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 303,
+ "origin_id": 140,
+ "origin_slot": 1,
+ "target_id": 81,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 307,
+ "origin_id": 141,
+ "origin_slot": 1,
+ "target_id": 81,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 138,
+ "origin_id": 84,
+ "origin_slot": 0,
+ "target_id": 82,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 146,
+ "origin_id": 50,
+ "origin_slot": 1,
+ "target_id": 84,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 147,
+ "origin_id": 71,
+ "origin_slot": 1,
+ "target_id": 84,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 142,
+ "origin_id": 50,
+ "origin_slot": 1,
+ "target_id": 84,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 308,
+ "origin_id": 141,
+ "origin_slot": 1,
+ "target_id": 84,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 149,
+ "origin_id": 86,
+ "origin_slot": 0,
+ "target_id": 85,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 156,
+ "origin_id": 71,
+ "origin_slot": 1,
+ "target_id": 86,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 152,
+ "origin_id": 50,
+ "origin_slot": 1,
+ "target_id": 86,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 309,
+ "origin_id": 141,
+ "origin_slot": 1,
+ "target_id": 86,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 158,
+ "origin_id": 89,
+ "origin_slot": 0,
+ "target_id": 88,
+ "target_slot": 1,
+ "type": "BOUNDING_BOX"
+ },
+ {
+ "id": 163,
+ "origin_id": 78,
+ "origin_slot": 1,
+ "target_id": 89,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 304,
+ "origin_id": 140,
+ "origin_slot": 1,
+ "target_id": 89,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 161,
+ "origin_id": 55,
+ "origin_slot": 1,
+ "target_id": 89,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 300,
+ "origin_id": 51,
+ "origin_slot": 0,
+ "target_id": 140,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 301,
+ "origin_id": 78,
+ "origin_slot": 1,
+ "target_id": 140,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 305,
+ "origin_id": 51,
+ "origin_slot": 1,
+ "target_id": 141,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 306,
+ "origin_id": 71,
+ "origin_slot": 1,
+ "target_id": 141,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 74,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 51,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 75,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 53,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 82,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 57,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 91,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 61,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 94,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 63,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 117,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 75,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 129,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 79,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 137,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 82,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 148,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 85,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 157,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 88,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 101,
+ "origin_id": 53,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 192,
+ "origin_id": 61,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 1,
+ "type": "IMAGE"
+ },
+ {
+ "id": 193,
+ "origin_id": 88,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 2,
+ "type": "IMAGE"
+ },
+ {
+ "id": 194,
+ "origin_id": 57,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 3,
+ "type": "IMAGE"
+ },
+ {
+ "id": 195,
+ "origin_id": 63,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 4,
+ "type": "IMAGE"
+ },
+ {
+ "id": 196,
+ "origin_id": 75,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 5,
+ "type": "IMAGE"
+ },
+ {
+ "id": 197,
+ "origin_id": 85,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 6,
+ "type": "IMAGE"
+ },
+ {
+ "id": 198,
+ "origin_id": 82,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 7,
+ "type": "IMAGE"
+ },
+ {
+ "id": 199,
+ "origin_id": 79,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 8,
+ "type": "IMAGE"
+ },
+ {
+ "id": 226,
+ "origin_id": 136,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 9,
+ "type": "IMAGE"
+ },
+ {
+ "id": 227,
+ "origin_id": 53,
+ "origin_slot": 0,
+ "target_id": 136,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 228,
+ "origin_id": 61,
+ "origin_slot": 0,
+ "target_id": 136,
+ "target_slot": 1,
+ "type": "IMAGE"
+ },
+ {
+ "id": 229,
+ "origin_id": 88,
+ "origin_slot": 0,
+ "target_id": 136,
+ "target_slot": 2,
+ "type": "IMAGE"
+ },
+ {
+ "id": 230,
+ "origin_id": 57,
+ "origin_slot": 0,
+ "target_id": 136,
+ "target_slot": 3,
+ "type": "IMAGE"
+ },
+ {
+ "id": 231,
+ "origin_id": 63,
+ "origin_slot": 0,
+ "target_id": 136,
+ "target_slot": 4,
+ "type": "IMAGE"
+ },
+ {
+ "id": 232,
+ "origin_id": 75,
+ "origin_slot": 0,
+ "target_id": 136,
+ "target_slot": 5,
+ "type": "IMAGE"
+ },
+ {
+ "id": 233,
+ "origin_id": 85,
+ "origin_slot": 0,
+ "target_id": 136,
+ "target_slot": 6,
+ "type": "IMAGE"
+ },
+ {
+ "id": 234,
+ "origin_id": 82,
+ "origin_slot": 0,
+ "target_id": 136,
+ "target_slot": 7,
+ "type": "IMAGE"
+ },
+ {
+ "id": 235,
+ "origin_id": 79,
+ "origin_slot": 0,
+ "target_id": 136,
+ "target_slot": 8,
+ "type": "IMAGE"
+ }
+ ],
+ "extra": {},
+ "category": "Image Tools/Crop",
+ "description": "Splits an image into a 3×3 grid of nine equal tiles."
+ }
+ ]
+ },
+ "extra": {
+ "ue_links": [],
+ "links_added_by_ue": []
+ }
+}
\ No newline at end of file
diff --git a/blueprints/Depth to Image (Z-Image-Turbo).json b/blueprints/Depth to Image (Z-Image-Turbo).json
index 0b657534f..fe9ef0f72 100644
--- a/blueprints/Depth to Image (Z-Image-Turbo).json
+++ b/blueprints/Depth to Image (Z-Image-Turbo).json
@@ -160,7 +160,7 @@
},
"revision": 0,
"config": {},
- "name": "local-Depth to Image (Z-Image-Turbo)",
+ "name": "Depth to Image (Z-Image-Turbo)",
"inputNode": {
"id": -10,
"bounding": [
@@ -1579,7 +1579,8 @@
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
- "category": "Image generation and editing/Depth to image"
+ "category": "Image generation and editing/Depth to image",
+ "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."
}
]
},
@@ -2482,4 +2484,4 @@
"VHS_KeepIntermediate": true
},
"version": 0.4
-}
+}
\ No newline at end of file
diff --git a/blueprints/Depth to Video (ltx 2.0).json b/blueprints/Depth to Video (ltx 2.0).json
index 98c39eea5..bd51e4476 100644
--- a/blueprints/Depth to Video (ltx 2.0).json
+++ b/blueprints/Depth to Video (ltx 2.0).json
@@ -261,7 +261,7 @@
},
"revision": 0,
"config": {},
- "name": "local-Depth to Video (LTX 2.0)",
+ "name": "Depth to Video (LTX 2.0)",
"inputNode": {
"id": -10,
"bounding": [
@@ -4233,7 +4233,8 @@
"extra": {
"workflowRendererVersion": "LG"
},
- "category": "Video generation and editing/Depth to video"
+ "category": "Video generation and editing/Depth to video",
+ "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."
}
]
},
@@ -5208,4 +5210,4 @@
"workflowRendererVersion": "LG"
},
"version": 0.4
-}
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..f509aefe0
--- /dev/null
+++ b/blueprints/First-Last-Frame to Video (LTX-2.3).json
@@ -0,0 +1,3361 @@
+{
+ "revision": 0,
+ "last_node_id": 228,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 228,
+ "type": "a5982aee-8136-4819-86a0-cf9d9e510ad6",
+ "pos": [
+ 1490,
+ 4730
+ ],
+ "size": [
+ 274.8169921875,
+ 276
+ ],
+ "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": []
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [
+ [
+ "222",
+ "text"
+ ],
+ [
+ "215",
+ "value"
+ ],
+ [
+ "216",
+ "value"
+ ],
+ [
+ "198",
+ "value"
+ ],
+ [
+ "205",
+ "value"
+ ],
+ [
+ "196",
+ "noise_seed"
+ ],
+ [
+ "224",
+ "ckpt_name"
+ ],
+ [
+ "225",
+ "text_encoder"
+ ]
+ ],
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1"
+ },
+ "widgets_values": [],
+ "title": "First-Last-Frame to Video (LTX-2.3)"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "a5982aee-8136-4819-86a0-cf9d9e510ad6",
+ "version": 1,
+ "state": {
+ "lastGroupId": 22,
+ "lastNodeId": 228,
+ "lastLinkId": 276,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "First-Last-Frame to Video (LTX-2.3)",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {
+ "noise_seed": true
+ },
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.68",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.5.2"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.8.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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.60",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.12.3",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.12.3",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.17.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {
+ "resize_type.width": true,
+ "resize_type.height": true
+ },
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {
+ "resize_type.width": true,
+ "resize_type.height": true
+ },
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.5.2",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.5.2"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.10.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.5.2"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.10.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.5.2"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.10.0",
+ "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": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "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/First-Last-Frame to Video",
+ "description": "Generates a video interpolating between first and last keyframes using LTX-2.3."
+ }
+ ]
+ },
+ "extra": {
+ "ue_links": []
+ }
+}
\ No newline at end of file
diff --git a/blueprints/First-Last-Frame to Video.json b/blueprints/First-Last-Frame to Video.json
new file mode 100644
index 000000000..84dfafbcd
--- /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/First-Last-Frame to Video",
+ "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/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 8c690fc68..2bbfdee51 100644
--- a/blueprints/Glow.json
+++ b/blueprints/Glow.json
@@ -268,7 +268,7 @@
"Node name for S&R": "GLSLShader"
},
"widgets_values": [
- "#version 300 es\nprecision mediump float;\n\nuniform sampler2D u_image0;\nuniform vec2 u_resolution;\nuniform int u_int0; // Blend mode\nuniform int u_int1; // Color tint\nuniform float u_float0; // Intensity\nuniform float u_float1; // Radius\nuniform float u_float2; // Threshold\n\nin vec2 v_texCoord;\nout vec4 fragColor;\n\nconst int BLEND_ADD = 0;\nconst int BLEND_SCREEN = 1;\nconst int BLEND_SOFT = 2;\nconst int BLEND_OVERLAY = 3;\nconst int BLEND_LIGHTEN = 4;\n\nconst float GOLDEN_ANGLE = 2.39996323;\nconst int MAX_SAMPLES = 48;\nconst vec3 LUMA = vec3(0.299, 0.587, 0.114);\n\nfloat hash(vec2 p) {\n p = fract(p * vec2(123.34, 456.21));\n p += dot(p, p + 45.32);\n return fract(p.x * p.y);\n}\n\nvec3 hexToRgb(int h) {\n return vec3(\n float((h >> 16) & 255),\n float((h >> 8) & 255),\n float(h & 255)\n ) * (1.0 / 255.0);\n}\n\nvec3 blend(vec3 base, vec3 glow, int mode) {\n if (mode == BLEND_SCREEN) {\n return 1.0 - (1.0 - base) * (1.0 - glow);\n }\n if (mode == BLEND_SOFT) {\n return mix(\n base - (1.0 - 2.0 * glow) * base * (1.0 - base),\n base + (2.0 * glow - 1.0) * (sqrt(base) - base),\n step(0.5, glow)\n );\n }\n if (mode == BLEND_OVERLAY) {\n return mix(\n 2.0 * base * glow,\n 1.0 - 2.0 * (1.0 - base) * (1.0 - glow),\n step(0.5, base)\n );\n }\n if (mode == BLEND_LIGHTEN) {\n return max(base, glow);\n }\n return base + glow;\n}\n\nvoid main() {\n vec4 original = texture(u_image0, v_texCoord);\n \n float intensity = u_float0 * 0.05;\n float radius = u_float1 * u_float1 * 0.012;\n \n if (intensity < 0.001 || radius < 0.1) {\n fragColor = original;\n return;\n }\n \n float threshold = 1.0 - u_float2 * 0.01;\n float t0 = threshold - 0.15;\n float t1 = threshold + 0.15;\n \n vec2 texelSize = 1.0 / u_resolution;\n float radius2 = radius * radius;\n \n float sampleScale = clamp(radius * 0.75, 0.35, 1.0);\n int samples = int(float(MAX_SAMPLES) * sampleScale);\n \n float noise = hash(gl_FragCoord.xy);\n float angleOffset = noise * GOLDEN_ANGLE;\n float radiusJitter = 0.85 + noise * 0.3;\n \n float ca = cos(GOLDEN_ANGLE);\n float sa = sin(GOLDEN_ANGLE);\n vec2 dir = vec2(cos(angleOffset), sin(angleOffset));\n \n vec3 glow = vec3(0.0);\n float totalWeight = 0.0;\n \n // Center tap\n float centerMask = smoothstep(t0, t1, dot(original.rgb, LUMA));\n glow += original.rgb * centerMask * 2.0;\n totalWeight += 2.0;\n \n for (int i = 1; i < MAX_SAMPLES; i++) {\n if (i >= samples) break;\n \n float fi = float(i);\n float dist = sqrt(fi / float(samples)) * radius * radiusJitter;\n \n vec2 offset = dir * dist * texelSize;\n vec3 c = texture(u_image0, v_texCoord + offset).rgb;\n float mask = smoothstep(t0, t1, dot(c, LUMA));\n \n float w = 1.0 - (dist * dist) / (radius2 * 1.5);\n w = max(w, 0.0);\n w *= w;\n \n glow += c * mask * w;\n totalWeight += w;\n \n dir = vec2(\n dir.x * ca - dir.y * sa,\n dir.x * sa + dir.y * ca\n );\n }\n \n glow *= intensity / max(totalWeight, 0.001);\n \n if (u_int1 > 0) {\n glow *= hexToRgb(u_int1);\n }\n \n vec3 result = blend(original.rgb, glow, u_int0);\n result += (noise - 0.5) * (1.0 / 255.0);\n \n fragColor = vec4(clamp(result, 0.0, 1.0), original.a);\n}",
+ "#version 300 es\nprecision mediump float;\n\nuniform sampler2D u_image0;\nuniform int u_int0; // Blend mode\nuniform int u_int1; // Color tint\nuniform float u_float0; // Intensity\nuniform float u_float1; // Radius\nuniform float u_float2; // Threshold\n\nin vec2 v_texCoord;\nout vec4 fragColor;\n\nconst int BLEND_ADD = 0;\nconst int BLEND_SCREEN = 1;\nconst int BLEND_SOFT = 2;\nconst int BLEND_OVERLAY = 3;\nconst int BLEND_LIGHTEN = 4;\n\nconst float GOLDEN_ANGLE = 2.39996323;\nconst int MAX_SAMPLES = 48;\nconst vec3 LUMA = vec3(0.299, 0.587, 0.114);\n\nfloat hash(vec2 p) {\n p = fract(p * vec2(123.34, 456.21));\n p += dot(p, p + 45.32);\n return fract(p.x * p.y);\n}\n\nvec3 hexToRgb(int h) {\n return vec3(\n float((h >> 16) & 255),\n float((h >> 8) & 255),\n float(h & 255)\n ) * (1.0 / 255.0);\n}\n\nvec3 blend(vec3 base, vec3 glow, int mode) {\n if (mode == BLEND_SCREEN) {\n return 1.0 - (1.0 - base) * (1.0 - glow);\n }\n if (mode == BLEND_SOFT) {\n return mix(\n base - (1.0 - 2.0 * glow) * base * (1.0 - base),\n base + (2.0 * glow - 1.0) * (sqrt(base) - base),\n step(0.5, glow)\n );\n }\n if (mode == BLEND_OVERLAY) {\n return mix(\n 2.0 * base * glow,\n 1.0 - 2.0 * (1.0 - base) * (1.0 - glow),\n step(0.5, base)\n );\n }\n if (mode == BLEND_LIGHTEN) {\n return max(base, glow);\n }\n return base + glow;\n}\n\nvoid main() {\n vec4 original = texture(u_image0, v_texCoord);\n \n float intensity = u_float0 * 0.05;\n float radius = u_float1 * u_float1 * 0.012;\n \n if (intensity < 0.001 || radius < 0.1) {\n fragColor = original;\n return;\n }\n \n float threshold = 1.0 - u_float2 * 0.01;\n float t0 = threshold - 0.15;\n float t1 = threshold + 0.15;\n \n vec2 texelSize = 1.0 / vec2(textureSize(u_image0, 0));\n float radius2 = radius * radius;\n \n float sampleScale = clamp(radius * 0.75, 0.35, 1.0);\n int samples = int(float(MAX_SAMPLES) * sampleScale);\n \n float noise = hash(gl_FragCoord.xy);\n float angleOffset = noise * GOLDEN_ANGLE;\n float radiusJitter = 0.85 + noise * 0.3;\n \n float ca = cos(GOLDEN_ANGLE);\n float sa = sin(GOLDEN_ANGLE);\n vec2 dir = vec2(cos(angleOffset), sin(angleOffset));\n \n vec3 glow = vec3(0.0);\n float totalWeight = 0.0;\n \n // Center tap\n float centerMask = smoothstep(t0, t1, dot(original.rgb, LUMA));\n glow += original.rgb * centerMask * 2.0;\n totalWeight += 2.0;\n \n for (int i = 1; i < MAX_SAMPLES; i++) {\n if (i >= samples) break;\n \n float fi = float(i);\n float dist = sqrt(fi / float(samples)) * radius * radiusJitter;\n \n vec2 offset = dir * dist * texelSize;\n vec3 c = texture(u_image0, v_texCoord + offset).rgb;\n float mask = smoothstep(t0, t1, dot(c, LUMA));\n \n float w = 1.0 - (dist * dist) / (radius2 * 1.5);\n w = max(w, 0.0);\n w *= w;\n \n glow += c * mask * w;\n totalWeight += w;\n \n dir = vec2(\n dir.x * ca - dir.y * sa,\n dir.x * sa + dir.y * ca\n );\n }\n \n glow *= intensity / max(totalWeight, 0.001);\n \n if (u_int1 > 0) {\n glow *= hexToRgb(u_int1);\n }\n \n vec3 result = blend(original.rgb, glow, u_int0);\n result += (noise - 0.5) * (1.0 / 255.0);\n \n fragColor = vec4(clamp(result, 0.0, 1.0), original.a);\n}",
"from_input"
]
},
@@ -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 b1d449e32..0ca8d9931 100644
--- a/blueprints/Image Blur.json
+++ b/blueprints/Image Blur.json
@@ -331,7 +331,7 @@
"Node name for S&R": "GLSLShader"
},
"widgets_values": [
- "#version 300 es\n#pragma passes 2\nprecision highp float;\n\n// Blur type constants\nconst int BLUR_GAUSSIAN = 0;\nconst int BLUR_BOX = 1;\nconst int BLUR_RADIAL = 2;\n\n// Radial blur config\nconst int RADIAL_SAMPLES = 12;\nconst float RADIAL_STRENGTH = 0.0003;\n\nuniform sampler2D u_image0;\nuniform vec2 u_resolution;\nuniform int u_int0; // Blur type (BLUR_GAUSSIAN, BLUR_BOX, BLUR_RADIAL)\nuniform float u_float0; // Blur radius/amount\nuniform int u_pass; // Pass index (0 = horizontal, 1 = vertical)\n\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\n\nfloat gaussian(float x, float sigma) {\n return exp(-(x * x) / (2.0 * sigma * sigma));\n}\n\nvoid main() {\n vec2 texelSize = 1.0 / u_resolution;\n float radius = max(u_float0, 0.0);\n\n // Radial (angular) blur - single pass, doesn't use separable\n if (u_int0 == BLUR_RADIAL) {\n // Only execute on first pass\n if (u_pass > 0) {\n fragColor0 = texture(u_image0, v_texCoord);\n return;\n }\n\n vec2 center = vec2(0.5);\n vec2 dir = v_texCoord - center;\n float dist = length(dir);\n\n if (dist < 1e-4) {\n fragColor0 = texture(u_image0, v_texCoord);\n return;\n }\n\n vec4 sum = vec4(0.0);\n float totalWeight = 0.0;\n float angleStep = radius * RADIAL_STRENGTH;\n\n dir /= dist;\n\n float cosStep = cos(angleStep);\n float sinStep = sin(angleStep);\n\n float negAngle = -float(RADIAL_SAMPLES) * angleStep;\n vec2 rotDir = vec2(\n dir.x * cos(negAngle) - dir.y * sin(negAngle),\n dir.x * sin(negAngle) + dir.y * cos(negAngle)\n );\n\n for (int i = -RADIAL_SAMPLES; i <= RADIAL_SAMPLES; i++) {\n vec2 uv = center + rotDir * dist;\n float w = 1.0 - abs(float(i)) / float(RADIAL_SAMPLES);\n sum += texture(u_image0, uv) * w;\n totalWeight += w;\n\n rotDir = vec2(\n rotDir.x * cosStep - rotDir.y * sinStep,\n rotDir.x * sinStep + rotDir.y * cosStep\n );\n }\n\n fragColor0 = sum / max(totalWeight, 0.001);\n return;\n }\n\n // Separable Gaussian / Box blur\n int samples = int(ceil(radius));\n\n if (samples == 0) {\n fragColor0 = texture(u_image0, v_texCoord);\n return;\n }\n\n // Direction: pass 0 = horizontal, pass 1 = vertical\n vec2 dir = (u_pass == 0) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);\n\n vec4 color = vec4(0.0);\n float totalWeight = 0.0;\n float sigma = radius / 2.0;\n\n for (int i = -samples; i <= samples; i++) {\n vec2 offset = dir * float(i) * texelSize;\n vec4 sample_color = texture(u_image0, v_texCoord + offset);\n\n float weight;\n if (u_int0 == BLUR_GAUSSIAN) {\n weight = gaussian(float(i), sigma);\n } else {\n // BLUR_BOX\n weight = 1.0;\n }\n\n color += sample_color * weight;\n totalWeight += weight;\n }\n\n fragColor0 = color / totalWeight;\n}\n",
+ "#version 300 es\n#pragma passes 2\nprecision highp float;\n\n// Blur type constants\nconst int BLUR_GAUSSIAN = 0;\nconst int BLUR_BOX = 1;\nconst int BLUR_RADIAL = 2;\n\n// Radial blur config\nconst int RADIAL_SAMPLES = 12;\nconst float RADIAL_STRENGTH = 0.0003;\n\nuniform sampler2D u_image0;\nuniform int u_int0; // Blur type (BLUR_GAUSSIAN, BLUR_BOX, BLUR_RADIAL)\nuniform float u_float0; // Blur radius/amount\nuniform int u_pass; // Pass index (0 = horizontal, 1 = vertical)\n\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\n\nfloat gaussian(float x, float sigma) {\n return exp(-(x * x) / (2.0 * sigma * sigma));\n}\n\nvoid main() {\n vec2 texelSize = 1.0 / vec2(textureSize(u_image0, 0));\n float radius = max(u_float0, 0.0);\n\n // Radial (angular) blur - single pass, doesn't use separable\n if (u_int0 == BLUR_RADIAL) {\n // Only execute on first pass\n if (u_pass > 0) {\n fragColor0 = texture(u_image0, v_texCoord);\n return;\n }\n\n vec2 center = vec2(0.5);\n vec2 dir = v_texCoord - center;\n float dist = length(dir);\n\n if (dist < 1e-4) {\n fragColor0 = texture(u_image0, v_texCoord);\n return;\n }\n\n vec4 sum = vec4(0.0);\n float totalWeight = 0.0;\n float angleStep = radius * RADIAL_STRENGTH;\n\n dir /= dist;\n\n float cosStep = cos(angleStep);\n float sinStep = sin(angleStep);\n\n float negAngle = -float(RADIAL_SAMPLES) * angleStep;\n vec2 rotDir = vec2(\n dir.x * cos(negAngle) - dir.y * sin(negAngle),\n dir.x * sin(negAngle) + dir.y * cos(negAngle)\n );\n\n for (int i = -RADIAL_SAMPLES; i <= RADIAL_SAMPLES; i++) {\n vec2 uv = center + rotDir * dist;\n float w = 1.0 - abs(float(i)) / float(RADIAL_SAMPLES);\n sum += texture(u_image0, uv) * w;\n totalWeight += w;\n\n rotDir = vec2(\n rotDir.x * cosStep - rotDir.y * sinStep,\n rotDir.x * sinStep + rotDir.y * cosStep\n );\n }\n\n fragColor0 = sum / max(totalWeight, 0.001);\n return;\n }\n\n // Separable Gaussian / Box blur\n int samples = int(ceil(radius));\n\n if (samples == 0) {\n fragColor0 = texture(u_image0, v_texCoord);\n return;\n }\n\n // Direction: pass 0 = horizontal, pass 1 = vertical\n vec2 dir = (u_pass == 0) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);\n\n vec4 color = vec4(0.0);\n float totalWeight = 0.0;\n float sigma = radius / 2.0;\n\n for (int i = -samples; i <= samples; i++) {\n vec2 offset = dir * float(i) * texelSize;\n vec4 sample_color = texture(u_image0, v_texCoord + offset);\n\n float weight;\n if (u_int0 == BLUR_GAUSSIAN) {\n weight = gaussian(float(i), sigma);\n } else {\n // BLUR_BOX\n weight = 1.0;\n }\n\n color += sample_color * weight;\n totalWeight += weight;\n }\n\n fragColor0 = color / totalWeight;\n}\n",
"from_input"
]
}
@@ -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..2fc5d6746 100644
--- a/blueprints/Image Captioning (gemini).json
+++ b/blueprints/Image Captioning (gemini).json
@@ -310,7 +310,8 @@
"extra": {
"workflowRendererVersion": "LG"
},
- "category": "Text generation/Image Captioning"
+ "category": "Text generation/Image Captioning",
+ "description": "Generates descriptive captions for images using Google's Gemini multimodal LLM."
}
]
}
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 Edit (FireRed Image Edit 1.1).json b/blueprints/Image Edit (FireRed Image Edit 1.1).json
new file mode 100644
index 000000000..b82c7d18b
--- /dev/null
+++ b/blueprints/Image Edit (FireRed Image Edit 1.1).json
@@ -0,0 +1,2149 @@
+{
+ "revision": 0,
+ "last_node_id": 213,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 213,
+ "type": "e35fbbeb-d7b1-46d1-a74e-959517d0fb1a",
+ "pos": [
+ -700,
+ -470
+ ],
+ "size": [
+ 500,
+ 0
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": null
+ },
+ {
+ "label": "image2 (optional)",
+ "name": "image2_1",
+ "type": "IMAGE",
+ "link": null
+ },
+ {
+ "label": "image3 (optional)",
+ "name": "image3_1",
+ "type": "IMAGE",
+ "link": null
+ },
+ {
+ "name": "prompt",
+ "type": "STRING",
+ "widget": {
+ "name": "prompt"
+ },
+ "link": null
+ },
+ {
+ "label": "enable_turbo_mode",
+ "name": "value",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "value"
+ },
+ "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
+ },
+ {
+ "name": "lora_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "lora_name"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": []
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [
+ [
+ "208",
+ "prompt"
+ ],
+ [
+ "207",
+ "value"
+ ],
+ [
+ "210",
+ "seed"
+ ],
+ [
+ "205",
+ "unet_name"
+ ],
+ [
+ "203",
+ "clip_name"
+ ],
+ [
+ "202",
+ "vae_name"
+ ],
+ [
+ "204",
+ "lora_name"
+ ],
+ [
+ "210",
+ "control_after_generate"
+ ]
+ ],
+ "cnr_id": "comfy-core",
+ "ver": "0.15.1",
+ "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": [],
+ "title": "Image Edit (FireRed Image Edit 1.1)"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "e35fbbeb-d7b1-46d1-a74e-959517d0fb1a",
+ "version": 1,
+ "state": {
+ "lastGroupId": 8,
+ "lastNodeId": 213,
+ "lastLinkId": 378,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Image Edit (FireRed Image Edit 1.1)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -1670,
+ -1370,
+ 151.744140625,
+ 240
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 1860,
+ -1340,
+ 120,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "1d810e30-f1fb-4d10-95f8-3c5f7db2c8b7",
+ "name": "image",
+ "type": "IMAGE",
+ "linkIds": [
+ 371
+ ],
+ "localized_name": "image",
+ "pos": [
+ -1538.255859375,
+ -1350
+ ]
+ },
+ {
+ "id": "a8decf32-2262-4cdd-9e6b-c0ca7d4cdebe",
+ "name": "image2_1",
+ "type": "IMAGE",
+ "linkIds": [
+ 355,
+ 356
+ ],
+ "label": "image2 (optional)",
+ "pos": [
+ -1538.255859375,
+ -1330
+ ]
+ },
+ {
+ "id": "3ff7a4ed-8e3d-45d4-b1d8-40ed88a6def6",
+ "name": "image3_1",
+ "type": "IMAGE",
+ "linkIds": [
+ 357,
+ 358
+ ],
+ "label": "image3 (optional)",
+ "pos": [
+ -1538.255859375,
+ -1310
+ ]
+ },
+ {
+ "id": "01d9e68c-c664-4584-9cde-66f60e54eb3c",
+ "name": "prompt",
+ "type": "STRING",
+ "linkIds": [
+ 359
+ ],
+ "pos": [
+ -1538.255859375,
+ -1290
+ ]
+ },
+ {
+ "id": "97d24b10-6540-48c4-81eb-a432832f5729",
+ "name": "value",
+ "type": "BOOLEAN",
+ "linkIds": [
+ 364
+ ],
+ "label": "enable_turbo_mode",
+ "pos": [
+ -1538.255859375,
+ -1270
+ ]
+ },
+ {
+ "id": "15890efb-ba15-41cd-91ef-5adad7a52167",
+ "name": "seed",
+ "type": "INT",
+ "linkIds": [
+ 372
+ ],
+ "pos": [
+ -1538.255859375,
+ -1250
+ ]
+ },
+ {
+ "id": "43f22fe2-6836-4f75-8146-04c84fbba75d",
+ "name": "unet_name",
+ "type": "COMBO",
+ "linkIds": [
+ 373
+ ],
+ "pos": [
+ -1538.255859375,
+ -1230
+ ]
+ },
+ {
+ "id": "cd5e4502-2aca-4645-9e2e-ca8719f05bf6",
+ "name": "clip_name",
+ "type": "COMBO",
+ "linkIds": [
+ 374
+ ],
+ "pos": [
+ -1538.255859375,
+ -1210
+ ]
+ },
+ {
+ "id": "f6ae73dc-39e8-44b2-958d-705ae159ea86",
+ "name": "vae_name",
+ "type": "COMBO",
+ "linkIds": [
+ 375
+ ],
+ "pos": [
+ -1538.255859375,
+ -1190
+ ]
+ },
+ {
+ "id": "66dc179d-e6c9-4485-a2db-a47d25b44363",
+ "name": "lora_name",
+ "type": "COMBO",
+ "linkIds": [
+ 376
+ ],
+ "pos": [
+ -1538.255859375,
+ -1170
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "712c5c76-8620-44e1-9c9d-0798b6cdb77a",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "linkIds": [
+ 292
+ ],
+ "localized_name": "IMAGE",
+ "pos": [
+ 1880,
+ -1320
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 193,
+ "type": "ModelSamplingAuraFlow",
+ "pos": [
+ 1010,
+ -1680
+ ],
+ "size": [
+ 290,
+ 110
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 326
+ },
+ {
+ "localized_name": "shift",
+ "name": "shift",
+ "type": "FLOAT",
+ "widget": {
+ "name": "shift"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 294
+ ]
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ModelSamplingAuraFlow",
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "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": [
+ 3.1
+ ]
+ },
+ {
+ "id": 194,
+ "type": "ComfySwitchNode",
+ "pos": [
+ 680,
+ -1690
+ ],
+ "size": [
+ 260,
+ 140
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "on_false",
+ "name": "on_false",
+ "type": "*",
+ "link": 324
+ },
+ {
+ "localized_name": "on_true",
+ "name": "on_true",
+ "type": "*",
+ "link": 325
+ },
+ {
+ "localized_name": "switch",
+ "name": "switch",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "switch"
+ },
+ "link": 323
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "*",
+ "links": [
+ 326
+ ]
+ }
+ ],
+ "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": {}
+ },
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ false
+ ]
+ },
+ {
+ "id": 195,
+ "type": "PrimitiveInt",
+ "pos": [
+ 190,
+ -1680
+ ],
+ "size": [
+ 230,
+ 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": [
+ 329
+ ]
+ }
+ ],
+ "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": {}
+ },
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 40,
+ "fixed"
+ ]
+ },
+ {
+ "id": 196,
+ "type": "CFGNorm",
+ "pos": [
+ 1010,
+ -1510
+ ],
+ "size": [
+ 290,
+ 110
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 294
+ },
+ {
+ "localized_name": "strength",
+ "name": "strength",
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "patched_model",
+ "name": "patched_model",
+ "type": "MODEL",
+ "links": [
+ 295
+ ]
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "CFGNorm",
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "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": [
+ 1
+ ]
+ },
+ {
+ "id": 197,
+ "type": "ComfySwitchNode",
+ "pos": [
+ 680,
+ -1250
+ ],
+ "size": [
+ 230,
+ 130
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "on_false",
+ "name": "on_false",
+ "type": "*",
+ "link": 333
+ },
+ {
+ "localized_name": "on_true",
+ "name": "on_true",
+ "type": "*",
+ "link": 334
+ },
+ {
+ "localized_name": "switch",
+ "name": "switch",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "switch"
+ },
+ "link": 336
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "*",
+ "links": [
+ 335
+ ]
+ }
+ ],
+ "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": {}
+ },
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ false
+ ]
+ },
+ {
+ "id": 198,
+ "type": "PrimitiveInt",
+ "pos": [
+ 190,
+ -1060
+ ],
+ "size": [
+ 230,
+ 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": [
+ 337
+ ]
+ }
+ ],
+ "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": {}
+ },
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 8,
+ "fixed"
+ ]
+ },
+ {
+ "id": 199,
+ "type": "PrimitiveFloat",
+ "pos": [
+ 190,
+ -1500
+ ],
+ "size": [
+ 230,
+ 110
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "FLOAT",
+ "widget": {
+ "name": "value"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "FLOAT",
+ "name": "FLOAT",
+ "type": "FLOAT",
+ "links": [
+ 333
+ ]
+ }
+ ],
+ "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": {}
+ },
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 4
+ ]
+ },
+ {
+ "id": 200,
+ "type": "PrimitiveFloat",
+ "pos": [
+ 190,
+ -1230
+ ],
+ "size": [
+ 230,
+ 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": [
+ 334
+ ]
+ }
+ ],
+ "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": {}
+ },
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 1
+ ]
+ },
+ {
+ "id": 201,
+ "type": "ComfySwitchNode",
+ "pos": [
+ 680,
+ -1470
+ ],
+ "size": [
+ 230,
+ 130
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "on_false",
+ "name": "on_false",
+ "type": "*",
+ "link": 329
+ },
+ {
+ "localized_name": "on_true",
+ "name": "on_true",
+ "type": "*",
+ "link": 337
+ },
+ {
+ "localized_name": "switch",
+ "name": "switch",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "switch"
+ },
+ "link": 330
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "*",
+ "links": [
+ 345
+ ]
+ }
+ ],
+ "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": {}
+ },
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ false
+ ]
+ },
+ {
+ "id": 202,
+ "type": "VAELoader",
+ "pos": [
+ -960,
+ -1100
+ ],
+ "size": [
+ 400,
+ 110
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "vae_name",
+ "name": "vae_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "vae_name"
+ },
+ "link": 375
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "VAE",
+ "name": "VAE",
+ "type": "VAE",
+ "slot_index": 0,
+ "links": [
+ 298,
+ 299,
+ 300,
+ 314
+ ]
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "VAELoader",
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "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": 203,
+ "type": "CLIPLoader",
+ "pos": [
+ -960,
+ -1400
+ ],
+ "size": [
+ 400,
+ 150
+ ],
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip_name",
+ "name": "clip_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name"
+ },
+ "link": 374
+ },
+ {
+ "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": [
+ 296,
+ 297
+ ]
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "CLIPLoader",
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "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",
+ "qwen_image",
+ "default"
+ ]
+ },
+ {
+ "id": 204,
+ "type": "LoraLoaderModelOnly",
+ "pos": [
+ 100,
+ -900
+ ],
+ "size": [
+ 400,
+ 140
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 316
+ },
+ {
+ "localized_name": "lora_name",
+ "name": "lora_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "lora_name"
+ },
+ "link": 376
+ },
+ {
+ "localized_name": "strength_model",
+ "name": "strength_model",
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength_model"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 325
+ ]
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoraLoaderModelOnly",
+ "cnr_id": "comfy-core",
+ "ver": "0.15.1",
+ "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",
+ 1
+ ]
+ },
+ {
+ "id": 205,
+ "type": "UNETLoader",
+ "pos": [
+ -960,
+ -1670
+ ],
+ "size": [
+ 400,
+ 110
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "unet_name",
+ "name": "unet_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "unet_name"
+ },
+ "link": 373
+ },
+ {
+ "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": [
+ 316,
+ 324
+ ]
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "UNETLoader",
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "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",
+ "default"
+ ]
+ },
+ {
+ "id": 206,
+ "type": "VAEEncode",
+ "pos": [
+ -390,
+ -810
+ ],
+ "size": [
+ 390,
+ 100
+ ],
+ "flags": {},
+ "order": 13,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "pixels",
+ "name": "pixels",
+ "type": "IMAGE",
+ "link": 368
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 300
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "LATENT",
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 303
+ ]
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "VAEEncode",
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 207,
+ "type": "PrimitiveBoolean",
+ "pos": [
+ 160,
+ -650
+ ],
+ "size": [
+ 400,
+ 100
+ ],
+ "flags": {},
+ "order": 14,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "value"
+ },
+ "link": 364
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOOLEAN",
+ "name": "BOOLEAN",
+ "type": "BOOLEAN",
+ "links": [
+ 323,
+ 330,
+ 336
+ ]
+ }
+ ],
+ "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": {}
+ },
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ false
+ ]
+ },
+ {
+ "id": 208,
+ "type": "TextEncodeQwenImageEditPlus",
+ "pos": [
+ -480,
+ -1690
+ ],
+ "size": [
+ 470,
+ 370
+ ],
+ "flags": {},
+ "order": 15,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 296
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "shape": 7,
+ "type": "VAE",
+ "link": 298
+ },
+ {
+ "localized_name": "image1",
+ "name": "image1",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 369
+ },
+ {
+ "localized_name": "image2",
+ "name": "image2",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 355
+ },
+ {
+ "localized_name": "image3",
+ "name": "image3",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 357
+ },
+ {
+ "localized_name": "prompt",
+ "name": "prompt",
+ "type": "STRING",
+ "widget": {
+ "name": "prompt"
+ },
+ "link": 359
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 312
+ ]
+ }
+ ],
+ "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": {}
+ },
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ ""
+ ],
+ "color": "#232",
+ "bgcolor": "#353"
+ },
+ {
+ "id": 209,
+ "type": "TextEncodeQwenImageEditPlus",
+ "pos": [
+ -470,
+ -1240
+ ],
+ "size": [
+ 460,
+ 290
+ ],
+ "flags": {},
+ "order": 16,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 297
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "shape": 7,
+ "type": "VAE",
+ "link": 299
+ },
+ {
+ "localized_name": "image1",
+ "name": "image1",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 370
+ },
+ {
+ "localized_name": "image2",
+ "name": "image2",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 356
+ },
+ {
+ "localized_name": "image3",
+ "name": "image3",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 358
+ },
+ {
+ "localized_name": "prompt",
+ "name": "prompt",
+ "type": "STRING",
+ "widget": {
+ "name": "prompt"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 313
+ ]
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "TextEncodeQwenImageEditPlus",
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "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": [
+ ""
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 210,
+ "type": "KSampler",
+ "pos": [
+ 1010,
+ -1340
+ ],
+ "size": [
+ 270,
+ 480
+ ],
+ "flags": {},
+ "order": 17,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 295
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 312
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 313
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 303
+ },
+ {
+ "localized_name": "seed",
+ "name": "seed",
+ "type": "INT",
+ "widget": {
+ "name": "seed"
+ },
+ "link": 372
+ },
+ {
+ "localized_name": "steps",
+ "name": "steps",
+ "type": "INT",
+ "widget": {
+ "name": "steps"
+ },
+ "link": 345
+ },
+ {
+ "localized_name": "cfg",
+ "name": "cfg",
+ "type": "FLOAT",
+ "widget": {
+ "name": "cfg"
+ },
+ "link": 335
+ },
+ {
+ "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",
+ "links": [
+ 273
+ ]
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "KSampler",
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "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": [
+ 43,
+ "fixed",
+ 40,
+ 4,
+ "euler",
+ "simple",
+ 1
+ ]
+ },
+ {
+ "id": 211,
+ "type": "VAEDecode",
+ "pos": [
+ 1440,
+ -1340
+ ],
+ "size": [
+ 230,
+ 100
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 18,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 273
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 314
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "slot_index": 0,
+ "links": [
+ 292
+ ]
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "VAEDecode",
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 212,
+ "type": "ResizeImageMaskNode",
+ "pos": [
+ -900,
+ -810
+ ],
+ "size": [
+ 280,
+ 110
+ ],
+ "flags": {},
+ "order": 19,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "input",
+ "name": "input",
+ "type": "IMAGE,MASK",
+ "link": 371
+ },
+ {
+ "localized_name": "resize_type",
+ "name": "resize_type",
+ "type": "COMFY_DYNAMICCOMBO_V3",
+ "widget": {
+ "name": "resize_type"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "resize_type.megapixels",
+ "name": "resize_type.megapixels",
+ "type": "FLOAT",
+ "widget": {
+ "name": "resize_type.megapixels"
+ },
+ "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": [
+ 368,
+ 369,
+ 370
+ ]
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ResizeImageMaskNode",
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ }
+ },
+ "widgets_values": [
+ "scale total pixels",
+ 1,
+ "lanczos"
+ ]
+ }
+ ],
+ "groups": [
+ {
+ "id": 1,
+ "title": "Model",
+ "bounding": [
+ -990,
+ -1770,
+ 460,
+ 870
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 2,
+ "title": "Prompt",
+ "bounding": [
+ -500,
+ -1770,
+ 510,
+ 870
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 7,
+ "title": "Original",
+ "bounding": [
+ 40,
+ -1770,
+ 530,
+ 410
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 8,
+ "title": "Lightning LoRA",
+ "bounding": [
+ 40,
+ -1330,
+ 560,
+ 610
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 326,
+ "origin_id": 194,
+ "origin_slot": 0,
+ "target_id": 193,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 324,
+ "origin_id": 205,
+ "origin_slot": 0,
+ "target_id": 194,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 325,
+ "origin_id": 204,
+ "origin_slot": 0,
+ "target_id": 194,
+ "target_slot": 1,
+ "type": "MODEL"
+ },
+ {
+ "id": 323,
+ "origin_id": 207,
+ "origin_slot": 0,
+ "target_id": 194,
+ "target_slot": 2,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 294,
+ "origin_id": 193,
+ "origin_slot": 0,
+ "target_id": 196,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 333,
+ "origin_id": 199,
+ "origin_slot": 0,
+ "target_id": 197,
+ "target_slot": 0,
+ "type": "FLOAT"
+ },
+ {
+ "id": 334,
+ "origin_id": 200,
+ "origin_slot": 0,
+ "target_id": 197,
+ "target_slot": 1,
+ "type": "FLOAT"
+ },
+ {
+ "id": 336,
+ "origin_id": 207,
+ "origin_slot": 0,
+ "target_id": 197,
+ "target_slot": 2,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 329,
+ "origin_id": 195,
+ "origin_slot": 0,
+ "target_id": 201,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 337,
+ "origin_id": 198,
+ "origin_slot": 0,
+ "target_id": 201,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 330,
+ "origin_id": 207,
+ "origin_slot": 0,
+ "target_id": 201,
+ "target_slot": 2,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 297,
+ "origin_id": 203,
+ "origin_slot": 0,
+ "target_id": 209,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 299,
+ "origin_id": 202,
+ "origin_slot": 0,
+ "target_id": 209,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 316,
+ "origin_id": 205,
+ "origin_slot": 0,
+ "target_id": 204,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 296,
+ "origin_id": 203,
+ "origin_slot": 0,
+ "target_id": 208,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 298,
+ "origin_id": 202,
+ "origin_slot": 0,
+ "target_id": 208,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 300,
+ "origin_id": 202,
+ "origin_slot": 0,
+ "target_id": 206,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 295,
+ "origin_id": 196,
+ "origin_slot": 0,
+ "target_id": 210,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 312,
+ "origin_id": 208,
+ "origin_slot": 0,
+ "target_id": 210,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 313,
+ "origin_id": 209,
+ "origin_slot": 0,
+ "target_id": 210,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 303,
+ "origin_id": 206,
+ "origin_slot": 0,
+ "target_id": 210,
+ "target_slot": 3,
+ "type": "LATENT"
+ },
+ {
+ "id": 345,
+ "origin_id": 201,
+ "origin_slot": 0,
+ "target_id": 210,
+ "target_slot": 5,
+ "type": "INT"
+ },
+ {
+ "id": 335,
+ "origin_id": 197,
+ "origin_slot": 0,
+ "target_id": 210,
+ "target_slot": 6,
+ "type": "FLOAT"
+ },
+ {
+ "id": 273,
+ "origin_id": 210,
+ "origin_slot": 0,
+ "target_id": 211,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 314,
+ "origin_id": 202,
+ "origin_slot": 0,
+ "target_id": 211,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 292,
+ "origin_id": 211,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 355,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 208,
+ "target_slot": 3,
+ "type": "IMAGE"
+ },
+ {
+ "id": 356,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 209,
+ "target_slot": 3,
+ "type": "IMAGE"
+ },
+ {
+ "id": 357,
+ "origin_id": -10,
+ "origin_slot": 2,
+ "target_id": 208,
+ "target_slot": 4,
+ "type": "IMAGE"
+ },
+ {
+ "id": 358,
+ "origin_id": -10,
+ "origin_slot": 2,
+ "target_id": 209,
+ "target_slot": 4,
+ "type": "IMAGE"
+ },
+ {
+ "id": 359,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 208,
+ "target_slot": 5,
+ "type": "STRING"
+ },
+ {
+ "id": 364,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 207,
+ "target_slot": 0,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 368,
+ "origin_id": 212,
+ "origin_slot": 0,
+ "target_id": 206,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 369,
+ "origin_id": 212,
+ "origin_slot": 0,
+ "target_id": 208,
+ "target_slot": 2,
+ "type": "IMAGE"
+ },
+ {
+ "id": 370,
+ "origin_id": 212,
+ "origin_slot": 0,
+ "target_id": 209,
+ "target_slot": 2,
+ "type": "IMAGE"
+ },
+ {
+ "id": 371,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 212,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 372,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 210,
+ "target_slot": 4,
+ "type": "INT"
+ },
+ {
+ "id": 373,
+ "origin_id": -10,
+ "origin_slot": 6,
+ "target_id": 205,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 374,
+ "origin_id": -10,
+ "origin_slot": 7,
+ "target_id": 203,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 375,
+ "origin_id": -10,
+ "origin_slot": 8,
+ "target_id": 202,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 376,
+ "origin_id": -10,
+ "origin_slot": 9,
+ "target_id": 204,
+ "target_slot": 1,
+ "type": "COMBO"
+ }
+ ],
+ "extra": {
+ "workflowRendererVersion": "LG"
+ },
+ "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."
+ }
+ ]
+ },
+ "extra": {
+ "ue_links": []
+ }
+}
\ No newline at end of file
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 78bbb7414..7f6fa7a4b 100644
--- a/blueprints/Image Edit (Flux.2 Klein 4B).json
+++ b/blueprints/Image Edit (Flux.2 Klein 4B).json
@@ -128,7 +128,7 @@
},
"revision": 0,
"config": {},
- "name": "local-Image Edit (Flux.2 Klein 4B)",
+ "name": "Image Edit (Flux.2 Klein 4B)",
"inputNode": {
"id": -10,
"bounding": [
@@ -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)."
}
]
},
diff --git a/blueprints/Image Edit (LongCat Image Edit).json b/blueprints/Image Edit (LongCat Image Edit).json
new file mode 100644
index 000000000..de1c155a2
--- /dev/null
+++ b/blueprints/Image Edit (LongCat Image Edit).json
@@ -0,0 +1,1428 @@
+{
+ "revision": 0,
+ "last_node_id": 176,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 176,
+ "type": "372a02a0-a79c-40b4-84a9-34f246fe0e9c",
+ "pos": [
+ 967.0861152473078,
+ 4977.534165136897
+ ],
+ "size": [
+ 330,
+ 380
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": null
+ },
+ {
+ "name": "prompt",
+ "type": "STRING",
+ "widget": {
+ "name": "prompt"
+ },
+ "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": [
+ [
+ "27",
+ "prompt"
+ ],
+ [
+ "33",
+ "steps"
+ ],
+ [
+ "33",
+ "cfg"
+ ],
+ [
+ "33",
+ "seed"
+ ],
+ [
+ "34",
+ "unet_name"
+ ],
+ [
+ "38",
+ "clip_name"
+ ],
+ [
+ "26",
+ "vae_name"
+ ]
+ ],
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ }
+ },
+ "widgets_values": [],
+ "title": "Image Edit (LongCat Image Edit)"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "372a02a0-a79c-40b4-84a9-34f246fe0e9c",
+ "version": 1,
+ "state": {
+ "lastGroupId": 8,
+ "lastNodeId": 176,
+ "lastLinkId": 376,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Image Edit (LongCat Image Edit)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -750,
+ 380,
+ 120,
+ 200
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 1680,
+ 340,
+ 120,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "616c4f3e-8b64-4711-bee2-5ecbe1814fe4",
+ "name": "image",
+ "type": "IMAGE",
+ "linkIds": [
+ 14
+ ],
+ "localized_name": "image",
+ "pos": [
+ -650,
+ 400
+ ]
+ },
+ {
+ "id": "d39759fc-a5a9-4b82-a88f-df9b953f1d98",
+ "name": "prompt",
+ "type": "STRING",
+ "linkIds": [
+ 36
+ ],
+ "pos": [
+ -650,
+ 420
+ ]
+ },
+ {
+ "id": "48627f43-cdf1-4ea9-9e11-ec13451a7323",
+ "name": "steps",
+ "type": "INT",
+ "linkIds": [
+ 37
+ ],
+ "pos": [
+ -650,
+ 440
+ ]
+ },
+ {
+ "id": "2213f872-d40f-4fc3-be01-b8fc73f1d92c",
+ "name": "cfg",
+ "type": "FLOAT",
+ "linkIds": [
+ 42
+ ],
+ "pos": [
+ -650,
+ 460
+ ]
+ },
+ {
+ "id": "2c7b3e65-e71e-4a9b-a9f8-d2e814ccb6af",
+ "name": "seed",
+ "type": "INT",
+ "linkIds": [
+ 43
+ ],
+ "pos": [
+ -650,
+ 480
+ ]
+ },
+ {
+ "id": "bddb2317-7210-48d5-81fd-6b2d6fac33f4",
+ "name": "unet_name",
+ "type": "COMBO",
+ "linkIds": [
+ 44
+ ],
+ "pos": [
+ -650,
+ 500
+ ]
+ },
+ {
+ "id": "a283167b-6d7f-4d19-ad86-1fff2335c08d",
+ "name": "clip_name",
+ "type": "COMBO",
+ "linkIds": [
+ 45
+ ],
+ "pos": [
+ -650,
+ 520
+ ]
+ },
+ {
+ "id": "e033047f-cc37-4043-b4a0-25d7bab661af",
+ "name": "vae_name",
+ "type": "COMBO",
+ "linkIds": [
+ 46
+ ],
+ "pos": [
+ -650,
+ 540
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "0a288e93-c03f-4805-80f3-4e320a6a492e",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "linkIds": [
+ 20
+ ],
+ "localized_name": "IMAGE",
+ "pos": [
+ 1700,
+ 360
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 26,
+ "type": "VAELoader",
+ "pos": [
+ -360,
+ 590
+ ],
+ "size": [
+ 270,
+ 110
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "vae_name",
+ "name": "vae_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "vae_name"
+ },
+ "link": 46
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "VAE",
+ "name": "VAE",
+ "type": "VAE",
+ "slot_index": 0,
+ "links": [
+ 4,
+ 5,
+ 6,
+ 7
+ ]
+ }
+ ],
+ "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"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ae.safetensors"
+ ]
+ },
+ {
+ "id": 27,
+ "type": "TextEncodeQwenImageEdit",
+ "pos": [
+ 10,
+ 200
+ ],
+ "size": [
+ 280,
+ 190
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 2
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "shape": 7,
+ "type": "VAE",
+ "link": 4
+ },
+ {
+ "localized_name": "image",
+ "name": "image",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 15
+ },
+ {
+ "localized_name": "prompt",
+ "name": "prompt",
+ "type": "STRING",
+ "widget": {
+ "name": "prompt"
+ },
+ "link": 36
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 8
+ ]
+ }
+ ],
+ "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": "TextEncodeQwenImageEdit"
+ },
+ "widgets_values": [
+ ""
+ ]
+ },
+ {
+ "id": 28,
+ "type": "TextEncodeQwenImageEdit",
+ "pos": [
+ 10,
+ 440
+ ],
+ "size": [
+ 280,
+ 190
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 3
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "shape": 7,
+ "type": "VAE",
+ "link": 5
+ },
+ {
+ "localized_name": "image",
+ "name": "image",
+ "shape": 7,
+ "type": "IMAGE",
+ "link": 16
+ },
+ {
+ "localized_name": "prompt",
+ "name": "prompt",
+ "type": "STRING",
+ "widget": {
+ "name": "prompt"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 9
+ ]
+ }
+ ],
+ "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": "TextEncodeQwenImageEdit"
+ },
+ "widgets_values": [
+ ""
+ ]
+ },
+ {
+ "id": 29,
+ "type": "FluxKontextMultiReferenceLatentMethod",
+ "pos": [
+ 660,
+ 200
+ ],
+ "size": [
+ 270,
+ 80
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "showAdvanced": false,
+ "inputs": [
+ {
+ "localized_name": "conditioning",
+ "name": "conditioning",
+ "type": "CONDITIONING",
+ "link": 10
+ },
+ {
+ "localized_name": "reference_latents_method",
+ "name": "reference_latents_method",
+ "type": "COMBO",
+ "widget": {
+ "name": "reference_latents_method"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 12
+ ]
+ }
+ ],
+ "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": "FluxKontextMultiReferenceLatentMethod"
+ },
+ "widgets_values": [
+ "index"
+ ]
+ },
+ {
+ "id": 30,
+ "type": "FluxGuidance",
+ "pos": [
+ 330,
+ 440
+ ],
+ "size": [
+ 230,
+ 110
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "conditioning",
+ "name": "conditioning",
+ "type": "CONDITIONING",
+ "link": 9
+ },
+ {
+ "localized_name": "guidance",
+ "name": "guidance",
+ "type": "FLOAT",
+ "widget": {
+ "name": "guidance"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 11
+ ]
+ }
+ ],
+ "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": "FluxGuidance"
+ },
+ "widgets_values": [
+ 4.5
+ ]
+ },
+ {
+ "id": 31,
+ "type": "FluxGuidance",
+ "pos": [
+ 330,
+ 200
+ ],
+ "size": [
+ 230,
+ 110
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "conditioning",
+ "name": "conditioning",
+ "type": "CONDITIONING",
+ "link": 8
+ },
+ {
+ "localized_name": "guidance",
+ "name": "guidance",
+ "type": "FLOAT",
+ "widget": {
+ "name": "guidance"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 10
+ ]
+ }
+ ],
+ "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": "FluxGuidance"
+ },
+ "widgets_values": [
+ 4.5
+ ]
+ },
+ {
+ "id": 32,
+ "type": "FluxKontextMultiReferenceLatentMethod",
+ "pos": [
+ 660,
+ 440
+ ],
+ "size": [
+ 270,
+ 80
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "conditioning",
+ "name": "conditioning",
+ "type": "CONDITIONING",
+ "link": 11
+ },
+ {
+ "localized_name": "reference_latents_method",
+ "name": "reference_latents_method",
+ "type": "COMBO",
+ "widget": {
+ "name": "reference_latents_method"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 13
+ ]
+ }
+ ],
+ "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": "FluxKontextMultiReferenceLatentMethod"
+ },
+ "widgets_values": [
+ "index"
+ ]
+ },
+ {
+ "id": 33,
+ "type": "KSampler",
+ "pos": [
+ 1080,
+ 210
+ ],
+ "size": [
+ 270,
+ 460
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 1
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 12
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 13
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 18
+ },
+ {
+ "localized_name": "seed",
+ "name": "seed",
+ "type": "INT",
+ "widget": {
+ "name": "seed"
+ },
+ "link": 43
+ },
+ {
+ "localized_name": "steps",
+ "name": "steps",
+ "type": "INT",
+ "widget": {
+ "name": "steps"
+ },
+ "link": 37
+ },
+ {
+ "localized_name": "cfg",
+ "name": "cfg",
+ "type": "FLOAT",
+ "widget": {
+ "name": "cfg"
+ },
+ "link": 42
+ },
+ {
+ "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": [
+ 19
+ ]
+ }
+ ],
+ "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": "KSampler"
+ },
+ "widgets_values": [
+ 43,
+ "fixed",
+ 50,
+ 4.5,
+ "euler",
+ "simple",
+ 1
+ ]
+ },
+ {
+ "id": 34,
+ "type": "UNETLoader",
+ "pos": [
+ -360,
+ 170
+ ],
+ "size": [
+ 270,
+ 110
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "unet_name",
+ "name": "unet_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "unet_name"
+ },
+ "link": 44
+ },
+ {
+ "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": [
+ 1
+ ]
+ }
+ ],
+ "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": "longcat_image_edit_bf16.safetensors",
+ "url": "https://huggingface.co/TalmajM/LongCat-Image-Edit_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/longcat_image_edit_bf16.safetensors",
+ "directory": "diffusion_models"
+ }
+ ]
+ },
+ "widgets_values": [
+ "longcat_image_edit_bf16.safetensors",
+ "default"
+ ]
+ },
+ {
+ "id": 35,
+ "type": "VAEEncode",
+ "pos": [
+ 710,
+ 790
+ ],
+ "size": [
+ 260,
+ 100
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "pixels",
+ "name": "pixels",
+ "type": "IMAGE",
+ "link": 17
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 6
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "LATENT",
+ "name": "LATENT",
+ "type": "LATENT",
+ "slot_index": 0,
+ "links": [
+ 18
+ ]
+ }
+ ],
+ "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": "VAEEncode"
+ }
+ },
+ {
+ "id": 36,
+ "type": "VAEDecode",
+ "pos": [
+ 1100,
+ 800
+ ],
+ "size": [
+ 230,
+ 100
+ ],
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 19
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 7
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "slot_index": 0,
+ "links": [
+ 20
+ ]
+ }
+ ],
+ "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": "VAEDecode"
+ }
+ },
+ {
+ "id": 37,
+ "type": "ImageScaleToTotalPixels",
+ "pos": [
+ -370,
+ 790
+ ],
+ "size": [
+ 270,
+ 140
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 14
+ },
+ {
+ "localized_name": "upscale_method",
+ "name": "upscale_method",
+ "type": "COMBO",
+ "widget": {
+ "name": "upscale_method"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "megapixels",
+ "name": "megapixels",
+ "type": "FLOAT",
+ "widget": {
+ "name": "megapixels"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "resolution_steps",
+ "name": "resolution_steps",
+ "type": "INT",
+ "widget": {
+ "name": "resolution_steps"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "slot_index": 0,
+ "links": [
+ 15,
+ 16,
+ 17
+ ]
+ }
+ ],
+ "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": "ImageScaleToTotalPixels"
+ },
+ "widgets_values": [
+ "lanczos",
+ 1,
+ 16
+ ]
+ },
+ {
+ "id": 38,
+ "type": "CLIPLoader",
+ "pos": [
+ -360,
+ 360
+ ],
+ "size": [
+ 270,
+ 150
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip_name",
+ "name": "clip_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name"
+ },
+ "link": 45
+ },
+ {
+ "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": {
+ "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_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"
+ }
+ ]
+ },
+ "widgets_values": [
+ "qwen_2.5_vl_7b_fp8_scaled.safetensors",
+ "longcat_image",
+ "default"
+ ]
+ }
+ ],
+ "groups": [
+ {
+ "id": 1,
+ "title": "Models",
+ "bounding": [
+ -380,
+ 100,
+ 320,
+ 630
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 3,
+ "title": "Conditioning",
+ "bounding": [
+ -30,
+ 100,
+ 1030,
+ 630
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 4,
+ "title": "Sample",
+ "bounding": [
+ 1030,
+ 100,
+ 360,
+ 630
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 2,
+ "origin_id": 38,
+ "origin_slot": 0,
+ "target_id": 27,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 4,
+ "origin_id": 26,
+ "origin_slot": 0,
+ "target_id": 27,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 15,
+ "origin_id": 37,
+ "origin_slot": 0,
+ "target_id": 27,
+ "target_slot": 2,
+ "type": "IMAGE"
+ },
+ {
+ "id": 3,
+ "origin_id": 38,
+ "origin_slot": 0,
+ "target_id": 28,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 5,
+ "origin_id": 26,
+ "origin_slot": 0,
+ "target_id": 28,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 16,
+ "origin_id": 37,
+ "origin_slot": 0,
+ "target_id": 28,
+ "target_slot": 2,
+ "type": "IMAGE"
+ },
+ {
+ "id": 10,
+ "origin_id": 31,
+ "origin_slot": 0,
+ "target_id": 29,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 9,
+ "origin_id": 28,
+ "origin_slot": 0,
+ "target_id": 30,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 8,
+ "origin_id": 27,
+ "origin_slot": 0,
+ "target_id": 31,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 11,
+ "origin_id": 30,
+ "origin_slot": 0,
+ "target_id": 32,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 1,
+ "origin_id": 34,
+ "origin_slot": 0,
+ "target_id": 33,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 12,
+ "origin_id": 29,
+ "origin_slot": 0,
+ "target_id": 33,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 13,
+ "origin_id": 32,
+ "origin_slot": 0,
+ "target_id": 33,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 18,
+ "origin_id": 35,
+ "origin_slot": 0,
+ "target_id": 33,
+ "target_slot": 3,
+ "type": "LATENT"
+ },
+ {
+ "id": 17,
+ "origin_id": 37,
+ "origin_slot": 0,
+ "target_id": 35,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 6,
+ "origin_id": 26,
+ "origin_slot": 0,
+ "target_id": 35,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 19,
+ "origin_id": 33,
+ "origin_slot": 0,
+ "target_id": 36,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 7,
+ "origin_id": 26,
+ "origin_slot": 0,
+ "target_id": 36,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 14,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 37,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 20,
+ "origin_id": 36,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 36,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 27,
+ "target_slot": 3,
+ "type": "STRING"
+ },
+ {
+ "id": 37,
+ "origin_id": -10,
+ "origin_slot": 2,
+ "target_id": 33,
+ "target_slot": 5,
+ "type": "INT"
+ },
+ {
+ "id": 42,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 33,
+ "target_slot": 6,
+ "type": "FLOAT"
+ },
+ {
+ "id": 43,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 33,
+ "target_slot": 4,
+ "type": "INT"
+ },
+ {
+ "id": 44,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 34,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 45,
+ "origin_id": -10,
+ "origin_slot": 6,
+ "target_id": 38,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 46,
+ "origin_id": -10,
+ "origin_slot": 7,
+ "target_id": 26,
+ "target_slot": 0,
+ "type": "COMBO"
+ }
+ ],
+ "extra": {},
+ "category": "Image generation and editing/Edit image",
+ "description": "Edits images via text instructions using LongCat Image Edit, an instruction-following image editing diffusion model."
+ }
+ ]
+ },
+ "extra": {
+ "ue_links": []
+ }
+}
\ No newline at end of file
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 Inpainting (Flux.1 Fill Dev).json b/blueprints/Image Inpainting (Flux.1 Fill Dev).json
new file mode 100644
index 000000000..c1326ed3d
--- /dev/null
+++ b/blueprints/Image Inpainting (Flux.1 Fill Dev).json
@@ -0,0 +1,1206 @@
+{
+ "revision": 0,
+ "last_node_id": 232,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 232,
+ "type": "6e8d6e38-bdc3-436c-be85-ef9e67e70e07",
+ "pos": [
+ 1270,
+ 4640
+ ],
+ "size": [
+ 400,
+ 470
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "image",
+ "localized_name": "pixels",
+ "name": "pixels",
+ "type": "IMAGE",
+ "link": null
+ },
+ {
+ "localized_name": "mask",
+ "name": "mask",
+ "type": "MASK",
+ "link": null
+ },
+ {
+ "label": "prompt",
+ "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_name1",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name1"
+ },
+ "link": null
+ },
+ {
+ "name": "clip_name2",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name2"
+ },
+ "link": null
+ },
+ {
+ "name": "vae_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "vae_name"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": []
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [
+ [
+ "23",
+ "text"
+ ],
+ [
+ "3",
+ "seed"
+ ],
+ [
+ "31",
+ "unet_name"
+ ],
+ [
+ "34",
+ "clip_name1"
+ ],
+ [
+ "34",
+ "clip_name2"
+ ],
+ [
+ "230",
+ "vae_name"
+ ]
+ ],
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1"
+ },
+ "widgets_values": [],
+ "title": "Image Inpainting (Flux.1 Fill Dev)"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "6e8d6e38-bdc3-436c-be85-ef9e67e70e07",
+ "version": 1,
+ "state": {
+ "lastGroupId": 22,
+ "lastNodeId": 232,
+ "lastLinkId": 286,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Image Inpainting (Flux.1 Fill Dev)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -850,
+ 164,
+ 120,
+ 200
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 1230,
+ 140,
+ 120,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "65727ee9-09d0-40c9-bd86-11e0823eb676",
+ "name": "pixels",
+ "type": "IMAGE",
+ "linkIds": [
+ 99
+ ],
+ "localized_name": "pixels",
+ "label": "image",
+ "pos": [
+ -750,
+ 184
+ ]
+ },
+ {
+ "id": "28424f77-56c5-49c1-ba41-6bd78287c186",
+ "name": "mask",
+ "type": "MASK",
+ "linkIds": [
+ 100
+ ],
+ "localized_name": "mask",
+ "pos": [
+ -750,
+ 204
+ ]
+ },
+ {
+ "id": "2339e5e0-8f8d-4600-b158-7d7dae5f0535",
+ "name": "text",
+ "type": "STRING",
+ "linkIds": [
+ 277
+ ],
+ "label": "prompt",
+ "pos": [
+ -750,
+ 224
+ ]
+ },
+ {
+ "id": "5f433d9b-b97e-4bac-bb88-eb668de2d5a7",
+ "name": "seed",
+ "type": "INT",
+ "linkIds": [
+ 282
+ ],
+ "pos": [
+ -750,
+ 244
+ ]
+ },
+ {
+ "id": "35a8b6c1-c92c-4c1a-9b24-2e9bae7808f6",
+ "name": "unet_name",
+ "type": "COMBO",
+ "linkIds": [
+ 283
+ ],
+ "pos": [
+ -750,
+ 264
+ ]
+ },
+ {
+ "id": "3af8f8be-bce8-4ba0-aea0-ccf6b377d5f6",
+ "name": "clip_name1",
+ "type": "COMBO",
+ "linkIds": [
+ 284
+ ],
+ "pos": [
+ -750,
+ 284
+ ]
+ },
+ {
+ "id": "d9a4af80-4fa1-4792-b955-78bdaef4596e",
+ "name": "clip_name2",
+ "type": "COMBO",
+ "linkIds": [
+ 285
+ ],
+ "pos": [
+ -750,
+ 304
+ ]
+ },
+ {
+ "id": "d59398cf-7e9c-4dae-8c5a-08c4756f256a",
+ "name": "vae_name",
+ "type": "COMBO",
+ "linkIds": [
+ 286
+ ],
+ "pos": [
+ -750,
+ 324
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "1dee24ec-54a8-41be-aa30-a8fb797d3d23",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "linkIds": [
+ 95
+ ],
+ "localized_name": "IMAGE",
+ "pos": [
+ 1250,
+ 160
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 34,
+ "type": "DualCLIPLoader",
+ "pos": [
+ -590,
+ 150
+ ],
+ "size": [
+ 320,
+ 180
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip_name1",
+ "name": "clip_name1",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name1"
+ },
+ "link": 284
+ },
+ {
+ "localized_name": "clip_name2",
+ "name": "clip_name2",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name2"
+ },
+ "link": 285
+ },
+ {
+ "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": [
+ 62
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "Node name for S&R": "DualCLIPLoader",
+ "models": [
+ {
+ "name": "clip_l.safetensors",
+ "url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors",
+ "directory": "text_encoders"
+ },
+ {
+ "name": "t5xxl_fp16.safetensors",
+ "url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors",
+ "directory": "text_encoders"
+ }
+ ]
+ },
+ "widgets_values": [
+ "clip_l.safetensors",
+ "t5xxl_fp16.safetensors",
+ "flux",
+ "default"
+ ]
+ },
+ {
+ "id": 229,
+ "type": "FluxGuidance",
+ "pos": [
+ 410,
+ -40
+ ],
+ "size": [
+ 320,
+ 110
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 7,
+ "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": [
+ 80
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "Node name for S&R": "FluxGuidance"
+ },
+ "widgets_values": [
+ 30
+ ]
+ },
+ {
+ "id": 230,
+ "type": "VAELoader",
+ "pos": [
+ -590,
+ 450
+ ],
+ "size": [
+ 320,
+ 110
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "vae_name",
+ "name": "vae_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "vae_name"
+ },
+ "link": 286
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "VAE",
+ "name": "VAE",
+ "type": "VAE",
+ "slot_index": 0,
+ "links": [
+ 60,
+ 82
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "Node name for S&R": "VAELoader",
+ "models": [
+ {
+ "name": "ae.safetensors",
+ "url": "https://huggingface.co/Comfy-Org/Lumina_Image_2.0_Repackaged/resolve/main/split_files/vae/ae.safetensors",
+ "directory": "vae"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ae.safetensors"
+ ]
+ },
+ {
+ "id": 31,
+ "type": "UNETLoader",
+ "pos": [
+ -590,
+ -90
+ ],
+ "size": [
+ 320,
+ 110
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "unet_name",
+ "name": "unet_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "unet_name"
+ },
+ "link": 283
+ },
+ {
+ "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": [
+ 85
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "Node name for S&R": "UNETLoader",
+ "models": [
+ {
+ "name": "flux1-fill-dev.safetensors",
+ "url": "https://huggingface.co/Comfy-Org/flux1-dev/resolve/main/split_files/diffusion_models/flux1-fill-dev.safetensors",
+ "directory": "diffusion_models"
+ }
+ ]
+ },
+ "widgets_values": [
+ "flux1-fill-dev.safetensors",
+ "default"
+ ]
+ },
+ {
+ "id": 46,
+ "type": "ConditioningZeroOut",
+ "pos": [
+ 90,
+ 420
+ ],
+ "size": [
+ 230,
+ 80
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "conditioning",
+ "name": "conditioning",
+ "type": "CONDITIONING",
+ "link": 101
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 102
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "Node name for S&R": "ConditioningZeroOut"
+ }
+ },
+ {
+ "id": 23,
+ "type": "CLIPTextEncode",
+ "pos": [
+ -160,
+ -70
+ ],
+ "size": [
+ 480,
+ 410
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 62
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": 277
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 41,
+ 101
+ ]
+ }
+ ],
+ "title": "CLIP Text Encode (Positive Prompt)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "Node name for S&R": "CLIPTextEncode"
+ },
+ "widgets_values": [
+ ""
+ ],
+ "color": "#232",
+ "bgcolor": "#353"
+ },
+ {
+ "id": 39,
+ "type": "DifferentialDiffusion",
+ "pos": [
+ 780,
+ -110
+ ],
+ "size": [
+ 280,
+ 110
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 85
+ },
+ {
+ "localized_name": "strength",
+ "name": "strength",
+ "shape": 7,
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "slot_index": 0,
+ "links": [
+ 86
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "Node name for S&R": "DifferentialDiffusion"
+ },
+ "widgets_values": [
+ 1
+ ]
+ },
+ {
+ "id": 231,
+ "type": "VAEDecode",
+ "pos": [
+ 780,
+ 590
+ ],
+ "size": [
+ 230,
+ 100
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 7
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 60
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "slot_index": 0,
+ "links": [
+ 95
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "Node name for S&R": "VAEDecode"
+ }
+ },
+ {
+ "id": 38,
+ "type": "InpaintModelConditioning",
+ "pos": [
+ 420,
+ 120
+ ],
+ "size": [
+ 310,
+ 200
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 80
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 102
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 82
+ },
+ {
+ "localized_name": "pixels",
+ "name": "pixels",
+ "type": "IMAGE",
+ "link": 99
+ },
+ {
+ "localized_name": "mask",
+ "name": "mask",
+ "type": "MASK",
+ "link": 100
+ },
+ {
+ "localized_name": "noise_mask",
+ "name": "noise_mask",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "noise_mask"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 77
+ ]
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "slot_index": 1,
+ "links": [
+ 78
+ ]
+ },
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "slot_index": 2,
+ "links": [
+ 88
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "Node name for S&R": "InpaintModelConditioning"
+ },
+ "widgets_values": [
+ true
+ ]
+ },
+ {
+ "id": 3,
+ "type": "KSampler",
+ "pos": [
+ 770,
+ 40
+ ],
+ "size": [
+ 290,
+ 470
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 86
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 77
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 78
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 88
+ },
+ {
+ "localized_name": "seed",
+ "name": "seed",
+ "type": "INT",
+ "widget": {
+ "name": "seed"
+ },
+ "link": 282
+ },
+ {
+ "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": [
+ 7
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "Node name for S&R": "KSampler"
+ },
+ "widgets_values": [
+ 0,
+ "randomize",
+ 20,
+ 1,
+ "euler",
+ "normal",
+ 1
+ ]
+ }
+ ],
+ "groups": [
+ {
+ "id": 1,
+ "title": "Load models",
+ "bounding": [
+ -620,
+ -160,
+ 410,
+ 790
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 3,
+ "title": "Prompt",
+ "bounding": [
+ -180,
+ -160,
+ 520,
+ 670
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 41,
+ "origin_id": 23,
+ "origin_slot": 0,
+ "target_id": 229,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 101,
+ "origin_id": 23,
+ "origin_slot": 0,
+ "target_id": 46,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 62,
+ "origin_id": 34,
+ "origin_slot": 0,
+ "target_id": 23,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 85,
+ "origin_id": 31,
+ "origin_slot": 0,
+ "target_id": 39,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 86,
+ "origin_id": 39,
+ "origin_slot": 0,
+ "target_id": 3,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 77,
+ "origin_id": 38,
+ "origin_slot": 0,
+ "target_id": 3,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 78,
+ "origin_id": 38,
+ "origin_slot": 1,
+ "target_id": 3,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 88,
+ "origin_id": 38,
+ "origin_slot": 2,
+ "target_id": 3,
+ "target_slot": 3,
+ "type": "LATENT"
+ },
+ {
+ "id": 7,
+ "origin_id": 3,
+ "origin_slot": 0,
+ "target_id": 231,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 60,
+ "origin_id": 230,
+ "origin_slot": 0,
+ "target_id": 231,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 80,
+ "origin_id": 229,
+ "origin_slot": 0,
+ "target_id": 38,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 102,
+ "origin_id": 46,
+ "origin_slot": 0,
+ "target_id": 38,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 82,
+ "origin_id": 230,
+ "origin_slot": 0,
+ "target_id": 38,
+ "target_slot": 2,
+ "type": "VAE"
+ },
+ {
+ "id": 99,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 38,
+ "target_slot": 3,
+ "type": "IMAGE"
+ },
+ {
+ "id": 100,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 38,
+ "target_slot": 4,
+ "type": "MASK"
+ },
+ {
+ "id": 95,
+ "origin_id": 231,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 277,
+ "origin_id": -10,
+ "origin_slot": 2,
+ "target_id": 23,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 282,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 3,
+ "target_slot": 4,
+ "type": "INT"
+ },
+ {
+ "id": 283,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 31,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 284,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 34,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 285,
+ "origin_id": -10,
+ "origin_slot": 6,
+ "target_id": 34,
+ "target_slot": 1,
+ "type": "COMBO"
+ },
+ {
+ "id": 286,
+ "origin_id": -10,
+ "origin_slot": 7,
+ "target_id": 230,
+ "target_slot": 0,
+ "type": "COMBO"
+ }
+ ],
+ "extra": {
+ "workflowRendererVersion": "LG"
+ },
+ "category": "Image generation and editing/Inpaint image",
+ "description": "Inpaints masked image regions using Flux.1 fill [dev], Black Forest Labs' inpainting/outpainting model."
+ }
+ ]
+ },
+ "extra": {
+ "ds": {
+ "scale": 0.8480949417360862,
+ "offset": [
+ 833.9510730024642,
+ 210.32152847588895
+ ]
+ },
+ "ue_links": []
+ }
+}
diff --git a/blueprints/Image Inpainting (Qwen-image).json b/blueprints/Image Inpainting (Qwen-image).json
index d06f31dd2..a06d57e19 100644
--- a/blueprints/Image Inpainting (Qwen-image).json
+++ b/blueprints/Image Inpainting (Qwen-image).json
@@ -124,7 +124,7 @@
},
"revision": 0,
"config": {},
- "name": "local-Image Inpainting (Qwen-image)",
+ "name": "Image Inpainting (Qwen-image)",
"inputNode": {
"id": -10,
"bounding": [
@@ -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."
}
]
},
@@ -1923,4 +1925,4 @@
"workflowRendererVersion": "LG"
},
"version": 0.4
-}
+}
\ No newline at end of file
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 bf2c4241a..6c07227c0 100644
--- a/blueprints/Image Outpainting (Qwen-Image).json
+++ b/blueprints/Image Outpainting (Qwen-Image).json
@@ -204,7 +204,7 @@
},
"revision": 0,
"config": {},
- "name": "local-Image Outpainting (Qwen-Image)",
+ "name": "Image Outpainting (Qwen-Image)",
"inputNode": {
"id": -10,
"bounding": [
@@ -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."
}
]
},
@@ -2749,4 +2752,4 @@
}
},
"version": 0.4
-}
+}
\ No newline at end of file
diff --git a/blueprints/Image Segmentation (SAM3).json b/blueprints/Image Segmentation (SAM3).json
new file mode 100644
index 000000000..b405bf623
--- /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": "Image Tools/Image Segmentation",
+ "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..bd803a0b1 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/Enhance",
+ "description": "Upscales images to higher resolution using Z-Image-Turbo."
}
]
},
diff --git a/blueprints/Image to Depth Map (Lotus).json b/blueprints/Image to Depth Map (Lotus).json
index 089f2cd42..12f10ba5b 100644
--- a/blueprints/Image to Depth Map (Lotus).json
+++ b/blueprints/Image to Depth Map (Lotus).json
@@ -99,7 +99,7 @@
},
"revision": 0,
"config": {},
- "name": "local-Image to Depth Map (Lotus)",
+ "name": "Image to Depth Map (Lotus)",
"inputNode": {
"id": -10,
"bounding": [
@@ -948,7 +948,8 @@
"extra": {
"workflowRendererVersion": "LG"
},
- "category": "Image generation and editing/Depth to image"
+ "category": "Image generation and editing/Depth to image",
+ "description": "Estimates a monocular depth map from an input image using the Lotus depth estimation model."
}
]
},
@@ -964,4 +965,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
similarity index 82%
rename from blueprints/Image to Layers(Qwen-Image Layered).json
rename to blueprints/Image to Layers(Qwen-Image-Layered).json
index 164ffbd8d..7b44f0563 100644
--- a/blueprints/Image to Layers(Qwen-Image Layered).json
+++ b/blueprints/Image to Layers(Qwen-Image-Layered).json
@@ -1,15 +1,14 @@
{
- "id": "1a761372-7c82-4016-b9bf-fa285967e1e9",
"revision": 0,
- "last_node_id": 83,
+ "last_node_id": 176,
"last_link_id": 0,
"nodes": [
{
- "id": 83,
- "type": "f754a936-daaf-4b6e-9658-41fdc54d301d",
+ "id": 176,
+ "type": "2d2e3c8e-53b3-4618-be52-6d1d99382f0e",
"pos": [
- 61.999827823554256,
- 153.3332507624185
+ -1150,
+ 200
],
"size": [
400,
@@ -56,6 +55,38 @@
"name": "layers"
},
"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": [
@@ -66,28 +97,41 @@
"links": []
}
],
+ "title": "Image to Layers (Qwen-Image-Layered)",
"properties": {
"proxyWidgets": [
[
- "-1",
+ "6",
"text"
],
[
- "-1",
+ "3",
"steps"
],
[
- "-1",
+ "3",
"cfg"
],
[
- "-1",
+ "83",
"layers"
],
[
"3",
"seed"
],
+ [
+ "37",
+ "unet_name"
+ ],
+ [
+ "38",
+ "clip_name"
+ ],
+ [
+ "39",
+ "vae_name"
+ ],
[
"3",
"control_after_generate"
@@ -95,6 +139,11 @@
],
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -103,25 +152,20 @@
"secondTabOffset": 80,
"secondTabWidth": 65
},
- "widgets_values": [
- "",
- 20,
- 2.5,
- 2
- ]
+ "widgets_values": []
}
],
"links": [],
- "groups": [],
+ "version": 0.4,
"definitions": {
"subgraphs": [
{
- "id": "f754a936-daaf-4b6e-9658-41fdc54d301d",
+ "id": "2d2e3c8e-53b3-4618-be52-6d1d99382f0e",
"version": 1,
"state": {
- "lastGroupId": 3,
- "lastNodeId": 83,
- "lastLinkId": 159,
+ "lastGroupId": 8,
+ "lastNodeId": 176,
+ "lastLinkId": 380,
"lastRerouteId": 0
},
"revision": 0,
@@ -130,10 +174,10 @@
"inputNode": {
"id": -10,
"bounding": [
- -510,
- 523,
+ -720,
+ 720,
120,
- 140
+ 220
]
},
"outputNode": {
@@ -156,8 +200,8 @@
],
"localized_name": "image",
"pos": [
- -410,
- 543
+ -620,
+ 740
]
},
{
@@ -168,8 +212,8 @@
150
],
"pos": [
- -410,
- 563
+ -620,
+ 760
]
},
{
@@ -180,8 +224,8 @@
153
],
"pos": [
- -410,
- 583
+ -620,
+ 780
]
},
{
@@ -192,8 +236,8 @@
154
],
"pos": [
- -410,
- 603
+ -620,
+ 800
]
},
{
@@ -204,8 +248,56 @@
159
],
"pos": [
- -410,
- 623
+ -620,
+ 820
+ ]
+ },
+ {
+ "id": "9f76338b-f4ca-4bb3-b61a-57b3f233061e",
+ "name": "seed",
+ "type": "INT",
+ "linkIds": [
+ 377
+ ],
+ "pos": [
+ -620,
+ 840
+ ]
+ },
+ {
+ "id": "8d0422d5-5eee-4f7e-9817-dc613cc62eca",
+ "name": "unet_name",
+ "type": "COMBO",
+ "linkIds": [
+ 378
+ ],
+ "pos": [
+ -620,
+ 860
+ ]
+ },
+ {
+ "id": "552eece2-a735-4d00-ae78-ded454622bc1",
+ "name": "clip_name",
+ "type": "COMBO",
+ "linkIds": [
+ 379
+ ],
+ "pos": [
+ -620,
+ 880
+ ]
+ },
+ {
+ "id": "1e6d141c-d0f9-4a2b-895c-b6780e57cfa0",
+ "name": "vae_name",
+ "type": "COMBO",
+ "linkIds": [
+ 380
+ ],
+ "pos": [
+ -620,
+ 900
]
}
],
@@ -231,14 +323,14 @@
"type": "CLIPLoader",
"pos": [
-320,
- 310
+ 360
],
"size": [
- 346.7470703125,
- 106
+ 350,
+ 150
],
"flags": {},
- "order": 0,
+ "order": 5,
"mode": 0,
"inputs": [
{
@@ -248,7 +340,7 @@
"widget": {
"name": "clip_name"
},
- "link": null
+ "link": 379
},
{
"localized_name": "type",
@@ -283,9 +375,14 @@
}
],
"properties": {
- "Node name for S&R": "CLIPLoader",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "CLIPLoader",
"models": [
{
"name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
@@ -312,14 +409,14 @@
"type": "VAELoader",
"pos": [
-320,
- 460
+ 580
],
"size": [
- 346.7470703125,
- 58
+ 350,
+ 110
],
"flags": {},
- "order": 1,
+ "order": 6,
"mode": 0,
"inputs": [
{
@@ -329,7 +426,7 @@
"widget": {
"name": "vae_name"
},
- "link": null
+ "link": 380
}
],
"outputs": [
@@ -345,9 +442,14 @@
}
],
"properties": {
- "Node name for S&R": "VAELoader",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "VAELoader",
"models": [
{
"name": "qwen_image_layered_vae.safetensors",
@@ -375,11 +477,11 @@
420
],
"size": [
- 425.27801513671875,
- 180.6060791015625
+ 430,
+ 190
],
"flags": {},
- "order": 3,
+ "order": 2,
"mode": 0,
"inputs": [
{
@@ -411,9 +513,14 @@
],
"title": "CLIP Text Encode (Negative Prompt)",
"properties": {
- "Node name for S&R": "CLIPTextEncode",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "CLIPTextEncode",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -432,12 +539,12 @@
"id": 70,
"type": "ReferenceLatent",
"pos": [
- 330,
- 670
+ 140,
+ 700
],
"size": [
- 204.1666717529297,
- 46
+ 210,
+ 50
],
"flags": {
"collapsed": true
@@ -470,9 +577,14 @@
}
],
"properties": {
- "Node name for S&R": "ReferenceLatent",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "ReferenceLatent",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -480,19 +592,18 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 69,
"type": "ReferenceLatent",
"pos": [
- 330,
- 710
+ 160,
+ 820
],
"size": [
- 204.1666717529297,
- 46
+ 210,
+ 50
],
"flags": {
"collapsed": true
@@ -525,9 +636,14 @@
}
],
"properties": {
- "Node name for S&R": "ReferenceLatent",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "ReferenceLatent",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -535,8 +651,7 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 66,
@@ -547,10 +662,10 @@
],
"size": [
270,
- 58
+ 110
],
"flags": {},
- "order": 4,
+ "order": 7,
"mode": 0,
"inputs": [
{
@@ -580,9 +695,14 @@
}
],
"properties": {
- "Node name for S&R": "ModelSamplingAuraFlow",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "ModelSamplingAuraFlow",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -600,11 +720,11 @@
"type": "LatentCutToBatch",
"pos": [
830,
- 160
+ 140
],
"size": [
270,
- 82
+ 140
],
"flags": {},
"order": 11,
@@ -646,9 +766,14 @@
}
],
"properties": {
- "Node name for S&R": "LatentCutToBatch",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "LatentCutToBatch",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -666,12 +791,12 @@
"id": 71,
"type": "VAEEncode",
"pos": [
- 100,
- 690
+ -280,
+ 780
],
"size": [
- 140,
- 46
+ 230,
+ 100
],
"flags": {
"collapsed": false
@@ -704,9 +829,14 @@
}
],
"properties": {
- "Node name for S&R": "VAEEncode",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "VAEEncode",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -714,24 +844,23 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
850,
- 310
+ 370
],
"size": [
210,
- 46
+ 50
],
"flags": {
"collapsed": true
},
- "order": 7,
+ "order": 3,
"mode": 0,
"inputs": [
{
@@ -759,9 +888,14 @@
}
],
"properties": {
- "Node name for S&R": "VAEDecode",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "VAEDecode",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -769,8 +903,7 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 6,
@@ -780,11 +913,11 @@
180
],
"size": [
- 422.84503173828125,
- 164.31304931640625
+ 430,
+ 170
],
"flags": {},
- "order": 6,
+ "order": 1,
"mode": 0,
"inputs": [
{
@@ -816,9 +949,14 @@
],
"title": "CLIP Text Encode (Positive Prompt)",
"properties": {
- "Node name for S&R": "CLIPTextEncode",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "CLIPTextEncode",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -838,14 +976,14 @@
"type": "KSampler",
"pos": [
530,
- 280
+ 340
],
"size": [
270,
400
],
"flags": {},
- "order": 5,
+ "order": 0,
"mode": 0,
"inputs": [
{
@@ -879,7 +1017,7 @@
"widget": {
"name": "seed"
},
- "link": null
+ "link": 377
},
{
"localized_name": "steps",
@@ -939,9 +1077,14 @@
}
],
"properties": {
- "Node name for S&R": "KSampler",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "KSampler",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -964,12 +1107,12 @@
"id": 78,
"type": "GetImageSize",
"pos": [
- 80,
- 790
+ -280,
+ 930
],
"size": [
- 210,
- 136
+ 230,
+ 140
],
"flags": {},
"order": 12,
@@ -1007,9 +1150,14 @@
}
],
"properties": {
- "Node name for S&R": "GetImageSize",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "GetImageSize",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -1017,23 +1165,23 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 83,
"type": "EmptyQwenImageLayeredLatentImage",
"pos": [
- 320,
- 790
+ -280,
+ 1120
],
"size": [
- 330.9341796875,
- 130
+ 340,
+ 200
],
"flags": {},
"order": 13,
"mode": 0,
+ "showAdvanced": true,
"inputs": [
{
"localized_name": "width",
@@ -1083,9 +1231,14 @@
}
],
"properties": {
- "Node name for S&R": "EmptyQwenImageLayeredLatentImage",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "EmptyQwenImageLayeredLatentImage",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -1109,11 +1262,11 @@
180
],
"size": [
- 346.7470703125,
- 82
+ 350,
+ 110
],
"flags": {},
- "order": 2,
+ "order": 4,
"mode": 0,
"inputs": [
{
@@ -1123,7 +1276,7 @@
"widget": {
"name": "unet_name"
},
- "link": null
+ "link": 378
},
{
"localized_name": "weight_dtype",
@@ -1147,9 +1300,14 @@
}
],
"properties": {
- "Node name for S&R": "UNETLoader",
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {},
+ "version": "7.7"
+ },
+ "Node name for S&R": "UNETLoader",
"models": [
{
"name": "qwen_image_layered_bf16.safetensors",
@@ -1191,8 +1349,8 @@
"bounding": [
-330,
110,
- 366.7470703125,
- 421.6
+ 370,
+ 610
],
"color": "#3f789e",
"font_size": 24,
@@ -1391,16 +1549,48 @@
"target_id": 83,
"target_slot": 2,
"type": "INT"
+ },
+ {
+ "id": 377,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 3,
+ "target_slot": 4,
+ "type": "INT"
+ },
+ {
+ "id": 378,
+ "origin_id": -10,
+ "origin_slot": 6,
+ "target_id": 37,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 379,
+ "origin_id": -10,
+ "origin_slot": 7,
+ "target_id": 38,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 380,
+ "origin_id": -10,
+ "origin_slot": 8,
+ "target_id": 39,
+ "target_slot": 0,
+ "type": "COMBO"
}
],
"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."
}
]
},
- "config": {},
"extra": {
"ds": {
"scale": 1.14,
@@ -1409,7 +1599,6 @@
6.855893974423647
]
},
- "workflowRendererVersion": "LG"
- },
- "version": 0.4
-}
+ "ue_links": []
+ }
+}
\ No newline at end of file
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 Video (LTX-2.3).json b/blueprints/Image to Video (LTX-2.3).json
new file mode 100644
index 000000000..3db524ea0
--- /dev/null
+++ b/blueprints/Image to Video (LTX-2.3).json
@@ -0,0 +1,4234 @@
+{
+ "revision": 0,
+ "last_node_id": 320,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 320,
+ "type": "2454ad83-157c-40dd-9f19-5daaf4041ce0",
+ "pos": [
+ 30,
+ 4150
+ ],
+ "size": [
+ 390,
+ 466.625
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "first_frame",
+ "localized_name": "input",
+ "name": "input",
+ "type": "IMAGE,MASK",
+ "link": null
+ },
+ {
+ "name": "value",
+ "type": "STRING",
+ "widget": {
+ "name": "value"
+ },
+ "link": null
+ },
+ {
+ "label": "width",
+ "name": "value_2",
+ "type": "INT",
+ "widget": {
+ "name": "value_2"
+ },
+ "link": null
+ },
+ {
+ "label": "height",
+ "name": "value_3",
+ "type": "INT",
+ "widget": {
+ "name": "value_3"
+ },
+ "link": null
+ },
+ {
+ "label": "duration",
+ "name": "value_4",
+ "type": "INT",
+ "widget": {
+ "name": "value_4"
+ },
+ "link": null
+ },
+ {
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "ckpt_name"
+ },
+ "link": null
+ },
+ {
+ "label": "distilled_lora",
+ "name": "lora_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "lora_name"
+ },
+ "link": null
+ },
+ {
+ "name": "text_encoder",
+ "type": "COMBO",
+ "widget": {
+ "name": "text_encoder"
+ },
+ "link": null
+ },
+ {
+ "label": "latent_upscale_model",
+ "name": "model_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "model_name"
+ },
+ "link": null
+ },
+ {
+ "label": "fps",
+ "name": "value_5",
+ "type": "INT",
+ "widget": {
+ "name": "value_5"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "VIDEO",
+ "name": "VIDEO",
+ "type": "VIDEO",
+ "links": []
+ }
+ ],
+ "title": "Image to Video (LTX-2.3)",
+ "properties": {
+ "proxyWidgets": [
+ [
+ "319",
+ "value"
+ ],
+ [
+ "312",
+ "value"
+ ],
+ [
+ "299",
+ "value"
+ ],
+ [
+ "301",
+ "value"
+ ],
+ [
+ "300",
+ "value"
+ ],
+ [
+ "316",
+ "ckpt_name"
+ ],
+ [
+ "277",
+ "control_after_generate"
+ ],
+ [
+ "277",
+ "noise_seed"
+ ],
+ [
+ "285",
+ "lora_name"
+ ],
+ [
+ "317",
+ "text_encoder"
+ ],
+ [
+ "311",
+ "model_name"
+ ]
+ ],
+ "ue_properties": {
+ "widget_ue_connectable": {
+ "value_1": true,
+ "value_2": true,
+ "value_3": true,
+ "value_4": true,
+ "lora_name": true,
+ "model_name": true,
+ "value_5": true
+ },
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": []
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "2454ad83-157c-40dd-9f19-5daaf4041ce0",
+ "version": 1,
+ "state": {
+ "lastGroupId": 25,
+ "lastNodeId": 323,
+ "lastLinkId": 631,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Image to Video (LTX-2.3)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ 730,
+ 4110,
+ 162.162109375,
+ 240
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 6590,
+ 4360,
+ 120,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "7afd6ea8-c738-4fd9-97b8-66fa905cd381",
+ "name": "input",
+ "type": "IMAGE,MASK",
+ "linkIds": [
+ 535
+ ],
+ "localized_name": "input",
+ "label": "first_frame",
+ "pos": [
+ 872.162109375,
+ 4130
+ ]
+ },
+ {
+ "id": "9494c550-4172-49c6-930e-5b508f775e77",
+ "name": "value",
+ "type": "STRING",
+ "linkIds": [
+ 595
+ ],
+ "pos": [
+ 872.162109375,
+ 4150
+ ]
+ },
+ {
+ "id": "58dbb3f6-f924-4548-96ef-e0e34610bd4e",
+ "name": "value_2",
+ "type": "INT",
+ "linkIds": [
+ 597
+ ],
+ "label": "width",
+ "pos": [
+ 872.162109375,
+ 4170
+ ]
+ },
+ {
+ "id": "6086d5b8-2586-448c-a641-dd14d76dd102",
+ "name": "value_3",
+ "type": "INT",
+ "linkIds": [
+ 598
+ ],
+ "label": "height",
+ "pos": [
+ 872.162109375,
+ 4190
+ ]
+ },
+ {
+ "id": "feb8c2eb-ae48-4fa8-bc24-929552d656c3",
+ "name": "value_4",
+ "type": "INT",
+ "linkIds": [
+ 599
+ ],
+ "label": "duration",
+ "pos": [
+ 872.162109375,
+ 4210
+ ]
+ },
+ {
+ "id": "d7255058-319a-4880-8f9a-7e542c8f3c3c",
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "linkIds": [
+ 601,
+ 604,
+ 605
+ ],
+ "pos": [
+ 872.162109375,
+ 4230
+ ]
+ },
+ {
+ "id": "4afce68d-8f65-4342-9d6d-ae0a7688c3e3",
+ "name": "lora_name",
+ "type": "COMBO",
+ "linkIds": [
+ 602
+ ],
+ "label": "distilled_lora",
+ "pos": [
+ 872.162109375,
+ 4250
+ ]
+ },
+ {
+ "id": "ab842b4b-c977-4679-b421-424722785b57",
+ "name": "text_encoder",
+ "type": "COMBO",
+ "linkIds": [
+ 606
+ ],
+ "pos": [
+ 872.162109375,
+ 4270
+ ]
+ },
+ {
+ "id": "9e47372d-28d9-4311-91e9-e90d03f4eb43",
+ "name": "model_name",
+ "type": "COMBO",
+ "linkIds": [
+ 607
+ ],
+ "label": "latent_upscale_model",
+ "pos": [
+ 872.162109375,
+ 4290
+ ]
+ },
+ {
+ "id": "3e32ce15-0ae7-4cd0-909f-a354e8e9c4c9",
+ "name": "value_5",
+ "type": "INT",
+ "linkIds": [
+ 624
+ ],
+ "label": "fps",
+ "pos": [
+ 872.162109375,
+ 4310
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "954ef307-c897-4eea-8b5c-5c6ce15a5357",
+ "name": "VIDEO",
+ "type": "VIDEO",
+ "linkIds": [
+ 536
+ ],
+ "localized_name": "VIDEO",
+ "pos": [
+ 6610,
+ 4380
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 276,
+ "type": "RandomNoise",
+ "pos": [
+ 4700,
+ 3650
+ ],
+ "size": [
+ 280,
+ 110
+ ],
+ "flags": {},
+ "order": 0,
+ "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": [
+ 490
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.75",
+ "Node name for S&R": "RandomNoise",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 42,
+ "fixed"
+ ]
+ },
+ {
+ "id": 277,
+ "type": "RandomNoise",
+ "pos": [
+ 3160,
+ 3630
+ ],
+ "size": [
+ 280,
+ 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": [
+ 483
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "Node name for S&R": "RandomNoise",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 519681071352364,
+ "randomize"
+ ]
+ },
+ {
+ "id": 278,
+ "type": "LTXVConcatAVLatent",
+ "pos": [
+ 4710,
+ 4490
+ ],
+ "size": [
+ 280,
+ 100
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "video_latent",
+ "name": "video_latent",
+ "type": "LATENT",
+ "link": 512
+ },
+ {
+ "localized_name": "audio_latent",
+ "name": "audio_latent",
+ "type": "LATENT",
+ "link": 513
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "links": [
+ 494
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "Node name for S&R": "LTXVConcatAVLatent",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 279,
+ "type": "LTXVAudioVAELoader",
+ "pos": [
+ 1660,
+ 4100
+ ],
+ "size": [
+ 430,
+ 110
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "ckpt_name",
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "ckpt_name"
+ },
+ "link": 604
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "Audio VAE",
+ "name": "Audio VAE",
+ "type": "VAE",
+ "links": [
+ 481,
+ 496
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.68",
+ "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-dev-fp8.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-dev-fp8.safetensors",
+ "directory": "checkpoints"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ltx-2.3-22b-dev-fp8.safetensors"
+ ]
+ },
+ {
+ "id": 280,
+ "type": "KSamplerSelect",
+ "pos": [
+ 4700,
+ 4100
+ ],
+ "size": [
+ 280,
+ 110
+ ],
+ "flags": {},
+ "order": 2,
+ "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": [
+ 492
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.75",
+ "Node name for S&R": "KSamplerSelect",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "euler_cfg_pp"
+ ]
+ },
+ {
+ "id": 281,
+ "type": "ManualSigmas",
+ "pos": [
+ 4700,
+ 4290
+ ],
+ "size": [
+ 280,
+ 110
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "sigmas",
+ "name": "sigmas",
+ "type": "STRING",
+ "widget": {
+ "name": "sigmas"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "SIGMAS",
+ "name": "SIGMAS",
+ "type": "SIGMAS",
+ "links": [
+ 493
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "Node name for S&R": "ManualSigmas",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "0.85, 0.7250, 0.4219, 0.0"
+ ]
+ },
+ {
+ "id": 282,
+ "type": "CFGGuider",
+ "pos": [
+ 4700,
+ 3850
+ ],
+ "size": [
+ 280,
+ 160
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 478
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 479
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 480
+ },
+ {
+ "localized_name": "cfg",
+ "name": "cfg",
+ "type": "FLOAT",
+ "widget": {
+ "name": "cfg"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "GUIDER",
+ "name": "GUIDER",
+ "type": "GUIDER",
+ "links": [
+ 491
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.71",
+ "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": 283,
+ "type": "SamplerCustomAdvanced",
+ "pos": [
+ 3550,
+ 3630
+ ],
+ "size": [
+ 230,
+ 170
+ ],
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "noise",
+ "name": "noise",
+ "type": "NOISE",
+ "link": 483
+ },
+ {
+ "localized_name": "guider",
+ "name": "guider",
+ "type": "GUIDER",
+ "link": 484
+ },
+ {
+ "localized_name": "sampler",
+ "name": "sampler",
+ "type": "SAMPLER",
+ "link": 485
+ },
+ {
+ "localized_name": "sigmas",
+ "name": "sigmas",
+ "type": "SIGMAS",
+ "link": 544
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 487
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "LATENT",
+ "links": [
+ 488
+ ]
+ },
+ {
+ "localized_name": "denoised_output",
+ "name": "denoised_output",
+ "type": "LATENT",
+ "links": []
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.60",
+ "Node name for S&R": "SamplerCustomAdvanced",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 284,
+ "type": "LTXVCropGuides",
+ "pos": [
+ 3830,
+ 3810
+ ],
+ "size": [
+ 250,
+ 120
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 475
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 476
+ },
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "link": 477
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": [
+ 479
+ ]
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": [
+ 480
+ ]
+ },
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "slot_index": 2,
+ "links": []
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.68",
+ "Node name for S&R": "LTXVCropGuides",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 285,
+ "type": "LoraLoaderModelOnly",
+ "pos": [
+ 1660,
+ 3890
+ ],
+ "size": [
+ 430,
+ 140
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 520
+ },
+ {
+ "localized_name": "lora_name",
+ "name": "lora_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "lora_name"
+ },
+ "link": 602
+ },
+ {
+ "localized_name": "strength_model",
+ "name": "strength_model",
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength_model"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 478,
+ 541
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.75",
+ "Node name for S&R": "LoraLoaderModelOnly",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65,
+ "models": [
+ {
+ "name": "ltx-2.3-22b-distilled-lora-384.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-distilled-lora-384.safetensors",
+ "directory": "loras"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ltx-2.3-22b-distilled-lora-384.safetensors",
+ 0.5
+ ]
+ },
+ {
+ "id": 286,
+ "type": "ResizeImagesByLongerEdge",
+ "pos": [
+ 2070,
+ 4810
+ ],
+ "size": [
+ 310,
+ 110
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 13,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "images",
+ "name": "images",
+ "type": "IMAGE",
+ "link": 523
+ },
+ {
+ "localized_name": "longer_edge",
+ "name": "longer_edge",
+ "type": "INT",
+ "widget": {
+ "name": "longer_edge"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "images",
+ "name": "images",
+ "type": "IMAGE",
+ "links": [
+ 505
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "Node name for S&R": "ResizeImagesByLongerEdge",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 1536
+ ]
+ },
+ {
+ "id": 287,
+ "type": "LTXVLatentUpsampler",
+ "pos": [
+ 4250,
+ 3760
+ ],
+ "size": [
+ 330,
+ 120
+ ],
+ "flags": {},
+ "order": 14,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 547
+ },
+ {
+ "localized_name": "upscale_model",
+ "name": "upscale_model",
+ "type": "LATENT_UPSCALE_MODEL",
+ "link": 545
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 554
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "LATENT",
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 548
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "Node name for S&R": "LTXVLatentUpsampler",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 288,
+ "type": "LTXVImgToVideoInplace",
+ "pos": [
+ 4230,
+ 4100
+ ],
+ "size": [
+ 300,
+ 180
+ ],
+ "flags": {},
+ "order": 15,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 552
+ },
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 515
+ },
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "link": 548
+ },
+ {
+ "localized_name": "strength",
+ "name": "strength",
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "bypass",
+ "name": "bypass",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "bypass"
+ },
+ "link": 543
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "links": [
+ 512
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXVImgToVideoInplace",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 1,
+ false
+ ]
+ },
+ {
+ "id": 289,
+ "type": "LTXVPreprocess",
+ "pos": [
+ 2100,
+ 5010
+ ],
+ "size": [
+ 290,
+ 110
+ ],
+ "flags": {},
+ "order": 16,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 505
+ },
+ {
+ "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": [
+ 510,
+ 515
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXVPreprocess",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 18
+ ]
+ },
+ {
+ "id": 290,
+ "type": "ResizeImageMaskNode",
+ "pos": [
+ 1660,
+ 4810
+ ],
+ "size": [
+ 300,
+ 160
+ ],
+ "flags": {},
+ "order": 17,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "input",
+ "name": "input",
+ "type": "IMAGE,MASK",
+ "link": 535
+ },
+ {
+ "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": 558
+ },
+ {
+ "localized_name": "height",
+ "name": "resize_type.height",
+ "type": "INT",
+ "widget": {
+ "name": "resize_type.height"
+ },
+ "link": 559
+ },
+ {
+ "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": [
+ 523
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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",
+ 1920,
+ 1088,
+ "center",
+ "lanczos"
+ ]
+ },
+ {
+ "id": 291,
+ "type": "KSamplerSelect",
+ "pos": [
+ 3160,
+ 4040
+ ],
+ "size": [
+ 280,
+ 110
+ ],
+ "flags": {},
+ "order": 4,
+ "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": [
+ 485
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "Node name for S&R": "KSamplerSelect",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "euler_ancestral_cfg_pp"
+ ]
+ },
+ {
+ "id": 292,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 2540,
+ 4830
+ ],
+ "size": [
+ 210,
+ 80
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 18,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 560
+ },
+ {
+ "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": [
+ 561
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "Node name for S&R": "ComfyMathExpression",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "a/2"
+ ]
+ },
+ {
+ "id": 293,
+ "type": "Reroute",
+ "pos": [
+ 3850,
+ 4050
+ ],
+ "size": [
+ 230,
+ 40
+ ],
+ "flags": {},
+ "order": 19,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 557
+ }
+ ],
+ "outputs": [
+ {
+ "name": "",
+ "type": "VAE",
+ "links": [
+ 552,
+ 553,
+ 554
+ ]
+ }
+ ],
+ "properties": {
+ "showOutputText": false,
+ "horizontal": false,
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ }
+ }
+ },
+ {
+ "id": 294,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 2550,
+ 4890
+ ],
+ "size": [
+ 210,
+ 80
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 20,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 562
+ },
+ {
+ "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": [
+ 563
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "Node name for S&R": "ComfyMathExpression",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "a/2"
+ ]
+ },
+ {
+ "id": 295,
+ "type": "EmptyLTXVLatentVideo",
+ "pos": [
+ 2870,
+ 4940
+ ],
+ "size": [
+ 280,
+ 200
+ ],
+ "flags": {},
+ "order": 21,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 561
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 563
+ },
+ {
+ "localized_name": "length",
+ "name": "length",
+ "type": "INT",
+ "widget": {
+ "name": "length"
+ },
+ "link": 631
+ },
+ {
+ "localized_name": "batch_size",
+ "name": "batch_size",
+ "type": "INT",
+ "widget": {
+ "name": "batch_size"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "LATENT",
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 511
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.60",
+ "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": 296,
+ "type": "LTXVImgToVideoInplace",
+ "pos": [
+ 3230,
+ 4810
+ ],
+ "size": [
+ 280,
+ 180
+ ],
+ "flags": {},
+ "order": 22,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 556
+ },
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 510
+ },
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "link": 511
+ },
+ {
+ "localized_name": "strength",
+ "name": "strength",
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "bypass",
+ "name": "bypass",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "bypass"
+ },
+ "link": 542
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "links": [
+ 497
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXVImgToVideoInplace",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 0.7,
+ false
+ ]
+ },
+ {
+ "id": 297,
+ "type": "LTXVAudioVAEDecode",
+ "pos": [
+ 5760,
+ 3970
+ ],
+ "size": [
+ 270,
+ 100
+ ],
+ "flags": {},
+ "order": 23,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 495
+ },
+ {
+ "label": "Audio VAE",
+ "localized_name": "audio_vae",
+ "name": "audio_vae",
+ "type": "VAE",
+ "link": 496
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "Audio",
+ "name": "Audio",
+ "type": "AUDIO",
+ "links": [
+ 534
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXVAudioVAEDecode",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 298,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 2540,
+ 5030
+ ],
+ "size": [
+ 210,
+ 80
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 24,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 564
+ },
+ {
+ "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": [
+ 566,
+ 591
+ ]
+ },
+ {
+ "localized_name": "INT",
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 565
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "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": 299,
+ "type": "PrimitiveInt",
+ "pos": [
+ 1190,
+ 4650
+ ],
+ "size": [
+ 370,
+ 110
+ ],
+ "flags": {},
+ "order": 25,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "INT",
+ "widget": {
+ "name": "value"
+ },
+ "link": 598
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "INT",
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 559,
+ 562
+ ]
+ }
+ ],
+ "title": "Height",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "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": 300,
+ "type": "PrimitiveInt",
+ "pos": [
+ 1190,
+ 4840
+ ],
+ "size": [
+ 370,
+ 110
+ ],
+ "flags": {},
+ "order": 26,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "INT",
+ "widget": {
+ "name": "value"
+ },
+ "link": 624
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "INT",
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 564,
+ 629
+ ]
+ }
+ ],
+ "title": "Frame Rate",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "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": 301,
+ "type": "PrimitiveInt",
+ "pos": [
+ 1190,
+ 4280
+ ],
+ "size": [
+ 370,
+ 110
+ ],
+ "flags": {},
+ "order": 27,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "INT",
+ "widget": {
+ "name": "value"
+ },
+ "link": 599
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "INT",
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 628
+ ]
+ }
+ ],
+ "title": "Duration",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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": 302,
+ "type": "PrimitiveBoolean",
+ "pos": [
+ 1190,
+ 4110
+ ],
+ "size": [
+ 370,
+ 100
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "value"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOOLEAN",
+ "name": "BOOLEAN",
+ "type": "BOOLEAN",
+ "links": [
+ 542,
+ 543
+ ]
+ }
+ ],
+ "title": "Switch to Text to Video?",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.0",
+ "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": 303,
+ "type": "CLIPTextEncode",
+ "pos": [
+ 2170,
+ 3640
+ ],
+ "size": [
+ 600,
+ 390
+ ],
+ "flags": {},
+ "order": 28,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 615
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": 625
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 526
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "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": 304,
+ "type": "LTXVConditioning",
+ "pos": [
+ 2800,
+ 3810
+ ],
+ "size": [
+ 280,
+ 130
+ ],
+ "flags": {},
+ "order": 29,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 526
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 527
+ },
+ {
+ "localized_name": "frame_rate",
+ "name": "frame_rate",
+ "type": "FLOAT",
+ "widget": {
+ "name": "frame_rate"
+ },
+ "link": 566
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": [
+ 475,
+ 518
+ ]
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": [
+ 476,
+ 519
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "Node name for S&R": "LTXVConditioning",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 24
+ ]
+ },
+ {
+ "id": 305,
+ "type": "LTXVEmptyLatentAudio",
+ "pos": [
+ 3540,
+ 4960
+ ],
+ "size": [
+ 280,
+ 170
+ ],
+ "flags": {},
+ "order": 30,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "audio_vae",
+ "name": "audio_vae",
+ "type": "VAE",
+ "link": 481
+ },
+ {
+ "localized_name": "frames_number",
+ "name": "frames_number",
+ "type": "INT",
+ "widget": {
+ "name": "frames_number"
+ },
+ "link": 630
+ },
+ {
+ "localized_name": "frame_rate",
+ "name": "frame_rate",
+ "type": "INT",
+ "widget": {
+ "name": "frame_rate"
+ },
+ "link": 565
+ },
+ {
+ "localized_name": "batch_size",
+ "name": "batch_size",
+ "type": "INT",
+ "widget": {
+ "name": "batch_size"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "Latent",
+ "name": "Latent",
+ "type": "LATENT",
+ "links": [
+ 498
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.68",
+ "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": 306,
+ "type": "ManualSigmas",
+ "pos": [
+ 3160,
+ 4220
+ ],
+ "size": [
+ 500,
+ 110
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "sigmas",
+ "name": "sigmas",
+ "type": "STRING",
+ "widget": {
+ "name": "sigmas"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "SIGMAS",
+ "name": "SIGMAS",
+ "type": "SIGMAS",
+ "links": [
+ 544
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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, 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875, 0.0"
+ ]
+ },
+ {
+ "id": 307,
+ "type": "LTXVSeparateAVLatent",
+ "pos": [
+ 3820,
+ 3630
+ ],
+ "size": [
+ 250,
+ 100
+ ],
+ "flags": {},
+ "order": 31,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "av_latent",
+ "name": "av_latent",
+ "type": "LATENT",
+ "link": 488
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "video_latent",
+ "name": "video_latent",
+ "type": "LATENT",
+ "links": [
+ 477,
+ 547
+ ]
+ },
+ {
+ "localized_name": "audio_latent",
+ "name": "audio_latent",
+ "type": "LATENT",
+ "links": [
+ 513
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "Node name for S&R": "LTXVSeparateAVLatent",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 308,
+ "type": "SamplerCustomAdvanced",
+ "pos": [
+ 5050,
+ 3650
+ ],
+ "size": [
+ 230,
+ 170
+ ],
+ "flags": {},
+ "order": 32,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "noise",
+ "name": "noise",
+ "type": "NOISE",
+ "link": 490
+ },
+ {
+ "localized_name": "guider",
+ "name": "guider",
+ "type": "GUIDER",
+ "link": 491
+ },
+ {
+ "localized_name": "sampler",
+ "name": "sampler",
+ "type": "SAMPLER",
+ "link": 492
+ },
+ {
+ "localized_name": "sigmas",
+ "name": "sigmas",
+ "type": "SIGMAS",
+ "link": 493
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 494
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "LATENT",
+ "links": [
+ 578
+ ]
+ },
+ {
+ "localized_name": "denoised_output",
+ "name": "denoised_output",
+ "type": "LATENT",
+ "links": []
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.75",
+ "Node name for S&R": "SamplerCustomAdvanced",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 309,
+ "type": "LTXVSeparateAVLatent",
+ "pos": [
+ 5390,
+ 3650
+ ],
+ "size": [
+ 230,
+ 100
+ ],
+ "flags": {},
+ "order": 33,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "av_latent",
+ "name": "av_latent",
+ "type": "LATENT",
+ "link": 578
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "video_latent",
+ "name": "video_latent",
+ "type": "LATENT",
+ "links": [
+ 539
+ ]
+ },
+ {
+ "localized_name": "audio_latent",
+ "name": "audio_latent",
+ "type": "LATENT",
+ "links": [
+ 495
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "Node name for S&R": "LTXVSeparateAVLatent",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 310,
+ "type": "CreateVideo",
+ "pos": [
+ 6050,
+ 4490
+ ],
+ "size": [
+ 280,
+ 130
+ ],
+ "flags": {},
+ "order": 34,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "images",
+ "name": "images",
+ "type": "IMAGE",
+ "link": 538
+ },
+ {
+ "localized_name": "audio",
+ "name": "audio",
+ "shape": 7,
+ "type": "AUDIO",
+ "link": 534
+ },
+ {
+ "localized_name": "fps",
+ "name": "fps",
+ "type": "FLOAT",
+ "widget": {
+ "name": "fps"
+ },
+ "link": 591
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "VIDEO",
+ "name": "VIDEO",
+ "type": "VIDEO",
+ "links": [
+ 536
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "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": 311,
+ "type": "LatentUpscaleModelLoader",
+ "pos": [
+ 1670,
+ 4550
+ ],
+ "size": [
+ 400,
+ 110
+ ],
+ "flags": {},
+ "order": 35,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model_name",
+ "name": "model_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "model_name"
+ },
+ "link": 607
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "LATENT_UPSCALE_MODEL",
+ "name": "LATENT_UPSCALE_MODEL",
+ "type": "LATENT_UPSCALE_MODEL",
+ "links": [
+ 545
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LatentUpscaleModelLoader",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65,
+ "models": [
+ {
+ "name": "ltx-2.3-spatial-upscaler-x2-1.1.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x2-1.1.safetensors",
+ "directory": "latent_upscale_models"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ltx-2.3-spatial-upscaler-x2-1.1.safetensors"
+ ]
+ },
+ {
+ "id": 312,
+ "type": "PrimitiveInt",
+ "pos": [
+ 1190,
+ 4470
+ ],
+ "size": [
+ 370,
+ 110
+ ],
+ "flags": {},
+ "order": 36,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "INT",
+ "widget": {
+ "name": "value"
+ },
+ "link": 597
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "INT",
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 558,
+ 560
+ ]
+ }
+ ],
+ "title": "Width",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "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": 313,
+ "type": "CLIPTextEncode",
+ "pos": [
+ 2180,
+ 4120
+ ],
+ "size": [
+ 600,
+ 170
+ ],
+ "flags": {},
+ "order": 37,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 627
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 527
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "Node name for S&R": "CLIPTextEncode",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "pc game, console game, video game, cartoon, childish, ugly"
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 314,
+ "type": "CFGGuider",
+ "pos": [
+ 3160,
+ 3810
+ ],
+ "size": [
+ 280,
+ 160
+ ],
+ "flags": {},
+ "order": 38,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 541
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 518
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 519
+ },
+ {
+ "localized_name": "cfg",
+ "name": "cfg",
+ "type": "FLOAT",
+ "widget": {
+ "name": "cfg"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "GUIDER",
+ "name": "GUIDER",
+ "type": "GUIDER",
+ "links": [
+ 484
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "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": 315,
+ "type": "VAEDecodeTiled",
+ "pos": [
+ 5750,
+ 3610
+ ],
+ "size": [
+ 280,
+ 200
+ ],
+ "flags": {},
+ "order": 39,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 539
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 553
+ },
+ {
+ "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": [
+ 538
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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,
+ 4
+ ]
+ },
+ {
+ "id": 316,
+ "type": "CheckpointLoaderSimple",
+ "pos": [
+ 1660,
+ 3660
+ ],
+ "size": [
+ 430,
+ 160
+ ],
+ "flags": {},
+ "order": 40,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "ckpt_name",
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "ckpt_name"
+ },
+ "link": 601
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 520
+ ]
+ },
+ {
+ "localized_name": "CLIP",
+ "name": "CLIP",
+ "type": "CLIP",
+ "links": []
+ },
+ {
+ "localized_name": "VAE",
+ "name": "VAE",
+ "type": "VAE",
+ "links": [
+ 556,
+ 557
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "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-dev-fp8.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-dev-fp8.safetensors",
+ "directory": "checkpoints"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ltx-2.3-22b-dev-fp8.safetensors"
+ ]
+ },
+ {
+ "id": 317,
+ "type": "LTXAVTextEncoderLoader",
+ "pos": [
+ 1660,
+ 4280
+ ],
+ "size": [
+ 430,
+ 170
+ ],
+ "flags": {},
+ "order": 41,
+ "mode": 0,
+ "showAdvanced": false,
+ "inputs": [
+ {
+ "localized_name": "text_encoder",
+ "name": "text_encoder",
+ "type": "COMBO",
+ "widget": {
+ "name": "text_encoder"
+ },
+ "link": 606
+ },
+ {
+ "localized_name": "ckpt_name",
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "ckpt_name"
+ },
+ "link": 605
+ },
+ {
+ "localized_name": "device",
+ "name": "device",
+ "type": "COMBO",
+ "widget": {
+ "name": "device"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CLIP",
+ "name": "CLIP",
+ "type": "CLIP",
+ "links": [
+ 615,
+ 627
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXAVTextEncoderLoader",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65,
+ "models": [
+ {
+ "name": "ltx-2.3-22b-dev-fp8.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-dev-fp8.safetensors",
+ "directory": "checkpoints"
+ },
+ {
+ "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"
+ }
+ ]
+ },
+ "widgets_values": [
+ "gemma_3_12B_it_fp4_mixed.safetensors",
+ "ltx-2.3-22b-dev-fp8.safetensors",
+ "default"
+ ]
+ },
+ {
+ "id": 318,
+ "type": "LTXVConcatAVLatent",
+ "pos": [
+ 3860,
+ 4830
+ ],
+ "size": [
+ 240,
+ 100
+ ],
+ "flags": {},
+ "order": 42,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "video_latent",
+ "name": "video_latent",
+ "type": "LATENT",
+ "link": 497
+ },
+ {
+ "localized_name": "audio_latent",
+ "name": "audio_latent",
+ "type": "LATENT",
+ "link": 498
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "links": [
+ 487
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXVConcatAVLatent",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 319,
+ "type": "PrimitiveStringMultiline",
+ "pos": [
+ 1190,
+ 3680
+ ],
+ "size": [
+ 370,
+ 350
+ ],
+ "flags": {},
+ "order": 43,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "STRING",
+ "widget": {
+ "name": "value"
+ },
+ "link": 595
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "STRING",
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 625
+ ]
+ }
+ ],
+ "title": "Prompt",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "Node name for S&R": "PrimitiveStringMultiline",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ ""
+ ]
+ },
+ {
+ "id": 323,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 1210,
+ 5040
+ ],
+ "size": [
+ 360,
+ 210
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 44,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 628
+ },
+ {
+ "label": "b",
+ "localized_name": "values.b",
+ "name": "values.b",
+ "shape": 7,
+ "type": "FLOAT,INT",
+ "link": 629
+ },
+ {
+ "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": [
+ 630,
+ 631
+ ]
+ }
+ ],
+ "title": "Math Expression (length)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "a * b + 1"
+ ]
+ }
+ ],
+ "groups": [
+ {
+ "id": 1,
+ "title": "Model",
+ "bounding": [
+ 1630,
+ 3550,
+ 480,
+ 1140
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 2,
+ "title": "Generate Low Resolution",
+ "bounding": [
+ 3130,
+ 3550,
+ 1000,
+ 1140
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 3,
+ "title": "Prompt",
+ "bounding": [
+ 2140,
+ 3550,
+ 960,
+ 1140
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 6,
+ "title": "Generate High Resolution",
+ "bounding": [
+ 4670,
+ 3550,
+ 990,
+ 1130
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 7,
+ "title": "Lantent Upscale",
+ "bounding": [
+ 4160,
+ 3550,
+ 480,
+ 1130
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 19,
+ "title": "Video Settings",
+ "bounding": [
+ 1150,
+ 3550,
+ 460,
+ 1610
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 20,
+ "title": "Image Preprocess",
+ "bounding": [
+ 1630,
+ 4720,
+ 830,
+ 440
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 21,
+ "title": "Empty Latent",
+ "bounding": [
+ 2820,
+ 4720,
+ 1310,
+ 450
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 22,
+ "title": "Number conversion",
+ "bounding": [
+ 2480,
+ 4720,
+ 310,
+ 440
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 512,
+ "origin_id": 288,
+ "origin_slot": 0,
+ "target_id": 278,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 513,
+ "origin_id": 307,
+ "origin_slot": 1,
+ "target_id": 278,
+ "target_slot": 1,
+ "type": "LATENT"
+ },
+ {
+ "id": 478,
+ "origin_id": 285,
+ "origin_slot": 0,
+ "target_id": 282,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 479,
+ "origin_id": 284,
+ "origin_slot": 0,
+ "target_id": 282,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 480,
+ "origin_id": 284,
+ "origin_slot": 1,
+ "target_id": 282,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 541,
+ "origin_id": 285,
+ "origin_slot": 0,
+ "target_id": 314,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 518,
+ "origin_id": 304,
+ "origin_slot": 0,
+ "target_id": 314,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 519,
+ "origin_id": 304,
+ "origin_slot": 1,
+ "target_id": 314,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 483,
+ "origin_id": 277,
+ "origin_slot": 0,
+ "target_id": 283,
+ "target_slot": 0,
+ "type": "NOISE"
+ },
+ {
+ "id": 484,
+ "origin_id": 314,
+ "origin_slot": 0,
+ "target_id": 283,
+ "target_slot": 1,
+ "type": "GUIDER"
+ },
+ {
+ "id": 485,
+ "origin_id": 291,
+ "origin_slot": 0,
+ "target_id": 283,
+ "target_slot": 2,
+ "type": "SAMPLER"
+ },
+ {
+ "id": 544,
+ "origin_id": 306,
+ "origin_slot": 0,
+ "target_id": 283,
+ "target_slot": 3,
+ "type": "SIGMAS"
+ },
+ {
+ "id": 487,
+ "origin_id": 318,
+ "origin_slot": 0,
+ "target_id": 283,
+ "target_slot": 4,
+ "type": "LATENT"
+ },
+ {
+ "id": 475,
+ "origin_id": 304,
+ "origin_slot": 0,
+ "target_id": 284,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 476,
+ "origin_id": 304,
+ "origin_slot": 1,
+ "target_id": 284,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 477,
+ "origin_id": 307,
+ "origin_slot": 0,
+ "target_id": 284,
+ "target_slot": 2,
+ "type": "LATENT"
+ },
+ {
+ "id": 520,
+ "origin_id": 316,
+ "origin_slot": 0,
+ "target_id": 285,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 523,
+ "origin_id": 290,
+ "origin_slot": 0,
+ "target_id": 286,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 547,
+ "origin_id": 307,
+ "origin_slot": 0,
+ "target_id": 287,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 545,
+ "origin_id": 311,
+ "origin_slot": 0,
+ "target_id": 287,
+ "target_slot": 1,
+ "type": "LATENT_UPSCALE_MODEL"
+ },
+ {
+ "id": 554,
+ "origin_id": 293,
+ "origin_slot": 0,
+ "target_id": 287,
+ "target_slot": 2,
+ "type": "VAE"
+ },
+ {
+ "id": 552,
+ "origin_id": 293,
+ "origin_slot": 0,
+ "target_id": 288,
+ "target_slot": 0,
+ "type": "VAE"
+ },
+ {
+ "id": 515,
+ "origin_id": 289,
+ "origin_slot": 0,
+ "target_id": 288,
+ "target_slot": 1,
+ "type": "IMAGE"
+ },
+ {
+ "id": 548,
+ "origin_id": 287,
+ "origin_slot": 0,
+ "target_id": 288,
+ "target_slot": 2,
+ "type": "LATENT"
+ },
+ {
+ "id": 543,
+ "origin_id": 302,
+ "origin_slot": 0,
+ "target_id": 288,
+ "target_slot": 4,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 505,
+ "origin_id": 286,
+ "origin_slot": 0,
+ "target_id": 289,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 558,
+ "origin_id": 312,
+ "origin_slot": 0,
+ "target_id": 290,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 559,
+ "origin_id": 299,
+ "origin_slot": 0,
+ "target_id": 290,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 560,
+ "origin_id": 312,
+ "origin_slot": 0,
+ "target_id": 292,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 557,
+ "origin_id": 316,
+ "origin_slot": 2,
+ "target_id": 293,
+ "target_slot": 0,
+ "type": "VAE"
+ },
+ {
+ "id": 562,
+ "origin_id": 299,
+ "origin_slot": 0,
+ "target_id": 294,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 561,
+ "origin_id": 292,
+ "origin_slot": 1,
+ "target_id": 295,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 563,
+ "origin_id": 294,
+ "origin_slot": 1,
+ "target_id": 295,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 556,
+ "origin_id": 316,
+ "origin_slot": 2,
+ "target_id": 296,
+ "target_slot": 0,
+ "type": "VAE"
+ },
+ {
+ "id": 510,
+ "origin_id": 289,
+ "origin_slot": 0,
+ "target_id": 296,
+ "target_slot": 1,
+ "type": "IMAGE"
+ },
+ {
+ "id": 511,
+ "origin_id": 295,
+ "origin_slot": 0,
+ "target_id": 296,
+ "target_slot": 2,
+ "type": "LATENT"
+ },
+ {
+ "id": 542,
+ "origin_id": 302,
+ "origin_slot": 0,
+ "target_id": 296,
+ "target_slot": 4,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 495,
+ "origin_id": 309,
+ "origin_slot": 1,
+ "target_id": 297,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 496,
+ "origin_id": 279,
+ "origin_slot": 0,
+ "target_id": 297,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 564,
+ "origin_id": 300,
+ "origin_slot": 0,
+ "target_id": 298,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 526,
+ "origin_id": 303,
+ "origin_slot": 0,
+ "target_id": 304,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 527,
+ "origin_id": 313,
+ "origin_slot": 0,
+ "target_id": 304,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 566,
+ "origin_id": 298,
+ "origin_slot": 0,
+ "target_id": 304,
+ "target_slot": 2,
+ "type": "FLOAT"
+ },
+ {
+ "id": 497,
+ "origin_id": 296,
+ "origin_slot": 0,
+ "target_id": 318,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 498,
+ "origin_id": 305,
+ "origin_slot": 0,
+ "target_id": 318,
+ "target_slot": 1,
+ "type": "LATENT"
+ },
+ {
+ "id": 481,
+ "origin_id": 279,
+ "origin_slot": 0,
+ "target_id": 305,
+ "target_slot": 0,
+ "type": "VAE"
+ },
+ {
+ "id": 565,
+ "origin_id": 298,
+ "origin_slot": 1,
+ "target_id": 305,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 488,
+ "origin_id": 283,
+ "origin_slot": 0,
+ "target_id": 307,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 490,
+ "origin_id": 276,
+ "origin_slot": 0,
+ "target_id": 308,
+ "target_slot": 0,
+ "type": "NOISE"
+ },
+ {
+ "id": 491,
+ "origin_id": 282,
+ "origin_slot": 0,
+ "target_id": 308,
+ "target_slot": 1,
+ "type": "GUIDER"
+ },
+ {
+ "id": 492,
+ "origin_id": 280,
+ "origin_slot": 0,
+ "target_id": 308,
+ "target_slot": 2,
+ "type": "SAMPLER"
+ },
+ {
+ "id": 493,
+ "origin_id": 281,
+ "origin_slot": 0,
+ "target_id": 308,
+ "target_slot": 3,
+ "type": "SIGMAS"
+ },
+ {
+ "id": 494,
+ "origin_id": 278,
+ "origin_slot": 0,
+ "target_id": 308,
+ "target_slot": 4,
+ "type": "LATENT"
+ },
+ {
+ "id": 578,
+ "origin_id": 308,
+ "origin_slot": 0,
+ "target_id": 309,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 539,
+ "origin_id": 309,
+ "origin_slot": 0,
+ "target_id": 315,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 553,
+ "origin_id": 293,
+ "origin_slot": 0,
+ "target_id": 315,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 538,
+ "origin_id": 315,
+ "origin_slot": 0,
+ "target_id": 310,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 534,
+ "origin_id": 297,
+ "origin_slot": 0,
+ "target_id": 310,
+ "target_slot": 1,
+ "type": "AUDIO"
+ },
+ {
+ "id": 591,
+ "origin_id": 298,
+ "origin_slot": 0,
+ "target_id": 310,
+ "target_slot": 2,
+ "type": "FLOAT"
+ },
+ {
+ "id": 535,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 290,
+ "target_slot": 0,
+ "type": "IMAGE,MASK"
+ },
+ {
+ "id": 536,
+ "origin_id": 310,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "VIDEO"
+ },
+ {
+ "id": 595,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 319,
+ "target_slot": 0,
+ "type": "STRING"
+ },
+ {
+ "id": 597,
+ "origin_id": -10,
+ "origin_slot": 2,
+ "target_id": 312,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 598,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 299,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 599,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 301,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 601,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 316,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 602,
+ "origin_id": -10,
+ "origin_slot": 6,
+ "target_id": 285,
+ "target_slot": 1,
+ "type": "COMBO"
+ },
+ {
+ "id": 604,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 279,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 605,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 317,
+ "target_slot": 1,
+ "type": "COMBO"
+ },
+ {
+ "id": 606,
+ "origin_id": -10,
+ "origin_slot": 7,
+ "target_id": 317,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 607,
+ "origin_id": -10,
+ "origin_slot": 8,
+ "target_id": 311,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 615,
+ "origin_id": 317,
+ "origin_slot": 0,
+ "target_id": 303,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 624,
+ "origin_id": -10,
+ "origin_slot": 9,
+ "target_id": 300,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 625,
+ "origin_id": 319,
+ "origin_slot": 0,
+ "target_id": 303,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 627,
+ "origin_id": 317,
+ "origin_slot": 0,
+ "target_id": 313,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 628,
+ "origin_id": 301,
+ "origin_slot": 0,
+ "target_id": 323,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 629,
+ "origin_id": 300,
+ "origin_slot": 0,
+ "target_id": 323,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 630,
+ "origin_id": 323,
+ "origin_slot": 1,
+ "target_id": 305,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 631,
+ "origin_id": 323,
+ "origin_slot": 1,
+ "target_id": 295,
+ "target_slot": 2,
+ "type": "INT"
+ }
+ ],
+ "extra": {
+ "workflowRendererVersion": "Vue-corrected"
+ },
+ "category": "Video generation and editing/Image to video",
+ "description": "Generates video from a single input image using LTX-2.3."
+ }
+ ]
+ },
+ "extra": {
+ "ue_links": []
+ }
+}
\ No newline at end of file
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/Pose to Image (Z-Image-Turbo).json b/blueprints/Pose to Image (Z-Image-Turbo).json
index a55410ba4..5c2749efe 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/Pose to image",
+ "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 ae369941c..1ce49351a 100644
--- a/blueprints/Pose to Video (LTX 2.0).json
+++ b/blueprints/Pose to Video (LTX 2.0).json
@@ -1,28 +1,26 @@
{
- "id": "01cd475b-52df-43bf-aafa-484a5976d2d2",
"revision": 0,
- "last_node_id": 160,
- "last_link_id": 410,
+ "last_node_id": 143,
+ "last_link_id": 0,
"nodes": [
{
- "id": 1,
- "type": "f0e58a6b-7246-4103-9fec-73b423634b1f",
+ "id": 143,
+ "type": "68857357-cbc2-4c3a-a786-c3a58d43f9b1",
"pos": [
- 210,
- 3830
+ 290,
+ 3960
],
"size": [
- 420,
+ 400,
500
],
"flags": {
"collapsed": false
},
- "order": 0,
+ "order": 13,
"mode": 0,
"inputs": [
{
- "label": "prompt",
"name": "text",
"type": "STRING",
"widget": {
@@ -31,33 +29,32 @@
"link": null
},
{
- "label": "first_frame_strength",
- "name": "strength",
- "type": "FLOAT",
- "widget": {
- "name": "strength"
- },
- "link": null
- },
- {
- "label": "disable_first_frame",
- "name": "bypass",
- "type": "BOOLEAN",
- "widget": {
- "name": "bypass"
- },
- "link": null
- },
- {
- "label": "first frame",
+ "label": "control_images",
"name": "image",
"type": "IMAGE",
"link": null
},
{
- "label": "control image",
- "name": "input",
- "type": "IMAGE,MASK",
+ "label": "first_frame",
+ "name": "image_1",
+ "type": "IMAGE",
+ "link": null
+ },
+ {
+ "label": "image_strength",
+ "name": "strength_1",
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength_1"
+ },
+ "link": null
+ },
+ {
+ "name": "noise_seed",
+ "type": "INT",
+ "widget": {
+ "name": "noise_seed"
+ },
"link": null
},
{
@@ -69,6 +66,7 @@
"link": null
},
{
+ "label": "control_lora",
"name": "lora_name",
"type": "COMBO",
"widget": {
@@ -77,7 +75,15 @@
"link": null
},
{
- "label": "distll_lora",
+ "name": "text_encoder",
+ "type": "COMBO",
+ "widget": {
+ "name": "text_encoder"
+ },
+ "link": null
+ },
+ {
+ "label": "distill_lora",
"name": "lora_name_1",
"type": "COMBO",
"widget": {
@@ -93,30 +99,6 @@
"name": "model_name"
},
"link": null
- },
- {
- "name": "resize_type.width",
- "type": "INT",
- "widget": {
- "name": "resize_type.width"
- },
- "link": null
- },
- {
- "name": "resize_type.height",
- "type": "INT",
- "widget": {
- "name": "resize_type.height"
- },
- "link": null
- },
- {
- "name": "length",
- "type": "INT",
- "widget": {
- "name": "length"
- },
- "link": null
}
],
"outputs": [
@@ -130,56 +112,49 @@
"properties": {
"proxyWidgets": [
[
- "-1",
+ "124",
"text"
],
[
- "-1",
- "resize_type.width"
- ],
- [
- "-1",
- "resize_type.height"
- ],
- [
- "-1",
- "length"
- ],
- [
- "-1",
+ "149",
"strength"
],
- [
- "-1",
- "bypass"
- ],
[
"126",
"noise_seed"
],
[
- "126",
- "control_after_generate"
- ],
- [
- "-1",
+ "103",
"ckpt_name"
],
[
- "-1",
+ "134",
"lora_name"
],
[
- "-1",
- "model_name"
+ "97",
+ "text_encoder"
],
[
- "-1",
- "lora_name_1"
+ "105",
+ "lora_name"
+ ],
+ [
+ "100",
+ "model_name"
]
],
"cnr_id": "comfy-core",
"ver": "0.7.0",
+ "ue_properties": {
+ "widget_ue_connectable": {
+ "lora_name": true,
+ "strength": true,
+ "bypass": true
+ },
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -188,52 +163,40 @@
"secondTabOffset": 80,
"secondTabWidth": 65
},
- "widgets_values": [
- "",
- 1280,
- 720,
- 97,
- 1,
- false,
- null,
- null,
- "ltx-2-19b-dev-fp8.safetensors",
- "ltx-2-19b-ic-lora-pose-control.safetensors",
- "ltx-2-spatial-upscaler-x2-1.0.safetensors",
- "ltx-2-19b-distilled-lora-384.safetensors"
- ]
+ "widgets_values": [],
+ "title": "Pose to Video (LTX 2.0)"
}
],
"links": [],
- "groups": [],
+ "version": 0.4,
"definitions": {
"subgraphs": [
{
- "id": "f0e58a6b-7246-4103-9fec-73b423634b1f",
+ "id": "68857357-cbc2-4c3a-a786-c3a58d43f9b1",
"version": 1,
"state": {
- "lastGroupId": 11,
- "lastNodeId": 160,
- "lastLinkId": 410,
+ "lastGroupId": 14,
+ "lastNodeId": 701,
+ "lastLinkId": 1774,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
- "name": "local-Pose to Video (LTX 2.0)",
+ "name": "Pose to Video (LTX 2.0)",
"inputNode": {
"id": -10,
"bounding": [
- -2220,
- 4180,
- 153.3203125,
- 280
+ -2050,
+ 4100,
+ 127.029296875,
+ 240
]
},
"outputNode": {
"id": -20,
"bounding": [
- 1750.2777777777776,
- 4091.1111111111113,
+ 1750,
+ 4090,
120,
60
]
@@ -246,154 +209,128 @@
"linkIds": [
345
],
- "label": "prompt",
"pos": [
- -2086.6796875,
+ -1942.970703125,
+ 4120
+ ]
+ },
+ {
+ "id": "35a07084-3ecf-482a-a330-b40278770ca3",
+ "name": "image",
+ "type": "IMAGE",
+ "linkIds": [
+ 348,
+ 380
+ ],
+ "label": "control_images",
+ "pos": [
+ -1942.970703125,
+ 4140
+ ]
+ },
+ {
+ "id": "bea20802-d654-4287-a8ef-0f834314bcf9",
+ "name": "image_1",
+ "type": "IMAGE",
+ "linkIds": [
+ 364,
+ 379
+ ],
+ "label": "first_frame",
+ "pos": [
+ -1942.970703125,
+ 4160
+ ]
+ },
+ {
+ "id": "b9b4151d-df88-40c0-a2bd-6e35b94557fe",
+ "name": "strength_1",
+ "type": "FLOAT",
+ "linkIds": [
+ 1758,
+ 1759
+ ],
+ "label": "image_strength",
+ "pos": [
+ -1942.970703125,
+ 4180
+ ]
+ },
+ {
+ "id": "b51f6a12-9152-4526-b115-443cfd23003f",
+ "name": "noise_seed",
+ "type": "INT",
+ "linkIds": [
+ 1767
+ ],
+ "pos": [
+ -1942.970703125,
4200
]
},
{
- "id": "59430efe-1090-4e36-8afe-b21ce7f4268b",
- "name": "strength",
- "type": "FLOAT",
+ "id": "47248f12-f174-4e35-854c-fa5eebea2903",
+ "name": "ckpt_name",
+ "type": "COMBO",
"linkIds": [
- 370,
- 371
+ 1768,
+ 1770,
+ 1771
],
- "label": "first_frame_strength",
"pos": [
- -2086.6796875,
+ -1942.970703125,
4220
]
},
{
- "id": "6145a9b9-68ed-4956-89f7-7a5ebdd5c99e",
- "name": "bypass",
- "type": "BOOLEAN",
+ "id": "6feb34cf-7972-4d3a-91fc-11070a84dc5f",
+ "name": "lora_name",
+ "type": "COMBO",
"linkIds": [
- 363,
- 368
+ 1769
],
- "label": "disable_first_frame",
+ "label": "control_lora",
"pos": [
- -2086.6796875,
+ -1942.970703125,
4240
]
},
{
- "id": "f7aa8c12-bdba-4bbd-84cf-b49cfc32a1dd",
- "name": "image",
- "type": "IMAGE",
+ "id": "6b423a3e-6c0e-445d-93c0-2cc3945400d1",
+ "name": "text_encoder",
+ "type": "COMBO",
"linkIds": [
- 398,
- 399
+ 1772
],
- "label": "first frame",
"pos": [
- -2086.6796875,
+ -1942.970703125,
4260
]
},
{
- "id": "da40a4c0-cd19-46c6-8eb3-62d0026fbe85",
- "name": "input",
- "type": "IMAGE,MASK",
+ "id": "ffd38c52-cc57-4e68-b140-94e7b03499b1",
+ "name": "lora_name_1",
+ "type": "COMBO",
"linkIds": [
- 400
+ 1773
],
- "label": "control image",
+ "label": "distill_lora",
"pos": [
- -2086.6796875,
+ -1942.970703125,
4280
]
},
{
- "id": "8005344b-99d6-4829-a619-c4e8ef640eb9",
- "name": "ckpt_name",
- "type": "COMBO",
- "linkIds": [
- 401,
- 402,
- 403
- ],
- "pos": [
- -2086.6796875,
- 4300
- ]
- },
- {
- "id": "25e7c4e8-850c-4f37-bc14-e3f4b5f228c0",
- "name": "lora_name",
- "type": "COMBO",
- "linkIds": [
- 404,
- 405
- ],
- "pos": [
- -2086.6796875,
- 4320
- ]
- },
- {
- "id": "f16a18dd-947e-400a-8889-02cf998f760a",
- "name": "lora_name_1",
- "type": "COMBO",
- "linkIds": [
- 406
- ],
- "label": "distll_lora",
- "pos": [
- -2086.6796875,
- 4340
- ]
- },
- {
- "id": "1abf156c-4c85-4ee5-8671-62df3177d835",
+ "id": "6d8b9605-acf0-4dd7-8d45-f824c2fd5895",
"name": "model_name",
"type": "COMBO",
"linkIds": [
- 407
+ 1774
],
"label": "upscale_model",
"pos": [
- -2086.6796875,
- 4360
- ]
- },
- {
- "id": "203402cf-4253-4daf-bf78-5def9496e0af",
- "name": "resize_type.width",
- "type": "INT",
- "linkIds": [
- 408
- ],
- "pos": [
- -2086.6796875,
- 4380
- ]
- },
- {
- "id": "e6d8ac4a-34d4-46c6-bcb2-4e66a696438c",
- "name": "resize_type.height",
- "type": "INT",
- "linkIds": [
- 409
- ],
- "pos": [
- -2086.6796875,
- 4400
- ]
- },
- {
- "id": "6aa6cf2c-bc4f-4f8b-be62-aa15793375dc",
- "name": "length",
- "type": "INT",
- "linkIds": [
- 410
- ],
- "pos": [
- -2086.6796875,
- 4420
+ -1942.970703125,
+ 4300
]
}
],
@@ -407,8 +344,8 @@
],
"localized_name": "VIDEO",
"pos": [
- 1770.2777777777776,
- 4111.111111111111
+ 1770,
+ 4110
]
}
],
@@ -418,15 +355,15 @@
"id": 93,
"type": "CFGGuider",
"pos": [
- -697.721823660531,
- 3671.1105325465196
+ -690,
+ 3710
],
"size": [
- 269.97395833333337,
- 98
+ 270,
+ 160
],
"flags": {},
- "order": 16,
+ "order": 7,
"mode": 0,
"inputs": [
{
@@ -470,6 +407,11 @@
"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": "CFGGuider",
"enableTabs": false,
"tabWidth": 65,
@@ -487,12 +429,12 @@
"id": 94,
"type": "KSamplerSelect",
"pos": [
- -697.721823660531,
- 3841.1107362825187
+ -690,
+ 3940
],
"size": [
- 269.97395833333337,
- 58
+ 270,
+ 110
],
"flags": {},
"order": 0,
@@ -521,6 +463,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.56",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "KSamplerSelect",
"enableTabs": false,
"tabWidth": 65,
@@ -538,12 +485,12 @@
"id": 99,
"type": "ManualSigmas",
"pos": [
- 410.27824286284044,
- 3851.110970278795
+ 450,
+ 3910
],
"size": [
- 269.97395833333337,
- 58
+ 270,
+ 110
],
"flags": {},
"order": 1,
@@ -572,6 +519,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "ManualSigmas",
"enableTabs": false,
"tabWidth": 65,
@@ -589,15 +541,15 @@
"id": 100,
"type": "LatentUpscaleModelLoader",
"pos": [
- -69.72208571196083,
- 3701.1104657166875
+ -70,
+ 3790
],
"size": [
- 389.97395833333337,
- 58
+ 390,
+ 110
],
"flags": {},
- "order": 2,
+ "order": 11,
"mode": 0,
"inputs": [
{
@@ -607,7 +559,7 @@
"widget": {
"name": "model_name"
},
- "link": 407
+ "link": 1774
}
],
"outputs": [
@@ -623,21 +575,26 @@
"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": "LatentUpscaleModelLoader",
- "models": [
- {
- "name": "ltx-2-spatial-upscaler-x2-1.0.safetensors",
- "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-spatial-upscaler-x2-1.0.safetensors",
- "directory": "latent_upscale_models"
- }
- ],
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
- "secondTabWidth": 65
+ "secondTabWidth": 65,
+ "models": [
+ {
+ "name": "ltx-2-spatial-upscaler-x2-1.0.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-spatial-upscaler-x2-1.0.safetensors",
+ "directory": "latent_upscale_models"
+ }
+ ]
},
"widgets_values": [
"ltx-2-spatial-upscaler-x2-1.0.safetensors"
@@ -647,15 +604,15 @@
"id": 101,
"type": "LTXVConcatAVLatent",
"pos": [
- 410.27824286284044,
- 4101.110949206838
+ 450,
+ 4220
],
"size": [
- 269.97395833333337,
- 46
+ 270,
+ 120
],
"flags": {},
- "order": 18,
+ "order": 12,
"mode": 0,
"inputs": [
{
@@ -684,6 +641,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "LTXVConcatAVLatent",
"enableTabs": false,
"tabWidth": 65,
@@ -692,22 +654,21 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 108,
"type": "CFGGuider",
"pos": [
- 410.27824286284044,
- 3701.1104657166875
+ 450,
+ 3720
],
"size": [
- 269.97395833333337,
- 98
+ 270,
+ 160
],
"flags": {},
- "order": 22,
+ "order": 18,
"mode": 0,
"inputs": [
{
@@ -751,6 +712,11 @@
"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": "CFGGuider",
"enableTabs": false,
"tabWidth": 65,
@@ -764,19 +730,101 @@
1
]
},
+ {
+ "id": 111,
+ "type": "LTXVEmptyLatentAudio",
+ "pos": [
+ -1100,
+ 4940
+ ],
+ "size": [
+ 270,
+ 170
+ ],
+ "flags": {},
+ "order": 20,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "audio_vae",
+ "name": "audio_vae",
+ "type": "VAE",
+ "link": 285
+ },
+ {
+ "localized_name": "frames_number",
+ "name": "frames_number",
+ "type": "INT",
+ "widget": {
+ "name": "frames_number"
+ },
+ "link": 329
+ },
+ {
+ "localized_name": "frame_rate",
+ "name": "frame_rate",
+ "type": "INT",
+ "widget": {
+ "name": "frame_rate"
+ },
+ "link": 354
+ },
+ {
+ "localized_name": "batch_size",
+ "name": "batch_size",
+ "type": "INT",
+ "widget": {
+ "name": "batch_size"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "Latent",
+ "name": "Latent",
+ "type": "LATENT",
+ "links": [
+ 300
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.68",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "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": 123,
"type": "SamplerCustomAdvanced",
"pos": [
- -387.72197839215096,
- 3521.1103425011374
+ -380,
+ 3530
],
"size": [
- 213.09895833333334,
- 106
+ 230,
+ 170
],
"flags": {},
- "order": 31,
+ "order": 29,
"mode": 0,
"inputs": [
{
@@ -829,6 +877,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.60",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "SamplerCustomAdvanced",
"enableTabs": false,
"tabWidth": 65,
@@ -837,22 +890,21 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 114,
"type": "LTXVConditioning",
"pos": [
- -1133.7215420073496,
- 4141.110347554622
+ -1130,
+ 4140
],
"size": [
- 269.97395833333337,
- 78
+ 270,
+ 130
],
"flags": {},
- "order": 27,
+ "order": 23,
"mode": 0,
"inputs": [
{
@@ -898,6 +950,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.56",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "LTXVConditioning",
"enableTabs": false,
"tabWidth": 65,
@@ -915,15 +972,15 @@
"id": 119,
"type": "CLIPTextEncode",
"pos": [
- -1163.7218246405453,
- 3881.1109034489627
+ -1160,
+ 3880
],
"size": [
400,
- 88
+ 200
],
"flags": {},
- "order": 12,
+ "order": 27,
"mode": 0,
"inputs": [
{
@@ -955,6 +1012,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.56",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "CLIPTextEncode",
"enableTabs": false,
"tabWidth": 65,
@@ -974,15 +1036,15 @@
"id": 116,
"type": "LTXVConcatAVLatent",
"pos": [
- -519.7217122979332,
- 4701.110031965835
+ -520,
+ 4830
],
"size": [
- 187.5,
- 46
+ 230,
+ 100
],
"flags": {},
- "order": 29,
+ "order": 25,
"mode": 0,
"inputs": [
{
@@ -1012,6 +1074,11 @@
"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": "LTXVConcatAVLatent",
"enableTabs": false,
"tabWidth": 65,
@@ -1020,22 +1087,21 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 122,
"type": "LTXVSeparateAVLatent",
"pos": [
- -393.72183921949465,
- 3801.1107787938904
+ -380,
+ 3810
],
"size": [
- 239.97395833333334,
- 46
+ 240,
+ 100
],
"flags": {},
- "order": 30,
+ "order": 28,
"mode": 0,
"inputs": [
{
@@ -1066,6 +1132,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "LTXVSeparateAVLatent",
"enableTabs": false,
"tabWidth": 65,
@@ -1074,22 +1145,21 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 124,
"type": "CLIPTextEncode",
"pos": [
- -1174.7214530029996,
- 3515.1112854387566
+ -1170,
+ 3510
],
"size": [
- 409.97395833333337,
- 88
+ 410,
+ 320
],
"flags": {},
- "order": 32,
+ "order": 30,
"mode": 0,
"inputs": [
{
@@ -1121,6 +1191,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.56",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "CLIPTextEncode",
"enableTabs": false,
"tabWidth": 65,
@@ -1140,15 +1215,15 @@
"id": 98,
"type": "KSamplerSelect",
"pos": [
- 410.27824286284044,
- 3981.1101681370833
+ 450,
+ 4070
],
"size": [
- 269.97395833333337,
- 58
+ 270,
+ 110
],
"flags": {},
- "order": 3,
+ "order": 2,
"mode": 0,
"inputs": [
{
@@ -1174,6 +1249,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.75",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "KSamplerSelect",
"enableTabs": false,
"tabWidth": 65,
@@ -1191,12 +1271,12 @@
"id": 105,
"type": "LoraLoaderModelOnly",
"pos": [
- -69.72208571196083,
- 3571.110499039739
+ -70,
+ 3570
],
"size": [
- 389.97395833333337,
- 82
+ 390,
+ 140
],
"flags": {},
"order": 15,
@@ -1215,7 +1295,7 @@
"widget": {
"name": "lora_name"
},
- "link": 406
+ "link": 1773
},
{
"localized_name": "strength_model",
@@ -1240,21 +1320,26 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.75",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "LoraLoaderModelOnly",
- "models": [
- {
- "name": "ltx-2-19b-distilled-lora-384.safetensors",
- "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-distilled-lora-384.safetensors",
- "directory": "loras"
- }
- ],
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
- "secondTabWidth": 65
+ "secondTabWidth": 65,
+ "models": [
+ {
+ "name": "ltx-2-19b-distilled-lora-384.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-distilled-lora-384.safetensors",
+ "directory": "loras"
+ }
+ ]
},
"widgets_values": [
"ltx-2-19b-distilled-lora-384.safetensors",
@@ -1265,15 +1350,15 @@
"id": 95,
"type": "LTXVScheduler",
"pos": [
- -699.7218704597861,
- 3981.1101681370833
+ -690,
+ 4130
],
"size": [
- 269.97395833333337,
- 154
+ 270,
+ 170
],
"flags": {},
- "order": 17,
+ "order": 8,
"mode": 0,
"inputs": [
{
@@ -1342,6 +1427,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.56",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "LTXVScheduler",
"enableTabs": false,
"tabWidth": 65,
@@ -1363,15 +1453,15 @@
"id": 126,
"type": "RandomNoise",
"pos": [
- -697.721823660531,
- 3521.1103425011374
+ -690,
+ 3520
],
"size": [
- 269.97395833333337,
- 82
+ 270,
+ 110
],
"flags": {},
- "order": 4,
+ "order": 31,
"mode": 0,
"inputs": [
{
@@ -1381,7 +1471,7 @@
"widget": {
"name": "noise_seed"
},
- "link": null
+ "link": 1767
}
],
"outputs": [
@@ -1397,6 +1487,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.56",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "RandomNoise",
"enableTabs": false,
"tabWidth": 65,
@@ -1408,22 +1503,22 @@
},
"widgets_values": [
0,
- "randomize"
+ "fixed"
]
},
{
"id": 107,
"type": "SamplerCustomAdvanced",
"pos": [
- 710.2782734905775,
- 3571.110499039739
+ 730,
+ 3570
],
"size": [
- 212.36979166666669,
- 106
+ 230,
+ 170
],
"flags": {},
- "order": 21,
+ "order": 17,
"mode": 0,
"inputs": [
{
@@ -1476,6 +1571,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.75",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "SamplerCustomAdvanced",
"enableTabs": false,
"tabWidth": 65,
@@ -1484,22 +1584,21 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
- "id": 143,
+ "id": 187,
"type": "RandomNoise",
"pos": [
- 410.27824286284044,
- 3571.110499039739
+ 450,
+ 3570
],
"size": [
- 269.97395833333337,
- 82
+ 270,
+ 110
],
"flags": {},
- "order": 5,
+ "order": 3,
"mode": 0,
"inputs": [
{
@@ -1525,6 +1624,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.56",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "RandomNoise",
"enableTabs": false,
"tabWidth": 65,
@@ -1543,12 +1647,12 @@
"id": 139,
"type": "LTXVAudioVAEDecode",
"pos": [
- 1130.2783163694094,
- 3841.1107362825187
+ 1130,
+ 3840
],
"size": [
- 239.97395833333334,
- 46
+ 240,
+ 100
],
"flags": {},
"order": 35,
@@ -1565,7 +1669,7 @@
"localized_name": "audio_vae",
"name": "audio_vae",
"type": "VAE",
- "link": 383
+ "link": 340
}
],
"outputs": [
@@ -1581,6 +1685,11 @@
"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": "LTXVAudioVAEDecode",
"enableTabs": false,
"tabWidth": 65,
@@ -1589,22 +1698,21 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 106,
"type": "CreateVideo",
"pos": [
- 1420.2783925712918,
- 3761.1104019496292
+ 1420,
+ 3760
],
"size": [
- 269.97395833333337,
- 78
+ 270,
+ 130
],
"flags": {},
- "order": 20,
+ "order": 16,
"mode": 0,
"inputs": [
{
@@ -1643,6 +1751,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "CreateVideo",
"enableTabs": false,
"tabWidth": 65,
@@ -1660,15 +1773,15 @@
"id": 134,
"type": "LoraLoaderModelOnly",
"pos": [
- -1649.721454901846,
- 3761.1104019496292
+ -1650,
+ 3750
],
"size": [
- 419.97395833333337,
- 82
+ 420,
+ 140
],
"flags": {},
- "order": 13,
+ "order": 33,
"mode": 0,
"inputs": [
{
@@ -1684,7 +1797,7 @@
"widget": {
"name": "lora_name"
},
- "link": 404
+ "link": 1769
},
{
"localized_name": "strength_model",
@@ -1710,21 +1823,26 @@
"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",
- "models": [
- {
- "name": "ltx-2-19b-ic-lora-pose-control.safetensors",
- "url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control/resolve/main/ltx-2-19b-ic-lora-pose-control.safetensors",
- "directory": "loras"
- }
- ],
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
- "secondTabWidth": 65
+ "secondTabWidth": 65,
+ "models": [
+ {
+ "name": "ltx-2-19b-ic-lora-pose-control.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control/resolve/main/ltx-2-19b-ic-lora-pose-control.safetensors",
+ "directory": "loras"
+ }
+ ]
},
"widgets_values": [
"ltx-2-19b-ic-lora-pose-control.safetensors",
@@ -1737,12 +1855,12 @@
"id": 138,
"type": "LTXVSeparateAVLatent",
"pos": [
- 730.2784619127078,
- 3731.1109580277
+ 740,
+ 3810
],
"size": [
- 193.2916015625,
- 46
+ 230,
+ 100
],
"flags": {},
"order": 34,
@@ -1777,6 +1895,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "LTXVSeparateAVLatent",
"enableTabs": false,
"tabWidth": 65,
@@ -1785,22 +1908,21 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
- "id": 144,
+ "id": 188,
"type": "VAEDecodeTiled",
"pos": [
- 1120.2783619435547,
- 3641.110599376351
+ 1120,
+ 3640
],
"size": [
- 269.97395833333337,
+ 270,
150
],
"flags": {},
- "order": 36,
+ "order": 38,
"mode": 0,
"inputs": [
{
@@ -1865,6 +1987,11 @@
"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": "VAEDecodeTiled",
"enableTabs": false,
"tabWidth": 65,
@@ -1885,15 +2012,15 @@
"id": 113,
"type": "VAEDecode",
"pos": [
- 1130.2783163694094,
- 3531.1113453160738
+ 1130,
+ 3530
],
"size": [
- 239.97395833333334,
- 46
+ 240,
+ 100
],
"flags": {},
- "order": 26,
+ "order": 22,
"mode": 0,
"inputs": [
{
@@ -1920,6 +2047,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.75",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "VAEDecode",
"enableTabs": false,
"tabWidth": 65,
@@ -1928,22 +2060,21 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 145,
"type": "PrimitiveInt",
"pos": [
- -1600,
- 4940
+ -1610,
+ 4800
],
"size": [
- 269.97395833333337,
- 82
+ 270,
+ 110
],
"flags": {},
- "order": 6,
+ "order": 4,
"mode": 0,
"inputs": [
{
@@ -1969,6 +2100,11 @@
"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": "PrimitiveInt",
"enableTabs": false,
"tabWidth": 65,
@@ -1987,15 +2123,15 @@
"id": 148,
"type": "PrimitiveFloat",
"pos": [
- -1600,
- 5070
+ -1610,
+ 4930
],
"size": [
- 269.97395833333337,
- 58
+ 270,
+ 110
],
"flags": {},
- "order": 7,
+ "order": 5,
"mode": 0,
"inputs": [
{
@@ -2022,6 +2158,11 @@
"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": "PrimitiveFloat",
"enableTabs": false,
"tabWidth": 65,
@@ -2035,19 +2176,105 @@
24
]
},
+ {
+ "id": 115,
+ "type": "EmptyLTXVLatentVideo",
+ "pos": [
+ -1100,
+ 4740
+ ],
+ "size": [
+ 270,
+ 200
+ ],
+ "flags": {},
+ "order": 24,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 296
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 297
+ },
+ {
+ "localized_name": "length",
+ "name": "length",
+ "type": "INT",
+ "widget": {
+ "name": "length"
+ },
+ "link": 330
+ },
+ {
+ "localized_name": "batch_size",
+ "name": "batch_size",
+ "type": "INT",
+ "widget": {
+ "name": "batch_size"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "LATENT",
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 360
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.60",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "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": 118,
"type": "Reroute",
"pos": [
- -229.7217758812614,
- 4211.111007032079
+ -350,
+ 3980
],
"size": [
- 75,
- 26
+ 230,
+ 40
],
"flags": {},
- "order": 14,
+ "order": 26,
"mode": 0,
"inputs": [
{
@@ -2069,22 +2296,29 @@
],
"properties": {
"showOutputText": false,
- "horizontal": false
+ "horizontal": false,
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ }
}
},
{
- "id": 151,
+ "id": 189,
"type": "LTXVImgToVideoInplace",
"pos": [
- -19.72161465663438,
- 4071.1107364662485
+ 180,
+ 4040
],
"size": [
- 269.97395833333337,
- 122
+ 260,
+ 190
],
- "flags": {},
- "order": 38,
+ "flags": {
+ "collapsed": false
+ },
+ "order": 39,
"mode": 0,
"inputs": [
{
@@ -2097,7 +2331,7 @@
"localized_name": "image",
"name": "image",
"type": "IMAGE",
- "link": 398
+ "link": 379
},
{
"localized_name": "latent",
@@ -2112,7 +2346,7 @@
"widget": {
"name": "strength"
},
- "link": 371
+ "link": 1759
},
{
"localized_name": "bypass",
@@ -2137,6 +2371,11 @@
"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": "LTXVImgToVideoInplace",
"enableTabs": false,
"tabWidth": 65,
@@ -2155,15 +2394,15 @@
"id": 104,
"type": "LTXVCropGuides",
"pos": [
- -9.721939801202097,
- 3841.1107362825187
+ -90,
+ 4210
],
"size": [
- 239.97395833333334,
- 66
+ 240,
+ 120
],
"flags": {},
- "order": 19,
+ "order": 14,
"mode": 0,
"inputs": [
{
@@ -2215,6 +2454,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.68",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "LTXVCropGuides",
"enableTabs": false,
"tabWidth": 65,
@@ -2223,22 +2467,21 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": []
+ }
},
{
"id": 112,
"type": "LTXVLatentUpsampler",
"pos": [
- -9.721939801202097,
- 3961.111517352274
+ -90,
+ 4030
],
"size": [
- 259.97395833333337,
- 66
+ 260,
+ 120
],
"flags": {},
- "order": 25,
+ "order": 21,
"mode": 0,
"inputs": [
{
@@ -2274,6 +2517,11 @@
"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": "LTXVLatentUpsampler",
"enableTabs": false,
"tabWidth": 65,
@@ -2282,22 +2530,117 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
+ }
+ },
+ {
+ "id": 154,
+ "type": "MarkdownNote",
+ "pos": [
+ -1640,
+ 5050
+ ],
+ "size": [
+ 350,
+ 170
+ ],
+ "flags": {
+ "collapsed": false
},
- "widgets_values": []
+ "order": 6,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [],
+ "title": "Frame Rate Note",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ }
+ },
+ "widgets_values": [
+ "Please make sure the frame rate value is the same in both boxes"
+ ],
+ "color": "#432",
+ "bgcolor": "#653"
+ },
+ {
+ "id": 96,
+ "type": "LTXVAudioVAELoader",
+ "pos": [
+ -1650,
+ 3970
+ ],
+ "size": [
+ 420,
+ 110
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "ckpt_name",
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "ckpt_name"
+ },
+ "link": 1770
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "Audio VAE",
+ "name": "Audio VAE",
+ "type": "VAE",
+ "links": [
+ 285,
+ 340
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.68",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "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-19b-dev-fp8.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-dev-fp8.safetensors",
+ "directory": "checkpoints"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ltx-2-19b-dev-fp8.safetensors"
+ ]
},
{
"id": 97,
"type": "LTXAVTextEncoderLoader",
"pos": [
- -1649.721454901846,
- 4041.1110828665023
+ -1650,
+ 4160
],
"size": [
- 419.97395833333337,
- 106
+ 420,
+ 150
],
"flags": {},
- "order": 8,
+ "order": 10,
"mode": 0,
"inputs": [
{
@@ -2307,7 +2650,7 @@
"widget": {
"name": "text_encoder"
},
- "link": 405
+ "link": 1772
},
{
"localized_name": "ckpt_name",
@@ -2316,7 +2659,7 @@
"widget": {
"name": "ckpt_name"
},
- "link": 403
+ "link": 1771
},
{
"localized_name": "device",
@@ -2342,7 +2685,19 @@
"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": "LTXAVTextEncoderLoader",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65,
"models": [
{
"name": "ltx-2-19b-dev-fp8.safetensors",
@@ -2354,17 +2709,10 @@
"url": "https://huggingface.co/Comfy-Org/ltx-2/resolve/main/split_files/text_encoders/gemma_3_12B_it_fp4_mixed.safetensors",
"directory": "text_encoders"
}
- ],
- "enableTabs": false,
- "tabWidth": 65,
- "tabXOffset": 10,
- "hasSecondTab": false,
- "secondTabText": "Send Back",
- "secondTabOffset": 80,
- "secondTabWidth": 65
+ ]
},
"widgets_values": [
- "ltx-2-19b-ic-lora-pose-control.safetensors",
+ "gemma_3_12B_it_fp4_mixed.safetensors",
"ltx-2-19b-dev-fp8.safetensors",
"default"
]
@@ -2373,15 +2721,15 @@
"id": 103,
"type": "CheckpointLoaderSimple",
"pos": [
- -1649.721454901846,
- 3591.1104777840524
+ -1650,
+ 3520
],
"size": [
- 419.97395833333337,
- 98
+ 420,
+ 160
],
"flags": {},
- "order": 9,
+ "order": 13,
"mode": 0,
"inputs": [
{
@@ -2391,7 +2739,7 @@
"widget": {
"name": "ckpt_name"
},
- "link": 401
+ "link": 1768
}
],
"outputs": [
@@ -2424,137 +2772,89 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.56",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"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-19b-dev-fp8.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-dev-fp8.safetensors",
"directory": "checkpoints"
}
- ],
- "enableTabs": false,
- "tabWidth": 65,
- "tabXOffset": 10,
- "hasSecondTab": false,
- "secondTabText": "Send Back",
- "secondTabOffset": 80,
- "secondTabWidth": 65
+ ]
},
"widgets_values": [
"ltx-2-19b-dev-fp8.safetensors"
]
},
{
- "id": 156,
- "type": "LTXVAudioVAELoader",
+ "id": 110,
+ "type": "GetImageSize",
"pos": [
- -1636.9543279290153,
- 3911.095334870057
+ -1610,
+ 4630
],
"size": [
- 399.0494791666667,
- 58
+ 260,
+ 120
],
"flags": {},
- "order": 10,
+ "order": 19,
"mode": 0,
"inputs": [
- {
- "localized_name": "ckpt_name",
- "name": "ckpt_name",
- "type": "COMBO",
- "widget": {
- "name": "ckpt_name"
- },
- "link": 402
- }
- ],
- "outputs": [
- {
- "localized_name": "Audio VAE",
- "name": "Audio VAE",
- "type": "VAE",
- "links": [
- 382,
- 383
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.11.0",
- "Node name for S&R": "LTXVAudioVAELoader"
- },
- "widgets_values": [
- "ltx-2-19b-dev-fp8.safetensors"
- ]
- },
- {
- "id": 149,
- "type": "LTXVImgToVideoInplace",
- "pos": [
- -1089.7215608128167,
- 4401.110560478942
- ],
- "size": [
- 269.97395833333337,
- 122
- ],
- "flags": {},
- "order": 37,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "vae",
- "name": "vae",
- "type": "VAE",
- "link": 359
- },
{
"localized_name": "image",
"name": "image",
"type": "IMAGE",
- "link": 399
- },
- {
- "localized_name": "latent",
- "name": "latent",
- "type": "LATENT",
- "link": 360
- },
- {
- "localized_name": "strength",
- "name": "strength",
- "type": "FLOAT",
- "widget": {
- "name": "strength"
- },
- "link": 370
- },
- {
- "localized_name": "bypass",
- "name": "bypass",
- "type": "BOOLEAN",
- "widget": {
- "name": "bypass"
- },
- "link": 363
+ "link": 381
}
],
"outputs": [
{
- "localized_name": "latent",
- "name": "latent",
- "type": "LATENT",
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
"links": [
- 357
+ 296
+ ]
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "links": [
+ 297
+ ]
+ },
+ {
+ "localized_name": "batch_size",
+ "name": "batch_size",
+ "type": "INT",
+ "links": [
+ 329,
+ 330
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.7.0",
- "Node name for S&R": "LTXVImgToVideoInplace",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "GetImageSize",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -2562,25 +2862,21 @@
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
- },
- "widgets_values": [
- 1,
- false
- ]
+ }
},
{
"id": 132,
"type": "LTXVAddGuide",
"pos": [
- -599.7217670603999,
- 4421.110609115862
+ -600,
+ 4550
],
"size": [
- 269.97395833333337,
- 162
+ 270,
+ 240
],
"flags": {},
- "order": 33,
+ "order": 32,
"mode": 0,
"inputs": [
{
@@ -2611,7 +2907,7 @@
"localized_name": "image",
"name": "image",
"type": "IMAGE",
- "link": 395
+ "link": 348
},
{
"localized_name": "frame_idx",
@@ -2663,6 +2959,11 @@
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.75",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
"Node name for S&R": "LTXVAddGuide",
"enableTabs": false,
"tabWidth": 65,
@@ -2678,114 +2979,76 @@
]
},
{
- "id": 154,
- "type": "MarkdownNote",
+ "id": 149,
+ "type": "LTXVImgToVideoInplace",
"pos": [
- -1630,
- 5190
+ -1090,
+ 4530
],
"size": [
- 350,
- 88
- ],
- "flags": {
- "collapsed": false
- },
- "order": 11,
- "mode": 0,
- "inputs": [],
- "outputs": [],
- "title": "Frame Rate Note",
- "properties": {},
- "widgets_values": [
- "Please make sure the frame rate value is the same in both boxes"
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 159,
- "type": "ResizeImageMaskNode",
- "pos": [
- -1610,
- 4580
- ],
- "size": [
- 284.375,
- 154
+ 270,
+ 180
],
"flags": {},
- "order": 39,
+ "order": 36,
"mode": 0,
"inputs": [
{
- "localized_name": "input",
- "name": "input",
- "type": "IMAGE,MASK",
- "link": 400
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 359
},
{
- "localized_name": "resize_type",
- "name": "resize_type",
- "type": "COMFY_DYNAMICCOMBO_V3",
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 364
+ },
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "link": 360
+ },
+ {
+ "localized_name": "strength",
+ "name": "strength",
+ "type": "FLOAT",
"widget": {
- "name": "resize_type"
+ "name": "strength"
},
- "link": null
+ "link": 1758
},
{
- "localized_name": "width",
- "name": "resize_type.width",
- "type": "INT",
+ "localized_name": "bypass",
+ "name": "bypass",
+ "type": "BOOLEAN",
"widget": {
- "name": "resize_type.width"
- },
- "link": 408
- },
- {
- "localized_name": "height",
- "name": "resize_type.height",
- "type": "INT",
- "widget": {
- "name": "resize_type.height"
- },
- "link": 409
- },
- {
- "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"
+ "name": "bypass"
},
"link": null
}
],
"outputs": [
{
- "localized_name": "resized",
- "name": "resized",
- "type": "IMAGE,MASK",
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
"links": [
- 391,
- 392,
- 395
+ 357
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.7.0",
- "Node name for S&R": "ResizeImageMaskNode",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "LTXVImgToVideoInplace",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -2795,139 +3058,69 @@
"secondTabWidth": 65
},
"widgets_values": [
- "scale dimensions",
- 1280,
- 720,
- "center",
- "lanczos"
+ 1,
+ false
]
},
{
- "id": 110,
- "type": "GetImageSize",
+ "id": 155,
+ "type": "ImageScaleBy",
"pos": [
- -1600,
- 4780
+ -1620,
+ 4440
],
"size": [
- 259.97395833333337,
- 66
+ 280,
+ 140
],
"flags": {},
- "order": 23,
+ "order": 37,
"mode": 0,
"inputs": [
{
"localized_name": "image",
"name": "image",
"type": "IMAGE",
- "link": 391
- }
- ],
- "outputs": [
- {
- "localized_name": "width",
- "name": "width",
- "type": "INT",
- "links": [
- 296
- ]
+ "link": 380
},
{
- "localized_name": "height",
- "name": "height",
- "type": "INT",
- "links": [
- 297
- ]
- },
- {
- "localized_name": "batch_size",
- "name": "batch_size",
- "type": "INT",
- "links": []
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.7.0",
- "Node name for S&R": "GetImageSize",
- "enableTabs": false,
- "tabWidth": 65,
- "tabXOffset": 10,
- "hasSecondTab": false,
- "secondTabText": "Send Back",
- "secondTabOffset": 80,
- "secondTabWidth": 65
- },
- "widgets_values": []
- },
- {
- "id": 115,
- "type": "EmptyLTXVLatentVideo",
- "pos": [
- -1099.721794809093,
- 4611.11072170357
- ],
- "size": [
- 269.97395833333337,
- 130
- ],
- "flags": {},
- "order": 28,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "width",
- "name": "width",
- "type": "INT",
+ "localized_name": "upscale_method",
+ "name": "upscale_method",
+ "type": "COMBO",
"widget": {
- "name": "width"
+ "name": "upscale_method"
},
- "link": 296
+ "link": null
},
{
- "localized_name": "height",
- "name": "height",
- "type": "INT",
+ "localized_name": "scale_by",
+ "name": "scale_by",
+ "type": "FLOAT",
"widget": {
- "name": "height"
- },
- "link": 297
- },
- {
- "localized_name": "length",
- "name": "length",
- "type": "INT",
- "widget": {
- "name": "length"
- },
- "link": 410
- },
- {
- "localized_name": "batch_size",
- "name": "batch_size",
- "type": "INT",
- "widget": {
- "name": "batch_size"
+ "name": "scale_by"
},
"link": null
}
],
"outputs": [
{
- "localized_name": "LATENT",
- "name": "LATENT",
- "type": "LATENT",
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
"links": [
- 360
+ 381
]
}
],
"properties": {
"cnr_id": "comfy-core",
- "ver": "0.3.60",
- "Node name for S&R": "EmptyLTXVLatentVideo",
+ "ver": "0.5.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "ImageScaleBy",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
@@ -2937,87 +3130,8 @@
"secondTabWidth": 65
},
"widgets_values": [
- 768,
- 512,
- 97,
- 1
- ]
- },
- {
- "id": 111,
- "type": "LTXVEmptyLatentAudio",
- "pos": [
- -1099.721794809093,
- 4811.110229576288
- ],
- "size": [
- 269.97395833333337,
- 106
- ],
- "flags": {},
- "order": 24,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "audio_vae",
- "name": "audio_vae",
- "type": "VAE",
- "link": 382
- },
- {
- "localized_name": "frames_number",
- "name": "frames_number",
- "type": "INT",
- "widget": {
- "name": "frames_number"
- },
- "link": null
- },
- {
- "localized_name": "frame_rate",
- "name": "frame_rate",
- "type": "INT",
- "widget": {
- "name": "frame_rate"
- },
- "link": 354
- },
- {
- "localized_name": "batch_size",
- "name": "batch_size",
- "type": "INT",
- "widget": {
- "name": "batch_size"
- },
- "link": null
- }
- ],
- "outputs": [
- {
- "localized_name": "Latent",
- "name": "Latent",
- "type": "LATENT",
- "links": [
- 300
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.3.68",
- "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
+ "lanczos",
+ 0.5
]
}
],
@@ -3028,8 +3142,8 @@
"bounding": [
-1660,
3440,
- 440,
- 820
+ 450,
+ 940
],
"color": "#3f789e",
"font_size": 24,
@@ -3041,8 +3155,8 @@
"bounding": [
-700,
3440,
- 570,
- 820
+ 580,
+ 940
],
"color": "#3f789e",
"font_size": 24,
@@ -3054,8 +3168,8 @@
"bounding": [
-1180,
3440,
- 440,
- 820
+ 450,
+ 940
],
"color": "#3f789e",
"font_size": 24,
@@ -3066,7 +3180,7 @@
"title": "Latent",
"bounding": [
-1180,
- 4290,
+ 4420,
1050,
680
],
@@ -3080,8 +3194,8 @@
"bounding": [
-100,
3440,
- 1090,
- 820
+ 1110,
+ 940
],
"color": "#3f789e",
"font_size": 24,
@@ -3091,10 +3205,10 @@
"id": 6,
"title": "Sampler",
"bounding": [
- 350,
+ 410,
3480,
- 620,
- 750
+ 590,
+ 880
],
"color": "#3f789e",
"font_size": 24,
@@ -3106,8 +3220,8 @@
"bounding": [
-90,
3480,
- 430,
- 310
+ 450,
+ 480
],
"color": "#3f789e",
"font_size": 24,
@@ -3117,8 +3231,8 @@
"id": 11,
"title": "Frame rate",
"bounding": [
- -1610,
- 4860,
+ -1620,
+ 4730,
290,
271.6
],
@@ -3184,6 +3298,22 @@
"target_slot": 2,
"type": "CONDITIONING"
},
+ {
+ "id": 285,
+ "origin_id": 96,
+ "origin_slot": 0,
+ "target_id": 111,
+ "target_slot": 0,
+ "type": "VAE"
+ },
+ {
+ "id": 329,
+ "origin_id": 110,
+ "origin_slot": 2,
+ "target_id": 111,
+ "target_slot": 1,
+ "type": "INT"
+ },
{
"id": 260,
"origin_id": 126,
@@ -3240,6 +3370,14 @@
"target_slot": 1,
"type": "INT"
},
+ {
+ "id": 330,
+ "origin_id": 110,
+ "origin_slot": 2,
+ "target_id": 115,
+ "target_slot": 2,
+ "type": "INT"
+ },
{
"id": 325,
"origin_id": 103,
@@ -3360,6 +3498,14 @@
"target_slot": 0,
"type": "LATENT"
},
+ {
+ "id": 340,
+ "origin_id": 96,
+ "origin_slot": 0,
+ "target_id": 139,
+ "target_slot": 1,
+ "type": "VAE"
+ },
{
"id": 337,
"origin_id": 138,
@@ -3490,23 +3636,31 @@
},
{
"id": 347,
- "origin_id": 143,
+ "origin_id": 187,
"origin_slot": 0,
"target_id": 107,
"target_slot": 0,
"type": "NOISE"
},
+ {
+ "id": 348,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 132,
+ "target_slot": 4,
+ "type": "IMAGE"
+ },
{
"id": 351,
"origin_id": 138,
"origin_slot": 0,
- "target_id": 144,
+ "target_id": 188,
"target_slot": 0,
"type": "LATENT"
},
{
"id": 352,
- "origin_id": 144,
+ "origin_id": 188,
"origin_slot": 0,
"target_id": 106,
"target_slot": 0,
@@ -3516,7 +3670,7 @@
"id": 353,
"origin_id": 103,
"origin_slot": 2,
- "target_id": 144,
+ "target_id": 188,
"target_slot": 1,
"type": "VAE"
},
@@ -3569,16 +3723,16 @@
"type": "LATENT"
},
{
- "id": 363,
+ "id": 364,
"origin_id": -10,
"origin_slot": 2,
"target_id": 149,
- "target_slot": 4,
- "type": "BOOLEAN"
+ "target_slot": 1,
+ "type": "IMAGE"
},
{
"id": 365,
- "origin_id": 151,
+ "origin_id": 189,
"origin_slot": 0,
"target_id": 101,
"target_slot": 0,
@@ -3588,7 +3742,7 @@
"id": 366,
"origin_id": 112,
"origin_slot": 0,
- "target_id": 151,
+ "target_id": 189,
"target_slot": 2,
"type": "LATENT"
},
@@ -3596,92 +3750,68 @@
"id": 367,
"origin_id": 118,
"origin_slot": 0,
- "target_id": 151,
+ "target_id": 189,
"target_slot": 0,
"type": "VAE"
},
{
"id": 368,
"origin_id": -10,
- "origin_slot": 2,
- "target_id": 151,
+ "origin_slot": 4,
+ "target_id": 189,
"target_slot": 4,
"type": "BOOLEAN"
},
{
- "id": 370,
+ "id": 379,
"origin_id": -10,
- "origin_slot": 1,
- "target_id": 149,
- "target_slot": 3,
- "type": "FLOAT"
- },
- {
- "id": 371,
- "origin_id": -10,
- "origin_slot": 1,
- "target_id": 151,
- "target_slot": 3,
- "type": "FLOAT"
- },
- {
- "id": 382,
- "origin_id": 156,
- "origin_slot": 0,
- "target_id": 111,
- "target_slot": 0,
- "type": "VAE"
- },
- {
- "id": 383,
- "origin_id": 156,
- "origin_slot": 0,
- "target_id": 139,
+ "origin_slot": 2,
+ "target_id": 189,
"target_slot": 1,
- "type": "VAE"
+ "type": "IMAGE"
},
{
- "id": 391,
- "origin_id": 159,
+ "id": 380,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 155,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 381,
+ "origin_id": 155,
"origin_slot": 0,
"target_id": 110,
"target_slot": 0,
"type": "IMAGE"
},
{
- "id": 395,
- "origin_id": 159,
- "origin_slot": 0,
- "target_id": 132,
- "target_slot": 4,
- "type": "IMAGE"
- },
- {
- "id": 398,
- "origin_id": -10,
- "origin_slot": 3,
- "target_id": 151,
- "target_slot": 1,
- "type": "IMAGE"
- },
- {
- "id": 399,
+ "id": 1758,
"origin_id": -10,
"origin_slot": 3,
"target_id": 149,
- "target_slot": 1,
- "type": "IMAGE"
+ "target_slot": 3,
+ "type": "FLOAT"
},
{
- "id": 400,
+ "id": 1759,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 189,
+ "target_slot": 3,
+ "type": "FLOAT"
+ },
+ {
+ "id": 1767,
"origin_id": -10,
"origin_slot": 4,
- "target_id": 159,
+ "target_id": 126,
"target_slot": 0,
- "type": "IMAGE,MASK"
+ "type": "INT"
},
{
- "id": 401,
+ "id": 1768,
"origin_id": -10,
"origin_slot": 5,
"target_id": 103,
@@ -3689,23 +3819,7 @@
"type": "COMBO"
},
{
- "id": 402,
- "origin_id": -10,
- "origin_slot": 5,
- "target_id": 156,
- "target_slot": 0,
- "type": "COMBO"
- },
- {
- "id": 403,
- "origin_id": -10,
- "origin_slot": 5,
- "target_id": 97,
- "target_slot": 1,
- "type": "COMBO"
- },
- {
- "id": 404,
+ "id": 1769,
"origin_id": -10,
"origin_slot": 6,
"target_id": 134,
@@ -3713,76 +3827,55 @@
"type": "COMBO"
},
{
- "id": 405,
+ "id": 1770,
"origin_id": -10,
- "origin_slot": 6,
+ "origin_slot": 5,
+ "target_id": 96,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 1771,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 97,
+ "target_slot": 1,
+ "type": "COMBO"
+ },
+ {
+ "id": 1772,
+ "origin_id": -10,
+ "origin_slot": 7,
"target_id": 97,
"target_slot": 0,
"type": "COMBO"
},
{
- "id": 406,
+ "id": 1773,
"origin_id": -10,
- "origin_slot": 7,
+ "origin_slot": 8,
"target_id": 105,
"target_slot": 1,
"type": "COMBO"
},
{
- "id": 407,
+ "id": 1774,
"origin_id": -10,
- "origin_slot": 8,
+ "origin_slot": 9,
"target_id": 100,
"target_slot": 0,
"type": "COMBO"
- },
- {
- "id": 408,
- "origin_id": -10,
- "origin_slot": 9,
- "target_id": 159,
- "target_slot": 2,
- "type": "INT"
- },
- {
- "id": 409,
- "origin_id": -10,
- "origin_slot": 10,
- "target_id": 159,
- "target_slot": 3,
- "type": "INT"
- },
- {
- "id": 410,
- "origin_id": -10,
- "origin_slot": 11,
- "target_id": 115,
- "target_slot": 2,
- "type": "INT"
}
],
"extra": {
"workflowRendererVersion": "LG"
},
- "category": "Video generation and editing/Pose to video"
+ "category": "Video generation and editing/Pose to video",
+ "description": "Generates video from pose reference frames using LTX-2, with optional synchronized audio."
}
]
},
- "config": {},
"extra": {
- "ds": {
- "scale": 1.3889423076923078,
- "offset": [
- 217.0560747663551,
- -3703.3333333333335
- ]
- },
- "frontendVersion": "1.37.10",
- "workflowRendererVersion": "LG",
- "VHS_latentpreview": false,
- "VHS_latentpreviewrate": 0,
- "VHS_MetadataImage": true,
- "VHS_KeepIntermediate": true
- },
- "version": 0.4
-}
+ "ue_links": []
+ }
+}
\ No newline at end of file
diff --git a/blueprints/Prompt Enhance.json b/blueprints/Prompt Enhance.json
index 5e57548ff..e260b1203 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 generation/Prompt enhance",
+ "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..732a4adc4
--- /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 generation and editing/Background Removal"
+ }
+ ]
+ },
+ "extra": {}
+}
\ No newline at end of file
diff --git a/blueprints/Sharpen.json b/blueprints/Sharpen.json
index bb79f61fc..3c4099c6b 100644
--- a/blueprints/Sharpen.json
+++ b/blueprints/Sharpen.json
@@ -267,7 +267,7 @@
"Node name for S&R": "GLSLShader"
},
"widgets_values": [
- "#version 300 es\nprecision highp float;\n\nuniform sampler2D u_image0;\nuniform vec2 u_resolution;\nuniform float u_float0; // strength [0.0 – 2.0] typical: 0.3–1.0\n\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\n\nvoid main() {\n vec2 texel = 1.0 / u_resolution;\n \n // Sample center and neighbors\n vec4 center = texture(u_image0, v_texCoord);\n vec4 top = texture(u_image0, v_texCoord + vec2( 0.0, -texel.y));\n vec4 bottom = texture(u_image0, v_texCoord + vec2( 0.0, texel.y));\n vec4 left = texture(u_image0, v_texCoord + vec2(-texel.x, 0.0));\n vec4 right = texture(u_image0, v_texCoord + vec2( texel.x, 0.0));\n \n // Edge enhancement (Laplacian)\n vec4 edges = center * 4.0 - top - bottom - left - right;\n \n // Add edges back scaled by strength\n vec4 sharpened = center + edges * u_float0;\n \n fragColor0 = vec4(clamp(sharpened.rgb, 0.0, 1.0), center.a);\n}",
+ "#version 300 es\nprecision highp float;\n\nuniform sampler2D u_image0;\nuniform float u_float0; // strength [0.0 – 2.0] typical: 0.3–1.0\n\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\n\nvoid main() {\n vec2 texel = 1.0 / vec2(textureSize(u_image0, 0));\n \n // Sample center and neighbors\n vec4 center = texture(u_image0, v_texCoord);\n vec4 top = texture(u_image0, v_texCoord + vec2( 0.0, -texel.y));\n vec4 bottom = texture(u_image0, v_texCoord + vec2( 0.0, texel.y));\n vec4 left = texture(u_image0, v_texCoord + vec2(-texel.x, 0.0));\n vec4 right = texture(u_image0, v_texCoord + vec2( texel.x, 0.0));\n \n // Edge enhancement (Laplacian)\n vec4 edges = center * 4.0 - top - bottom - left - right;\n \n // Add edges back scaled by strength\n vec4 sharpened = center + edges * u_float0;\n \n fragColor0 = vec4(clamp(sharpened.rgb, 0.0, 1.0), center.a);\n}",
"from_input"
]
}
@@ -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/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 (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/Text to Image (Ernie Image).json b/blueprints/Text to Image (Ernie Image).json
new file mode 100644
index 000000000..2bab20d69
--- /dev/null
+++ b/blueprints/Text to Image (Ernie Image).json
@@ -0,0 +1,2190 @@
+{
+ "revision": 0,
+ "last_node_id": 88,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 88,
+ "type": "03921aea-a70e-44b4-bc77-f6bda10f2120",
+ "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": "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
+ },
+ {
+ "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": [
+ [
+ "78",
+ "value"
+ ],
+ [
+ "76",
+ "value"
+ ],
+ [
+ "71",
+ "width"
+ ],
+ [
+ "71",
+ "height"
+ ],
+ [
+ "70",
+ "steps"
+ ],
+ [
+ "70",
+ "cfg"
+ ],
+ [
+ "70",
+ "seed"
+ ],
+ [
+ "66",
+ "unet_name"
+ ],
+ [
+ "62",
+ "clip_name"
+ ],
+ [
+ "91",
+ "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)"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "03921aea-a70e-44b4-bc77-f6bda10f2120",
+ "version": 1,
+ "state": {
+ "lastGroupId": 6,
+ "lastNodeId": 99,
+ "lastLinkId": 124,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Text to Image (Ernie Image)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -1350,
+ 370,
+ 163.50390625,
+ 260
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 1110,
+ 260,
+ 120,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "504de359-52a4-49aa-b6be-23c1cdb0cbde",
+ "name": "value",
+ "type": "STRING",
+ "linkIds": [
+ 102
+ ],
+ "label": "prompt",
+ "pos": [
+ -1206.49609375,
+ 390
+ ]
+ },
+ {
+ "id": "29f699c6-9263-41f6-b37d-69b9fc3913dd",
+ "name": "value_1",
+ "type": "BOOLEAN",
+ "linkIds": [
+ 103
+ ],
+ "label": "prompt_enhancement",
+ "pos": [
+ -1206.49609375,
+ 410
+ ]
+ },
+ {
+ "id": "968e6213-d1e9-4268-8f47-1d6b9a39a43e",
+ "name": "width",
+ "type": "INT",
+ "linkIds": [
+ 104,
+ 113
+ ],
+ "pos": [
+ -1206.49609375,
+ 430
+ ]
+ },
+ {
+ "id": "181c49ef-740d-4385-aa11-79718951ccb9",
+ "name": "height",
+ "type": "INT",
+ "linkIds": [
+ 105,
+ 114
+ ],
+ "pos": [
+ -1206.49609375,
+ 450
+ ]
+ },
+ {
+ "id": "1e85f808-66a1-41df-be52-334142b35419",
+ "name": "steps",
+ "type": "INT",
+ "linkIds": [
+ 106
+ ],
+ "pos": [
+ -1206.49609375,
+ 470
+ ]
+ },
+ {
+ "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": [
+ 109
+ ],
+ "pos": [
+ -1206.49609375,
+ 530
+ ]
+ },
+ {
+ "id": "886301c7-6e88-4cec-96fa-8ae20e8340c5",
+ "name": "clip_name",
+ "type": "COMBO",
+ "linkIds": [
+ 110
+ ],
+ "pos": [
+ -1206.49609375,
+ 550
+ ]
+ },
+ {
+ "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": [
+ 121
+ ],
+ "pos": [
+ -1206.49609375,
+ 590
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "f4cb34c8-4090-4281-b428-7338a339d274",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "linkIds": [
+ 84
+ ],
+ "localized_name": "IMAGE",
+ "pos": [
+ 1130,
+ 280
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 71,
+ "type": "EmptyFlux2LatentImage",
+ "pos": [
+ -460,
+ 1040
+ ],
+ "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.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": [
+ 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": 83
+ },
+ {
+ "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": 106
+ },
+ {
+ "localized_name": "cfg",
+ "name": "cfg",
+ "type": "FLOAT",
+ "widget": {
+ "name": "cfg"
+ },
+ "link": 107
+ },
+ {
+ "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": [
+ 182596410725960,
+ "randomize",
+ 20,
+ 4,
+ "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": 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,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 82
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 83
+ ]
+ }
+ ],
+ "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": "#223",
+ "bgcolor": "#335"
+ },
+ {
+ "id": 83,
+ "type": "StringReplace",
+ "pos": [
+ -500,
+ -640
+ ],
+ "size": [
+ 430,
+ 450
+ ],
+ "flags": {},
+ "order": 12,
+ "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": 92
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "STRING",
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 115
+ ]
+ }
+ ],
+ "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": 78,
+ "type": "PrimitiveStringMultiline",
+ "pos": [
+ -950,
+ -650
+ ],
+ "size": [
+ 400,
+ 200
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "STRING",
+ "widget": {
+ "name": "value"
+ },
+ "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": "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": "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": [
+ 89
+ ]
+ }
+ ],
+ "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": 76,
+ "type": "PrimitiveBoolean",
+ "pos": [
+ -500,
+ 60
+ ],
+ "size": [
+ 270,
+ 100
+ ],
+ "flags": {},
+ "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": [
+ {
+ "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,
+ 82
+ ]
+ }
+ ],
+ "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": [
+ -460,
+ 770
+ ],
+ "size": [
+ 270,
+ 110
+ ],
+ "flags": {},
+ "order": 1,
+ "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": [
+ 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": "CLIPLoader",
+ "pos": [
+ -500,
+ -150
+ ],
+ "size": [
+ 510,
+ 150
+ ],
+ "flags": {},
+ "order": 13,
+ "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": [
+ 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": "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": "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": [
+ "",
+ "{width}",
+ ""
+ ]
+ },
+ {
+ "id": 95,
+ "type": "StringReplace",
+ "pos": [
+ 220,
+ -640
+ ],
+ "size": [
+ 250,
+ 450
+ ],
+ "flags": {},
+ "order": 17,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "string",
+ "name": "string",
+ "type": "STRING",
+ "widget": {
+ "name": "string"
+ },
+ "link": 117
+ },
+ {
+ "localized_name": "find",
+ "name": "find",
+ "type": "STRING",
+ "widget": {
+ "name": "find"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "replace",
+ "name": "replace",
+ "type": "STRING",
+ "widget": {
+ "name": "replace"
+ },
+ "link": 118
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "STRING",
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 119
+ ]
+ }
+ ],
+ "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": 97,
+ "type": "PreviewAny",
+ "pos": [
+ 970,
+ -650
+ ],
+ "size": [
+ 570,
+ 790
+ ],
+ "flags": {},
+ "order": 18,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "source",
+ "name": "source",
+ "type": "*",
+ "link": 124
+ }
+ ],
+ "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": [
+ -480,
+ 940,
+ 310,
+ 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,
+ 320,
+ 670
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 5,
+ "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": 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": 100,
+ "origin_id": 75,
+ "origin_slot": 0,
+ "target_id": 67,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 82,
+ "origin_id": 62,
+ "origin_slot": 0,
+ "target_id": 72,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 92,
+ "origin_id": 78,
+ "origin_slot": 0,
+ "target_id": 83,
+ "target_slot": 2,
+ "type": "STRING"
+ },
+ {
+ "id": 87,
+ "origin_id": 78,
+ "origin_slot": 0,
+ "target_id": 75,
+ "target_slot": 0,
+ "type": "STRING"
+ },
+ {
+ "id": 89,
+ "origin_id": 74,
+ "origin_slot": 0,
+ "target_id": 75,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "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": "IMAGE"
+ },
+ {
+ "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": 71,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 105,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 71,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 106,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 70,
+ "target_slot": 5,
+ "type": "INT"
+ },
+ {
+ "id": 107,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 70,
+ "target_slot": 6,
+ "type": "FLOAT"
+ },
+ {
+ "id": 108,
+ "origin_id": -10,
+ "origin_slot": 6,
+ "target_id": 70,
+ "target_slot": 4,
+ "type": "INT"
+ },
+ {
+ "id": 109,
+ "origin_id": -10,
+ "origin_slot": 7,
+ "target_id": 66,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 110,
+ "origin_id": -10,
+ "origin_slot": 8,
+ "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": {},
+ "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."
+ }
+ ]
+ },
+ "extra": {
+ "ue_links": []
+ }
+}
diff --git a/blueprints/Text to Image (Flux.1 Dev).json b/blueprints/Text to Image (Flux.1 Dev).json
new file mode 100644
index 000000000..6d8446e81
--- /dev/null
+++ b/blueprints/Text to Image (Flux.1 Dev).json
@@ -0,0 +1,1047 @@
+{
+ "revision": 0,
+ "last_node_id": 193,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 193,
+ "type": "1fd98b34-59ef-4d8d-afbf-58bdd7a1cd35",
+ "pos": [
+ -1210,
+ -1770
+ ],
+ "size": [
+ 400,
+ 380
+ ],
+ "flags": {},
+ "order": 0,
+ "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": "seed",
+ "type": "INT",
+ "widget": {
+ "name": "seed"
+ },
+ "link": null
+ },
+ {
+ "name": "unet_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "unet_name"
+ },
+ "link": null
+ },
+ {
+ "name": "clip_name1",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name1"
+ },
+ "link": null
+ },
+ {
+ "name": "clip_name2",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name2"
+ },
+ "link": null
+ },
+ {
+ "name": "vae_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "vae_name"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": []
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [
+ [
+ "45",
+ "text"
+ ],
+ [
+ "27",
+ "width"
+ ],
+ [
+ "27",
+ "height"
+ ],
+ [
+ "31",
+ "seed"
+ ],
+ [
+ "38",
+ "unet_name"
+ ],
+ [
+ "40",
+ "clip_name1"
+ ],
+ [
+ "40",
+ "clip_name2"
+ ],
+ [
+ "39",
+ "vae_name"
+ ],
+ [
+ "31",
+ "control_after_generate"
+ ]
+ ],
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1"
+ },
+ "widgets_values": [],
+ "title": "Text to Image (Flux.1 Dev)"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "1fd98b34-59ef-4d8d-afbf-58bdd7a1cd35",
+ "version": 1,
+ "state": {
+ "lastGroupId": 8,
+ "lastNodeId": 193,
+ "lastLinkId": 388,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Text to Image (Flux.1 Dev)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -1090,
+ 411,
+ 120,
+ 200
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 540,
+ 100,
+ 120,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "669e384e-5e26-4291-9bac-e1d1f04b4a16",
+ "name": "text",
+ "type": "STRING",
+ "linkIds": [
+ 68
+ ],
+ "label": "prompt",
+ "pos": [
+ -990,
+ 431
+ ]
+ },
+ {
+ "id": "5a5c0b01-5836-4ca6-a24f-68c0a4fb9802",
+ "name": "width",
+ "type": "INT",
+ "linkIds": [
+ 69
+ ],
+ "pos": [
+ -990,
+ 451
+ ]
+ },
+ {
+ "id": "5e01104a-ed7f-457b-aaee-934e8ecc088d",
+ "name": "height",
+ "type": "INT",
+ "linkIds": [
+ 70
+ ],
+ "pos": [
+ -990,
+ 471
+ ]
+ },
+ {
+ "id": "ea5ea317-a484-4605-8138-8628a4b8e502",
+ "name": "seed",
+ "type": "INT",
+ "linkIds": [
+ 382
+ ],
+ "pos": [
+ -990,
+ 491
+ ]
+ },
+ {
+ "id": "ea2332f5-bd49-4e2e-8c7a-95817dc56ed6",
+ "name": "unet_name",
+ "type": "COMBO",
+ "linkIds": [
+ 385
+ ],
+ "pos": [
+ -990,
+ 511
+ ]
+ },
+ {
+ "id": "4fca3f43-c05f-4337-bf84-2afe67e43739",
+ "name": "clip_name1",
+ "type": "COMBO",
+ "linkIds": [
+ 386
+ ],
+ "pos": [
+ -990,
+ 531
+ ]
+ },
+ {
+ "id": "357a679f-1370-4cd5-9269-0d5ae1986b49",
+ "name": "clip_name2",
+ "type": "COMBO",
+ "linkIds": [
+ 387
+ ],
+ "pos": [
+ -990,
+ 551
+ ]
+ },
+ {
+ "id": "924ffec5-81f8-4585-8761-5a80d5d775bc",
+ "name": "vae_name",
+ "type": "COMBO",
+ "linkIds": [
+ 388
+ ],
+ "pos": [
+ -990,
+ 571
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "2185cb4d-8689-4cf8-b345-75319fb46a8e",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "linkIds": [
+ 9
+ ],
+ "localized_name": "IMAGE",
+ "pos": [
+ 560,
+ 120
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 39,
+ "type": "VAELoader",
+ "pos": [
+ -800,
+ 670
+ ],
+ "size": [
+ 270,
+ 110
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "vae_name",
+ "name": "vae_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "vae_name"
+ },
+ "link": 388
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "VAE",
+ "name": "VAE",
+ "type": "VAE",
+ "links": [
+ 58
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "VAELoader",
+ "models": [
+ {
+ "name": "ae.safetensors",
+ "url": "https://huggingface.co/Comfy-Org/Lumina_Image_2.0_Repackaged/resolve/main/split_files/vae/ae.safetensors",
+ "directory": "vae"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ae.safetensors"
+ ]
+ },
+ {
+ "id": 38,
+ "type": "UNETLoader",
+ "pos": [
+ -800,
+ 160
+ ],
+ "size": [
+ 270,
+ 110
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "unet_name",
+ "name": "unet_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "unet_name"
+ },
+ "link": 385
+ },
+ {
+ "localized_name": "weight_dtype",
+ "name": "weight_dtype",
+ "type": "COMBO",
+ "widget": {
+ "name": "weight_dtype"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 61
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "UNETLoader",
+ "models": [
+ {
+ "name": "flux1-dev.safetensors",
+ "url": "https://huggingface.co/Comfy-Org/flux1-dev/resolve/main/flux1-dev.safetensors",
+ "directory": "diffusion_models"
+ }
+ ]
+ },
+ "widgets_values": [
+ "flux1-dev.safetensors",
+ "default"
+ ]
+ },
+ {
+ "id": 40,
+ "type": "DualCLIPLoader",
+ "pos": [
+ -800,
+ 380
+ ],
+ "size": [
+ 270,
+ 180
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip_name1",
+ "name": "clip_name1",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name1"
+ },
+ "link": 386
+ },
+ {
+ "localized_name": "clip_name2",
+ "name": "clip_name2",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name2"
+ },
+ "link": 387
+ },
+ {
+ "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": [
+ 64
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "DualCLIPLoader",
+ "models": [
+ {
+ "name": "clip_l.safetensors",
+ "url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors",
+ "directory": "text_encoders"
+ },
+ {
+ "name": "t5xxl_fp16.safetensors",
+ "url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors",
+ "directory": "text_encoders"
+ }
+ ]
+ },
+ "widgets_values": [
+ "clip_l.safetensors",
+ "t5xxl_fp16.safetensors",
+ "flux",
+ "default"
+ ]
+ },
+ {
+ "id": 27,
+ "type": "EmptySD3LatentImage",
+ "pos": [
+ -420,
+ 640
+ ],
+ "size": [
+ 270,
+ 170
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 69
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 70
+ },
+ {
+ "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": [
+ 51
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "EmptySD3LatentImage"
+ },
+ "widgets_values": [
+ 1024,
+ 1024,
+ 1
+ ]
+ },
+ {
+ "id": 45,
+ "type": "CLIPTextEncode",
+ "pos": [
+ -460,
+ 150
+ ],
+ "size": [
+ 330,
+ 220
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 64
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": 68
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 65,
+ 66
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "CLIPTextEncode"
+ },
+ "widgets_values": [
+ ""
+ ]
+ },
+ {
+ "id": 31,
+ "type": "KSampler",
+ "pos": [
+ -50,
+ 260
+ ],
+ "size": [
+ 320,
+ 350
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 61
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 65
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 63
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 51
+ },
+ {
+ "localized_name": "seed",
+ "name": "seed",
+ "type": "INT",
+ "widget": {
+ "name": "seed"
+ },
+ "link": 382
+ },
+ {
+ "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": [
+ 52
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "KSampler"
+ },
+ "widgets_values": [
+ 0,
+ "randomize",
+ 20,
+ 1,
+ "euler",
+ "simple",
+ 1
+ ]
+ },
+ {
+ "id": 8,
+ "type": "VAEDecode",
+ "pos": [
+ 20,
+ 120
+ ],
+ "size": [
+ 230,
+ 100
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 0,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 52
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 58
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "slot_index": 0,
+ "links": [
+ 9
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "VAEDecode"
+ }
+ },
+ {
+ "id": 42,
+ "type": "ConditioningZeroOut",
+ "pos": [
+ -350,
+ 420
+ ],
+ "size": [
+ 230,
+ 80
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "conditioning",
+ "name": "conditioning",
+ "type": "CONDITIONING",
+ "link": 66
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 63
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "ConditioningZeroOut"
+ }
+ }
+ ],
+ "groups": [
+ {
+ "id": 1,
+ "title": "Model",
+ "bounding": [
+ -820,
+ 70,
+ 320,
+ 750
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 2,
+ "title": "Image Size",
+ "bounding": [
+ -470,
+ 570,
+ 380,
+ 250
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 3,
+ "title": "Prompt",
+ "bounding": [
+ -470,
+ 70,
+ 380,
+ 470
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 52,
+ "origin_id": 31,
+ "origin_slot": 0,
+ "target_id": 8,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 58,
+ "origin_id": 39,
+ "origin_slot": 0,
+ "target_id": 8,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 61,
+ "origin_id": 38,
+ "origin_slot": 0,
+ "target_id": 31,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 63,
+ "origin_id": 42,
+ "origin_slot": 0,
+ "target_id": 31,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 51,
+ "origin_id": 27,
+ "origin_slot": 0,
+ "target_id": 31,
+ "target_slot": 3,
+ "type": "LATENT"
+ },
+ {
+ "id": 9,
+ "origin_id": 8,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 64,
+ "origin_id": 40,
+ "origin_slot": 0,
+ "target_id": 45,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 65,
+ "origin_id": 45,
+ "origin_slot": 0,
+ "target_id": 31,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 66,
+ "origin_id": 45,
+ "origin_slot": 0,
+ "target_id": 42,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 68,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 45,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 69,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 27,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 70,
+ "origin_id": -10,
+ "origin_slot": 2,
+ "target_id": 27,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 382,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 31,
+ "target_slot": 4,
+ "type": "INT"
+ },
+ {
+ "id": 385,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 38,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 386,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 40,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 387,
+ "origin_id": -10,
+ "origin_slot": 6,
+ "target_id": 40,
+ "target_slot": 1,
+ "type": "COMBO"
+ },
+ {
+ "id": 388,
+ "origin_id": -10,
+ "origin_slot": 7,
+ "target_id": 39,
+ "target_slot": 0,
+ "type": "COMBO"
+ }
+ ],
+ "extra": {
+ "workflowRendererVersion": "LG"
+ },
+ "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."
+ }
+ ]
+ },
+ "extra": {
+ "ds": {
+ "scale": 0.7513148009015777,
+ "offset": [
+ 1726.1426909346173,
+ 146.66925047394233
+ ]
+ },
+ "ue_links": []
+ }
+}
diff --git a/blueprints/Text to Image (Flux.1 Krea Dev).json b/blueprints/Text to Image (Flux.1 Krea Dev).json
new file mode 100644
index 000000000..0d7fa03c4
--- /dev/null
+++ b/blueprints/Text to Image (Flux.1 Krea Dev).json
@@ -0,0 +1,1041 @@
+{
+ "revision": 0,
+ "last_node_id": 196,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 196,
+ "type": "aa0a207e-bf0e-477c-a87f-f58fcf5f7749",
+ "pos": [
+ 1010,
+ 130
+ ],
+ "size": [
+ 410,
+ 460
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [
+ {
+ "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": "seed",
+ "type": "INT",
+ "widget": {
+ "name": "seed"
+ },
+ "link": null
+ },
+ {
+ "name": "unet_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "unet_name"
+ },
+ "link": null
+ },
+ {
+ "name": "clip_name1",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name1"
+ },
+ "link": null
+ },
+ {
+ "name": "clip_name2",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name2"
+ },
+ "link": null
+ },
+ {
+ "name": "vae_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "vae_name"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": []
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [
+ [
+ "195",
+ "text"
+ ],
+ [
+ "27",
+ "width"
+ ],
+ [
+ "27",
+ "height"
+ ],
+ [
+ "31",
+ "seed"
+ ],
+ [
+ "38",
+ "unet_name"
+ ],
+ [
+ "40",
+ "clip_name1"
+ ],
+ [
+ "40",
+ "clip_name2"
+ ],
+ [
+ "39",
+ "vae_name"
+ ]
+ ],
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1"
+ },
+ "widgets_values": [],
+ "title": "Text to Image (Flux.1 Krea Dev)"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "aa0a207e-bf0e-477c-a87f-f58fcf5f7749",
+ "version": 1,
+ "state": {
+ "lastGroupId": 8,
+ "lastNodeId": 196,
+ "lastLinkId": 395,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Text to Image (Flux.1 Krea Dev)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -1050,
+ 426,
+ 120,
+ 200
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 620,
+ 140,
+ 120,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "c2515318-6e10-4ad9-9466-e6aa855bc849",
+ "name": "text",
+ "type": "STRING",
+ "linkIds": [
+ 71
+ ],
+ "pos": [
+ -950,
+ 446
+ ]
+ },
+ {
+ "id": "09f20672-c8a3-4180-823a-5a6af0113e4f",
+ "name": "width",
+ "type": "INT",
+ "linkIds": [
+ 72
+ ],
+ "pos": [
+ -950,
+ 466
+ ]
+ },
+ {
+ "id": "7f54c952-896e-4356-bfb2-970e1c8f2eb7",
+ "name": "height",
+ "type": "INT",
+ "linkIds": [
+ 73
+ ],
+ "pos": [
+ -950,
+ 486
+ ]
+ },
+ {
+ "id": "e2dc1c86-2fb4-4b80-b560-f30560af1897",
+ "name": "seed",
+ "type": "INT",
+ "linkIds": [
+ 391
+ ],
+ "pos": [
+ -950,
+ 506
+ ]
+ },
+ {
+ "id": "34b172e7-85b2-444a-9a4d-1221f272c46e",
+ "name": "unet_name",
+ "type": "COMBO",
+ "linkIds": [
+ 392
+ ],
+ "pos": [
+ -950,
+ 526
+ ]
+ },
+ {
+ "id": "073b7440-d943-4a2f-a3a1-fbdb8fcda9f9",
+ "name": "clip_name1",
+ "type": "COMBO",
+ "linkIds": [
+ 393
+ ],
+ "pos": [
+ -950,
+ 546
+ ]
+ },
+ {
+ "id": "55c1286a-4aca-41fc-b967-ae3d3fa7bc85",
+ "name": "clip_name2",
+ "type": "COMBO",
+ "linkIds": [
+ 394
+ ],
+ "pos": [
+ -950,
+ 566
+ ]
+ },
+ {
+ "id": "2241e4fc-9219-4be7-bf6d-3493b579ab5a",
+ "name": "vae_name",
+ "type": "COMBO",
+ "linkIds": [
+ 395
+ ],
+ "pos": [
+ -950,
+ 586
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "5310184a-f0a2-405f-9917-dd2a352a4fac",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "linkIds": [
+ 9
+ ],
+ "localized_name": "IMAGE",
+ "pos": [
+ 640,
+ 160
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 40,
+ "type": "DualCLIPLoader",
+ "pos": [
+ -780,
+ 360
+ ],
+ "size": [
+ 270,
+ 180
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip_name1",
+ "name": "clip_name1",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name1"
+ },
+ "link": 393
+ },
+ {
+ "localized_name": "clip_name2",
+ "name": "clip_name2",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name2"
+ },
+ "link": 394
+ },
+ {
+ "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": [
+ 64
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "DualCLIPLoader",
+ "models": [
+ {
+ "name": "clip_l.safetensors",
+ "url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors",
+ "directory": "text_encoders"
+ },
+ {
+ "name": "t5xxl_fp16.safetensors",
+ "url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors",
+ "directory": "text_encoders"
+ }
+ ]
+ },
+ "widgets_values": [
+ "clip_l.safetensors",
+ "t5xxl_fp16.safetensors",
+ "flux",
+ "default"
+ ]
+ },
+ {
+ "id": 39,
+ "type": "VAELoader",
+ "pos": [
+ -770,
+ 630
+ ],
+ "size": [
+ 240,
+ 110
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "vae_name",
+ "name": "vae_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "vae_name"
+ },
+ "link": 395
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "VAE",
+ "name": "VAE",
+ "type": "VAE",
+ "links": [
+ 58
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "VAELoader",
+ "models": [
+ {
+ "name": "ae.safetensors",
+ "url": "https://huggingface.co/Comfy-Org/Lumina_Image_2.0_Repackaged/resolve/main/split_files/vae/ae.safetensors",
+ "directory": "vae"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ae.safetensors"
+ ]
+ },
+ {
+ "id": 38,
+ "type": "UNETLoader",
+ "pos": [
+ -780,
+ 170
+ ],
+ "size": [
+ 270,
+ 110
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "unet_name",
+ "name": "unet_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "unet_name"
+ },
+ "link": 392
+ },
+ {
+ "localized_name": "weight_dtype",
+ "name": "weight_dtype",
+ "type": "COMBO",
+ "widget": {
+ "name": "weight_dtype"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 61
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "UNETLoader",
+ "models": [
+ {
+ "name": "flux1-krea-dev_fp8_scaled.safetensors",
+ "url": "https://huggingface.co/Comfy-Org/FLUX.1-Krea-dev_ComfyUI/resolve/main/split_files/diffusion_models/flux1-krea-dev_fp8_scaled.safetensors",
+ "directory": "diffusion_models"
+ }
+ ]
+ },
+ "widgets_values": [
+ "flux1-krea-dev_fp8_scaled.safetensors",
+ "default"
+ ]
+ },
+ {
+ "id": 195,
+ "type": "CLIPTextEncode",
+ "pos": [
+ -440,
+ 180
+ ],
+ "size": [
+ 330,
+ 210
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 64
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": 71
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 65,
+ 66
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.47",
+ "Node name for S&R": "CLIPTextEncode"
+ },
+ "widgets_values": [
+ ""
+ ]
+ },
+ {
+ "id": 27,
+ "type": "EmptySD3LatentImage",
+ "pos": [
+ -390,
+ 650
+ ],
+ "size": [
+ 270,
+ 170
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 72
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 73
+ },
+ {
+ "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": [
+ 51
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "EmptySD3LatentImage"
+ },
+ "widgets_values": [
+ 1024,
+ 1024,
+ 1
+ ]
+ },
+ {
+ "id": 31,
+ "type": "KSampler",
+ "pos": [
+ 0,
+ 130
+ ],
+ "size": [
+ 320,
+ 350
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 61
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 65
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 63
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 51
+ },
+ {
+ "localized_name": "seed",
+ "name": "seed",
+ "type": "INT",
+ "widget": {
+ "name": "seed"
+ },
+ "link": 391
+ },
+ {
+ "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": [
+ 52
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "KSampler"
+ },
+ "widgets_values": [
+ 0,
+ "randomize",
+ 20,
+ 1,
+ "euler",
+ "simple",
+ 1
+ ]
+ },
+ {
+ "id": 8,
+ "type": "VAEDecode",
+ "pos": [
+ 340,
+ 140
+ ],
+ "size": [
+ 230,
+ 100
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 0,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 52
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 58
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "slot_index": 0,
+ "links": [
+ 9
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "VAEDecode"
+ }
+ },
+ {
+ "id": 42,
+ "type": "ConditioningZeroOut",
+ "pos": [
+ -340,
+ 430
+ ],
+ "size": [
+ 230,
+ 80
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "conditioning",
+ "name": "conditioning",
+ "type": "CONDITIONING",
+ "link": 66
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 63
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.40",
+ "Node name for S&R": "ConditioningZeroOut"
+ }
+ }
+ ],
+ "groups": [
+ {
+ "id": 1,
+ "title": "Model",
+ "bounding": [
+ -800,
+ 90,
+ 310,
+ 750
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 2,
+ "title": "Image Size",
+ "bounding": [
+ -460,
+ 560,
+ 400,
+ 280
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 3,
+ "title": "Prompt",
+ "bounding": [
+ -460,
+ 90,
+ 400,
+ 440
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 66,
+ "origin_id": 195,
+ "origin_slot": 0,
+ "target_id": 42,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 52,
+ "origin_id": 31,
+ "origin_slot": 0,
+ "target_id": 8,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 58,
+ "origin_id": 39,
+ "origin_slot": 0,
+ "target_id": 8,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 61,
+ "origin_id": 38,
+ "origin_slot": 0,
+ "target_id": 31,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 65,
+ "origin_id": 195,
+ "origin_slot": 0,
+ "target_id": 31,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 63,
+ "origin_id": 42,
+ "origin_slot": 0,
+ "target_id": 31,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 51,
+ "origin_id": 27,
+ "origin_slot": 0,
+ "target_id": 31,
+ "target_slot": 3,
+ "type": "LATENT"
+ },
+ {
+ "id": 64,
+ "origin_id": 40,
+ "origin_slot": 0,
+ "target_id": 195,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 9,
+ "origin_id": 8,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 71,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 195,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 72,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 27,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 73,
+ "origin_id": -10,
+ "origin_slot": 2,
+ "target_id": 27,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 391,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 31,
+ "target_slot": 4,
+ "type": "INT"
+ },
+ {
+ "id": 392,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 38,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 393,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 40,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 394,
+ "origin_id": -10,
+ "origin_slot": 6,
+ "target_id": 40,
+ "target_slot": 1,
+ "type": "COMBO"
+ },
+ {
+ "id": 395,
+ "origin_id": -10,
+ "origin_slot": 7,
+ "target_id": 39,
+ "target_slot": 0,
+ "type": "COMBO"
+ }
+ ],
+ "extra": {
+ "workflowRendererVersion": "LG"
+ },
+ "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."
+ }
+ ]
+ },
+ "extra": {
+ "ds": {
+ "scale": 0.735584459955559,
+ "offset": [
+ 1936.5815687336737,
+ 303.78330847702625
+ ]
+ },
+ "ue_links": []
+ }
+}
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
new file mode 100644
index 000000000..9e11b7a86
--- /dev/null
+++ b/blueprints/Text to Image (NetaYume Lumina).json
@@ -0,0 +1,1470 @@
+{
+ "revision": 0,
+ "last_node_id": 219,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 219,
+ "type": "fc9485c9-2acd-482e-94f1-b5fa702f2536",
+ "pos": [
+ -1900,
+ 2330
+ ],
+ "size": [
+ 400,
+ 540
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "value",
+ "type": "STRING",
+ "widget": {
+ "name": "value"
+ },
+ "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": "ckpt_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "ckpt_name"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": []
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [
+ [
+ "62",
+ "value"
+ ],
+ [
+ "53",
+ "width"
+ ],
+ [
+ "53",
+ "height"
+ ],
+ [
+ "55",
+ "seed"
+ ],
+ [
+ "56",
+ "ckpt_name"
+ ],
+ [
+ "55",
+ "control_after_generate"
+ ]
+ ],
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ }
+ },
+ "widgets_values": [],
+ "title": "Text to Image (NetaYume Lumina)"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "fc9485c9-2acd-482e-94f1-b5fa702f2536",
+ "version": 1,
+ "state": {
+ "lastGroupId": 8,
+ "lastNodeId": 219,
+ "lastLinkId": 395,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Text to Image (NetaYume Lumina)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -600,
+ 90,
+ 120,
+ 140
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 1740.333330193419,
+ 286.3333328495138,
+ 120,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "b80a1e0c-e8a6-4c4f-8eb1-825cb7e4fdcf",
+ "name": "value",
+ "type": "STRING",
+ "linkIds": [
+ 36
+ ],
+ "pos": [
+ -500,
+ 110
+ ]
+ },
+ {
+ "id": "6583bb32-7cff-4921-a771-1f0dcdf779e6",
+ "name": "width",
+ "type": "INT",
+ "linkIds": [
+ 39
+ ],
+ "pos": [
+ -500,
+ 130
+ ]
+ },
+ {
+ "id": "c486937a-46c0-431b-8775-057897843cbd",
+ "name": "height",
+ "type": "INT",
+ "linkIds": [
+ 40
+ ],
+ "pos": [
+ -500,
+ 150
+ ]
+ },
+ {
+ "id": "9c85c0cc-c906-405a-a4d9-43b93c47cb53",
+ "name": "seed",
+ "type": "INT",
+ "linkIds": [
+ 42
+ ],
+ "pos": [
+ -500,
+ 170
+ ]
+ },
+ {
+ "id": "f7e288ec-fa1f-4a1d-b721-6b605de9cb51",
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "linkIds": [
+ 43
+ ],
+ "pos": [
+ -500,
+ 190
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "ea4b872b-a294-4cbf-99a9-70e55c0f8b3e",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "linkIds": [
+ 16
+ ],
+ "localized_name": "IMAGE",
+ "pos": [
+ 1760.333330193419,
+ 306.3333328495138
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 53,
+ "type": "EmptySD3LatentImage",
+ "pos": [
+ -220,
+ 370
+ ],
+ "size": [
+ 320,
+ 170
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 39
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 40
+ },
+ {
+ "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": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "EmptySD3LatentImage"
+ },
+ "widgets_values": [
+ 1024,
+ 1024,
+ 1
+ ]
+ },
+ {
+ "id": 54,
+ "type": "ModelSamplingAuraFlow",
+ "pos": [
+ 650,
+ 40
+ ],
+ "size": [
+ 310,
+ 110
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 12
+ },
+ {
+ "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": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "ModelSamplingAuraFlow"
+ },
+ "widgets_values": [
+ 4
+ ]
+ },
+ {
+ "id": 55,
+ "type": "KSampler",
+ "pos": [
+ 650,
+ 200
+ ],
+ "size": [
+ 320,
+ 350
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 13
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 32
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 23
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 17
+ },
+ {
+ "localized_name": "seed",
+ "name": "seed",
+ "type": "INT",
+ "widget": {
+ "name": "seed"
+ },
+ "link": 42
+ },
+ {
+ "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": [
+ 14
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "KSampler"
+ },
+ "widgets_values": [
+ 0,
+ "randomize",
+ 30,
+ 4,
+ "res_multistep",
+ "simple",
+ 1
+ ]
+ },
+ {
+ "id": 56,
+ "type": "CheckpointLoaderSimple",
+ "pos": [
+ -220,
+ 70
+ ],
+ "size": [
+ 320,
+ 160
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "ckpt_name",
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "ckpt_name"
+ },
+ "link": 43
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "slot_index": 0,
+ "links": [
+ 12
+ ]
+ },
+ {
+ "localized_name": "CLIP",
+ "name": "CLIP",
+ "type": "CLIP",
+ "slot_index": 1,
+ "links": [
+ 22,
+ 35
+ ]
+ },
+ {
+ "localized_name": "VAE",
+ "name": "VAE",
+ "type": "VAE",
+ "slot_index": 2,
+ "links": [
+ 8
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "CheckpointLoaderSimple",
+ "models": [
+ {
+ "name": "NetaYumev35_pretrained_all_in_one.safetensors",
+ "url": "https://huggingface.co/duongve/NetaYume-Lumina-Image-2.0/resolve/main/NetaYumev35_pretrained_all_in_one.safetensors",
+ "directory": "checkpoints"
+ }
+ ]
+ },
+ "widgets_values": [
+ "NetaYumev35_pretrained_all_in_one.safetensors"
+ ]
+ },
+ {
+ "id": 57,
+ "type": "a07fdf06-1bda-4dac-bdbd-63ee8ebca1c9",
+ "pos": [
+ 180,
+ 360
+ ],
+ "size": [
+ 400,
+ 140
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 22
+ },
+ {
+ "name": "value",
+ "type": "STRING",
+ "widget": {
+ "name": "value"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 23
+ ]
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [
+ [
+ "218",
+ "value"
+ ]
+ ],
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ }
+ },
+ "widgets_values": [],
+ "color": "#223",
+ "bgcolor": "#335"
+ },
+ {
+ "id": 217,
+ "type": "VAEDecode",
+ "pos": [
+ 1040,
+ 210
+ ],
+ "size": [
+ 230,
+ 100
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 14
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 8
+ }
+ ],
+ "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": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "VAEDecode"
+ }
+ },
+ {
+ "id": 59,
+ "type": "MarkdownNote",
+ "pos": [
+ 640,
+ -390
+ ],
+ "size": [
+ 370,
+ 280
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [],
+ "title": "Note: Prompt",
+ "properties": {},
+ "widgets_values": [
+ "Check the prompt book [here](https://nieta-art.feishu.cn/wiki/RY3GwpT59icIQlkWXEfcCqIMnQd)\n\nYou should keep the prefix part fixed until the **Prompt Start** tag\n\n@whatever in the prompt is for artist tags, such as @comfyanonymous\n\nYou can find more artist tags [here](https://gumgum10.github.io/gumgum.github.io/)\n"
+ ],
+ "color": "#222",
+ "bgcolor": "#000"
+ },
+ {
+ "id": 60,
+ "type": "StringConcatenate",
+ "pos": [
+ 170,
+ -370
+ ],
+ "size": [
+ 400,
+ 250
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "string_a",
+ "name": "string_a",
+ "type": "STRING",
+ "widget": {
+ "name": "string_a"
+ },
+ "link": 30
+ },
+ {
+ "localized_name": "string_b",
+ "name": "string_b",
+ "type": "STRING",
+ "widget": {
+ "name": "string_b"
+ },
+ "link": 31
+ },
+ {
+ "localized_name": "delimiter",
+ "name": "delimiter",
+ "type": "STRING",
+ "widget": {
+ "name": "delimiter"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "STRING",
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 34
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.70",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "StringConcatenate"
+ },
+ "widgets_values": [
+ "",
+ "",
+ ""
+ ]
+ },
+ {
+ "id": 61,
+ "type": "CLIPTextEncode",
+ "pos": [
+ 170,
+ 60
+ ],
+ "size": [
+ 430,
+ 190
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 35
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": 34
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 32
+ ]
+ }
+ ],
+ "title": "CLIP Text Encode (Positive Prompt)",
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "CLIPTextEncode"
+ },
+ "widgets_values": [
+ ""
+ ],
+ "color": "#232",
+ "bgcolor": "#353"
+ },
+ {
+ "id": 62,
+ "type": "PrimitiveStringMultiline",
+ "pos": [
+ -240,
+ -210
+ ],
+ "size": [
+ 370,
+ 140
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "STRING",
+ "widget": {
+ "name": "value"
+ },
+ "link": 36
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "STRING",
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 31
+ ]
+ }
+ ],
+ "title": "Prompt",
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.70",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "PrimitiveStringMultiline"
+ },
+ "widgets_values": [
+ ""
+ ]
+ },
+ {
+ "id": 63,
+ "type": "PrimitiveStringMultiline",
+ "pos": [
+ -240,
+ -390
+ ],
+ "size": [
+ 370,
+ 140
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "STRING",
+ "widget": {
+ "name": "value"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "STRING",
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 30
+ ]
+ }
+ ],
+ "title": "System prompt",
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.70",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "PrimitiveStringMultiline"
+ },
+ "widgets_values": [
+ "You are an assistant designed to generate high quality anime images based on textual prompts. "
+ ]
+ }
+ ],
+ "groups": [
+ {
+ "id": 1,
+ "title": "Model",
+ "bounding": [
+ -250,
+ -30,
+ 370,
+ 280
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 2,
+ "title": "Image Size",
+ "bounding": [
+ -250,
+ 280,
+ 370,
+ 290
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 3,
+ "title": "Prompt",
+ "bounding": [
+ 150,
+ -30,
+ 460,
+ 600
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 4,
+ "title": "Prompt Builder",
+ "bounding": [
+ -250,
+ -460,
+ 840,
+ 400
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 12,
+ "origin_id": 56,
+ "origin_slot": 0,
+ "target_id": 54,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 13,
+ "origin_id": 54,
+ "origin_slot": 0,
+ "target_id": 55,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 23,
+ "origin_id": 57,
+ "origin_slot": 0,
+ "target_id": 55,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 17,
+ "origin_id": 53,
+ "origin_slot": 0,
+ "target_id": 55,
+ "target_slot": 3,
+ "type": "LATENT"
+ },
+ {
+ "id": 14,
+ "origin_id": 55,
+ "origin_slot": 0,
+ "target_id": 217,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 8,
+ "origin_id": 56,
+ "origin_slot": 2,
+ "target_id": 217,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 22,
+ "origin_id": 56,
+ "origin_slot": 1,
+ "target_id": 57,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 16,
+ "origin_id": 217,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 30,
+ "origin_id": 63,
+ "origin_slot": 0,
+ "target_id": 60,
+ "target_slot": 0,
+ "type": "STRING"
+ },
+ {
+ "id": 31,
+ "origin_id": 62,
+ "origin_slot": 0,
+ "target_id": 60,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 32,
+ "origin_id": 61,
+ "origin_slot": 0,
+ "target_id": 55,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 34,
+ "origin_id": 60,
+ "origin_slot": 0,
+ "target_id": 61,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 35,
+ "origin_id": 56,
+ "origin_slot": 1,
+ "target_id": 61,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 36,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 62,
+ "target_slot": 0,
+ "type": "STRING"
+ },
+ {
+ "id": 39,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 53,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 40,
+ "origin_id": -10,
+ "origin_slot": 2,
+ "target_id": 53,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 42,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 55,
+ "target_slot": 4,
+ "type": "INT"
+ },
+ {
+ "id": 43,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 56,
+ "target_slot": 0,
+ "type": "COMBO"
+ }
+ ],
+ "extra": {
+ "workflowRendererVersion": "LG"
+ },
+ "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",
+ "version": 1,
+ "state": {
+ "lastGroupId": 8,
+ "lastNodeId": 219,
+ "lastLinkId": 395,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "CLIP Text Encode (Negative Prompt)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -150,
+ 675,
+ 120,
+ 80
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 905.2780151367188,
+ 675,
+ 128.6640625,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "47264a97-6fc9-454d-920f-b8a43fee0489",
+ "name": "clip",
+ "type": "CLIP",
+ "linkIds": [
+ 5
+ ],
+ "localized_name": "clip",
+ "pos": [
+ -50,
+ 695
+ ]
+ },
+ {
+ "id": "7cdb7919-1dad-4bd2-928d-c543c3fd712e",
+ "name": "value",
+ "type": "STRING",
+ "linkIds": [
+ 22
+ ],
+ "pos": [
+ -50,
+ 715
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "c3f17ad9-6954-4333-bf8e-e1cf886c351b",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "linkIds": [
+ 6
+ ],
+ "localized_name": "CONDITIONING",
+ "pos": [
+ 925.2780151367188,
+ 695
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 64,
+ "type": "StringConcatenate",
+ "pos": [
+ 420,
+ 720
+ ],
+ "size": [
+ 400,
+ 200
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "string_a",
+ "name": "string_a",
+ "type": "STRING",
+ "widget": {
+ "name": "string_a"
+ },
+ "link": 19
+ },
+ {
+ "localized_name": "string_b",
+ "name": "string_b",
+ "type": "STRING",
+ "widget": {
+ "name": "string_b"
+ },
+ "link": 20
+ },
+ {
+ "localized_name": "delimiter",
+ "name": "delimiter",
+ "type": "STRING",
+ "widget": {
+ "name": "delimiter"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "STRING",
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 21
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.70",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "StringConcatenate"
+ },
+ "widgets_values": [
+ "",
+ "",
+ ""
+ ]
+ },
+ {
+ "id": 65,
+ "type": "PrimitiveStringMultiline",
+ "pos": [
+ 30,
+ 720
+ ],
+ "size": [
+ 370,
+ 130
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "STRING",
+ "widget": {
+ "name": "value"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "STRING",
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 19
+ ]
+ }
+ ],
+ "title": "System prompt",
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.70",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "PrimitiveStringMultiline"
+ },
+ "widgets_values": [
+ "You are an assistant designed to generate low-quality images based on textual prompts "
+ ]
+ },
+ {
+ "id": 218,
+ "type": "PrimitiveStringMultiline",
+ "pos": [
+ 30,
+ 900
+ ],
+ "size": [
+ 370,
+ 130
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "STRING",
+ "widget": {
+ "name": "value"
+ },
+ "link": 22
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "STRING",
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 20
+ ]
+ }
+ ],
+ "title": "System prompt",
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.70",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "PrimitiveStringMultiline"
+ },
+ "widgets_values": [
+ "blurry, worst quality, low quality, jpeg artifacts, signature, watermark, username, error, deformed hands, bad anatomy, extra limbs, poorly drawn hands, poorly drawn face, mutation, deformed, extra eyes, extra arms, extra legs, malformed limbs, fused fingers, too many fingers, long neck, cross-eyed, bad proportions, missing arms, missing legs, extra digit, fewer digits, cropped"
+ ]
+ },
+ {
+ "id": 67,
+ "type": "CLIPTextEncode",
+ "pos": [
+ 420,
+ 410
+ ],
+ "size": [
+ 430,
+ 190
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 5
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": 21
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 6
+ ]
+ }
+ ],
+ "title": "CLIP Text Encode (Negative Prompt)",
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "CLIPTextEncode"
+ },
+ "widgets_values": [
+ "You are an assistant designed to generate low-quality images based on textual prompts blurry, worst quality, low quality, jpeg artifacts, signature, watermark, username, error, deformed hands, bad anatomy, extra limbs, poorly drawn hands, poorly drawn face, mutation, deformed, extra eyes, extra arms, extra legs, malformed limbs, fused fingers, too many fingers, long neck, cross-eyed, bad proportions, missing arms, missing legs, extra digit, fewer digits, cropped"
+ ],
+ "color": "#223",
+ "bgcolor": "#335"
+ }
+ ],
+ "groups": [],
+ "links": [
+ {
+ "id": 19,
+ "origin_id": 65,
+ "origin_slot": 0,
+ "target_id": 64,
+ "target_slot": 0,
+ "type": "STRING"
+ },
+ {
+ "id": 20,
+ "origin_id": 218,
+ "origin_slot": 0,
+ "target_id": 64,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 21,
+ "origin_id": 64,
+ "origin_slot": 0,
+ "target_id": 67,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 5,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 67,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 6,
+ "origin_id": 67,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 22,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 218,
+ "target_slot": 0,
+ "type": "STRING"
+ }
+ ],
+ "extra": {
+ "workflowRendererVersion": "LG"
+ },
+ "description": "Encodes a negative text prompt via CLIP for classifier-free guidance in anime-style generation (NetaYume Lumina)."
+ }
+ ]
+ },
+ "extra": {
+ "ue_links": []
+ }
+}
diff --git a/blueprints/Text to Image (Qwen-Image 2512).json b/blueprints/Text to Image (Qwen-Image 2512).json
new file mode 100644
index 000000000..09612be8b
--- /dev/null
+++ b/blueprints/Text to Image (Qwen-Image 2512).json
@@ -0,0 +1,1952 @@
+{
+ "revision": 0,
+ "last_node_id": 263,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 263,
+ "type": "fd6ee5f8-a0a9-487a-8b44-8cb65957532a",
+ "pos": [
+ 750,
+ 760
+ ],
+ "size": [
+ 400,
+ 0
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "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
+ },
+ {
+ "label": "enable_turbo_mode",
+ "name": "value",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "value"
+ },
+ "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
+ },
+ {
+ "name": "lora_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "lora_name"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": []
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [
+ [
+ "249",
+ "text"
+ ],
+ [
+ "252",
+ "width"
+ ],
+ [
+ "252",
+ "height"
+ ],
+ [
+ "256",
+ "value"
+ ],
+ [
+ "253",
+ "seed"
+ ],
+ [
+ "248",
+ "unet_name"
+ ],
+ [
+ "245",
+ "clip_name"
+ ],
+ [
+ "246",
+ "vae_name"
+ ],
+ [
+ "259",
+ "lora_name"
+ ]
+ ],
+ "ue_properties": {
+ "widget_ue_connectable": {
+ "value": true
+ },
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.4",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [],
+ "title": "Text to Image (Qwen-Image 2512)"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "fd6ee5f8-a0a9-487a-8b44-8cb65957532a",
+ "version": 1,
+ "state": {
+ "lastGroupId": 7,
+ "lastNodeId": 263,
+ "lastLinkId": 375,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Text to Image (Qwen-Image 2512)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -1080,
+ 1480,
+ 151.744140625,
+ 220
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 1550,
+ 1460,
+ 120,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "74d26021-a723-4a90-8e33-5d805a7e5deb",
+ "name": "text",
+ "type": "STRING",
+ "linkIds": [
+ 360
+ ],
+ "pos": [
+ -948.255859375,
+ 1500
+ ]
+ },
+ {
+ "id": "b55f69e6-c7cb-4641-9e1f-2cb1c1942ed0",
+ "name": "width",
+ "type": "INT",
+ "linkIds": [
+ 361
+ ],
+ "pos": [
+ -948.255859375,
+ 1520
+ ]
+ },
+ {
+ "id": "3e80284d-aba3-43cd-ab7b-ac2a619ef18c",
+ "name": "height",
+ "type": "INT",
+ "linkIds": [
+ 362
+ ],
+ "pos": [
+ -948.255859375,
+ 1540
+ ]
+ },
+ {
+ "id": "de06e137-6cec-4cb3-a6bb-737022310a7b",
+ "name": "value",
+ "type": "BOOLEAN",
+ "linkIds": [
+ 370
+ ],
+ "label": "enable_turbo_mode",
+ "pos": [
+ -948.255859375,
+ 1560
+ ]
+ },
+ {
+ "id": "9e500dee-a5b9-481b-ac46-64bab4bd3530",
+ "name": "seed",
+ "type": "INT",
+ "linkIds": [
+ 371
+ ],
+ "pos": [
+ -948.255859375,
+ 1580
+ ]
+ },
+ {
+ "id": "33422b12-24e5-41c6-96fc-f9a8dadd5d94",
+ "name": "unet_name",
+ "type": "COMBO",
+ "linkIds": [
+ 372
+ ],
+ "pos": [
+ -948.255859375,
+ 1600
+ ]
+ },
+ {
+ "id": "5cf753e4-236e-468e-9a06-6b8e238badc8",
+ "name": "clip_name",
+ "type": "COMBO",
+ "linkIds": [
+ 373
+ ],
+ "pos": [
+ -948.255859375,
+ 1620
+ ]
+ },
+ {
+ "id": "790e775c-a639-4e5f-9007-e2ee6764dc5e",
+ "name": "vae_name",
+ "type": "COMBO",
+ "linkIds": [
+ 374
+ ],
+ "pos": [
+ -948.255859375,
+ 1640
+ ]
+ },
+ {
+ "id": "3ebed521-3fe9-4922-ae26-2483e03d9305",
+ "name": "lora_name",
+ "type": "COMBO",
+ "linkIds": [
+ 375
+ ],
+ "pos": [
+ -948.255859375,
+ 1660
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "7db1f9e2-40ee-4f9f-bb24-a0db7b96d45e",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "linkIds": [
+ 333
+ ],
+ "localized_name": "IMAGE",
+ "pos": [
+ 1570,
+ 1480
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 245,
+ "type": "CLIPLoader",
+ "pos": [
+ -590,
+ 1370
+ ],
+ "size": [
+ 280,
+ 150
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip_name",
+ "name": "clip_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name"
+ },
+ "link": 373
+ },
+ {
+ "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": [
+ 314,
+ 315
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "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/Qwen-Image_ComfyUI/resolve/main/split_files/text_encoders/qwen_2.5_vl_7b_fp8_scaled.safetensors",
+ "directory": "text_encoders"
+ },
+ {
+ "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"
+ }
+ ]
+ },
+ "widgets_values": [
+ "qwen_2.5_vl_7b_fp8_scaled.safetensors",
+ "qwen_image",
+ "default"
+ ]
+ },
+ {
+ "id": 246,
+ "type": "VAELoader",
+ "pos": [
+ -580,
+ 1620
+ ],
+ "size": [
+ 280,
+ 110
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "vae_name",
+ "name": "vae_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "vae_name"
+ },
+ "link": 374
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "VAE",
+ "name": "VAE",
+ "type": "VAE",
+ "slot_index": 0,
+ "links": [
+ 323
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "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/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors",
+ "directory": "vae"
+ },
+ {
+ "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"
+ }
+ ]
+ },
+ "widgets_values": [
+ "qwen_image_vae.safetensors"
+ ]
+ },
+ {
+ "id": 247,
+ "type": "ModelSamplingAuraFlow",
+ "pos": [
+ 1040,
+ 1110
+ ],
+ "size": [
+ 250,
+ 110
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 367
+ },
+ {
+ "localized_name": "shift",
+ "name": "shift",
+ "type": "FLOAT",
+ "widget": {
+ "name": "shift"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 316
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "Node name for S&R": "ModelSamplingAuraFlow",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 3.1000000000000005
+ ]
+ },
+ {
+ "id": 248,
+ "type": "UNETLoader",
+ "pos": [
+ -590,
+ 1140
+ ],
+ "size": [
+ 280,
+ 130
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "unet_name",
+ "name": "unet_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "unet_name"
+ },
+ "link": 372
+ },
+ {
+ "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": [
+ 312,
+ 324
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "Node name for S&R": "UNETLoader",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65,
+ "models": [
+ {
+ "name": "qwen_image_2512_fp8_e4m3fn.safetensors",
+ "url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_2512_fp8_e4m3fn.safetensors",
+ "directory": "diffusion_models"
+ },
+ {
+ "name": "qwen_image_2512_fp8_e4m3fn.safetensors",
+ "url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_2512_fp8_e4m3fn.safetensors",
+ "directory": "diffusion_models"
+ }
+ ]
+ },
+ "widgets_values": [
+ "qwen_image_2512_fp8_e4m3fn.safetensors",
+ "default"
+ ]
+ },
+ {
+ "id": 249,
+ "type": "CLIPTextEncode",
+ "pos": [
+ -200,
+ 1140
+ ],
+ "size": [
+ 360,
+ 420
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 314
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": 360
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 317
+ ]
+ }
+ ],
+ "title": "CLIP Text Encode (Positive Prompt)",
+ "properties": {
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "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": 250,
+ "type": "CLIPTextEncode",
+ "pos": [
+ -200,
+ 1610
+ ],
+ "size": [
+ 370,
+ 170
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 315
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 318
+ ]
+ }
+ ],
+ "title": "CLIP Text Encode (Negative Prompt)",
+ "properties": {
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "Node name for S&R": "CLIPTextEncode",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感。构图混乱。文字模糊,扭曲"
+ ],
+ "color": "#322",
+ "bgcolor": "#533"
+ },
+ {
+ "id": 251,
+ "type": "VAEDecode",
+ "pos": [
+ 1320,
+ 1120
+ ],
+ "size": [
+ 230,
+ 100
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 322
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 323
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "slot_index": 0,
+ "links": [
+ 333
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "Node name for S&R": "VAEDecode",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 252,
+ "type": "EmptySD3LatentImage",
+ "pos": [
+ -550,
+ 1930
+ ],
+ "size": [
+ 230,
+ 170
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 361
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 362
+ },
+ {
+ "localized_name": "batch_size",
+ "name": "batch_size",
+ "type": "INT",
+ "widget": {
+ "name": "batch_size"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "LATENT",
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 319
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "Node name for S&R": "EmptySD3LatentImage",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 1328,
+ 1328,
+ 1
+ ]
+ },
+ {
+ "id": 253,
+ "type": "KSampler",
+ "pos": [
+ 1040,
+ 1250
+ ],
+ "size": [
+ 250,
+ 350
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 316
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 317
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 318
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 319
+ },
+ {
+ "localized_name": "seed",
+ "name": "seed",
+ "type": "INT",
+ "widget": {
+ "name": "seed"
+ },
+ "link": 371
+ },
+ {
+ "localized_name": "steps",
+ "name": "steps",
+ "type": "INT",
+ "widget": {
+ "name": "steps"
+ },
+ "link": 368
+ },
+ {
+ "localized_name": "cfg",
+ "name": "cfg",
+ "type": "FLOAT",
+ "widget": {
+ "name": "cfg"
+ },
+ "link": 369
+ },
+ {
+ "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": [
+ 322
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "Node name for S&R": "KSampler",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 464857551335368,
+ "randomize",
+ 50,
+ 4,
+ "euler",
+ "simple",
+ 1
+ ]
+ },
+ {
+ "id": 254,
+ "type": "PrimitiveInt",
+ "pos": [
+ 300,
+ 1150
+ ],
+ "size": [
+ 230,
+ 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": [
+ 355
+ ]
+ }
+ ],
+ "title": "Int (Steps)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.12.3",
+ "Node name for S&R": "PrimitiveInt",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 50,
+ "fixed"
+ ]
+ },
+ {
+ "id": 255,
+ "type": "PrimitiveFloat",
+ "pos": [
+ 300,
+ 1290
+ ],
+ "size": [
+ 230,
+ 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": [
+ 357
+ ]
+ }
+ ],
+ "title": "Float (CFG)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.12.3",
+ "Node name for S&R": "PrimitiveFloat",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 4
+ ]
+ },
+ {
+ "id": 256,
+ "type": "PrimitiveBoolean",
+ "pos": [
+ 300,
+ 2060
+ ],
+ "size": [
+ 230,
+ 100
+ ],
+ "flags": {},
+ "order": 13,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "value"
+ },
+ "link": 370
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOOLEAN",
+ "name": "BOOLEAN",
+ "type": "BOOLEAN",
+ "links": [
+ 326,
+ 358,
+ 359
+ ]
+ }
+ ],
+ "title": "Enable 4 Steps LoRA?",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.12.3",
+ "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": 257,
+ "type": "PrimitiveInt",
+ "pos": [
+ 290,
+ 1540
+ ],
+ "size": [
+ 230,
+ 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": [
+ 347,
+ 354
+ ]
+ }
+ ],
+ "title": "Int (Steps)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.12.3",
+ "Node name for S&R": "PrimitiveInt",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 4,
+ "fixed"
+ ]
+ },
+ {
+ "id": 258,
+ "type": "PrimitiveFloat",
+ "pos": [
+ 290,
+ 1670
+ ],
+ "size": [
+ 230,
+ 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": [
+ 356
+ ]
+ }
+ ],
+ "title": "Float (CFG)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.12.3",
+ "Node name for S&R": "PrimitiveFloat",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 1
+ ]
+ },
+ {
+ "id": 259,
+ "type": "LoraLoaderModelOnly",
+ "pos": [
+ 240,
+ 1820
+ ],
+ "size": [
+ 330,
+ 140
+ ],
+ "flags": {},
+ "order": 14,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 312
+ },
+ {
+ "localized_name": "lora_name",
+ "name": "lora_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "lora_name"
+ },
+ "link": 375
+ },
+ {
+ "localized_name": "strength_model",
+ "name": "strength_model",
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength_model"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 325
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.49",
+ "Node name for S&R": "LoraLoaderModelOnly",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65,
+ "models": [
+ {
+ "name": "Qwen-Image-2512-Lightning-4steps-V1.0-fp32.safetensors",
+ "url": "https://huggingface.co/lightx2v/Qwen-Image-2512-Lightning/resolve/main/Qwen-Image-2512-Lightning-4steps-V1.0-fp32.safetensors",
+ "directory": "loras"
+ }
+ ]
+ },
+ "widgets_values": [
+ "Qwen-Image-2512-Lightning-4steps-V1.0-fp32.safetensors",
+ 1
+ ]
+ },
+ {
+ "id": 260,
+ "type": "ComfySwitchNode",
+ "pos": [
+ 710,
+ 1170
+ ],
+ "size": [
+ 230,
+ 130
+ ],
+ "flags": {},
+ "order": 15,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "on_false",
+ "name": "on_false",
+ "type": "*",
+ "link": 324
+ },
+ {
+ "localized_name": "on_true",
+ "name": "on_true",
+ "type": "*",
+ "link": 325
+ },
+ {
+ "localized_name": "switch",
+ "name": "switch",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "switch"
+ },
+ "link": 326
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "*",
+ "links": [
+ 367
+ ]
+ }
+ ],
+ "title": "Switch (model)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.12.3",
+ "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": 261,
+ "type": "ComfySwitchNode",
+ "pos": [
+ 710,
+ 1420
+ ],
+ "size": [
+ 230,
+ 130
+ ],
+ "flags": {},
+ "order": 16,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "on_false",
+ "name": "on_false",
+ "type": "*",
+ "link": 355
+ },
+ {
+ "localized_name": "on_true",
+ "name": "on_true",
+ "type": "*",
+ "link": 354
+ },
+ {
+ "localized_name": "switch",
+ "name": "switch",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "switch"
+ },
+ "link": 359
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "*",
+ "links": [
+ 368
+ ]
+ }
+ ],
+ "title": "Switch (steps)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.12.3",
+ "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": 262,
+ "type": "ComfySwitchNode",
+ "pos": [
+ 710,
+ 1660
+ ],
+ "size": [
+ 230,
+ 130
+ ],
+ "flags": {},
+ "order": 17,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "on_false",
+ "name": "on_false",
+ "type": "*",
+ "link": 357
+ },
+ {
+ "localized_name": "on_true",
+ "name": "on_true",
+ "type": "*",
+ "link": 356
+ },
+ {
+ "localized_name": "switch",
+ "name": "switch",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "switch"
+ },
+ "link": 358
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "*",
+ "links": [
+ 369
+ ]
+ }
+ ],
+ "title": "Switch (cfg)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.12.3",
+ "Node name for S&R": "ComfySwitchNode",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ false
+ ]
+ }
+ ],
+ "groups": [
+ {
+ "id": 1,
+ "title": "Model",
+ "bounding": [
+ -640,
+ 1060,
+ 390,
+ 740
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 2,
+ "title": "Image size",
+ "bounding": [
+ -630,
+ 1830,
+ 380,
+ 290
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 3,
+ "title": "Prompt",
+ "bounding": [
+ -220,
+ 1060,
+ 400,
+ 740
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 5,
+ "title": "4-steps LoRA",
+ "bounding": [
+ 210,
+ 1460,
+ 410,
+ 550
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 6,
+ "title": "Original Settings",
+ "bounding": [
+ 210,
+ 1060,
+ 420,
+ 370
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 7,
+ "title": "Swtich",
+ "bounding": [
+ 660,
+ 1060,
+ 320,
+ 750
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 312,
+ "origin_id": 248,
+ "origin_slot": 0,
+ "target_id": 259,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 314,
+ "origin_id": 245,
+ "origin_slot": 0,
+ "target_id": 249,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 315,
+ "origin_id": 245,
+ "origin_slot": 0,
+ "target_id": 250,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 322,
+ "origin_id": 253,
+ "origin_slot": 0,
+ "target_id": 251,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 323,
+ "origin_id": 246,
+ "origin_slot": 0,
+ "target_id": 251,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 316,
+ "origin_id": 247,
+ "origin_slot": 0,
+ "target_id": 253,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 317,
+ "origin_id": 249,
+ "origin_slot": 0,
+ "target_id": 253,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 318,
+ "origin_id": 250,
+ "origin_slot": 0,
+ "target_id": 253,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 319,
+ "origin_id": 252,
+ "origin_slot": 0,
+ "target_id": 253,
+ "target_slot": 3,
+ "type": "LATENT"
+ },
+ {
+ "id": 324,
+ "origin_id": 248,
+ "origin_slot": 0,
+ "target_id": 260,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 325,
+ "origin_id": 259,
+ "origin_slot": 0,
+ "target_id": 260,
+ "target_slot": 1,
+ "type": "MODEL"
+ },
+ {
+ "id": 326,
+ "origin_id": 256,
+ "origin_slot": 0,
+ "target_id": 260,
+ "target_slot": 2,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 333,
+ "origin_id": 251,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 347,
+ "origin_id": 257,
+ "origin_slot": 0,
+ "target_id": 253,
+ "target_slot": 4,
+ "type": "INT"
+ },
+ {
+ "id": 354,
+ "origin_id": 257,
+ "origin_slot": 0,
+ "target_id": 261,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 355,
+ "origin_id": 254,
+ "origin_slot": 0,
+ "target_id": 261,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 356,
+ "origin_id": 258,
+ "origin_slot": 0,
+ "target_id": 262,
+ "target_slot": 1,
+ "type": "FLOAT"
+ },
+ {
+ "id": 357,
+ "origin_id": 255,
+ "origin_slot": 0,
+ "target_id": 262,
+ "target_slot": 0,
+ "type": "FLOAT"
+ },
+ {
+ "id": 358,
+ "origin_id": 256,
+ "origin_slot": 0,
+ "target_id": 262,
+ "target_slot": 2,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 359,
+ "origin_id": 256,
+ "origin_slot": 0,
+ "target_id": 261,
+ "target_slot": 2,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 360,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 249,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 361,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 252,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 362,
+ "origin_id": -10,
+ "origin_slot": 2,
+ "target_id": 252,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 367,
+ "origin_id": 260,
+ "origin_slot": 0,
+ "target_id": 247,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 368,
+ "origin_id": 261,
+ "origin_slot": 0,
+ "target_id": 253,
+ "target_slot": 5,
+ "type": "INT"
+ },
+ {
+ "id": 369,
+ "origin_id": 262,
+ "origin_slot": 0,
+ "target_id": 253,
+ "target_slot": 6,
+ "type": "FLOAT"
+ },
+ {
+ "id": 370,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 256,
+ "target_slot": 0,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 371,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 253,
+ "target_slot": 4,
+ "type": "INT"
+ },
+ {
+ "id": 372,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 248,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 373,
+ "origin_id": -10,
+ "origin_slot": 6,
+ "target_id": 245,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 374,
+ "origin_id": -10,
+ "origin_slot": 7,
+ "target_id": 246,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 375,
+ "origin_id": -10,
+ "origin_slot": 8,
+ "target_id": 259,
+ "target_slot": 1,
+ "type": "COMBO"
+ }
+ ],
+ "extra": {
+ "workflowRendererVersion": "Vue-corrected"
+ },
+ "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."
+ }
+ ]
+ },
+ "extra": {
+ "ue_links": []
+ }
+}
\ No newline at end of file
diff --git a/blueprints/Text to Image (Qwen-Image).json b/blueprints/Text to Image (Qwen-Image).json
new file mode 100644
index 000000000..e78d5a962
--- /dev/null
+++ b/blueprints/Text to Image (Qwen-Image).json
@@ -0,0 +1,1882 @@
+{
+ "revision": 0,
+ "last_node_id": 76,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 76,
+ "type": "e5cfe5ba-2ae0-4bc4-869f-ab2228cb44d3",
+ "pos": [
+ 30,
+ 10
+ ],
+ "size": [
+ 470,
+ 660
+ ],
+ "flags": {},
+ "order": 2,
+ "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": "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": "lightning_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
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": []
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [
+ [
+ "6",
+ "text"
+ ],
+ [
+ "58",
+ "width"
+ ],
+ [
+ "58",
+ "height"
+ ],
+ [
+ "3",
+ "seed"
+ ],
+ [
+ "37",
+ "unet_name"
+ ],
+ [
+ "38",
+ "clip_name"
+ ],
+ [
+ "39",
+ "vae_name"
+ ],
+ [
+ "73",
+ "lora_name"
+ ],
+ [
+ "86",
+ "value"
+ ],
+ [
+ "3",
+ "control_after_generate"
+ ]
+ ],
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "ue_properties": {
+ "widget_ue_connectable": {
+ "text": true,
+ "lora_name": true,
+ "value": true
+ },
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ }
+ },
+ "widgets_values": [],
+ "title": "Text to Image (Qwen-Image)"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "e5cfe5ba-2ae0-4bc4-869f-ab2228cb44d3",
+ "version": 1,
+ "state": {
+ "lastGroupId": 5,
+ "lastNodeId": 87,
+ "lastLinkId": 153,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Text to Image (Qwen-Image)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ -810,
+ 290,
+ 151.744140625,
+ 220
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 2580,
+ 340,
+ 120,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "846fd1a5-9f4a-4e83-af40-27cafe99e5c6",
+ "name": "text",
+ "type": "STRING",
+ "linkIds": [
+ 132
+ ],
+ "label": "prompt",
+ "pos": [
+ -678.255859375,
+ 310
+ ]
+ },
+ {
+ "id": "e941d29f-bb7f-4001-a956-90a9b29ae9f9",
+ "name": "width",
+ "type": "INT",
+ "linkIds": [
+ 134
+ ],
+ "pos": [
+ -678.255859375,
+ 330
+ ]
+ },
+ {
+ "id": "df798f50-87ba-481b-b847-ca8b7c7efff3",
+ "name": "height",
+ "type": "INT",
+ "linkIds": [
+ 135
+ ],
+ "pos": [
+ -678.255859375,
+ 350
+ ]
+ },
+ {
+ "id": "3fcf7667-f697-43ee-bdee-0d3fed39e777",
+ "name": "seed",
+ "type": "INT",
+ "linkIds": [
+ 136
+ ],
+ "pos": [
+ -678.255859375,
+ 370
+ ]
+ },
+ {
+ "id": "e8d70f26-d9f5-4633-a39e-0bf6cf93d566",
+ "name": "unet_name",
+ "type": "COMBO",
+ "linkIds": [
+ 137
+ ],
+ "pos": [
+ -678.255859375,
+ 390
+ ]
+ },
+ {
+ "id": "8c9b537a-c6c9-4365-96ad-dbbb82d917e0",
+ "name": "clip_name",
+ "type": "COMBO",
+ "linkIds": [
+ 138
+ ],
+ "pos": [
+ -678.255859375,
+ 410
+ ]
+ },
+ {
+ "id": "7cc2f92b-6e2f-4e4e-a316-b61f58ed1442",
+ "name": "vae_name",
+ "type": "COMBO",
+ "linkIds": [
+ 139
+ ],
+ "pos": [
+ -678.255859375,
+ 430
+ ]
+ },
+ {
+ "id": "3cb1ba7c-583c-4f92-afc1-71463161e2a4",
+ "name": "lora_name",
+ "type": "COMBO",
+ "linkIds": [
+ 140
+ ],
+ "label": "lightning_lora",
+ "pos": [
+ -678.255859375,
+ 450
+ ]
+ },
+ {
+ "id": "4278102d-766c-4c6b-af2e-0fb9f26bbb27",
+ "name": "value",
+ "type": "BOOLEAN",
+ "linkIds": [
+ 153
+ ],
+ "label": "enable_turbo_mode",
+ "pos": [
+ -678.255859375,
+ 470
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "2af20250-dc7a-4643-bc84-0a180d9ca62b",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "linkIds": [
+ 110
+ ],
+ "localized_name": "IMAGE",
+ "pos": [
+ 2600,
+ 360
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 39,
+ "type": "VAELoader",
+ "pos": [
+ -260,
+ 510
+ ],
+ "size": [
+ 330,
+ 110
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "vae_name",
+ "name": "vae_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "vae_name"
+ },
+ "link": 139
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "VAE",
+ "name": "VAE",
+ "type": "VAE",
+ "slot_index": 0,
+ "links": [
+ 76
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "VAELoader",
+ "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
+ },
+ "widgets_values": [
+ "qwen_image_vae.safetensors"
+ ]
+ },
+ {
+ "id": 38,
+ "type": "CLIPLoader",
+ "pos": [
+ -260,
+ 280
+ ],
+ "size": [
+ 330,
+ 150
+ ],
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip_name",
+ "name": "clip_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "clip_name"
+ },
+ "link": 138
+ },
+ {
+ "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": [
+ 74,
+ 75
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "CLIPLoader",
+ "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
+ },
+ "widgets_values": [
+ "qwen_2.5_vl_7b_fp8_scaled.safetensors",
+ "qwen_image",
+ "default"
+ ]
+ },
+ {
+ "id": 58,
+ "type": "EmptySD3LatentImage",
+ "pos": [
+ -240,
+ 810
+ ],
+ "size": [
+ 270,
+ 170
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 134
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 135
+ },
+ {
+ "localized_name": "batch_size",
+ "name": "batch_size",
+ "type": "INT",
+ "widget": {
+ "name": "batch_size"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "LATENT",
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 107
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "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": [
+ 1328,
+ 1328,
+ 1
+ ]
+ },
+ {
+ "id": 66,
+ "type": "ModelSamplingAuraFlow",
+ "pos": [
+ 1780,
+ 180
+ ],
+ "size": [
+ 300,
+ 110
+ ],
+ "flags": {},
+ "order": 13,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 147
+ },
+ {
+ "localized_name": "shift",
+ "name": "shift",
+ "type": "FLOAT",
+ "widget": {
+ "name": "shift"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 125
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "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.1000000000000005
+ ]
+ },
+ {
+ "id": 37,
+ "type": "UNETLoader",
+ "pos": [
+ -260,
+ 80
+ ],
+ "size": [
+ 330,
+ 110
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "unet_name",
+ "name": "unet_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "unet_name"
+ },
+ "link": 137
+ },
+ {
+ "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": [
+ 129,
+ 142
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "UNETLoader",
+ "models": [
+ {
+ "name": "qwen_image_fp8_e4m3fn.safetensors",
+ "url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_fp8_e4m3fn.safetensors",
+ "directory": "diffusion_models"
+ }
+ ],
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "qwen_image_fp8_e4m3fn.safetensors",
+ "default"
+ ]
+ },
+ {
+ "id": 6,
+ "type": "CLIPTextEncode",
+ "pos": [
+ 120,
+ 60
+ ],
+ "size": [
+ 440,
+ 340
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 74
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": 132
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 46
+ ]
+ }
+ ],
+ "title": "CLIP Text Encode (Positive Prompt)",
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "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": 7,
+ "type": "CLIPTextEncode",
+ "pos": [
+ 130,
+ 480
+ ],
+ "size": [
+ 430,
+ 180
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 75
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "slot_index": 0,
+ "links": [
+ 52
+ ]
+ }
+ ],
+ "title": "CLIP Text Encode (Negative Prompt)",
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "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": "#322",
+ "bgcolor": "#533"
+ },
+ {
+ "id": 8,
+ "type": "VAEDecode",
+ "pos": [
+ 2190,
+ 350
+ ],
+ "size": [
+ 230,
+ 100
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 8,
+ "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": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "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": 73,
+ "type": "LoraLoaderModelOnly",
+ "pos": [
+ 670,
+ 500
+ ],
+ "size": [
+ 400,
+ 140
+ ],
+ "flags": {},
+ "order": 14,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 129
+ },
+ {
+ "localized_name": "lora_name",
+ "name": "lora_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "lora_name"
+ },
+ "link": 140
+ },
+ {
+ "localized_name": "strength_model",
+ "name": "strength_model",
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength_model"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 141
+ ]
+ }
+ ],
+ "properties": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.49",
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "Node name for S&R": "LoraLoaderModelOnly",
+ "models": [
+ {
+ "name": "Qwen-Image-Lightning-8steps-V1.0.safetensors",
+ "url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V1.0.safetensors",
+ "directory": "loras"
+ }
+ ]
+ },
+ "widgets_values": [
+ "Qwen-Image-Lightning-8steps-V1.0.safetensors",
+ 1
+ ]
+ },
+ {
+ "id": 3,
+ "type": "KSampler",
+ "pos": [
+ 1780,
+ 330
+ ],
+ "size": [
+ 300,
+ 480
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 125
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 46
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 52
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 107
+ },
+ {
+ "localized_name": "seed",
+ "name": "seed",
+ "type": "INT",
+ "widget": {
+ "name": "seed"
+ },
+ "link": 136
+ },
+ {
+ "localized_name": "steps",
+ "name": "steps",
+ "type": "INT",
+ "widget": {
+ "name": "steps"
+ },
+ "link": 148
+ },
+ {
+ "localized_name": "cfg",
+ "name": "cfg",
+ "type": "FLOAT",
+ "widget": {
+ "name": "cfg"
+ },
+ "link": 149
+ },
+ {
+ "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": {
+ "cnr_id": "comfy-core",
+ "ver": "0.3.48",
+ "ue_properties": {
+ "version": "7.7",
+ "widget_ue_connectable": {},
+ "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": [
+ 50347169638278,
+ "randomize",
+ 8,
+ 1,
+ "euler",
+ "simple",
+ 1
+ ]
+ },
+ {
+ "id": 78,
+ "type": "ComfySwitchNode",
+ "pos": [
+ 1320,
+ 180
+ ],
+ "size": [
+ 270,
+ 130
+ ],
+ "flags": {},
+ "order": 15,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "on_false",
+ "name": "on_false",
+ "type": "*",
+ "link": 142
+ },
+ {
+ "localized_name": "on_true",
+ "name": "on_true",
+ "type": "*",
+ "link": 141
+ },
+ {
+ "localized_name": "switch",
+ "name": "switch",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "switch"
+ },
+ "link": 150
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "*",
+ "links": [
+ 147
+ ]
+ }
+ ],
+ "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"
+ },
+ "widgets_values": [
+ false
+ ]
+ },
+ {
+ "id": 79,
+ "type": "PrimitiveInt",
+ "pos": [
+ 680,
+ 710
+ ],
+ "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": [
+ 143
+ ]
+ }
+ ],
+ "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"
+ },
+ "widgets_values": [
+ 8,
+ "fixed"
+ ]
+ },
+ {
+ "id": 81,
+ "type": "PrimitiveFloat",
+ "pos": [
+ 680,
+ 870
+ ],
+ "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": [
+ 144
+ ]
+ }
+ ],
+ "title": "CFG",
+ "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": "PrimitiveFloat"
+ },
+ "widgets_values": [
+ 1
+ ]
+ },
+ {
+ "id": 82,
+ "type": "ComfySwitchNode",
+ "pos": [
+ 1320,
+ 400
+ ],
+ "size": [
+ 270,
+ 130
+ ],
+ "flags": {},
+ "order": 16,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "on_false",
+ "name": "on_false",
+ "type": "*",
+ "link": 146
+ },
+ {
+ "localized_name": "on_true",
+ "name": "on_true",
+ "type": "*",
+ "link": 143
+ },
+ {
+ "localized_name": "switch",
+ "name": "switch",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "switch"
+ },
+ "link": 151
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "*",
+ "links": [
+ 148
+ ]
+ }
+ ],
+ "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"
+ },
+ "widgets_values": [
+ false
+ ]
+ },
+ {
+ "id": 83,
+ "type": "ComfySwitchNode",
+ "pos": [
+ 1320,
+ 600
+ ],
+ "size": [
+ 270,
+ 130
+ ],
+ "flags": {},
+ "order": 17,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "on_false",
+ "name": "on_false",
+ "type": "*",
+ "link": 145
+ },
+ {
+ "localized_name": "on_true",
+ "name": "on_true",
+ "type": "*",
+ "link": 144
+ },
+ {
+ "localized_name": "switch",
+ "name": "switch",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "switch"
+ },
+ "link": 152
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "*",
+ "links": [
+ 149
+ ]
+ }
+ ],
+ "title": "Switch (CFG)",
+ "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"
+ },
+ "widgets_values": [
+ false
+ ]
+ },
+ {
+ "id": 84,
+ "type": "PrimitiveInt",
+ "pos": [
+ 680,
+ 60
+ ],
+ "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": [
+ 146
+ ]
+ }
+ ],
+ "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"
+ },
+ "widgets_values": [
+ 20,
+ "fixed"
+ ]
+ },
+ {
+ "id": 85,
+ "type": "PrimitiveFloat",
+ "pos": [
+ 680,
+ 230
+ ],
+ "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": [
+ 145
+ ]
+ }
+ ],
+ "title": "CFG",
+ "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": "PrimitiveFloat"
+ },
+ "widgets_values": [
+ 4
+ ]
+ },
+ {
+ "id": 86,
+ "type": "PrimitiveBoolean",
+ "pos": [
+ 710,
+ 1070
+ ],
+ "size": [
+ 270,
+ 100
+ ],
+ "flags": {},
+ "order": 18,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "value"
+ },
+ "link": 153
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOOLEAN",
+ "name": "BOOLEAN",
+ "type": "BOOLEAN",
+ "links": [
+ 150,
+ 151,
+ 152
+ ]
+ }
+ ],
+ "title": "Enable Lightning 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"
+ },
+ "widgets_values": [
+ false
+ ]
+ },
+ {
+ "id": 87,
+ "type": "MarkdownNote",
+ "pos": [
+ 620,
+ -160
+ ],
+ "size": [
+ 500,
+ 120
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ }
+ },
+ "widgets_values": [
+ "Try 50 steps, if you want original the [qwen image](https://huggingface.co/Qwen/Qwen-Image)'s setting, but it will takes longer"
+ ],
+ "color": "#222",
+ "bgcolor": "#000"
+ }
+ ],
+ "groups": [
+ {
+ "id": 1,
+ "title": "Step1 - Load models",
+ "bounding": [
+ -280,
+ -20,
+ 360,
+ 700
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 2,
+ "title": "Step2 - Image size",
+ "bounding": [
+ -280,
+ 710,
+ 360,
+ 300
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 3,
+ "title": "Step3 - Prompt",
+ "bounding": [
+ 110,
+ -20,
+ 470,
+ 700
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 4,
+ "title": "Lightx2v 8steps LoRA",
+ "bounding": [
+ 610,
+ 390,
+ 520,
+ 620
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 5,
+ "title": "Original Settings",
+ "bounding": [
+ 610,
+ -20,
+ 520,
+ 380
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 74,
+ "origin_id": 38,
+ "origin_slot": 0,
+ "target_id": 6,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 75,
+ "origin_id": 38,
+ "origin_slot": 0,
+ "target_id": 7,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 129,
+ "origin_id": 37,
+ "origin_slot": 0,
+ "target_id": 73,
+ "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": 125,
+ "origin_id": 66,
+ "origin_slot": 0,
+ "target_id": 3,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 46,
+ "origin_id": 6,
+ "origin_slot": 0,
+ "target_id": 3,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 52,
+ "origin_id": 7,
+ "origin_slot": 0,
+ "target_id": 3,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 107,
+ "origin_id": 58,
+ "origin_slot": 0,
+ "target_id": 3,
+ "target_slot": 3,
+ "type": "LATENT"
+ },
+ {
+ "id": 110,
+ "origin_id": 8,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 132,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 6,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 134,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 58,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 135,
+ "origin_id": -10,
+ "origin_slot": 2,
+ "target_id": 58,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 136,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 3,
+ "target_slot": 4,
+ "type": "INT"
+ },
+ {
+ "id": 137,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 37,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 138,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 38,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 139,
+ "origin_id": -10,
+ "origin_slot": 6,
+ "target_id": 39,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 140,
+ "origin_id": -10,
+ "origin_slot": 7,
+ "target_id": 73,
+ "target_slot": 1,
+ "type": "COMBO"
+ },
+ {
+ "id": 141,
+ "origin_id": 73,
+ "origin_slot": 0,
+ "target_id": 78,
+ "target_slot": 1,
+ "type": "MODEL"
+ },
+ {
+ "id": 142,
+ "origin_id": 37,
+ "origin_slot": 0,
+ "target_id": 78,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 143,
+ "origin_id": 79,
+ "origin_slot": 0,
+ "target_id": 82,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 144,
+ "origin_id": 81,
+ "origin_slot": 0,
+ "target_id": 83,
+ "target_slot": 1,
+ "type": "FLOAT"
+ },
+ {
+ "id": 145,
+ "origin_id": 85,
+ "origin_slot": 0,
+ "target_id": 83,
+ "target_slot": 0,
+ "type": "FLOAT"
+ },
+ {
+ "id": 146,
+ "origin_id": 84,
+ "origin_slot": 0,
+ "target_id": 82,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 147,
+ "origin_id": 78,
+ "origin_slot": 0,
+ "target_id": 66,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 148,
+ "origin_id": 82,
+ "origin_slot": 0,
+ "target_id": 3,
+ "target_slot": 5,
+ "type": "INT"
+ },
+ {
+ "id": 149,
+ "origin_id": 83,
+ "origin_slot": 0,
+ "target_id": 3,
+ "target_slot": 6,
+ "type": "FLOAT"
+ },
+ {
+ "id": 150,
+ "origin_id": 86,
+ "origin_slot": 0,
+ "target_id": 78,
+ "target_slot": 2,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 151,
+ "origin_id": 86,
+ "origin_slot": 0,
+ "target_id": 82,
+ "target_slot": 2,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 152,
+ "origin_id": 86,
+ "origin_slot": 0,
+ "target_id": 83,
+ "target_slot": 2,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 153,
+ "origin_id": -10,
+ "origin_slot": 8,
+ "target_id": 86,
+ "target_slot": 0,
+ "type": "BOOLEAN"
+ }
+ ],
+ "extra": {
+ "workflowRendererVersion": "LG"
+ },
+ "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."
+ }
+ ]
+ },
+ "extra": {}
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..f44a216dd
--- /dev/null
+++ b/blueprints/Text to Video (LTX-2.3).json
@@ -0,0 +1,4297 @@
+{
+ "revision": 0,
+ "last_node_id": 324,
+ "last_link_id": 0,
+ "nodes": [
+ {
+ "id": 324,
+ "type": "871cf29d-2726-43a5-b61e-01fa939d699d",
+ "pos": [
+ -300,
+ 4290
+ ],
+ "size": [
+ 400,
+ 170
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "value",
+ "type": "STRING",
+ "widget": {
+ "name": "value"
+ },
+ "link": null
+ },
+ {
+ "label": "width",
+ "name": "value_2",
+ "type": "INT",
+ "widget": {
+ "name": "value_2"
+ },
+ "link": null
+ },
+ {
+ "label": "height",
+ "name": "value_3",
+ "type": "INT",
+ "widget": {
+ "name": "value_3"
+ },
+ "link": null
+ },
+ {
+ "label": "duration",
+ "name": "value_4",
+ "type": "INT",
+ "widget": {
+ "name": "value_4"
+ },
+ "link": null
+ },
+ {
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "ckpt_name"
+ },
+ "link": null
+ },
+ {
+ "label": "distilled_lora",
+ "name": "lora_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "lora_name"
+ },
+ "link": null
+ },
+ {
+ "name": "text_encoder",
+ "type": "COMBO",
+ "widget": {
+ "name": "text_encoder"
+ },
+ "link": null
+ },
+ {
+ "label": "latent_upscale_model",
+ "name": "model_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "model_name"
+ },
+ "link": null
+ },
+ {
+ "label": "fps",
+ "name": "value_1",
+ "type": "INT",
+ "widget": {
+ "name": "value_1"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "VIDEO",
+ "name": "VIDEO",
+ "type": "VIDEO",
+ "links": []
+ }
+ ],
+ "properties": {
+ "proxyWidgets": [
+ [
+ "320",
+ "value"
+ ],
+ [
+ "314",
+ "value"
+ ],
+ [
+ "301",
+ "value"
+ ],
+ [
+ "303",
+ "value"
+ ],
+ [
+ "318",
+ "ckpt_name"
+ ],
+ [
+ "287",
+ "lora_name"
+ ],
+ [
+ "319",
+ "text_encoder"
+ ],
+ [
+ "313",
+ "model_name"
+ ],
+ [
+ "302",
+ "value"
+ ],
+ [
+ "279",
+ "noise_seed"
+ ],
+ [
+ "279",
+ "control_after_generate"
+ ]
+ ],
+ "ue_properties": {
+ "widget_ue_connectable": {
+ "value_1": true,
+ "value_2": true,
+ "value_3": true,
+ "value_4": true,
+ "lora_name": true,
+ "model_name": true
+ },
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [],
+ "title": "Text to Video (LTX-2.3)"
+ }
+ ],
+ "links": [],
+ "version": 0.4,
+ "definitions": {
+ "subgraphs": [
+ {
+ "id": "871cf29d-2726-43a5-b61e-01fa939d699d",
+ "version": 1,
+ "state": {
+ "lastGroupId": 26,
+ "lastNodeId": 324,
+ "lastLinkId": 631,
+ "lastRerouteId": 0
+ },
+ "revision": 0,
+ "config": {},
+ "name": "Text to Video (LTX-2.3)",
+ "inputNode": {
+ "id": -10,
+ "bounding": [
+ 720,
+ 4240,
+ 162.162109375,
+ 220
+ ]
+ },
+ "outputNode": {
+ "id": -20,
+ "bounding": [
+ 6100,
+ 4160,
+ 120,
+ 60
+ ]
+ },
+ "inputs": [
+ {
+ "id": "9494c550-4172-49c6-930e-5b508f775e77",
+ "name": "value",
+ "type": "STRING",
+ "linkIds": [
+ 595
+ ],
+ "pos": [
+ 862.162109375,
+ 4260
+ ]
+ },
+ {
+ "id": "58dbb3f6-f924-4548-96ef-e0e34610bd4e",
+ "name": "value_2",
+ "type": "INT",
+ "linkIds": [
+ 597
+ ],
+ "label": "width",
+ "pos": [
+ 862.162109375,
+ 4280
+ ]
+ },
+ {
+ "id": "6086d5b8-2586-448c-a641-dd14d76dd102",
+ "name": "value_3",
+ "type": "INT",
+ "linkIds": [
+ 598
+ ],
+ "label": "height",
+ "pos": [
+ 862.162109375,
+ 4300
+ ]
+ },
+ {
+ "id": "feb8c2eb-ae48-4fa8-bc24-929552d656c3",
+ "name": "value_4",
+ "type": "INT",
+ "linkIds": [
+ 599
+ ],
+ "label": "duration",
+ "pos": [
+ 862.162109375,
+ 4320
+ ]
+ },
+ {
+ "id": "d7255058-319a-4880-8f9a-7e542c8f3c3c",
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "linkIds": [
+ 601,
+ 604,
+ 605
+ ],
+ "pos": [
+ 862.162109375,
+ 4340
+ ]
+ },
+ {
+ "id": "4afce68d-8f65-4342-9d6d-ae0a7688c3e3",
+ "name": "lora_name",
+ "type": "COMBO",
+ "linkIds": [
+ 602
+ ],
+ "label": "distilled_lora",
+ "pos": [
+ 862.162109375,
+ 4360
+ ]
+ },
+ {
+ "id": "ab842b4b-c977-4679-b421-424722785b57",
+ "name": "text_encoder",
+ "type": "COMBO",
+ "linkIds": [
+ 606
+ ],
+ "pos": [
+ 862.162109375,
+ 4380
+ ]
+ },
+ {
+ "id": "9e47372d-28d9-4311-91e9-e90d03f4eb43",
+ "name": "model_name",
+ "type": "COMBO",
+ "linkIds": [
+ 607
+ ],
+ "label": "latent_upscale_model",
+ "pos": [
+ 862.162109375,
+ 4400
+ ]
+ },
+ {
+ "id": "7951b137-465e-4844-b05f-88b89f0e1ba8",
+ "name": "value_1",
+ "type": "INT",
+ "linkIds": [
+ 627
+ ],
+ "label": "fps",
+ "pos": [
+ 862.162109375,
+ 4420
+ ]
+ }
+ ],
+ "outputs": [
+ {
+ "id": "954ef307-c897-4eea-8b5c-5c6ce15a5357",
+ "name": "VIDEO",
+ "type": "VIDEO",
+ "linkIds": [
+ 536
+ ],
+ "localized_name": "VIDEO",
+ "pos": [
+ 6120,
+ 4180
+ ]
+ }
+ ],
+ "widgets": [],
+ "nodes": [
+ {
+ "id": 278,
+ "type": "RandomNoise",
+ "pos": [
+ 4720,
+ 3750
+ ],
+ "size": [
+ 280,
+ 110
+ ],
+ "flags": {},
+ "order": 0,
+ "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": [
+ 490
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.75",
+ "Node name for S&R": "RandomNoise",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 42,
+ "fixed"
+ ]
+ },
+ {
+ "id": 279,
+ "type": "RandomNoise",
+ "pos": [
+ 3200,
+ 3900
+ ],
+ "size": [
+ 280,
+ 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": [
+ 483
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "Node name for S&R": "RandomNoise",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 343011291748534,
+ "randomize"
+ ]
+ },
+ {
+ "id": 280,
+ "type": "LTXVConcatAVLatent",
+ "pos": [
+ 4730,
+ 4520
+ ],
+ "size": [
+ 280,
+ 100
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "video_latent",
+ "name": "video_latent",
+ "type": "LATENT",
+ "link": 512
+ },
+ {
+ "localized_name": "audio_latent",
+ "name": "audio_latent",
+ "type": "LATENT",
+ "link": 513
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "links": [
+ 494
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "Node name for S&R": "LTXVConcatAVLatent",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 281,
+ "type": "LTXVAudioVAELoader",
+ "pos": [
+ 1660,
+ 4140
+ ],
+ "size": [
+ 430,
+ 110
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "ckpt_name",
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "ckpt_name"
+ },
+ "link": 604
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "Audio VAE",
+ "name": "Audio VAE",
+ "type": "VAE",
+ "links": [
+ 481,
+ 496
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.68",
+ "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-dev-fp8.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-dev-fp8.safetensors",
+ "directory": "checkpoints"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ltx-2.3-22b-dev-fp8.safetensors"
+ ]
+ },
+ {
+ "id": 282,
+ "type": "KSamplerSelect",
+ "pos": [
+ 4720,
+ 4160
+ ],
+ "size": [
+ 280,
+ 110
+ ],
+ "flags": {},
+ "order": 2,
+ "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": [
+ 492
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.75",
+ "Node name for S&R": "KSamplerSelect",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "euler_cfg_pp"
+ ]
+ },
+ {
+ "id": 283,
+ "type": "ManualSigmas",
+ "pos": [
+ 4720,
+ 4340
+ ],
+ "size": [
+ 280,
+ 110
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "sigmas",
+ "name": "sigmas",
+ "type": "STRING",
+ "widget": {
+ "name": "sigmas"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "SIGMAS",
+ "name": "SIGMAS",
+ "type": "SIGMAS",
+ "links": [
+ 493
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "Node name for S&R": "ManualSigmas",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "0.85, 0.7250, 0.4219, 0.0"
+ ]
+ },
+ {
+ "id": 284,
+ "type": "CFGGuider",
+ "pos": [
+ 4720,
+ 3930
+ ],
+ "size": [
+ 280,
+ 160
+ ],
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 478
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 479
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 480
+ },
+ {
+ "localized_name": "cfg",
+ "name": "cfg",
+ "type": "FLOAT",
+ "widget": {
+ "name": "cfg"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "GUIDER",
+ "name": "GUIDER",
+ "type": "GUIDER",
+ "links": [
+ 491
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.71",
+ "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": 285,
+ "type": "SamplerCustomAdvanced",
+ "pos": [
+ 3620,
+ 3990
+ ],
+ "size": [
+ 230,
+ 170
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "noise",
+ "name": "noise",
+ "type": "NOISE",
+ "link": 483
+ },
+ {
+ "localized_name": "guider",
+ "name": "guider",
+ "type": "GUIDER",
+ "link": 484
+ },
+ {
+ "localized_name": "sampler",
+ "name": "sampler",
+ "type": "SAMPLER",
+ "link": 485
+ },
+ {
+ "localized_name": "sigmas",
+ "name": "sigmas",
+ "type": "SIGMAS",
+ "link": 544
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 487
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "LATENT",
+ "links": [
+ 488
+ ]
+ },
+ {
+ "localized_name": "denoised_output",
+ "name": "denoised_output",
+ "type": "LATENT",
+ "links": []
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.60",
+ "Node name for S&R": "SamplerCustomAdvanced",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 286,
+ "type": "LTXVCropGuides",
+ "pos": [
+ 3900,
+ 3700
+ ],
+ "size": [
+ 250,
+ 120
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 475
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 476
+ },
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "link": 477
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": [
+ 479
+ ]
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": [
+ 480
+ ]
+ },
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "slot_index": 2,
+ "links": []
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.68",
+ "Node name for S&R": "LTXVCropGuides",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 287,
+ "type": "LoraLoaderModelOnly",
+ "pos": [
+ 1660,
+ 3910
+ ],
+ "size": [
+ 430,
+ 140
+ ],
+ "flags": {},
+ "order": 13,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 520
+ },
+ {
+ "localized_name": "lora_name",
+ "name": "lora_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "lora_name"
+ },
+ "link": 602
+ },
+ {
+ "localized_name": "strength_model",
+ "name": "strength_model",
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength_model"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 478,
+ 541
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.75",
+ "Node name for S&R": "LoraLoaderModelOnly",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65,
+ "models": [
+ {
+ "name": "ltx-2.3-22b-distilled-lora-384.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-distilled-lora-384.safetensors",
+ "directory": "loras"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ltx-2.3-22b-distilled-lora-384.safetensors",
+ 0.5
+ ]
+ },
+ {
+ "id": 288,
+ "type": "ResizeImagesByLongerEdge",
+ "pos": [
+ 2120,
+ 5040
+ ],
+ "size": [
+ 310,
+ 110
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 14,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "images",
+ "name": "images",
+ "type": "IMAGE",
+ "link": 523
+ },
+ {
+ "localized_name": "longer_edge",
+ "name": "longer_edge",
+ "type": "INT",
+ "widget": {
+ "name": "longer_edge"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "images",
+ "name": "images",
+ "type": "IMAGE",
+ "links": [
+ 505
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "Node name for S&R": "ResizeImagesByLongerEdge",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 1536
+ ]
+ },
+ {
+ "id": 289,
+ "type": "LTXVLatentUpsampler",
+ "pos": [
+ 4270,
+ 3910
+ ],
+ "size": [
+ 330,
+ 120
+ ],
+ "flags": {},
+ "order": 15,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 547
+ },
+ {
+ "localized_name": "upscale_model",
+ "name": "upscale_model",
+ "type": "LATENT_UPSCALE_MODEL",
+ "link": 545
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 554
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "LATENT",
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 548
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "Node name for S&R": "LTXVLatentUpsampler",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 290,
+ "type": "LTXVImgToVideoInplace",
+ "pos": [
+ 4280,
+ 4150
+ ],
+ "size": [
+ 300,
+ 180
+ ],
+ "flags": {},
+ "order": 16,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 552
+ },
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 515
+ },
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "link": 548
+ },
+ {
+ "localized_name": "strength",
+ "name": "strength",
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "bypass",
+ "name": "bypass",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "bypass"
+ },
+ "link": 543
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "links": [
+ 512
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXVImgToVideoInplace",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 1,
+ false
+ ]
+ },
+ {
+ "id": 291,
+ "type": "LTXVPreprocess",
+ "pos": [
+ 2130,
+ 5190
+ ],
+ "size": [
+ 290,
+ 110
+ ],
+ "flags": {},
+ "order": 17,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 505
+ },
+ {
+ "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": [
+ 510,
+ 515
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXVPreprocess",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 18
+ ]
+ },
+ {
+ "id": 292,
+ "type": "ResizeImageMaskNode",
+ "pos": [
+ 1670,
+ 5040
+ ],
+ "size": [
+ 300,
+ 160
+ ],
+ "flags": {},
+ "order": 18,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "input",
+ "name": "input",
+ "type": "IMAGE,MASK",
+ "link": 626
+ },
+ {
+ "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": 558
+ },
+ {
+ "localized_name": "height",
+ "name": "resize_type.height",
+ "type": "INT",
+ "widget": {
+ "name": "resize_type.height"
+ },
+ "link": 559
+ },
+ {
+ "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": [
+ 523
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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",
+ 1920,
+ 1088,
+ "center",
+ "lanczos"
+ ]
+ },
+ {
+ "id": 293,
+ "type": "KSamplerSelect",
+ "pos": [
+ 3200,
+ 4350
+ ],
+ "size": [
+ 280,
+ 110
+ ],
+ "flags": {},
+ "order": 4,
+ "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": [
+ 485
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "Node name for S&R": "KSamplerSelect",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "euler_ancestral_cfg_pp"
+ ]
+ },
+ {
+ "id": 294,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 2530,
+ 5070
+ ],
+ "size": [
+ 230,
+ 170
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 19,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 560
+ },
+ {
+ "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": [
+ 561
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "Node name for S&R": "ComfyMathExpression",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "a/2"
+ ]
+ },
+ {
+ "id": 295,
+ "type": "Reroute",
+ "pos": [
+ 3930,
+ 4090
+ ],
+ "size": [
+ 80,
+ 30
+ ],
+ "flags": {},
+ "order": 20,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 557
+ }
+ ],
+ "outputs": [
+ {
+ "name": "",
+ "type": "VAE",
+ "links": [
+ 552,
+ 553,
+ 554
+ ]
+ }
+ ],
+ "properties": {
+ "showOutputText": false,
+ "horizontal": false,
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ }
+ }
+ },
+ {
+ "id": 296,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 2530,
+ 5130
+ ],
+ "size": [
+ 230,
+ 170
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 21,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 562
+ },
+ {
+ "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": [
+ 563
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "Node name for S&R": "ComfyMathExpression",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "a/2"
+ ]
+ },
+ {
+ "id": 297,
+ "type": "EmptyLTXVLatentVideo",
+ "pos": [
+ 2980,
+ 5200
+ ],
+ "size": [
+ 280,
+ 200
+ ],
+ "flags": {},
+ "order": 22,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "width",
+ "name": "width",
+ "type": "INT",
+ "widget": {
+ "name": "width"
+ },
+ "link": 561
+ },
+ {
+ "localized_name": "height",
+ "name": "height",
+ "type": "INT",
+ "widget": {
+ "name": "height"
+ },
+ "link": 563
+ },
+ {
+ "localized_name": "length",
+ "name": "length",
+ "type": "INT",
+ "widget": {
+ "name": "length"
+ },
+ "link": 631
+ },
+ {
+ "localized_name": "batch_size",
+ "name": "batch_size",
+ "type": "INT",
+ "widget": {
+ "name": "batch_size"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "LATENT",
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 511
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.60",
+ "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": 298,
+ "type": "LTXVImgToVideoInplace",
+ "pos": [
+ 3420,
+ 4990
+ ],
+ "size": [
+ 280,
+ 180
+ ],
+ "flags": {},
+ "order": 23,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 556
+ },
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "IMAGE",
+ "link": 510
+ },
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "link": 511
+ },
+ {
+ "localized_name": "strength",
+ "name": "strength",
+ "type": "FLOAT",
+ "widget": {
+ "name": "strength"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "bypass",
+ "name": "bypass",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "bypass"
+ },
+ "link": 542
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "links": [
+ 497
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXVImgToVideoInplace",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 0.7,
+ false
+ ]
+ },
+ {
+ "id": 299,
+ "type": "LTXVAudioVAEDecode",
+ "pos": [
+ 5770,
+ 3940
+ ],
+ "size": [
+ 270,
+ 100
+ ],
+ "flags": {},
+ "order": 24,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 495
+ },
+ {
+ "label": "Audio VAE",
+ "localized_name": "audio_vae",
+ "name": "audio_vae",
+ "type": "VAE",
+ "link": 496
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "Audio",
+ "name": "Audio",
+ "type": "AUDIO",
+ "links": [
+ 534
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXVAudioVAEDecode",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 300,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 2530,
+ 5270
+ ],
+ "size": [
+ 230,
+ 170
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 25,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 564
+ },
+ {
+ "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": [
+ 566,
+ 591
+ ]
+ },
+ {
+ "localized_name": "INT",
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 565
+ ]
+ }
+ ],
+ "title": "Math Expression (fps)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "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": 301,
+ "type": "PrimitiveInt",
+ "pos": [
+ 1160,
+ 4530
+ ],
+ "size": [
+ 370,
+ 110
+ ],
+ "flags": {},
+ "order": 26,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "INT",
+ "widget": {
+ "name": "value"
+ },
+ "link": 598
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "INT",
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 559,
+ 562
+ ]
+ }
+ ],
+ "title": "Height",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "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": 302,
+ "type": "PrimitiveInt",
+ "pos": [
+ 1160,
+ 4680
+ ],
+ "size": [
+ 370,
+ 110
+ ],
+ "flags": {},
+ "order": 27,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "INT",
+ "widget": {
+ "name": "value"
+ },
+ "link": 627
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "INT",
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 564,
+ 629
+ ]
+ }
+ ],
+ "title": "Frame Rate",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "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": 303,
+ "type": "PrimitiveInt",
+ "pos": [
+ 1160,
+ 4230
+ ],
+ "size": [
+ 370,
+ 110
+ ],
+ "flags": {},
+ "order": 28,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "INT",
+ "widget": {
+ "name": "value"
+ },
+ "link": 599
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "INT",
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 628
+ ]
+ }
+ ],
+ "title": "Duration",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "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": 304,
+ "type": "PrimitiveBoolean",
+ "pos": [
+ 1170,
+ 4080
+ ],
+ "size": [
+ 370,
+ 100
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "BOOLEAN",
+ "widget": {
+ "name": "value"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "BOOLEAN",
+ "name": "BOOLEAN",
+ "type": "BOOLEAN",
+ "links": [
+ 542,
+ 543
+ ]
+ }
+ ],
+ "title": "Switch to Text to Video?",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.0",
+ "Node name for S&R": "PrimitiveBoolean",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ true
+ ]
+ },
+ {
+ "id": 305,
+ "type": "CLIPTextEncode",
+ "pos": [
+ 2170,
+ 3640
+ ],
+ "size": [
+ 550,
+ 740
+ ],
+ "flags": {},
+ "order": 29,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 615
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": 623
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 526
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "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": 306,
+ "type": "LTXVConditioning",
+ "pos": [
+ 2790,
+ 3670
+ ],
+ "size": [
+ 280,
+ 130
+ ],
+ "flags": {},
+ "order": 30,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 526
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 527
+ },
+ {
+ "localized_name": "frame_rate",
+ "name": "frame_rate",
+ "type": "FLOAT",
+ "widget": {
+ "name": "frame_rate"
+ },
+ "link": 566
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": [
+ 475,
+ 518
+ ]
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": [
+ 476,
+ 519
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "Node name for S&R": "LTXVConditioning",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ 24
+ ]
+ },
+ {
+ "id": 307,
+ "type": "LTXVEmptyLatentAudio",
+ "pos": [
+ 2970,
+ 4970
+ ],
+ "size": [
+ 280,
+ 170
+ ],
+ "flags": {},
+ "order": 31,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "audio_vae",
+ "name": "audio_vae",
+ "type": "VAE",
+ "link": 481
+ },
+ {
+ "localized_name": "frames_number",
+ "name": "frames_number",
+ "type": "INT",
+ "widget": {
+ "name": "frames_number"
+ },
+ "link": 630
+ },
+ {
+ "localized_name": "frame_rate",
+ "name": "frame_rate",
+ "type": "INT",
+ "widget": {
+ "name": "frame_rate"
+ },
+ "link": 565
+ },
+ {
+ "localized_name": "batch_size",
+ "name": "batch_size",
+ "type": "INT",
+ "widget": {
+ "name": "batch_size"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "Latent",
+ "name": "Latent",
+ "type": "LATENT",
+ "links": [
+ 498
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.68",
+ "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": 308,
+ "type": "ManualSigmas",
+ "pos": [
+ 3200,
+ 4550
+ ],
+ "size": [
+ 500,
+ 110
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "sigmas",
+ "name": "sigmas",
+ "type": "STRING",
+ "widget": {
+ "name": "sigmas"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "SIGMAS",
+ "name": "SIGMAS",
+ "type": "SIGMAS",
+ "links": [
+ 544
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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, 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875, 0.0"
+ ]
+ },
+ {
+ "id": 309,
+ "type": "LTXVSeparateAVLatent",
+ "pos": [
+ 3890,
+ 3910
+ ],
+ "size": [
+ 250,
+ 100
+ ],
+ "flags": {},
+ "order": 32,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "av_latent",
+ "name": "av_latent",
+ "type": "LATENT",
+ "link": 488
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "video_latent",
+ "name": "video_latent",
+ "type": "LATENT",
+ "links": [
+ 477,
+ 547
+ ]
+ },
+ {
+ "localized_name": "audio_latent",
+ "name": "audio_latent",
+ "type": "LATENT",
+ "links": [
+ 513
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "Node name for S&R": "LTXVSeparateAVLatent",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 310,
+ "type": "SamplerCustomAdvanced",
+ "pos": [
+ 5070,
+ 3750
+ ],
+ "size": [
+ 230,
+ 170
+ ],
+ "flags": {},
+ "order": 33,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "noise",
+ "name": "noise",
+ "type": "NOISE",
+ "link": 490
+ },
+ {
+ "localized_name": "guider",
+ "name": "guider",
+ "type": "GUIDER",
+ "link": 491
+ },
+ {
+ "localized_name": "sampler",
+ "name": "sampler",
+ "type": "SAMPLER",
+ "link": 492
+ },
+ {
+ "localized_name": "sigmas",
+ "name": "sigmas",
+ "type": "SIGMAS",
+ "link": 493
+ },
+ {
+ "localized_name": "latent_image",
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 494
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "output",
+ "name": "output",
+ "type": "LATENT",
+ "links": [
+ 578
+ ]
+ },
+ {
+ "localized_name": "denoised_output",
+ "name": "denoised_output",
+ "type": "LATENT",
+ "links": []
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.75",
+ "Node name for S&R": "SamplerCustomAdvanced",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 311,
+ "type": "LTXVSeparateAVLatent",
+ "pos": [
+ 5410,
+ 3750
+ ],
+ "size": [
+ 230,
+ 100
+ ],
+ "flags": {},
+ "order": 34,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "av_latent",
+ "name": "av_latent",
+ "type": "LATENT",
+ "link": 578
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "video_latent",
+ "name": "video_latent",
+ "type": "LATENT",
+ "links": [
+ 539
+ ]
+ },
+ {
+ "localized_name": "audio_latent",
+ "name": "audio_latent",
+ "type": "LATENT",
+ "links": [
+ 495
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "Node name for S&R": "LTXVSeparateAVLatent",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 312,
+ "type": "CreateVideo",
+ "pos": [
+ 5740,
+ 4610
+ ],
+ "size": [
+ 280,
+ 130
+ ],
+ "flags": {},
+ "order": 35,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "images",
+ "name": "images",
+ "type": "IMAGE",
+ "link": 538
+ },
+ {
+ "localized_name": "audio",
+ "name": "audio",
+ "shape": 7,
+ "type": "AUDIO",
+ "link": 534
+ },
+ {
+ "localized_name": "fps",
+ "name": "fps",
+ "type": "FLOAT",
+ "widget": {
+ "name": "fps"
+ },
+ "link": 591
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "VIDEO",
+ "name": "VIDEO",
+ "type": "VIDEO",
+ "links": [
+ 536
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.5.1",
+ "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": 313,
+ "type": "LatentUpscaleModelLoader",
+ "pos": [
+ 1670,
+ 4600
+ ],
+ "size": [
+ 400,
+ 110
+ ],
+ "flags": {},
+ "order": 36,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model_name",
+ "name": "model_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "model_name"
+ },
+ "link": 607
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "LATENT_UPSCALE_MODEL",
+ "name": "LATENT_UPSCALE_MODEL",
+ "type": "LATENT_UPSCALE_MODEL",
+ "links": [
+ 545
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LatentUpscaleModelLoader",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65,
+ "models": [
+ {
+ "name": "ltx-2.3-spatial-upscaler-x2-1.1.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x2-1.1.safetensors",
+ "directory": "latent_upscale_models"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ltx-2.3-spatial-upscaler-x2-1.1.safetensors"
+ ]
+ },
+ {
+ "id": 314,
+ "type": "PrimitiveInt",
+ "pos": [
+ 1160,
+ 4380
+ ],
+ "size": [
+ 370,
+ 110
+ ],
+ "flags": {},
+ "order": 37,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "INT",
+ "widget": {
+ "name": "value"
+ },
+ "link": 597
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "INT",
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 558,
+ 560
+ ]
+ }
+ ],
+ "title": "Width",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "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": 315,
+ "type": "CLIPTextEncode",
+ "pos": [
+ 2180,
+ 4480
+ ],
+ "size": [
+ 530,
+ 240
+ ],
+ "flags": {},
+ "order": 38,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "clip",
+ "name": "clip",
+ "type": "CLIP",
+ "link": 625
+ },
+ {
+ "localized_name": "text",
+ "name": "text",
+ "type": "STRING",
+ "widget": {
+ "name": "text"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CONDITIONING",
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 527
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "Node name for S&R": "CLIPTextEncode",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "pc game, console game, video game, cartoon, childish, ugly"
+ ],
+ "color": "#323",
+ "bgcolor": "#535"
+ },
+ {
+ "id": 316,
+ "type": "CFGGuider",
+ "pos": [
+ 3200,
+ 4100
+ ],
+ "size": [
+ 280,
+ 160
+ ],
+ "flags": {},
+ "order": 39,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "model",
+ "name": "model",
+ "type": "MODEL",
+ "link": 541
+ },
+ {
+ "localized_name": "positive",
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 518
+ },
+ {
+ "localized_name": "negative",
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 519
+ },
+ {
+ "localized_name": "cfg",
+ "name": "cfg",
+ "type": "FLOAT",
+ "widget": {
+ "name": "cfg"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "GUIDER",
+ "name": "GUIDER",
+ "type": "GUIDER",
+ "links": [
+ 484
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.64",
+ "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": 317,
+ "type": "VAEDecodeTiled",
+ "pos": [
+ 5760,
+ 3650
+ ],
+ "size": [
+ 280,
+ 200
+ ],
+ "flags": {},
+ "order": 40,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "samples",
+ "name": "samples",
+ "type": "LATENT",
+ "link": 539
+ },
+ {
+ "localized_name": "vae",
+ "name": "vae",
+ "type": "VAE",
+ "link": 553
+ },
+ {
+ "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": [
+ 538
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.14.1",
+ "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,
+ 4
+ ]
+ },
+ {
+ "id": 318,
+ "type": "CheckpointLoaderSimple",
+ "pos": [
+ 1660,
+ 3660
+ ],
+ "size": [
+ 430,
+ 160
+ ],
+ "flags": {},
+ "order": 41,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "ckpt_name",
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "ckpt_name"
+ },
+ "link": 601
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "MODEL",
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 520
+ ]
+ },
+ {
+ "localized_name": "CLIP",
+ "name": "CLIP",
+ "type": "CLIP",
+ "links": []
+ },
+ {
+ "localized_name": "VAE",
+ "name": "VAE",
+ "type": "VAE",
+ "links": [
+ 556,
+ 557
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.3.56",
+ "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-dev-fp8.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-dev-fp8.safetensors",
+ "directory": "checkpoints"
+ }
+ ]
+ },
+ "widgets_values": [
+ "ltx-2.3-22b-dev-fp8.safetensors"
+ ]
+ },
+ {
+ "id": 319,
+ "type": "LTXAVTextEncoderLoader",
+ "pos": [
+ 1660,
+ 4340
+ ],
+ "size": [
+ 430,
+ 170
+ ],
+ "flags": {},
+ "order": 42,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "text_encoder",
+ "name": "text_encoder",
+ "type": "COMBO",
+ "widget": {
+ "name": "text_encoder"
+ },
+ "link": 606
+ },
+ {
+ "localized_name": "ckpt_name",
+ "name": "ckpt_name",
+ "type": "COMBO",
+ "widget": {
+ "name": "ckpt_name"
+ },
+ "link": 605
+ },
+ {
+ "localized_name": "device",
+ "name": "device",
+ "type": "COMBO",
+ "widget": {
+ "name": "device"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "CLIP",
+ "name": "CLIP",
+ "type": "CLIP",
+ "links": [
+ 615,
+ 625
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXAVTextEncoderLoader",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65,
+ "models": [
+ {
+ "name": "ltx-2.3-22b-dev-fp8.safetensors",
+ "url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-dev-fp8.safetensors",
+ "directory": "checkpoints"
+ },
+ {
+ "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"
+ }
+ ]
+ },
+ "widgets_values": [
+ "gemma_3_12B_it_fp4_mixed.safetensors",
+ "ltx-2.3-22b-dev-fp8.safetensors",
+ "default"
+ ]
+ },
+ {
+ "id": 320,
+ "type": "PrimitiveStringMultiline",
+ "pos": [
+ 1160,
+ 3680
+ ],
+ "size": [
+ 370,
+ 350
+ ],
+ "flags": {},
+ "order": 43,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "value",
+ "name": "value",
+ "type": "STRING",
+ "widget": {
+ "name": "value"
+ },
+ "link": 595
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "STRING",
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 623
+ ]
+ }
+ ],
+ "title": "Prompt",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "Node name for S&R": "PrimitiveStringMultiline",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ ""
+ ]
+ },
+ {
+ "id": 321,
+ "type": "LTXVConcatAVLatent",
+ "pos": [
+ 3820,
+ 4990
+ ],
+ "size": [
+ 240,
+ 100
+ ],
+ "flags": {},
+ "order": 44,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "video_latent",
+ "name": "video_latent",
+ "type": "LATENT",
+ "link": 497
+ },
+ {
+ "localized_name": "audio_latent",
+ "name": "audio_latent",
+ "type": "LATENT",
+ "link": 498
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "latent",
+ "name": "latent",
+ "type": "LATENT",
+ "links": [
+ 487
+ ]
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.7.0",
+ "Node name for S&R": "LTXVConcatAVLatent",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ }
+ },
+ {
+ "id": 322,
+ "type": "LoadImage",
+ "pos": [
+ 1150,
+ 4940
+ ],
+ "size": [
+ 400,
+ 480
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "localized_name": "image",
+ "name": "image",
+ "type": "COMBO",
+ "widget": {
+ "name": "image"
+ },
+ "link": null
+ },
+ {
+ "localized_name": "choose file to upload",
+ "name": "upload",
+ "type": "IMAGEUPLOAD",
+ "widget": {
+ "name": "upload"
+ },
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "localized_name": "IMAGE",
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 626
+ ]
+ },
+ {
+ "localized_name": "MASK",
+ "name": "MASK",
+ "type": "MASK",
+ "links": null
+ }
+ ],
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "version": "7.7",
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.16.3",
+ "Node name for S&R": "LoadImage",
+ "enableTabs": false,
+ "tabWidth": 65,
+ "tabXOffset": 10,
+ "hasSecondTab": false,
+ "secondTabText": "Send Back",
+ "secondTabOffset": 80,
+ "secondTabWidth": 65
+ },
+ "widgets_values": [
+ "example.png",
+ "image"
+ ]
+ },
+ {
+ "id": 323,
+ "type": "ComfyMathExpression",
+ "pos": [
+ 2540,
+ 5370
+ ],
+ "size": [
+ 260,
+ 190
+ ],
+ "flags": {
+ "collapsed": true
+ },
+ "order": 45,
+ "mode": 0,
+ "inputs": [
+ {
+ "label": "a",
+ "localized_name": "values.a",
+ "name": "values.a",
+ "type": "FLOAT,INT",
+ "link": 628
+ },
+ {
+ "label": "b",
+ "localized_name": "values.b",
+ "name": "values.b",
+ "shape": 7,
+ "type": "FLOAT,INT",
+ "link": 629
+ },
+ {
+ "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": [
+ 630,
+ 631
+ ]
+ }
+ ],
+ "title": "Math Expression (length)",
+ "properties": {
+ "ue_properties": {
+ "widget_ue_connectable": {},
+ "input_ue_unconnectable": {}
+ },
+ "cnr_id": "comfy-core",
+ "ver": "0.18.1",
+ "Node name for S&R": "ComfyMathExpression"
+ },
+ "widgets_values": [
+ "a * b + 1"
+ ]
+ }
+ ],
+ "groups": [
+ {
+ "id": 1,
+ "title": "Model",
+ "bounding": [
+ 1630,
+ 3550,
+ 480,
+ 1270
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 2,
+ "title": "Generate Low Resolution",
+ "bounding": [
+ 3150,
+ 3550,
+ 1020,
+ 1270
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 3,
+ "title": "Prompt",
+ "bounding": [
+ 2140,
+ 3550,
+ 980,
+ 1270
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 6,
+ "title": "Generate High Resolution",
+ "bounding": [
+ 4690,
+ 3550,
+ 960,
+ 1270
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 7,
+ "title": "Lantent Upscale",
+ "bounding": [
+ 4200,
+ 3550,
+ 460,
+ 1270
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 19,
+ "title": "Video Settings",
+ "bounding": [
+ 1110,
+ 3550,
+ 490,
+ 1270
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 20,
+ "title": "Image Preprocess",
+ "bounding": [
+ 1630,
+ 4850,
+ 830,
+ 610
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 21,
+ "title": "Empty Latent",
+ "bounding": [
+ 2830,
+ 4850,
+ 1340,
+ 610
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 22,
+ "title": "Number conversion",
+ "bounding": [
+ 2490,
+ 4850,
+ 320,
+ 610
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ },
+ {
+ "id": 26,
+ "title": "Image will not affect the video",
+ "bounding": [
+ 1110,
+ 4850,
+ 490,
+ 610
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "flags": {}
+ }
+ ],
+ "links": [
+ {
+ "id": 512,
+ "origin_id": 290,
+ "origin_slot": 0,
+ "target_id": 280,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 513,
+ "origin_id": 309,
+ "origin_slot": 1,
+ "target_id": 280,
+ "target_slot": 1,
+ "type": "LATENT"
+ },
+ {
+ "id": 478,
+ "origin_id": 287,
+ "origin_slot": 0,
+ "target_id": 284,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 479,
+ "origin_id": 286,
+ "origin_slot": 0,
+ "target_id": 284,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 480,
+ "origin_id": 286,
+ "origin_slot": 1,
+ "target_id": 284,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 541,
+ "origin_id": 287,
+ "origin_slot": 0,
+ "target_id": 316,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 518,
+ "origin_id": 306,
+ "origin_slot": 0,
+ "target_id": 316,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 519,
+ "origin_id": 306,
+ "origin_slot": 1,
+ "target_id": 316,
+ "target_slot": 2,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 483,
+ "origin_id": 279,
+ "origin_slot": 0,
+ "target_id": 285,
+ "target_slot": 0,
+ "type": "NOISE"
+ },
+ {
+ "id": 484,
+ "origin_id": 316,
+ "origin_slot": 0,
+ "target_id": 285,
+ "target_slot": 1,
+ "type": "GUIDER"
+ },
+ {
+ "id": 485,
+ "origin_id": 293,
+ "origin_slot": 0,
+ "target_id": 285,
+ "target_slot": 2,
+ "type": "SAMPLER"
+ },
+ {
+ "id": 544,
+ "origin_id": 308,
+ "origin_slot": 0,
+ "target_id": 285,
+ "target_slot": 3,
+ "type": "SIGMAS"
+ },
+ {
+ "id": 487,
+ "origin_id": 321,
+ "origin_slot": 0,
+ "target_id": 285,
+ "target_slot": 4,
+ "type": "LATENT"
+ },
+ {
+ "id": 475,
+ "origin_id": 306,
+ "origin_slot": 0,
+ "target_id": 286,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 476,
+ "origin_id": 306,
+ "origin_slot": 1,
+ "target_id": 286,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 477,
+ "origin_id": 309,
+ "origin_slot": 0,
+ "target_id": 286,
+ "target_slot": 2,
+ "type": "LATENT"
+ },
+ {
+ "id": 520,
+ "origin_id": 318,
+ "origin_slot": 0,
+ "target_id": 287,
+ "target_slot": 0,
+ "type": "MODEL"
+ },
+ {
+ "id": 523,
+ "origin_id": 292,
+ "origin_slot": 0,
+ "target_id": 288,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 547,
+ "origin_id": 309,
+ "origin_slot": 0,
+ "target_id": 289,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 545,
+ "origin_id": 313,
+ "origin_slot": 0,
+ "target_id": 289,
+ "target_slot": 1,
+ "type": "LATENT_UPSCALE_MODEL"
+ },
+ {
+ "id": 554,
+ "origin_id": 295,
+ "origin_slot": 0,
+ "target_id": 289,
+ "target_slot": 2,
+ "type": "VAE"
+ },
+ {
+ "id": 552,
+ "origin_id": 295,
+ "origin_slot": 0,
+ "target_id": 290,
+ "target_slot": 0,
+ "type": "VAE"
+ },
+ {
+ "id": 515,
+ "origin_id": 291,
+ "origin_slot": 0,
+ "target_id": 290,
+ "target_slot": 1,
+ "type": "IMAGE"
+ },
+ {
+ "id": 548,
+ "origin_id": 289,
+ "origin_slot": 0,
+ "target_id": 290,
+ "target_slot": 2,
+ "type": "LATENT"
+ },
+ {
+ "id": 543,
+ "origin_id": 304,
+ "origin_slot": 0,
+ "target_id": 290,
+ "target_slot": 4,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 505,
+ "origin_id": 288,
+ "origin_slot": 0,
+ "target_id": 291,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 558,
+ "origin_id": 314,
+ "origin_slot": 0,
+ "target_id": 292,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 559,
+ "origin_id": 301,
+ "origin_slot": 0,
+ "target_id": 292,
+ "target_slot": 3,
+ "type": "INT"
+ },
+ {
+ "id": 560,
+ "origin_id": 314,
+ "origin_slot": 0,
+ "target_id": 294,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 557,
+ "origin_id": 318,
+ "origin_slot": 2,
+ "target_id": 295,
+ "target_slot": 0,
+ "type": "VAE"
+ },
+ {
+ "id": 562,
+ "origin_id": 301,
+ "origin_slot": 0,
+ "target_id": 296,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 561,
+ "origin_id": 294,
+ "origin_slot": 1,
+ "target_id": 297,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 563,
+ "origin_id": 296,
+ "origin_slot": 1,
+ "target_id": 297,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 556,
+ "origin_id": 318,
+ "origin_slot": 2,
+ "target_id": 298,
+ "target_slot": 0,
+ "type": "VAE"
+ },
+ {
+ "id": 510,
+ "origin_id": 291,
+ "origin_slot": 0,
+ "target_id": 298,
+ "target_slot": 1,
+ "type": "IMAGE"
+ },
+ {
+ "id": 511,
+ "origin_id": 297,
+ "origin_slot": 0,
+ "target_id": 298,
+ "target_slot": 2,
+ "type": "LATENT"
+ },
+ {
+ "id": 542,
+ "origin_id": 304,
+ "origin_slot": 0,
+ "target_id": 298,
+ "target_slot": 4,
+ "type": "BOOLEAN"
+ },
+ {
+ "id": 495,
+ "origin_id": 311,
+ "origin_slot": 1,
+ "target_id": 299,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 496,
+ "origin_id": 281,
+ "origin_slot": 0,
+ "target_id": 299,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 564,
+ "origin_id": 302,
+ "origin_slot": 0,
+ "target_id": 300,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 526,
+ "origin_id": 305,
+ "origin_slot": 0,
+ "target_id": 306,
+ "target_slot": 0,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 527,
+ "origin_id": 315,
+ "origin_slot": 0,
+ "target_id": 306,
+ "target_slot": 1,
+ "type": "CONDITIONING"
+ },
+ {
+ "id": 566,
+ "origin_id": 300,
+ "origin_slot": 0,
+ "target_id": 306,
+ "target_slot": 2,
+ "type": "FLOAT"
+ },
+ {
+ "id": 497,
+ "origin_id": 298,
+ "origin_slot": 0,
+ "target_id": 321,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 498,
+ "origin_id": 307,
+ "origin_slot": 0,
+ "target_id": 321,
+ "target_slot": 1,
+ "type": "LATENT"
+ },
+ {
+ "id": 481,
+ "origin_id": 281,
+ "origin_slot": 0,
+ "target_id": 307,
+ "target_slot": 0,
+ "type": "VAE"
+ },
+ {
+ "id": 565,
+ "origin_id": 300,
+ "origin_slot": 1,
+ "target_id": 307,
+ "target_slot": 2,
+ "type": "INT"
+ },
+ {
+ "id": 488,
+ "origin_id": 285,
+ "origin_slot": 0,
+ "target_id": 309,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 490,
+ "origin_id": 278,
+ "origin_slot": 0,
+ "target_id": 310,
+ "target_slot": 0,
+ "type": "NOISE"
+ },
+ {
+ "id": 491,
+ "origin_id": 284,
+ "origin_slot": 0,
+ "target_id": 310,
+ "target_slot": 1,
+ "type": "GUIDER"
+ },
+ {
+ "id": 492,
+ "origin_id": 282,
+ "origin_slot": 0,
+ "target_id": 310,
+ "target_slot": 2,
+ "type": "SAMPLER"
+ },
+ {
+ "id": 493,
+ "origin_id": 283,
+ "origin_slot": 0,
+ "target_id": 310,
+ "target_slot": 3,
+ "type": "SIGMAS"
+ },
+ {
+ "id": 494,
+ "origin_id": 280,
+ "origin_slot": 0,
+ "target_id": 310,
+ "target_slot": 4,
+ "type": "LATENT"
+ },
+ {
+ "id": 578,
+ "origin_id": 310,
+ "origin_slot": 0,
+ "target_id": 311,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 539,
+ "origin_id": 311,
+ "origin_slot": 0,
+ "target_id": 317,
+ "target_slot": 0,
+ "type": "LATENT"
+ },
+ {
+ "id": 553,
+ "origin_id": 295,
+ "origin_slot": 0,
+ "target_id": 317,
+ "target_slot": 1,
+ "type": "VAE"
+ },
+ {
+ "id": 538,
+ "origin_id": 317,
+ "origin_slot": 0,
+ "target_id": 312,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 534,
+ "origin_id": 299,
+ "origin_slot": 0,
+ "target_id": 312,
+ "target_slot": 1,
+ "type": "AUDIO"
+ },
+ {
+ "id": 591,
+ "origin_id": 300,
+ "origin_slot": 0,
+ "target_id": 312,
+ "target_slot": 2,
+ "type": "FLOAT"
+ },
+ {
+ "id": 536,
+ "origin_id": 312,
+ "origin_slot": 0,
+ "target_id": -20,
+ "target_slot": 0,
+ "type": "VIDEO"
+ },
+ {
+ "id": 595,
+ "origin_id": -10,
+ "origin_slot": 0,
+ "target_id": 320,
+ "target_slot": 0,
+ "type": "STRING"
+ },
+ {
+ "id": 597,
+ "origin_id": -10,
+ "origin_slot": 1,
+ "target_id": 314,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 598,
+ "origin_id": -10,
+ "origin_slot": 2,
+ "target_id": 301,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 599,
+ "origin_id": -10,
+ "origin_slot": 3,
+ "target_id": 303,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 601,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 318,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 602,
+ "origin_id": -10,
+ "origin_slot": 5,
+ "target_id": 287,
+ "target_slot": 1,
+ "type": "COMBO"
+ },
+ {
+ "id": 604,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 281,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 605,
+ "origin_id": -10,
+ "origin_slot": 4,
+ "target_id": 319,
+ "target_slot": 1,
+ "type": "COMBO"
+ },
+ {
+ "id": 606,
+ "origin_id": -10,
+ "origin_slot": 6,
+ "target_id": 319,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 607,
+ "origin_id": -10,
+ "origin_slot": 7,
+ "target_id": 313,
+ "target_slot": 0,
+ "type": "COMBO"
+ },
+ {
+ "id": 615,
+ "origin_id": 319,
+ "origin_slot": 0,
+ "target_id": 305,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 623,
+ "origin_id": 320,
+ "origin_slot": 0,
+ "target_id": 305,
+ "target_slot": 1,
+ "type": "STRING"
+ },
+ {
+ "id": 625,
+ "origin_id": 319,
+ "origin_slot": 0,
+ "target_id": 315,
+ "target_slot": 0,
+ "type": "CLIP"
+ },
+ {
+ "id": 626,
+ "origin_id": 322,
+ "origin_slot": 0,
+ "target_id": 292,
+ "target_slot": 0,
+ "type": "IMAGE"
+ },
+ {
+ "id": 627,
+ "origin_id": -10,
+ "origin_slot": 8,
+ "target_id": 302,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 628,
+ "origin_id": 303,
+ "origin_slot": 0,
+ "target_id": 323,
+ "target_slot": 0,
+ "type": "INT"
+ },
+ {
+ "id": 629,
+ "origin_id": 302,
+ "origin_slot": 0,
+ "target_id": 323,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 630,
+ "origin_id": 323,
+ "origin_slot": 1,
+ "target_id": 307,
+ "target_slot": 1,
+ "type": "INT"
+ },
+ {
+ "id": 631,
+ "origin_id": 323,
+ "origin_slot": 1,
+ "target_id": 297,
+ "target_slot": 2,
+ "type": "INT"
+ }
+ ],
+ "extra": {
+ "workflowRendererVersion": "Vue-corrected"
+ },
+ "category": "Video generation and editing/Text to video",
+ "description": "Generates video from text prompts using LTX-2.3, Lightricks' video diffusion model."
+ }
+ ]
+ },
+ "extra": {
+ "ue_links": []
+ }
+}
\ No newline at end of file
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 b673eb703..79a4c954f 100644
--- a/blueprints/Unsharp Mask.json
+++ b/blueprints/Unsharp Mask.json
@@ -383,7 +383,7 @@
"Node name for S&R": "GLSLShader"
},
"widgets_values": [
- "#version 300 es\nprecision highp float;\n\nuniform sampler2D u_image0;\nuniform vec2 u_resolution;\nuniform float u_float0; // amount [0.0 - 3.0] typical: 0.5-1.5\nuniform float u_float1; // radius [0.5 - 10.0] blur radius in pixels\nuniform float u_float2; // threshold [0.0 - 0.1] min difference to sharpen\n\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\n\nfloat gaussian(float x, float sigma) {\n return exp(-(x * x) / (2.0 * sigma * sigma));\n}\n\nfloat getLuminance(vec3 color) {\n return dot(color, vec3(0.2126, 0.7152, 0.0722));\n}\n\nvoid main() {\n vec2 texel = 1.0 / u_resolution;\n float radius = max(u_float1, 0.5);\n float amount = u_float0;\n float threshold = u_float2;\n\n vec4 original = texture(u_image0, v_texCoord);\n\n // Gaussian blur for the \"unsharp\" mask\n int samples = int(ceil(radius));\n float sigma = radius / 2.0;\n\n vec4 blurred = vec4(0.0);\n float totalWeight = 0.0;\n\n for (int x = -samples; x <= samples; x++) {\n for (int y = -samples; y <= samples; y++) {\n vec2 offset = vec2(float(x), float(y)) * texel;\n vec4 sample_color = texture(u_image0, v_texCoord + offset);\n\n float dist = length(vec2(float(x), float(y)));\n float weight = gaussian(dist, sigma);\n blurred += sample_color * weight;\n totalWeight += weight;\n }\n }\n blurred /= totalWeight;\n\n // Unsharp mask = original - blurred\n vec3 mask = original.rgb - blurred.rgb;\n\n // Luminance-based threshold with smooth falloff\n float lumaDelta = abs(getLuminance(original.rgb) - getLuminance(blurred.rgb));\n float thresholdScale = smoothstep(0.0, threshold, lumaDelta);\n mask *= thresholdScale;\n\n // Sharpen: original + mask * amount\n vec3 sharpened = original.rgb + mask * amount;\n\n fragColor0 = vec4(clamp(sharpened, 0.0, 1.0), original.a);\n}\n",
+ "#version 300 es\nprecision highp float;\n\nuniform sampler2D u_image0;\nuniform float u_float0; // amount [0.0 - 3.0] typical: 0.5-1.5\nuniform float u_float1; // radius [0.5 - 10.0] blur radius in pixels\nuniform float u_float2; // threshold [0.0 - 0.1] min difference to sharpen\n\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\n\nfloat gaussian(float x, float sigma) {\n return exp(-(x * x) / (2.0 * sigma * sigma));\n}\n\nfloat getLuminance(vec3 color) {\n return dot(color, vec3(0.2126, 0.7152, 0.0722));\n}\n\nvoid main() {\n vec2 texel = 1.0 / vec2(textureSize(u_image0, 0));\n float radius = max(u_float1, 0.5);\n float amount = u_float0;\n float threshold = u_float2;\n\n vec4 original = texture(u_image0, v_texCoord);\n\n // Gaussian blur for the \"unsharp\" mask\n int samples = int(ceil(radius));\n float sigma = radius / 2.0;\n\n vec4 blurred = vec4(0.0);\n float totalWeight = 0.0;\n\n for (int x = -samples; x <= samples; x++) {\n for (int y = -samples; y <= samples; y++) {\n vec2 offset = vec2(float(x), float(y)) * texel;\n vec4 sample_color = texture(u_image0, v_texCoord + offset);\n\n float dist = length(vec2(float(x), float(y)));\n float weight = gaussian(dist, sigma);\n blurred += sample_color * weight;\n totalWeight += weight;\n }\n }\n blurred /= totalWeight;\n\n // Unsharp mask = original - blurred\n vec3 mask = original.rgb - blurred.rgb;\n\n // Luminance-based threshold with smooth falloff\n float lumaDelta = abs(getLuminance(original.rgb) - getLuminance(blurred.rgb));\n float thresholdScale = smoothstep(0.0, threshold, lumaDelta);\n mask *= thresholdScale;\n\n // Sharpen: original + mask * amount\n vec3 sharpened = original.rgb + mask * amount;\n\n fragColor0 = vec4(clamp(sharpened, 0.0, 1.0), original.a);\n}\n",
"from_input"
]
}
@@ -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..7642b23c1 100644
--- a/blueprints/Video Captioning (Gemini).json
+++ b/blueprints/Video Captioning (Gemini).json
@@ -307,7 +307,8 @@
"extra": {
"workflowRendererVersion": "LG"
},
- "category": "Text generation/Video Captioning"
+ "category": "Text generation/Video Captioning",
+ "description": "Generates descriptive captions for video input using Google's Gemini multimodal LLM."
}
]
}
diff --git a/blueprints/Video Inpaint(Wan2.1 VACE).json b/blueprints/Video Inpaint(Wan2.1 VACE).json
index f404e6773..a658be5f8 100644
--- a/blueprints/Video Inpaint(Wan2.1 VACE).json
+++ b/blueprints/Video Inpaint(Wan2.1 VACE).json
@@ -165,7 +165,7 @@
},
"revision": 0,
"config": {},
- "name": "local-Video Inpaint(Wan2.1 VACE)",
+ "name": "Video Inpaint (Wan 2.1 VACE)",
"inputNode": {
"id": -10,
"bounding": [
@@ -2368,7 +2368,8 @@
"extra": {
"workflowRendererVersion": "LG"
},
- "category": "Video generation and editing/Inpaint video"
+ "category": "Video generation and editing/Inpaint video",
+ "description": "Inpaints masked regions in video frames using Wan 2.1 VACE."
}
]
},
diff --git a/blueprints/Video Segmentation (SAM3).json b/blueprints/Video Segmentation (SAM3).json
new file mode 100644
index 000000000..4d9a13412
--- /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": "Video Tools",
+ "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..73476e36b 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/Enhance video",
+ "description": "Upscales video to 4× resolution using a GAN-based upscaling model."
}
]
},
"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 9ebd0efe2..df3841871 100644
--- a/comfy/cli_args.py
+++ b/comfy/cli_args.py
@@ -90,8 +90,8 @@ parser.add_argument("--force-channels-last", action="store_true", help="Force ch
parser.add_argument("--directml", type=int, nargs="?", metavar="DIRECTML_DEVICE", const=-1, help="Use torch-directml.")
parser.add_argument("--oneapi-device-selector", type=str, default=None, metavar="SELECTOR_STRING", help="Sets the oneAPI device(s) this instance will use.")
-parser.add_argument("--disable-ipex-optimize", action="store_true", help="Disables ipex.optimize default when loading models with Intel's Extension for Pytorch.")
parser.add_argument("--supports-fp8-compute", action="store_true", help="ComfyUI will act like if the device supports fp8 compute.")
+parser.add_argument("--enable-triton-backend", action="store_true", help="ComfyUI will enable the use of Triton backend in comfy-kitchen. Is disabled at launch by default.")
class LatentPreviewMethod(enum.Enum):
NoPreviews = "none"
@@ -141,8 +141,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).")
@@ -238,6 +237,8 @@ database_default_path = os.path.abspath(
)
parser.add_argument("--database-url", type=str, default=f"sqlite:///{database_default_path}", help="Specify the database URL, e.g. for an in-memory database you can use 'sqlite:///:memory:'.")
parser.add_argument("--enable-assets", action="store_true", help="Enable the assets system (API routes, database synchronization, and background scanning).")
+parser.add_argument("--feature-flag", type=str, action='append', default=[], metavar="KEY[=VALUE]", help="Set a server feature flag. Use KEY=VALUE to set an explicit value, or bare KEY to set it to true. Can be specified multiple times. Boolean values (true/false) and numbers are auto-converted. Examples: --feature-flag show_signin_button=true or --feature-flag show_signin_button")
+parser.add_argument("--list-feature-flags", action="store_true", help="Print the registry of known CLI-settable feature flags as JSON and exit.")
if comfy.options.args_parsing:
args = parser.parse_args()
diff --git a/comfy/context_windows.py b/comfy/context_windows.py
index cb44ee6e8..db57537a2 100644
--- a/comfy/context_windows.py
+++ b/comfy/context_windows.py
@@ -63,7 +63,11 @@ class IndexListContextWindow(ContextWindowABC):
dim = self.dim
if dim == 0 and full.shape[dim] == 1:
return full
- idx = tuple([slice(None)] * dim + [self.index_list])
+ indices = self.index_list
+ anchor_idx = getattr(self, 'causal_anchor_index', None)
+ if anchor_idx is not None and anchor_idx >= 0:
+ indices = [anchor_idx] + list(indices)
+ idx = tuple([slice(None)] * dim + [indices])
window = full[idx]
if retain_index_list:
idx = tuple([slice(None)] * dim + [retain_index_list])
@@ -113,7 +117,14 @@ def slice_cond(cond_value, window: IndexListContextWindow, x_in: torch.Tensor, d
# skip leading latent positions that have no corresponding conditioning (e.g. reference frames)
if temporal_offset > 0:
- indices = [i - temporal_offset for i in window.index_list[temporal_offset:]]
+ anchor_idx = getattr(window, 'causal_anchor_index', None)
+ if anchor_idx is not None and anchor_idx >= 0:
+ # anchor occupies one of the no-cond positions, so skip one fewer from window.index_list
+ skip_count = temporal_offset - 1
+ else:
+ skip_count = temporal_offset
+
+ indices = [i - temporal_offset for i in window.index_list[skip_count:]]
indices = [i for i in indices if 0 <= i]
else:
indices = list(window.index_list)
@@ -150,7 +161,8 @@ class ContextFuseMethod:
ContextResults = collections.namedtuple("ContextResults", ['window_idx', 'sub_conds_out', 'sub_conds', 'window'])
class IndexListContextHandler(ContextHandlerABC):
def __init__(self, context_schedule: ContextSchedule, fuse_method: ContextFuseMethod, context_length: int=1, context_overlap: int=0, context_stride: int=1,
- closed_loop: bool=False, dim:int=0, freenoise: bool=False, cond_retain_index_list: list[int]=[], split_conds_to_windows: bool=False):
+ closed_loop: bool=False, dim:int=0, freenoise: bool=False, cond_retain_index_list: list[int]=[], split_conds_to_windows: bool=False,
+ causal_window_fix: bool=True):
self.context_schedule = context_schedule
self.fuse_method = fuse_method
self.context_length = context_length
@@ -162,6 +174,7 @@ class IndexListContextHandler(ContextHandlerABC):
self.freenoise = freenoise
self.cond_retain_index_list = [int(x.strip()) for x in cond_retain_index_list.split(",")] if cond_retain_index_list else []
self.split_conds_to_windows = split_conds_to_windows
+ self.causal_window_fix = causal_window_fix
self.callbacks = {}
@@ -318,6 +331,14 @@ class IndexListContextHandler(ContextHandlerABC):
# allow processing to end between context window executions for faster Cancel
comfy.model_management.throw_exception_if_processing_interrupted()
+ # causal_window_fix: prepend a pre-window frame that will be stripped post-forward
+ anchor_applied = False
+ if self.causal_window_fix:
+ anchor_idx = window.index_list[0] - 1
+ if 0 <= anchor_idx < x_in.size(self.dim):
+ window.causal_anchor_index = anchor_idx
+ anchor_applied = True
+
for callback in comfy.patcher_extension.get_all_callbacks(IndexListCallbacks.EVALUATE_CONTEXT_WINDOWS, self.callbacks):
callback(self, model, x_in, conds, timestep, model_options, window_idx, window, model_options, device, first_device)
@@ -332,6 +353,12 @@ class IndexListContextHandler(ContextHandlerABC):
if device is not None:
for i in range(len(sub_conds_out)):
sub_conds_out[i] = sub_conds_out[i].to(x_in.device)
+
+ # strip causal_window_fix anchor if applied
+ if anchor_applied:
+ for i in range(len(sub_conds_out)):
+ sub_conds_out[i] = sub_conds_out[i].narrow(self.dim, 1, sub_conds_out[i].shape[self.dim] - 1)
+
results.append(ContextResults(window_idx, sub_conds_out, sub_conds, window))
return results
diff --git a/comfy/deploy_environment.py b/comfy/deploy_environment.py
new file mode 100644
index 000000000..8c99a3584
--- /dev/null
+++ b/comfy/deploy_environment.py
@@ -0,0 +1,34 @@
+import functools
+import logging
+import os
+
+logger = logging.getLogger(__name__)
+
+_DEFAULT_DEPLOY_ENV = "local-git"
+_ENV_FILENAME = ".comfy_environment"
+
+# Resolve the ComfyUI install directory (the parent of this `comfy/` package).
+# We deliberately avoid `folder_paths.base_path` here because that is overridden
+# by the `--base-directory` CLI arg to a user-supplied path, whereas the
+# `.comfy_environment` marker is written by launchers/installers next to the
+# ComfyUI install itself.
+_COMFY_INSTALL_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+
+
+@functools.cache
+def get_deploy_environment() -> str:
+ env_file = os.path.join(_COMFY_INSTALL_DIR, _ENV_FILENAME)
+ try:
+ with open(env_file, encoding="utf-8") as f:
+ # Cap the read so a malformed or maliciously crafted file (e.g.
+ # a single huge line with no newline) can't blow up memory.
+ first_line = f.readline(128).strip()
+ value = "".join(c for c in first_line if 32 <= ord(c) < 127)
+ if value:
+ return value
+ except FileNotFoundError:
+ pass
+ except Exception as e:
+ logger.error("Failed to read %s: %s", env_file, e)
+
+ return _DEFAULT_DEPLOY_ENV
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 6978eb717..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)
@@ -1810,3 +1839,119 @@ def sample_sa_solver(model, x, sigmas, extra_args=None, callback=None, disable=F
def sample_sa_solver_pece(model, x, sigmas, extra_args=None, callback=None, disable=False, tau_func=None, s_noise=1.0, noise_sampler=None, predictor_order=3, corrector_order=4, simple_order_2=False):
"""Stochastic Adams Solver with PECE (Predict–Evaluate–Correct–Evaluate) mode (NeurIPS 2023)."""
return sample_sa_solver(model, x, sigmas, extra_args=extra_args, callback=callback, disable=disable, tau_func=tau_func, s_noise=s_noise, noise_sampler=noise_sampler, predictor_order=predictor_order, corrector_order=corrector_order, use_pece=True, simple_order_2=simple_order_2)
+
+
+@torch.no_grad()
+def sample_ar_video(model, x, sigmas, extra_args=None, callback=None, disable=None,
+ num_frame_per_block=1):
+ """
+ Autoregressive video sampler: block-by-block denoising with KV cache
+ and flow-match re-noising for Causal Forcing / Self-Forcing models.
+
+ Requires a Causal-WAN compatible model (diffusion_model must expose
+ init_kv_caches / init_crossattn_caches) and 5-D latents [B,C,T,H,W].
+
+ All AR-loop parameters are passed via the SamplerARVideo node, not read
+ from the checkpoint or transformer_options.
+ """
+ extra_args = {} if extra_args is None else extra_args
+ model_options = extra_args.get("model_options", {})
+ transformer_options = model_options.get("transformer_options", {})
+
+ if x.ndim != 5:
+ raise ValueError(
+ f"ar_video sampler requires 5-D video latents [B,C,T,H,W], got {x.ndim}-D tensor with shape {x.shape}. "
+ "This sampler is only compatible with autoregressive video models (e.g. Causal-WAN)."
+ )
+
+ inner_model = model.inner_model.inner_model
+ causal_model = inner_model.diffusion_model
+
+ if not (hasattr(causal_model, "init_kv_caches") and hasattr(causal_model, "init_crossattn_caches")):
+ raise TypeError(
+ "ar_video sampler requires a Causal-WAN compatible model whose diffusion_model "
+ "exposes init_kv_caches() and init_crossattn_caches(). The loaded checkpoint "
+ "does not support this interface — choose a different sampler."
+ )
+
+ seed = extra_args.get("seed", 0)
+
+ bs, c, lat_t, lat_h, lat_w = x.shape
+ frame_seq_len = -(-lat_h // 2) * -(-lat_w // 2) # ceiling division
+ num_blocks = -(-lat_t // num_frame_per_block) # ceiling division
+ device = x.device
+ model_dtype = inner_model.get_dtype()
+
+ kv_caches = causal_model.init_kv_caches(bs, lat_t * frame_seq_len, device, model_dtype)
+ crossattn_caches = causal_model.init_crossattn_caches(bs, device, model_dtype)
+
+ 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
+
+ try:
+ for block_idx in trange(num_blocks, disable=disable):
+ bf = min(num_frame_per_block, lat_t - current_start_frame)
+ fs, fe = current_start_frame, current_start_frame + bf
+ noisy_input = x[:, :, fs:fe]
+
+ ar_state = {
+ "start_frame": current_start_frame,
+ "kv_caches": kv_caches,
+ "crossattn_caches": crossattn_caches,
+ }
+ transformer_options["ar_state"] = ar_state
+
+ for i in range(num_sigma_steps):
+ denoised = model(noisy_input, sigmas[i] * s_in, **extra_args)
+
+ if callback is not None:
+ scaled_i = step_count * num_sigma_steps // total_real_steps
+ callback({"x": noisy_input, "i": scaled_i, "sigma": sigmas[i],
+ "sigma_hat": sigmas[i], "denoised": denoised})
+
+ if sigmas[i + 1] == 0:
+ noisy_input = denoised
+ else:
+ sigma_next = sigmas[i + 1]
+ torch.manual_seed(seed + block_idx * 1000 + i)
+ fresh_noise = torch.randn_like(denoised)
+ noisy_input = (1.0 - sigma_next) * denoised + sigma_next * fresh_noise
+
+ for cache in kv_caches:
+ cache["end"] -= bf * frame_seq_len
+
+ step_count += 1
+
+ output[:, :, fs:fe] = noisy_input
+
+ for cache in kv_caches:
+ cache["end"] -= bf * frame_seq_len
+ zero_sigma = sigmas.new_zeros([1])
+ _ = model(noisy_input, zero_sigma * s_in, **extra_args)
+
+ current_start_frame += bf
+ finally:
+ transformer_options.pop("ar_state", None)
+
+ return output
diff --git a/comfy/latent_formats.py b/comfy/latent_formats.py
index 6a57bca1c..6e37080bb 100644
--- a/comfy/latent_formats.py
+++ b/comfy/latent_formats.py
@@ -9,6 +9,7 @@ class LatentFormat:
latent_rgb_factors_reshape = None
taesd_decoder_name = None
spacial_downscale_ratio = 8
+ temporal_downscale_ratio = 1
def process_in(self, latent):
return latent * self.scale_factor
@@ -149,6 +150,7 @@ class SD3(LatentFormat):
class StableAudio1(LatentFormat):
latent_channels = 64
latent_dimensions = 1
+ temporal_downscale_ratio = 2048
class Flux(SD3):
latent_channels = 16
@@ -224,6 +226,7 @@ class Flux2(LatentFormat):
self.latent_rgb_factors_bias = [-0.0329, -0.0718, -0.0851]
self.latent_rgb_factors_reshape = lambda t: t.reshape(t.shape[0], 32, 2, 2, t.shape[-2], t.shape[-1]).permute(0, 1, 4, 2, 5, 3).reshape(t.shape[0], 32, t.shape[-2] * 2, t.shape[-1] * 2)
+ self.taesd_decoder_name = "taef2_decoder"
def process_in(self, latent):
return latent
@@ -234,6 +237,7 @@ class Flux2(LatentFormat):
class Mochi(LatentFormat):
latent_channels = 12
latent_dimensions = 3
+ temporal_downscale_ratio = 6
def __init__(self):
self.scale_factor = 1.0
@@ -277,6 +281,7 @@ class LTXV(LatentFormat):
latent_channels = 128
latent_dimensions = 3
spacial_downscale_ratio = 32
+ temporal_downscale_ratio = 8
def __init__(self):
self.latent_rgb_factors = [
@@ -420,6 +425,7 @@ class LTXAV(LTXV):
class HunyuanVideo(LatentFormat):
latent_channels = 16
latent_dimensions = 3
+ temporal_downscale_ratio = 4
scale_factor = 0.476986
latent_rgb_factors = [
[-0.0395, -0.0331, 0.0445],
@@ -446,6 +452,7 @@ class HunyuanVideo(LatentFormat):
class Cosmos1CV8x8x8(LatentFormat):
latent_channels = 16
latent_dimensions = 3
+ temporal_downscale_ratio = 8
latent_rgb_factors = [
[ 0.1817, 0.2284, 0.2423],
@@ -471,6 +478,7 @@ class Cosmos1CV8x8x8(LatentFormat):
class Wan21(LatentFormat):
latent_channels = 16
latent_dimensions = 3
+ temporal_downscale_ratio = 4
latent_rgb_factors = [
[-0.1299, -0.1692, 0.2932],
@@ -733,6 +741,7 @@ class HunyuanVideo15(LatentFormat):
latent_channels = 32
latent_dimensions = 3
spacial_downscale_ratio = 16
+ temporal_downscale_ratio = 4
scale_factor = 1.03682
taesd_decoder_name = "lighttaehy1_5"
@@ -758,6 +767,7 @@ class ACEAudio(LatentFormat):
class ACEAudio15(LatentFormat):
latent_channels = 64
latent_dimensions = 1
+ temporal_downscale_ratio = 1764
class ChromaRadiance(LatentFormat):
latent_channels = 3
@@ -783,3 +793,36 @@ class ZImagePixelSpace(ChromaRadiance):
No VAE encoding/decoding — the model operates directly on RGB pixels.
"""
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/cogvideo/__init__.py b/comfy/ldm/cogvideo/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/comfy/ldm/cogvideo/model.py b/comfy/ldm/cogvideo/model.py
new file mode 100644
index 000000000..fb475ed53
--- /dev/null
+++ b/comfy/ldm/cogvideo/model.py
@@ -0,0 +1,573 @@
+# CogVideoX 3D Transformer - ported to ComfyUI native ops
+# Architecture reference: diffusers CogVideoXTransformer3DModel
+# Style reference: comfy/ldm/wan/model.py
+
+import math
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+from comfy.ldm.modules.attention import optimized_attention
+import comfy.patcher_extension
+import comfy.ldm.common_dit
+
+
+def _get_1d_rotary_pos_embed(dim, pos, theta=10000.0):
+ """Returns (cos, sin) each with shape [seq_len, dim].
+
+ Frequencies are computed at dim//2 resolution then repeat_interleaved
+ to full dim, matching CogVideoX's interleaved (real, imag) pair format.
+ """
+ freqs = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float32, device=pos.device) / dim))
+ angles = torch.outer(pos.float(), freqs.float())
+ cos = angles.cos().repeat_interleave(2, dim=-1).float()
+ sin = angles.sin().repeat_interleave(2, dim=-1).float()
+ return (cos, sin)
+
+
+def apply_rotary_emb(x, freqs_cos_sin):
+ """Apply CogVideoX rotary embedding to query or key tensor.
+
+ x: [B, heads, seq_len, head_dim]
+ freqs_cos_sin: (cos, sin) each [seq_len, head_dim//2]
+
+ Uses interleaved pair rotation (same as diffusers CogVideoX/Flux).
+ head_dim is reshaped to (-1, 2) pairs, rotated, then flattened back.
+ """
+ cos, sin = freqs_cos_sin
+ cos = cos[None, None, :, :].to(x.device)
+ sin = sin[None, None, :, :].to(x.device)
+
+ # Interleaved pairs: [B, H, S, D] -> [B, H, S, D//2, 2] -> (real, imag)
+ x_real, x_imag = x.reshape(*x.shape[:-1], -1, 2).unbind(-1)
+ x_rotated = torch.stack([-x_imag, x_real], dim=-1).flatten(3)
+
+ return (x.float() * cos + x_rotated.float() * sin).to(x.dtype)
+
+
+def get_timestep_embedding(timesteps, dim, flip_sin_to_cos=True, downscale_freq_shift=0, scale=1, max_period=10000):
+ half = dim // 2
+ freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32, device=timesteps.device) / half)
+ args = timesteps[:, None].float() * freqs[None] * scale
+ embedding = torch.cat([torch.sin(args), torch.cos(args)], dim=-1)
+ if flip_sin_to_cos:
+ embedding = torch.cat([embedding[:, half:], embedding[:, :half]], dim=-1)
+ if dim % 2:
+ embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
+ return embedding
+
+
+def get_3d_sincos_pos_embed(embed_dim, spatial_size, temporal_size, spatial_interpolation_scale=1.0, temporal_interpolation_scale=1.0, device=None):
+ if isinstance(spatial_size, int):
+ spatial_size = (spatial_size, spatial_size)
+
+ grid_w = torch.arange(spatial_size[0], dtype=torch.float32, device=device) / spatial_interpolation_scale
+ grid_h = torch.arange(spatial_size[1], dtype=torch.float32, device=device) / spatial_interpolation_scale
+ grid_t = torch.arange(temporal_size, dtype=torch.float32, device=device) / temporal_interpolation_scale
+
+ grid_t, grid_h, grid_w = torch.meshgrid(grid_t, grid_h, grid_w, indexing="ij")
+
+ embed_dim_spatial = 2 * (embed_dim // 3)
+ embed_dim_temporal = embed_dim // 3
+
+ pos_embed_spatial = _get_2d_sincos_pos_embed(embed_dim_spatial, grid_h, grid_w, device=device)
+ pos_embed_temporal = _get_1d_sincos_pos_embed(embed_dim_temporal, grid_t[:, 0, 0], device=device)
+
+ T, H, W = grid_t.shape
+ pos_embed_temporal = pos_embed_temporal.unsqueeze(1).unsqueeze(1).expand(-1, H, W, -1)
+ pos_embed = torch.cat([pos_embed_temporal, pos_embed_spatial], dim=-1)
+
+ return pos_embed
+
+
+def _get_2d_sincos_pos_embed(embed_dim, grid_h, grid_w, device=None):
+ T, H, W = grid_h.shape
+ half_dim = embed_dim // 2
+ pos_h = _get_1d_sincos_pos_embed(half_dim, grid_h.reshape(-1), device=device).reshape(T, H, W, half_dim)
+ pos_w = _get_1d_sincos_pos_embed(half_dim, grid_w.reshape(-1), device=device).reshape(T, H, W, half_dim)
+ return torch.cat([pos_h, pos_w], dim=-1)
+
+
+def _get_1d_sincos_pos_embed(embed_dim, pos, device=None):
+ half = embed_dim // 2
+ freqs = torch.exp(-math.log(10000.0) * torch.arange(start=0, end=half, dtype=torch.float32, device=device) / half)
+ args = pos.float().reshape(-1)[:, None] * freqs[None]
+ embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
+ if embed_dim % 2:
+ embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
+ return embedding
+
+
+
+class CogVideoXPatchEmbed(nn.Module):
+ def __init__(self, patch_size=2, patch_size_t=None, in_channels=16, dim=1920,
+ text_dim=4096, bias=True, sample_width=90, sample_height=60,
+ sample_frames=49, temporal_compression_ratio=4,
+ max_text_seq_length=226, spatial_interpolation_scale=1.875,
+ temporal_interpolation_scale=1.0, use_positional_embeddings=True,
+ use_learned_positional_embeddings=True,
+ device=None, dtype=None, operations=None):
+ super().__init__()
+ self.patch_size = patch_size
+ self.patch_size_t = patch_size_t
+ self.dim = dim
+ self.sample_height = sample_height
+ self.sample_width = sample_width
+ self.sample_frames = sample_frames
+ self.temporal_compression_ratio = temporal_compression_ratio
+ self.max_text_seq_length = max_text_seq_length
+ self.spatial_interpolation_scale = spatial_interpolation_scale
+ self.temporal_interpolation_scale = temporal_interpolation_scale
+ self.use_positional_embeddings = use_positional_embeddings
+ self.use_learned_positional_embeddings = use_learned_positional_embeddings
+
+ if patch_size_t is None:
+ self.proj = operations.Conv2d(in_channels, dim, kernel_size=patch_size, stride=patch_size, bias=bias, device=device, dtype=dtype)
+ else:
+ self.proj = operations.Linear(in_channels * patch_size * patch_size * patch_size_t, dim, device=device, dtype=dtype)
+
+ self.text_proj = operations.Linear(text_dim, dim, device=device, dtype=dtype)
+
+ if use_positional_embeddings or use_learned_positional_embeddings:
+ persistent = use_learned_positional_embeddings
+ pos_embedding = self._get_positional_embeddings(sample_height, sample_width, sample_frames)
+ self.register_buffer("pos_embedding", pos_embedding, persistent=persistent)
+
+ def _get_positional_embeddings(self, sample_height, sample_width, sample_frames, device=None):
+ post_patch_height = sample_height // self.patch_size
+ post_patch_width = sample_width // self.patch_size
+ post_time_compression_frames = (sample_frames - 1) // self.temporal_compression_ratio + 1
+ if self.patch_size_t is not None:
+ post_time_compression_frames = post_time_compression_frames // self.patch_size_t
+ num_patches = post_patch_height * post_patch_width * post_time_compression_frames
+
+ pos_embedding = get_3d_sincos_pos_embed(
+ self.dim,
+ (post_patch_width, post_patch_height),
+ post_time_compression_frames,
+ self.spatial_interpolation_scale,
+ self.temporal_interpolation_scale,
+ device=device,
+ )
+ pos_embedding = pos_embedding.reshape(-1, self.dim)
+ joint_pos_embedding = pos_embedding.new_zeros(
+ 1, self.max_text_seq_length + num_patches, self.dim, requires_grad=False
+ )
+ joint_pos_embedding.data[:, self.max_text_seq_length:].copy_(pos_embedding)
+ return joint_pos_embedding
+
+ def forward(self, text_embeds, image_embeds):
+ input_dtype = text_embeds.dtype
+ text_embeds = self.text_proj(text_embeds.to(self.text_proj.weight.dtype)).to(input_dtype)
+ batch_size, num_frames, channels, height, width = image_embeds.shape
+
+ proj_dtype = self.proj.weight.dtype
+ if self.patch_size_t is None:
+ image_embeds = image_embeds.reshape(-1, channels, height, width)
+ image_embeds = self.proj(image_embeds.to(proj_dtype)).to(input_dtype)
+ image_embeds = image_embeds.view(batch_size, num_frames, *image_embeds.shape[1:])
+ image_embeds = image_embeds.flatten(3).transpose(2, 3)
+ image_embeds = image_embeds.flatten(1, 2)
+ else:
+ p = self.patch_size
+ p_t = self.patch_size_t
+ image_embeds = image_embeds.permute(0, 1, 3, 4, 2)
+ image_embeds = image_embeds.reshape(
+ batch_size, num_frames // p_t, p_t, height // p, p, width // p, p, channels
+ )
+ image_embeds = image_embeds.permute(0, 1, 3, 5, 7, 2, 4, 6).flatten(4, 7).flatten(1, 3)
+ image_embeds = self.proj(image_embeds.to(proj_dtype)).to(input_dtype)
+
+ embeds = torch.cat([text_embeds, image_embeds], dim=1).contiguous()
+
+ if self.use_positional_embeddings or self.use_learned_positional_embeddings:
+ text_seq_length = text_embeds.shape[1]
+ num_image_patches = image_embeds.shape[1]
+
+ if self.use_learned_positional_embeddings:
+ image_pos = self.pos_embedding[
+ :, self.max_text_seq_length:self.max_text_seq_length + num_image_patches
+ ].to(device=embeds.device, dtype=embeds.dtype)
+ else:
+ image_pos = get_3d_sincos_pos_embed(
+ self.dim,
+ (width // self.patch_size, height // self.patch_size),
+ num_image_patches // ((height // self.patch_size) * (width // self.patch_size)),
+ self.spatial_interpolation_scale,
+ self.temporal_interpolation_scale,
+ device=embeds.device,
+ ).reshape(1, num_image_patches, self.dim).to(dtype=embeds.dtype)
+
+ # Build joint: zeros for text + sincos for image
+ joint_pos = torch.zeros(1, text_seq_length + num_image_patches, self.dim, device=embeds.device, dtype=embeds.dtype)
+ joint_pos[:, text_seq_length:] = image_pos
+ embeds = embeds + joint_pos
+
+ return embeds
+
+
+class CogVideoXLayerNormZero(nn.Module):
+ def __init__(self, time_dim, dim, elementwise_affine=True, eps=1e-5, bias=True,
+ device=None, dtype=None, operations=None):
+ super().__init__()
+ self.silu = nn.SiLU()
+ self.linear = operations.Linear(time_dim, 6 * dim, bias=bias, device=device, dtype=dtype)
+ self.norm = operations.LayerNorm(dim, eps=eps, elementwise_affine=elementwise_affine, device=device, dtype=dtype)
+
+ def forward(self, hidden_states, encoder_hidden_states, temb):
+ shift, scale, gate, enc_shift, enc_scale, enc_gate = self.linear(self.silu(temb)).chunk(6, dim=1)
+ hidden_states = self.norm(hidden_states) * (1 + scale)[:, None, :] + shift[:, None, :]
+ encoder_hidden_states = self.norm(encoder_hidden_states) * (1 + enc_scale)[:, None, :] + enc_shift[:, None, :]
+ return hidden_states, encoder_hidden_states, gate[:, None, :], enc_gate[:, None, :]
+
+
+class CogVideoXAdaLayerNorm(nn.Module):
+ def __init__(self, time_dim, dim, elementwise_affine=True, eps=1e-5,
+ device=None, dtype=None, operations=None):
+ super().__init__()
+ self.silu = nn.SiLU()
+ self.linear = operations.Linear(time_dim, 2 * dim, device=device, dtype=dtype)
+ self.norm = operations.LayerNorm(dim, eps=eps, elementwise_affine=elementwise_affine, device=device, dtype=dtype)
+
+ def forward(self, x, temb):
+ temb = self.linear(self.silu(temb))
+ shift, scale = temb.chunk(2, dim=1)
+ x = self.norm(x) * (1 + scale)[:, None, :] + shift[:, None, :]
+ return x
+
+
+class CogVideoXBlock(nn.Module):
+ def __init__(self, dim, num_heads, head_dim, time_dim,
+ eps=1e-5, ff_inner_dim=None, ff_bias=True,
+ device=None, dtype=None, operations=None):
+ super().__init__()
+ self.dim = dim
+ self.num_heads = num_heads
+ self.head_dim = head_dim
+
+ self.norm1 = CogVideoXLayerNormZero(time_dim, dim, eps=eps, device=device, dtype=dtype, operations=operations)
+
+ # Self-attention (joint text + latent)
+ self.q = operations.Linear(dim, dim, bias=True, device=device, dtype=dtype)
+ self.k = operations.Linear(dim, dim, bias=True, device=device, dtype=dtype)
+ self.v = operations.Linear(dim, dim, bias=True, device=device, dtype=dtype)
+ self.norm_q = operations.LayerNorm(head_dim, eps=1e-6, elementwise_affine=True, device=device, dtype=dtype)
+ self.norm_k = operations.LayerNorm(head_dim, eps=1e-6, elementwise_affine=True, device=device, dtype=dtype)
+ self.attn_out = operations.Linear(dim, dim, bias=True, device=device, dtype=dtype)
+
+ self.norm2 = CogVideoXLayerNormZero(time_dim, dim, eps=eps, device=device, dtype=dtype, operations=operations)
+
+ # Feed-forward (GELU approximate)
+ inner_dim = ff_inner_dim or dim * 4
+ self.ff_proj = operations.Linear(dim, inner_dim, bias=ff_bias, device=device, dtype=dtype)
+ self.ff_out = operations.Linear(inner_dim, dim, bias=ff_bias, device=device, dtype=dtype)
+
+ def forward(self, hidden_states, encoder_hidden_states, temb, image_rotary_emb=None, transformer_options=None):
+ if transformer_options is None:
+ transformer_options = {}
+ text_seq_length = encoder_hidden_states.size(1)
+
+ # Norm & modulate
+ norm_hidden, norm_encoder, gate_msa, enc_gate_msa = self.norm1(hidden_states, encoder_hidden_states, temb)
+
+ # Joint self-attention
+ qkv_input = torch.cat([norm_encoder, norm_hidden], dim=1)
+ b, s, _ = qkv_input.shape
+ n, d = self.num_heads, self.head_dim
+
+ q = self.q(qkv_input).view(b, s, n, d)
+ k = self.k(qkv_input).view(b, s, n, d)
+ v = self.v(qkv_input)
+
+ q = self.norm_q(q).view(b, s, n, d)
+ k = self.norm_k(k).view(b, s, n, d)
+
+ # Apply rotary embeddings to image tokens only (diffusers format: [B, heads, seq, head_dim])
+ if image_rotary_emb is not None:
+ q_img = q[:, text_seq_length:].transpose(1, 2) # [B, heads, img_seq, head_dim]
+ k_img = k[:, text_seq_length:].transpose(1, 2)
+ q_img = apply_rotary_emb(q_img, image_rotary_emb)
+ k_img = apply_rotary_emb(k_img, image_rotary_emb)
+ q = torch.cat([q[:, :text_seq_length], q_img.transpose(1, 2)], dim=1)
+ k = torch.cat([k[:, :text_seq_length], k_img.transpose(1, 2)], dim=1)
+
+ attn_out = optimized_attention(
+ q.reshape(b, s, n * d),
+ k.reshape(b, s, n * d),
+ v,
+ heads=self.num_heads,
+ transformer_options=transformer_options,
+ )
+
+ attn_out = self.attn_out(attn_out)
+
+ attn_encoder, attn_hidden = attn_out.split([text_seq_length, s - text_seq_length], dim=1)
+
+ hidden_states = hidden_states + gate_msa * attn_hidden
+ encoder_hidden_states = encoder_hidden_states + enc_gate_msa * attn_encoder
+
+ # Norm & modulate for FF
+ norm_hidden, norm_encoder, gate_ff, enc_gate_ff = self.norm2(hidden_states, encoder_hidden_states, temb)
+
+ # Feed-forward (GELU on concatenated text + latent)
+ ff_input = torch.cat([norm_encoder, norm_hidden], dim=1)
+ ff_output = self.ff_out(F.gelu(self.ff_proj(ff_input), approximate="tanh"))
+
+ hidden_states = hidden_states + gate_ff * ff_output[:, text_seq_length:]
+ encoder_hidden_states = encoder_hidden_states + enc_gate_ff * ff_output[:, :text_seq_length]
+
+ return hidden_states, encoder_hidden_states
+
+
+class CogVideoXTransformer3DModel(nn.Module):
+ def __init__(self,
+ num_attention_heads=30,
+ attention_head_dim=64,
+ in_channels=16,
+ out_channels=16,
+ flip_sin_to_cos=True,
+ freq_shift=0,
+ time_embed_dim=512,
+ ofs_embed_dim=None,
+ text_embed_dim=4096,
+ num_layers=30,
+ dropout=0.0,
+ attention_bias=True,
+ sample_width=90,
+ sample_height=60,
+ sample_frames=49,
+ patch_size=2,
+ patch_size_t=None,
+ temporal_compression_ratio=4,
+ max_text_seq_length=226,
+ spatial_interpolation_scale=1.875,
+ temporal_interpolation_scale=1.0,
+ use_rotary_positional_embeddings=False,
+ use_learned_positional_embeddings=False,
+ patch_bias=True,
+ image_model=None,
+ device=None,
+ dtype=None,
+ operations=None,
+ ):
+ super().__init__()
+ self.dtype = dtype
+ dim = num_attention_heads * attention_head_dim
+ self.dim = dim
+ self.num_attention_heads = num_attention_heads
+ self.attention_head_dim = attention_head_dim
+ self.in_channels = in_channels
+ self.out_channels = out_channels
+ self.patch_size = patch_size
+ self.patch_size_t = patch_size_t
+ self.max_text_seq_length = max_text_seq_length
+ self.use_rotary_positional_embeddings = use_rotary_positional_embeddings
+
+ # 1. Patch embedding
+ self.patch_embed = CogVideoXPatchEmbed(
+ patch_size=patch_size,
+ patch_size_t=patch_size_t,
+ in_channels=in_channels,
+ dim=dim,
+ text_dim=text_embed_dim,
+ bias=patch_bias,
+ sample_width=sample_width,
+ sample_height=sample_height,
+ sample_frames=sample_frames,
+ temporal_compression_ratio=temporal_compression_ratio,
+ max_text_seq_length=max_text_seq_length,
+ spatial_interpolation_scale=spatial_interpolation_scale,
+ temporal_interpolation_scale=temporal_interpolation_scale,
+ use_positional_embeddings=not use_rotary_positional_embeddings,
+ use_learned_positional_embeddings=use_learned_positional_embeddings,
+ device=device, dtype=torch.float32, operations=operations,
+ )
+
+ # 2. Time embedding
+ self.time_proj_dim = dim
+ self.time_proj_flip = flip_sin_to_cos
+ self.time_proj_shift = freq_shift
+ self.time_embedding_linear_1 = operations.Linear(dim, time_embed_dim, device=device, dtype=dtype)
+ self.time_embedding_act = nn.SiLU()
+ self.time_embedding_linear_2 = operations.Linear(time_embed_dim, time_embed_dim, device=device, dtype=dtype)
+
+ # Optional OFS embedding (CogVideoX 1.5 I2V)
+ self.ofs_proj_dim = ofs_embed_dim
+ if ofs_embed_dim:
+ self.ofs_embedding_linear_1 = operations.Linear(ofs_embed_dim, ofs_embed_dim, device=device, dtype=dtype)
+ self.ofs_embedding_act = nn.SiLU()
+ self.ofs_embedding_linear_2 = operations.Linear(ofs_embed_dim, ofs_embed_dim, device=device, dtype=dtype)
+ else:
+ self.ofs_embedding_linear_1 = None
+
+ # 3. Transformer blocks
+ self.blocks = nn.ModuleList([
+ CogVideoXBlock(
+ dim=dim,
+ num_heads=num_attention_heads,
+ head_dim=attention_head_dim,
+ time_dim=time_embed_dim,
+ eps=1e-5,
+ device=device, dtype=dtype, operations=operations,
+ )
+ for _ in range(num_layers)
+ ])
+
+ self.norm_final = operations.LayerNorm(dim, eps=1e-5, elementwise_affine=True, device=device, dtype=dtype)
+
+ # 4. Output
+ self.norm_out = CogVideoXAdaLayerNorm(
+ time_dim=time_embed_dim, dim=dim, eps=1e-5,
+ device=device, dtype=dtype, operations=operations,
+ )
+
+ if patch_size_t is None:
+ output_dim = patch_size * patch_size * out_channels
+ else:
+ output_dim = patch_size * patch_size * patch_size_t * out_channels
+
+ self.proj_out = operations.Linear(dim, output_dim, device=device, dtype=dtype)
+
+ self.spatial_interpolation_scale = spatial_interpolation_scale
+ self.temporal_interpolation_scale = temporal_interpolation_scale
+ self.temporal_compression_ratio = temporal_compression_ratio
+
+ def forward(self, x, timestep, context, ofs=None, transformer_options=None, **kwargs):
+ 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, ofs, transformer_options, **kwargs)
+
+ def _forward(self, x, timestep, context, ofs=None, transformer_options=None, **kwargs):
+ if transformer_options is None:
+ transformer_options = {}
+ # ComfyUI passes [B, C, T, H, W]
+ batch_size, channels, t, h, w = x.shape
+
+ # Pad to patch size (temporal + spatial), same pattern as WAN
+ p_t = self.patch_size_t if self.patch_size_t is not None else 1
+ x = comfy.ldm.common_dit.pad_to_patch_size(x, (p_t, self.patch_size, self.patch_size))
+
+ # CogVideoX expects [B, T, C, H, W]
+ x = x.permute(0, 2, 1, 3, 4)
+ batch_size, num_frames, channels, height, width = x.shape
+
+ # Time embedding
+ t_emb = get_timestep_embedding(timestep, self.time_proj_dim, self.time_proj_flip, self.time_proj_shift)
+ t_emb = t_emb.to(dtype=x.dtype)
+ emb = self.time_embedding_linear_2(self.time_embedding_act(self.time_embedding_linear_1(t_emb)))
+
+ if self.ofs_embedding_linear_1 is not None and ofs is not None:
+ ofs_emb = get_timestep_embedding(ofs, self.ofs_proj_dim, self.time_proj_flip, self.time_proj_shift)
+ ofs_emb = ofs_emb.to(dtype=x.dtype)
+ ofs_emb = self.ofs_embedding_linear_2(self.ofs_embedding_act(self.ofs_embedding_linear_1(ofs_emb)))
+ emb = emb + ofs_emb
+
+ # Patch embedding
+ hidden_states = self.patch_embed(context, x)
+
+ text_seq_length = context.shape[1]
+ encoder_hidden_states = hidden_states[:, :text_seq_length]
+ hidden_states = hidden_states[:, text_seq_length:]
+
+ # Rotary embeddings (if used)
+ image_rotary_emb = None
+ if self.use_rotary_positional_embeddings:
+ post_patch_height = height // self.patch_size
+ post_patch_width = width // self.patch_size
+ if self.patch_size_t is None:
+ post_time = num_frames
+ else:
+ post_time = num_frames // self.patch_size_t
+ image_rotary_emb = self._get_rotary_emb(post_patch_height, post_patch_width, post_time, device=x.device)
+
+ # Transformer blocks
+ for i, block in enumerate(self.blocks):
+ hidden_states, encoder_hidden_states = block(
+ hidden_states=hidden_states,
+ encoder_hidden_states=encoder_hidden_states,
+ temb=emb,
+ image_rotary_emb=image_rotary_emb,
+ transformer_options=transformer_options,
+ )
+
+ hidden_states = self.norm_final(hidden_states)
+
+ # Output projection
+ hidden_states = self.norm_out(hidden_states, temb=emb)
+ hidden_states = self.proj_out(hidden_states)
+
+ # Unpatchify
+ p = self.patch_size
+ p_t = self.patch_size_t
+
+ if p_t is None:
+ output = hidden_states.reshape(batch_size, num_frames, height // p, width // p, -1, p, p)
+ output = output.permute(0, 1, 4, 2, 5, 3, 6).flatten(5, 6).flatten(3, 4)
+ else:
+ output = hidden_states.reshape(
+ batch_size, (num_frames + p_t - 1) // p_t, height // p, width // p, -1, p_t, p, p
+ )
+ output = output.permute(0, 1, 5, 4, 2, 6, 3, 7).flatten(6, 7).flatten(4, 5).flatten(1, 2)
+
+ # Back to ComfyUI format [B, C, T, H, W] and crop padding
+ output = output.permute(0, 2, 1, 3, 4)[:, :, :t, :h, :w]
+ return output
+
+ def _get_rotary_emb(self, h, w, t, device):
+ """Compute CogVideoX 3D rotary positional embeddings.
+
+ For CogVideoX 1.5 (patch_size_t != None): uses "slice" mode — grid positions
+ are integer arange computed at max_size, then sliced to actual size.
+ For CogVideoX 1.0 (patch_size_t == None): uses "linspace" mode with crop coords
+ scaled by spatial_interpolation_scale.
+ """
+ d = self.attention_head_dim
+ dim_t = d // 4
+ dim_h = d // 8 * 3
+ dim_w = d // 8 * 3
+
+ if self.patch_size_t is not None:
+ # CogVideoX 1.5: "slice" mode — positions are simple integer indices
+ # Compute at max(sample_size, actual_size) then slice to actual
+ base_h = self.patch_embed.sample_height // self.patch_size
+ base_w = self.patch_embed.sample_width // self.patch_size
+ max_h = max(base_h, h)
+ max_w = max(base_w, w)
+
+ grid_h = torch.arange(max_h, device=device, dtype=torch.float32)
+ grid_w = torch.arange(max_w, device=device, dtype=torch.float32)
+ grid_t = torch.arange(t, device=device, dtype=torch.float32)
+ else:
+ # CogVideoX 1.0: "linspace" mode with interpolation scale
+ grid_h = torch.linspace(0, h - 1, h, device=device, dtype=torch.float32) * self.spatial_interpolation_scale
+ grid_w = torch.linspace(0, w - 1, w, device=device, dtype=torch.float32) * self.spatial_interpolation_scale
+ grid_t = torch.arange(t, device=device, dtype=torch.float32)
+
+ freqs_t = _get_1d_rotary_pos_embed(dim_t, grid_t)
+ freqs_h = _get_1d_rotary_pos_embed(dim_h, grid_h)
+ freqs_w = _get_1d_rotary_pos_embed(dim_w, grid_w)
+
+ t_cos, t_sin = freqs_t
+ h_cos, h_sin = freqs_h
+ w_cos, w_sin = freqs_w
+
+ # Slice to actual size (for "slice" mode where grids may be larger)
+ t_cos, t_sin = t_cos[:t], t_sin[:t]
+ h_cos, h_sin = h_cos[:h], h_sin[:h]
+ w_cos, w_sin = w_cos[:w], w_sin[:w]
+
+ # Broadcast and concatenate into [T*H*W, head_dim]
+ t_cos = t_cos[:, None, None, :].expand(-1, h, w, -1)
+ t_sin = t_sin[:, None, None, :].expand(-1, h, w, -1)
+ h_cos = h_cos[None, :, None, :].expand(t, -1, w, -1)
+ h_sin = h_sin[None, :, None, :].expand(t, -1, w, -1)
+ w_cos = w_cos[None, None, :, :].expand(t, h, -1, -1)
+ w_sin = w_sin[None, None, :, :].expand(t, h, -1, -1)
+
+ cos = torch.cat([t_cos, h_cos, w_cos], dim=-1).reshape(t * h * w, -1)
+ sin = torch.cat([t_sin, h_sin, w_sin], dim=-1).reshape(t * h * w, -1)
+ return (cos, sin)
diff --git a/comfy/ldm/cogvideo/vae.py b/comfy/ldm/cogvideo/vae.py
new file mode 100644
index 000000000..d4e6f321e
--- /dev/null
+++ b/comfy/ldm/cogvideo/vae.py
@@ -0,0 +1,566 @@
+# CogVideoX VAE - ported to ComfyUI native ops
+# Architecture reference: diffusers AutoencoderKLCogVideoX
+# Style reference: comfy/ldm/wan/vae.py
+
+import numpy as np
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+import comfy.ops
+ops = comfy.ops.disable_weight_init
+
+
+class CausalConv3d(nn.Module):
+ """Causal 3D convolution with temporal padding.
+
+ Uses comfy.ops.Conv3d with autopad='causal_zero' fast path: when input has
+ a single temporal frame and no cache, the 3D conv weight is sliced to act
+ as a 2D conv, avoiding computation on zero-padded temporal dimensions.
+ """
+ def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1, pad_mode="constant"):
+ super().__init__()
+ if isinstance(kernel_size, int):
+ kernel_size = (kernel_size,) * 3
+
+ time_kernel, height_kernel, width_kernel = kernel_size
+ self.time_kernel_size = time_kernel
+ self.pad_mode = pad_mode
+
+ height_pad = (height_kernel - 1) // 2
+ width_pad = (width_kernel - 1) // 2
+ self.time_causal_padding = (width_pad, width_pad, height_pad, height_pad, time_kernel - 1, 0)
+
+ stride = stride if isinstance(stride, tuple) else (stride, 1, 1)
+ dilation = (dilation, 1, 1)
+ self.conv = ops.Conv3d(
+ in_channels, out_channels, kernel_size,
+ stride=stride, dilation=dilation,
+ padding=(0, height_pad, width_pad),
+ )
+
+ def forward(self, x, conv_cache=None):
+ if self.pad_mode == "replicate":
+ x = F.pad(x, self.time_causal_padding, mode="replicate")
+ conv_cache = None
+ else:
+ kernel_t = self.time_kernel_size
+ if kernel_t > 1:
+ if conv_cache is None and x.shape[2] == 1:
+ # Fast path: single frame, no cache. All temporal padding
+ # frames are copies of the input (replicate-style), so the
+ # 3D conv reduces to a 2D conv with summed temporal kernel.
+ w = comfy.ops.cast_to_input(self.conv.weight, x)
+ b = comfy.ops.cast_to_input(self.conv.bias, x) if self.conv.bias is not None else None
+ w2d = w.sum(dim=2, keepdim=True)
+ out = F.conv3d(x, w2d, b,
+ self.conv.stride, self.conv.padding,
+ self.conv.dilation, self.conv.groups)
+ return out, None
+ cached = [conv_cache] if conv_cache is not None else [x[:, :, :1]] * (kernel_t - 1)
+ x = torch.cat(cached + [x], dim=2)
+ conv_cache = x[:, :, -self.time_kernel_size + 1:].clone() if self.time_kernel_size > 1 else None
+
+ out = self.conv(x)
+ return out, conv_cache
+
+
+def _interpolate_zq(zq, target_size):
+ """Interpolate latent z to target (T, H, W), matching CogVideoX's first-frame-special handling."""
+ t = target_size[0]
+ if t > 1 and t % 2 == 1:
+ z_first = F.interpolate(zq[:, :, :1], size=(1, target_size[1], target_size[2]))
+ z_rest = F.interpolate(zq[:, :, 1:], size=(t - 1, target_size[1], target_size[2]))
+ return torch.cat([z_first, z_rest], dim=2)
+ return F.interpolate(zq, size=target_size)
+
+
+class SpatialNorm3D(nn.Module):
+ """Spatially conditioned normalization."""
+ def __init__(self, f_channels, zq_channels, groups=32):
+ super().__init__()
+ self.norm_layer = ops.GroupNorm(num_channels=f_channels, num_groups=groups, eps=1e-6, affine=True)
+ self.conv_y = CausalConv3d(zq_channels, f_channels, kernel_size=1, stride=1)
+ self.conv_b = CausalConv3d(zq_channels, f_channels, kernel_size=1, stride=1)
+
+ def forward(self, f, zq, conv_cache=None):
+ new_cache = {}
+ conv_cache = conv_cache or {}
+
+ if zq.shape[-3:] != f.shape[-3:]:
+ zq = _interpolate_zq(zq, f.shape[-3:])
+
+ conv_y, new_cache["conv_y"] = self.conv_y(zq, conv_cache=conv_cache.get("conv_y"))
+ conv_b, new_cache["conv_b"] = self.conv_b(zq, conv_cache=conv_cache.get("conv_b"))
+
+ return self.norm_layer(f) * conv_y + conv_b, new_cache
+
+
+class ResnetBlock3D(nn.Module):
+ """3D ResNet block with optional spatial norm."""
+ def __init__(self, in_channels, out_channels=None, temb_channels=512, groups=32,
+ eps=1e-6, act_fn="silu", spatial_norm_dim=None, pad_mode="first"):
+ super().__init__()
+ out_channels = out_channels or in_channels
+ self.in_channels = in_channels
+ self.out_channels = out_channels
+ self.spatial_norm_dim = spatial_norm_dim
+
+ if act_fn == "silu":
+ self.nonlinearity = nn.SiLU()
+ elif act_fn == "swish":
+ self.nonlinearity = nn.SiLU()
+ else:
+ self.nonlinearity = nn.SiLU()
+
+ if spatial_norm_dim is None:
+ self.norm1 = ops.GroupNorm(num_channels=in_channels, num_groups=groups, eps=eps)
+ self.norm2 = ops.GroupNorm(num_channels=out_channels, num_groups=groups, eps=eps)
+ else:
+ self.norm1 = SpatialNorm3D(in_channels, spatial_norm_dim, groups=groups)
+ self.norm2 = SpatialNorm3D(out_channels, spatial_norm_dim, groups=groups)
+
+ self.conv1 = CausalConv3d(in_channels, out_channels, kernel_size=3, pad_mode=pad_mode)
+
+ if temb_channels > 0:
+ self.temb_proj = ops.Linear(temb_channels, out_channels)
+
+ self.conv2 = CausalConv3d(out_channels, out_channels, kernel_size=3, pad_mode=pad_mode)
+
+ if in_channels != out_channels:
+ self.conv_shortcut = ops.Conv3d(in_channels, out_channels, kernel_size=1, stride=1, padding=0)
+ else:
+ self.conv_shortcut = None
+
+ def forward(self, x, temb=None, zq=None, conv_cache=None):
+ new_cache = {}
+ conv_cache = conv_cache or {}
+ residual = x
+
+ if zq is not None:
+ x, new_cache["norm1"] = self.norm1(x, zq, conv_cache=conv_cache.get("norm1"))
+ else:
+ x = self.norm1(x)
+
+ x = self.nonlinearity(x)
+ x, new_cache["conv1"] = self.conv1(x, conv_cache=conv_cache.get("conv1"))
+
+ if temb is not None and hasattr(self, "temb_proj"):
+ x = x + self.temb_proj(self.nonlinearity(temb))[:, :, None, None, None]
+
+ if zq is not None:
+ x, new_cache["norm2"] = self.norm2(x, zq, conv_cache=conv_cache.get("norm2"))
+ else:
+ x = self.norm2(x)
+
+ x = self.nonlinearity(x)
+ x, new_cache["conv2"] = self.conv2(x, conv_cache=conv_cache.get("conv2"))
+
+ if self.conv_shortcut is not None:
+ residual = self.conv_shortcut(residual)
+
+ return x + residual, new_cache
+
+
+class Downsample3D(nn.Module):
+ """3D downsampling with optional temporal compression."""
+ def __init__(self, in_channels, out_channels, kernel_size=3, stride=2, padding=0, compress_time=False):
+ super().__init__()
+ self.conv = ops.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=padding)
+ self.compress_time = compress_time
+
+ def forward(self, x):
+ if self.compress_time:
+ b, c, t, h, w = x.shape
+ x = x.permute(0, 3, 4, 1, 2).reshape(b * h * w, c, t)
+ if t % 2 == 1:
+ x_first, x_rest = x[..., 0], x[..., 1:]
+ if x_rest.shape[-1] > 0:
+ x_rest = F.avg_pool1d(x_rest, kernel_size=2, stride=2)
+ x = torch.cat([x_first[..., None], x_rest], dim=-1)
+ x = x.reshape(b, h, w, c, x.shape[-1]).permute(0, 3, 4, 1, 2)
+ else:
+ x = F.avg_pool1d(x, kernel_size=2, stride=2)
+ x = x.reshape(b, h, w, c, x.shape[-1]).permute(0, 3, 4, 1, 2)
+
+ pad = (0, 1, 0, 1)
+ x = F.pad(x, pad, mode="constant", value=0)
+ b, c, t, h, w = x.shape
+ x = x.permute(0, 2, 1, 3, 4).reshape(b * t, c, h, w)
+ x = self.conv(x)
+ x = x.reshape(b, t, x.shape[1], x.shape[2], x.shape[3]).permute(0, 2, 1, 3, 4)
+ return x
+
+
+class Upsample3D(nn.Module):
+ """3D upsampling with optional temporal decompression."""
+ def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1, compress_time=False):
+ super().__init__()
+ self.conv = ops.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=padding)
+ self.compress_time = compress_time
+
+ def forward(self, x):
+ if self.compress_time:
+ if x.shape[2] > 1 and x.shape[2] % 2 == 1:
+ x_first, x_rest = x[:, :, 0], x[:, :, 1:]
+ x_first = F.interpolate(x_first, scale_factor=2.0)
+ x_rest = F.interpolate(x_rest, scale_factor=2.0)
+ x = torch.cat([x_first[:, :, None, :, :], x_rest], dim=2)
+ elif x.shape[2] > 1:
+ x = F.interpolate(x, scale_factor=2.0)
+ else:
+ x = x.squeeze(2)
+ x = F.interpolate(x, scale_factor=2.0)
+ x = x[:, :, None, :, :]
+ else:
+ b, c, t, h, w = x.shape
+ x = x.permute(0, 2, 1, 3, 4).reshape(b * t, c, h, w)
+ x = F.interpolate(x, scale_factor=2.0)
+ x = x.reshape(b, t, c, *x.shape[2:]).permute(0, 2, 1, 3, 4)
+
+ b, c, t, h, w = x.shape
+ x = x.permute(0, 2, 1, 3, 4).reshape(b * t, c, h, w)
+ x = self.conv(x)
+ x = x.reshape(b, t, *x.shape[1:]).permute(0, 2, 1, 3, 4)
+ return x
+
+
+class DownBlock3D(nn.Module):
+ def __init__(self, in_channels, out_channels, temb_channels=0, num_layers=1,
+ eps=1e-6, act_fn="silu", groups=32, add_downsample=True,
+ compress_time=False, pad_mode="first"):
+ super().__init__()
+ self.resnets = nn.ModuleList([
+ ResnetBlock3D(
+ in_channels=in_channels if i == 0 else out_channels,
+ out_channels=out_channels,
+ temb_channels=temb_channels,
+ groups=groups, eps=eps, act_fn=act_fn, pad_mode=pad_mode,
+ )
+ for i in range(num_layers)
+ ])
+ self.downsamplers = nn.ModuleList([Downsample3D(out_channels, out_channels, compress_time=compress_time)]) if add_downsample else None
+
+ def forward(self, x, temb=None, zq=None, conv_cache=None):
+ new_cache = {}
+ conv_cache = conv_cache or {}
+ for i, resnet in enumerate(self.resnets):
+ x, new_cache[f"resnet_{i}"] = resnet(x, temb, zq, conv_cache=conv_cache.get(f"resnet_{i}"))
+ if self.downsamplers is not None:
+ for ds in self.downsamplers:
+ x = ds(x)
+ return x, new_cache
+
+
+class MidBlock3D(nn.Module):
+ def __init__(self, in_channels, temb_channels=0, num_layers=1,
+ eps=1e-6, act_fn="silu", groups=32, spatial_norm_dim=None, pad_mode="first"):
+ super().__init__()
+ self.resnets = nn.ModuleList([
+ ResnetBlock3D(
+ in_channels=in_channels, out_channels=in_channels,
+ temb_channels=temb_channels, groups=groups, eps=eps,
+ act_fn=act_fn, spatial_norm_dim=spatial_norm_dim, pad_mode=pad_mode,
+ )
+ for _ in range(num_layers)
+ ])
+
+ def forward(self, x, temb=None, zq=None, conv_cache=None):
+ new_cache = {}
+ conv_cache = conv_cache or {}
+ for i, resnet in enumerate(self.resnets):
+ x, new_cache[f"resnet_{i}"] = resnet(x, temb, zq, conv_cache=conv_cache.get(f"resnet_{i}"))
+ return x, new_cache
+
+
+class UpBlock3D(nn.Module):
+ def __init__(self, in_channels, out_channels, temb_channels=0, num_layers=1,
+ eps=1e-6, act_fn="silu", groups=32, spatial_norm_dim=16,
+ add_upsample=True, compress_time=False, pad_mode="first"):
+ super().__init__()
+ self.resnets = nn.ModuleList([
+ ResnetBlock3D(
+ in_channels=in_channels if i == 0 else out_channels,
+ out_channels=out_channels,
+ temb_channels=temb_channels, groups=groups, eps=eps,
+ act_fn=act_fn, spatial_norm_dim=spatial_norm_dim, pad_mode=pad_mode,
+ )
+ for i in range(num_layers)
+ ])
+ self.upsamplers = nn.ModuleList([Upsample3D(out_channels, out_channels, compress_time=compress_time)]) if add_upsample else None
+
+ def forward(self, x, temb=None, zq=None, conv_cache=None):
+ new_cache = {}
+ conv_cache = conv_cache or {}
+ for i, resnet in enumerate(self.resnets):
+ x, new_cache[f"resnet_{i}"] = resnet(x, temb, zq, conv_cache=conv_cache.get(f"resnet_{i}"))
+ if self.upsamplers is not None:
+ for us in self.upsamplers:
+ x = us(x)
+ return x, new_cache
+
+
+class Encoder3D(nn.Module):
+ def __init__(self, in_channels=3, out_channels=16,
+ block_out_channels=(128, 256, 256, 512),
+ layers_per_block=3, act_fn="silu",
+ eps=1e-6, groups=32, pad_mode="first",
+ temporal_compression_ratio=4):
+ super().__init__()
+ temporal_compress_level = int(np.log2(temporal_compression_ratio))
+
+ self.conv_in = CausalConv3d(in_channels, block_out_channels[0], kernel_size=3, pad_mode=pad_mode)
+
+ self.down_blocks = nn.ModuleList()
+ output_channel = block_out_channels[0]
+ for i in range(len(block_out_channels)):
+ input_channel = output_channel
+ output_channel = block_out_channels[i]
+ is_final = i == len(block_out_channels) - 1
+ compress_time = i < temporal_compress_level
+
+ self.down_blocks.append(DownBlock3D(
+ in_channels=input_channel, out_channels=output_channel,
+ temb_channels=0, num_layers=layers_per_block,
+ eps=eps, act_fn=act_fn, groups=groups,
+ add_downsample=not is_final, compress_time=compress_time,
+ ))
+
+ self.mid_block = MidBlock3D(
+ in_channels=block_out_channels[-1], temb_channels=0,
+ num_layers=2, eps=eps, act_fn=act_fn, groups=groups, pad_mode=pad_mode,
+ )
+
+ self.norm_out = ops.GroupNorm(groups, block_out_channels[-1], eps=1e-6)
+ self.conv_act = nn.SiLU()
+ self.conv_out = CausalConv3d(block_out_channels[-1], 2 * out_channels, kernel_size=3, pad_mode=pad_mode)
+
+ def forward(self, x, conv_cache=None):
+ new_cache = {}
+ conv_cache = conv_cache or {}
+
+ x, new_cache["conv_in"] = self.conv_in(x, conv_cache=conv_cache.get("conv_in"))
+
+ for i, block in enumerate(self.down_blocks):
+ key = f"down_block_{i}"
+ x, new_cache[key] = block(x, None, None, conv_cache.get(key))
+
+ x, new_cache["mid_block"] = self.mid_block(x, None, None, conv_cache=conv_cache.get("mid_block"))
+
+ x = self.norm_out(x)
+ x = self.conv_act(x)
+ x, new_cache["conv_out"] = self.conv_out(x, conv_cache=conv_cache.get("conv_out"))
+
+ return x, new_cache
+
+
+class Decoder3D(nn.Module):
+ def __init__(self, in_channels=16, out_channels=3,
+ block_out_channels=(128, 256, 256, 512),
+ layers_per_block=3, act_fn="silu",
+ eps=1e-6, groups=32, pad_mode="first",
+ temporal_compression_ratio=4):
+ super().__init__()
+ reversed_channels = list(reversed(block_out_channels))
+ temporal_compress_level = int(np.log2(temporal_compression_ratio))
+
+ self.conv_in = CausalConv3d(in_channels, reversed_channels[0], kernel_size=3, pad_mode=pad_mode)
+
+ self.mid_block = MidBlock3D(
+ in_channels=reversed_channels[0], temb_channels=0,
+ num_layers=2, eps=eps, act_fn=act_fn, groups=groups,
+ spatial_norm_dim=in_channels, pad_mode=pad_mode,
+ )
+
+ self.up_blocks = nn.ModuleList()
+ output_channel = reversed_channels[0]
+ for i in range(len(block_out_channels)):
+ prev_channel = output_channel
+ output_channel = reversed_channels[i]
+ is_final = i == len(block_out_channels) - 1
+ compress_time = i < temporal_compress_level
+
+ self.up_blocks.append(UpBlock3D(
+ in_channels=prev_channel, out_channels=output_channel,
+ temb_channels=0, num_layers=layers_per_block + 1,
+ eps=eps, act_fn=act_fn, groups=groups,
+ spatial_norm_dim=in_channels,
+ add_upsample=not is_final, compress_time=compress_time,
+ ))
+
+ self.norm_out = SpatialNorm3D(reversed_channels[-1], in_channels, groups=groups)
+ self.conv_act = nn.SiLU()
+ self.conv_out = CausalConv3d(reversed_channels[-1], out_channels, kernel_size=3, pad_mode=pad_mode)
+
+ def forward(self, sample, conv_cache=None):
+ new_cache = {}
+ conv_cache = conv_cache or {}
+
+ x, new_cache["conv_in"] = self.conv_in(sample, conv_cache=conv_cache.get("conv_in"))
+
+ x, new_cache["mid_block"] = self.mid_block(x, None, sample, conv_cache=conv_cache.get("mid_block"))
+
+ for i, block in enumerate(self.up_blocks):
+ key = f"up_block_{i}"
+ x, new_cache[key] = block(x, None, sample, conv_cache=conv_cache.get(key))
+
+ x, new_cache["norm_out"] = self.norm_out(x, sample, conv_cache=conv_cache.get("norm_out"))
+ x = self.conv_act(x)
+ x, new_cache["conv_out"] = self.conv_out(x, conv_cache=conv_cache.get("conv_out"))
+
+ return x, new_cache
+
+
+
+class AutoencoderKLCogVideoX(nn.Module):
+ """CogVideoX VAE. Spatial tiling/slicing handled by ComfyUI's VAE wrapper.
+
+ Uses rolling temporal decode: conv_in + mid_block + temporal up_blocks run
+ on the full (low-res) tensor, then the expensive spatial-only up_blocks +
+ norm_out + conv_out are processed in small temporal chunks with conv_cache
+ carrying causal state between chunks. This keeps peak VRAM proportional to
+ chunk_size rather than total frame count.
+ """
+
+ def __init__(self,
+ in_channels=3, out_channels=3,
+ block_out_channels=(128, 256, 256, 512),
+ latent_channels=16, layers_per_block=3,
+ act_fn="silu", eps=1e-6, groups=32,
+ temporal_compression_ratio=4,
+ ):
+ super().__init__()
+ self.latent_channels = latent_channels
+ self.temporal_compression_ratio = temporal_compression_ratio
+
+ self.encoder = Encoder3D(
+ in_channels=in_channels, out_channels=latent_channels,
+ block_out_channels=block_out_channels, layers_per_block=layers_per_block,
+ act_fn=act_fn, eps=eps, groups=groups,
+ temporal_compression_ratio=temporal_compression_ratio,
+ )
+ self.decoder = Decoder3D(
+ in_channels=latent_channels, out_channels=out_channels,
+ block_out_channels=block_out_channels, layers_per_block=layers_per_block,
+ act_fn=act_fn, eps=eps, groups=groups,
+ temporal_compression_ratio=temporal_compression_ratio,
+ )
+
+ self.num_latent_frames_batch_size = 2
+ self.num_sample_frames_batch_size = 8
+
+ def encode(self, x):
+ t = x.shape[2]
+ frame_batch = self.num_sample_frames_batch_size
+ remainder = t % frame_batch
+ conv_cache = None
+ enc = []
+
+ # Process remainder frames first so only the first chunk can have an
+ # odd temporal dimension — where Downsample3D's first-frame-special
+ # handling in temporal compression is actually correct.
+ if remainder > 0:
+ chunk, conv_cache = self.encoder(x[:, :, :remainder], conv_cache=conv_cache)
+ enc.append(chunk.to(x.device))
+
+ for start in range(remainder, t, frame_batch):
+ chunk, conv_cache = self.encoder(x[:, :, start:start + frame_batch], conv_cache=conv_cache)
+ enc.append(chunk.to(x.device))
+
+ enc = torch.cat(enc, dim=2)
+ mean, _ = enc.chunk(2, dim=1)
+ return mean
+
+ def decode(self, z):
+ return self._decode_rolling(z)
+
+ def _decode_batched(self, z):
+ """Original batched decode - processes 2 latent frames through full decoder."""
+ t = z.shape[2]
+ frame_batch = self.num_latent_frames_batch_size
+ num_batches = max(t // frame_batch, 1)
+ conv_cache = None
+ dec = []
+ for i in range(num_batches):
+ remaining = t % frame_batch
+ start = frame_batch * i + (0 if i == 0 else remaining)
+ end = frame_batch * (i + 1) + remaining
+ chunk, conv_cache = self.decoder(z[:, :, start:end], conv_cache=conv_cache)
+ dec.append(chunk.cpu())
+ return torch.cat(dec, dim=2).to(z.device)
+
+ def _decode_rolling(self, z):
+ """Rolling decode - processes low-res layers on full tensor, then rolls
+ through expensive high-res layers in temporal chunks."""
+ decoder = self.decoder
+ device = z.device
+
+ # Determine which up_blocks have temporal upsample vs spatial-only.
+ # Temporal up_blocks are cheap (low res), spatial-only are expensive.
+ temporal_compress_level = int(np.log2(self.temporal_compression_ratio))
+ split_at = temporal_compress_level # first N up_blocks do temporal upsample
+
+ # Phase 1: conv_in + mid_block + temporal up_blocks on full tensor (low/medium res)
+ x, _ = decoder.conv_in(z)
+ x, _ = decoder.mid_block(x, None, z)
+
+ for i in range(split_at):
+ x, _ = decoder.up_blocks[i](x, None, z)
+
+ # Phase 2: remaining spatial-only up_blocks + norm_out + conv_out in temporal chunks
+ remaining_blocks = list(range(split_at, len(decoder.up_blocks)))
+ chunk_size = 4 # pixel frames per chunk through high-res layers
+ t_expanded = x.shape[2]
+
+ if t_expanded <= chunk_size or len(remaining_blocks) == 0:
+ # Small enough to process in one go
+ for i in remaining_blocks:
+ x, _ = decoder.up_blocks[i](x, None, z)
+ x, _ = decoder.norm_out(x, z)
+ x = decoder.conv_act(x)
+ x, _ = decoder.conv_out(x)
+ return x
+
+ # Expand z temporally once to match Phase 2's time dimension.
+ # z stays at latent spatial resolution so this is small (~16 MB vs ~1.3 GB
+ # for the old approach of pre-interpolating to every pixel resolution).
+ z_time_expanded = _interpolate_zq(z, (t_expanded, z.shape[3], z.shape[4]))
+
+ # Process in temporal chunks, interpolating spatially per-chunk to avoid
+ # allocating full [B, C, t_expanded, H, W] tensors at each resolution.
+ dec_out = []
+ conv_caches = {}
+
+ for chunk_start in range(0, t_expanded, chunk_size):
+ chunk_end = min(chunk_start + chunk_size, t_expanded)
+ x_chunk = x[:, :, chunk_start:chunk_end]
+ z_t_chunk = z_time_expanded[:, :, chunk_start:chunk_end]
+ z_spatial_cache = {}
+
+ for i in remaining_blocks:
+ block = decoder.up_blocks[i]
+ cache_key = f"up_block_{i}"
+ hw_key = (x_chunk.shape[3], x_chunk.shape[4])
+ if hw_key not in z_spatial_cache:
+ if z_t_chunk.shape[3] == hw_key[0] and z_t_chunk.shape[4] == hw_key[1]:
+ z_spatial_cache[hw_key] = z_t_chunk
+ else:
+ z_spatial_cache[hw_key] = F.interpolate(z_t_chunk, size=(z_t_chunk.shape[2], hw_key[0], hw_key[1]))
+ x_chunk, new_cache = block(x_chunk, None, z_spatial_cache[hw_key], conv_cache=conv_caches.get(cache_key))
+ conv_caches[cache_key] = new_cache
+
+ hw_key = (x_chunk.shape[3], x_chunk.shape[4])
+ if hw_key not in z_spatial_cache:
+ z_spatial_cache[hw_key] = F.interpolate(z_t_chunk, size=(z_t_chunk.shape[2], hw_key[0], hw_key[1]))
+ x_chunk, new_cache = decoder.norm_out(x_chunk, z_spatial_cache[hw_key], conv_cache=conv_caches.get("norm_out"))
+ conv_caches["norm_out"] = new_cache
+ x_chunk = decoder.conv_act(x_chunk)
+ x_chunk, new_cache = decoder.conv_out(x_chunk, conv_cache=conv_caches.get("conv_out"))
+ conv_caches["conv_out"] = new_cache
+
+ dec_out.append(x_chunk.cpu())
+ del z_spatial_cache
+
+ del x, z_time_expanded
+ return torch.cat(dec_out, dim=2).to(device)
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/lightricks/av_model.py b/comfy/ldm/lightricks/av_model.py
index 6f2ba41ef..bc09fb77e 100644
--- a/comfy/ldm/lightricks/av_model.py
+++ b/comfy/ldm/lightricks/av_model.py
@@ -16,31 +16,31 @@ from comfy.ldm.lightricks.model import (
from comfy.ldm.lightricks.symmetric_patchifier import AudioPatchifier
from comfy.ldm.lightricks.embeddings_connector import Embeddings1DConnector
import comfy.ldm.common_dit
+import comfy.model_prefetch
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):
@@ -715,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
@@ -907,9 +910,11 @@ class LTXAVModel(LTXVModel):
"""Process transformer blocks for LTXAV."""
patches_replace = transformer_options.get("patches_replace", {})
blocks_replace = patches_replace.get("dit", {})
+ prefetch_queue = comfy.model_prefetch.make_prefetch_queue(list(self.transformer_blocks), vx.device, transformer_options)
# Process transformer blocks
for i, block in enumerate(self.transformer_blocks):
+ comfy.model_prefetch.prefetch_queue_pop(prefetch_queue, vx.device, block)
if ("double_block", i) in blocks_replace:
def block_wrap(args):
@@ -982,6 +987,8 @@ class LTXAVModel(LTXVModel):
a_prompt_timestep=a_prompt_timestep,
)
+ comfy.model_prefetch.prefetch_queue_pop(prefetch_queue, vx.device, None)
+
return [vx, ax]
def _process_output(self, x, embedded_timestep, keyframe_idxs, **kwargs):
diff --git a/comfy/ldm/lightricks/model.py b/comfy/ldm/lightricks/model.py
index bfbc08357..e0a4a0f9b 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/modules/attention.py b/comfy/ldm/modules/attention.py
index b193fe5e8..a68cb8439 100644
--- a/comfy/ldm/modules/attention.py
+++ b/comfy/ldm/modules/attention.py
@@ -14,6 +14,8 @@ from .sub_quadratic_attention import efficient_dot_product_attention
from comfy import model_management
+TORCH_HAS_GQA = model_management.torch_version_numeric >= (2, 5)
+
if model_management.xformers_enabled():
import xformers
import xformers.ops
@@ -150,7 +152,12 @@ def attention_basic(q, k, v, heads, mask=None, attn_precision=None, skip_reshape
b, _, dim_head = q.shape
dim_head //= heads
- scale = dim_head ** -0.5
+ if kwargs.get("enable_gqa", False) and q.shape[-3] != k.shape[-3]:
+ n_rep = q.shape[-3] // k.shape[-3]
+ k = k.repeat_interleave(n_rep, dim=-3)
+ v = v.repeat_interleave(n_rep, dim=-3)
+
+ scale = kwargs.get("scale", dim_head ** -0.5)
h = heads
if skip_reshape:
@@ -219,6 +226,10 @@ def attention_sub_quad(query, key, value, heads, mask=None, attn_precision=None,
b, _, dim_head = query.shape
dim_head //= heads
+ if "scale" in kwargs:
+ # Pre-scale query to match requested scale (cancels internal 1/sqrt(dim_head))
+ query = query * (kwargs["scale"] * dim_head ** 0.5)
+
if skip_reshape:
query = query.reshape(b * heads, -1, dim_head)
value = value.reshape(b * heads, -1, dim_head)
@@ -290,7 +301,7 @@ def attention_split(q, k, v, heads, mask=None, attn_precision=None, skip_reshape
b, _, dim_head = q.shape
dim_head //= heads
- scale = dim_head ** -0.5
+ scale = kwargs.get("scale", dim_head ** -0.5)
if skip_reshape:
q, k, v = map(
@@ -500,8 +511,13 @@ def attention_pytorch(q, k, v, heads, mask=None, attn_precision=None, skip_resha
if mask.ndim == 3:
mask = mask.unsqueeze(1)
+ # Pass through extra SDPA kwargs (scale, enable_gqa) if provided
+ # enable_gqa requires PyTorch 2.5+; older versions use manual KV expansion above
+ sdpa_keys = ("scale", "enable_gqa") if TORCH_HAS_GQA else ("scale",)
+ sdpa_extra = {k: v for k, v in kwargs.items() if k in sdpa_keys}
+
if SDP_BATCH_LIMIT >= b:
- out = comfy.ops.scaled_dot_product_attention(q, k, v, attn_mask=mask, dropout_p=0.0, is_causal=False)
+ out = comfy.ops.scaled_dot_product_attention(q, k, v, attn_mask=mask, dropout_p=0.0, is_causal=False, **sdpa_extra)
if not skip_output_reshape:
out = (
out.transpose(1, 2).reshape(b, -1, heads * dim_head)
@@ -519,7 +535,7 @@ def attention_pytorch(q, k, v, heads, mask=None, attn_precision=None, skip_resha
k[i : i + SDP_BATCH_LIMIT],
v[i : i + SDP_BATCH_LIMIT],
attn_mask=m,
- dropout_p=0.0, is_causal=False
+ dropout_p=0.0, is_causal=False, **sdpa_extra
).transpose(1, 2).reshape(-1, q.shape[2], heads * dim_head)
return out
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..7fdc97871
--- /dev/null
+++ b/comfy/ldm/moge/geometry.py
@@ -0,0 +1,189 @@
+"""Pure-torch + scipy geometry helpers for MoGe inference and mesh export."""
+
+from __future__ import annotations
+
+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..6876c4af2
--- /dev/null
+++ b/comfy/ldm/moge/model.py
@@ -0,0 +1,347 @@
+"""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 __future__ import annotations
+
+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..235a59212
--- /dev/null
+++ b/comfy/ldm/moge/modules.py
@@ -0,0 +1,204 @@
+"""Building blocks for MoGe: residual conv stack, resamplers, MLP, DINOv2 encoder, v1 head."""
+
+from __future__ import annotations
+
+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..de53ebe68
--- /dev/null
+++ b/comfy/ldm/moge/panorama.py
@@ -0,0 +1,313 @@
+"""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 __future__ import annotations
+
+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/ar_model.py b/comfy/ldm/wan/ar_model.py
new file mode 100644
index 000000000..d72f53602
--- /dev/null
+++ b/comfy/ldm/wan/ar_model.py
@@ -0,0 +1,276 @@
+"""
+CausalWanModel: Wan 2.1 backbone with KV-cached causal self-attention for
+autoregressive (frame-by-frame) video generation via Causal Forcing.
+
+Weight-compatible with the standard WanModel -- same layer names, same shapes.
+The difference is purely in the forward pass: this model processes one temporal
+block at a time and maintains a KV cache across blocks.
+
+Reference: https://github.com/thu-ml/Causal-Forcing
+"""
+
+import torch
+import torch.nn as nn
+
+from comfy.ldm.modules.attention import optimized_attention
+from comfy.ldm.flux.math import apply_rope1
+from comfy.ldm.wan.model import (
+ sinusoidal_embedding_1d,
+ repeat_e,
+ WanModel,
+ WanAttentionBlock,
+)
+import comfy.ldm.common_dit
+import comfy.model_management
+
+
+class CausalWanSelfAttention(nn.Module):
+ """Self-attention with KV cache support for autoregressive inference."""
+
+ def __init__(self, dim, num_heads, window_size=(-1, -1), qk_norm=True,
+ eps=1e-6, operation_settings={}):
+ assert dim % num_heads == 0
+ super().__init__()
+ self.dim = dim
+ self.num_heads = num_heads
+ self.head_dim = dim // num_heads
+ self.qk_norm = qk_norm
+ self.eps = eps
+
+ ops = operation_settings.get("operations")
+ device = operation_settings.get("device")
+ dtype = operation_settings.get("dtype")
+
+ self.q = ops.Linear(dim, dim, device=device, dtype=dtype)
+ self.k = ops.Linear(dim, dim, device=device, dtype=dtype)
+ self.v = ops.Linear(dim, dim, device=device, dtype=dtype)
+ self.o = ops.Linear(dim, dim, device=device, dtype=dtype)
+ self.norm_q = ops.RMSNorm(dim, eps=eps, elementwise_affine=True, device=device, dtype=dtype) if qk_norm else nn.Identity()
+ self.norm_k = ops.RMSNorm(dim, eps=eps, elementwise_affine=True, device=device, dtype=dtype) if qk_norm else nn.Identity()
+
+ def forward(self, x, freqs, kv_cache=None, transformer_options={}):
+ b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
+
+ q = apply_rope1(self.norm_q(self.q(x)).view(b, s, n, d), freqs)
+ k = apply_rope1(self.norm_k(self.k(x)).view(b, s, n, d), freqs)
+ v = self.v(x).view(b, s, n, d)
+
+ if kv_cache is None:
+ x = optimized_attention(
+ q.view(b, s, n * d),
+ k.view(b, s, n * d),
+ v.view(b, s, n * d),
+ heads=self.num_heads,
+ transformer_options=transformer_options,
+ )
+ else:
+ end = kv_cache["end"]
+ new_end = end + s
+
+ # Roped K and plain V go into cache
+ kv_cache["k"][:, end:new_end] = k
+ kv_cache["v"][:, end:new_end] = v
+ kv_cache["end"] = new_end
+
+ x = optimized_attention(
+ q.view(b, s, n * d),
+ kv_cache["k"][:, :new_end].view(b, new_end, n * d),
+ kv_cache["v"][:, :new_end].view(b, new_end, n * d),
+ heads=self.num_heads,
+ transformer_options=transformer_options,
+ )
+
+ x = self.o(x)
+ return x
+
+
+class CausalWanAttentionBlock(WanAttentionBlock):
+ """Transformer block with KV-cached self-attention and cross-attention caching."""
+
+ def __init__(self, cross_attn_type, dim, ffn_dim, num_heads,
+ window_size=(-1, -1), qk_norm=True, cross_attn_norm=False,
+ eps=1e-6, operation_settings={}):
+ super().__init__(cross_attn_type, dim, ffn_dim, num_heads,
+ window_size, qk_norm, cross_attn_norm, eps,
+ operation_settings=operation_settings)
+ self.self_attn = CausalWanSelfAttention(
+ dim, num_heads, window_size, qk_norm, eps,
+ operation_settings=operation_settings)
+
+ def forward(self, x, e, freqs, context, context_img_len=257,
+ kv_cache=None, crossattn_cache=None, transformer_options={}):
+ if e.ndim < 4:
+ e = (comfy.model_management.cast_to(self.modulation, dtype=x.dtype, device=x.device) + e).chunk(6, dim=1)
+ else:
+ e = (comfy.model_management.cast_to(self.modulation, dtype=x.dtype, device=x.device).unsqueeze(0) + e).unbind(2)
+
+ # Self-attention with optional KV cache
+ x = x.contiguous()
+ y = self.self_attn(
+ torch.addcmul(repeat_e(e[0], x), self.norm1(x), 1 + repeat_e(e[1], x)),
+ freqs, kv_cache=kv_cache, transformer_options=transformer_options)
+ x = torch.addcmul(x, y, repeat_e(e[2], x))
+ del y
+
+ # Cross-attention with optional caching
+ if crossattn_cache is not None and crossattn_cache.get("is_init"):
+ q = self.cross_attn.norm_q(self.cross_attn.q(self.norm3(x)))
+ x_ca = optimized_attention(
+ q, crossattn_cache["k"], crossattn_cache["v"],
+ heads=self.num_heads, transformer_options=transformer_options)
+ x = x + self.cross_attn.o(x_ca)
+ else:
+ x = x + self.cross_attn(self.norm3(x), context, context_img_len=context_img_len, transformer_options=transformer_options)
+ if crossattn_cache is not None:
+ crossattn_cache["k"] = self.cross_attn.norm_k(self.cross_attn.k(context))
+ crossattn_cache["v"] = self.cross_attn.v(context)
+ crossattn_cache["is_init"] = True
+
+ # FFN
+ y = self.ffn(torch.addcmul(repeat_e(e[3], x), self.norm2(x), 1 + repeat_e(e[4], x)))
+ x = torch.addcmul(x, y, repeat_e(e[5], x))
+ return x
+
+
+class CausalWanModel(WanModel):
+ """
+ Wan 2.1 diffusion backbone with causal KV-cache support.
+
+ Same weight structure as WanModel -- loads identical state dicts.
+ Adds forward_block() for frame-by-frame autoregressive inference.
+ """
+
+ def __init__(self,
+ model_type='t2v',
+ patch_size=(1, 2, 2),
+ text_len=512,
+ in_dim=16,
+ dim=2048,
+ ffn_dim=8192,
+ freq_dim=256,
+ text_dim=4096,
+ out_dim=16,
+ num_heads=16,
+ num_layers=32,
+ window_size=(-1, -1),
+ qk_norm=True,
+ cross_attn_norm=True,
+ eps=1e-6,
+ image_model=None,
+ device=None,
+ dtype=None,
+ operations=None):
+ super().__init__(
+ model_type=model_type, 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,
+ wan_attn_block_class=CausalWanAttentionBlock,
+ device=device, dtype=dtype, operations=operations)
+
+ def forward_block(self, x, timestep, context, start_frame,
+ kv_caches, crossattn_caches, clip_fea=None):
+ """
+ Forward one temporal block for autoregressive inference.
+
+ Args:
+ x: [B, C, block_frames, H, W] input latent for the current block
+ timestep: [B, block_frames] per-frame timesteps
+ context: [B, L, text_dim] raw text embeddings (pre-text_embedding)
+ start_frame: temporal frame index for RoPE offset
+ kv_caches: list of per-layer KV cache dicts
+ crossattn_caches: list of per-layer cross-attention cache dicts
+ clip_fea: optional CLIP features for I2V
+
+ Returns:
+ flow_pred: [B, C_out, block_frames, H, W] flow prediction
+ """
+ x = comfy.ldm.common_dit.pad_to_patch_size(x, self.patch_size)
+ bs, c, t, h, w = x.shape
+
+ x = self.patch_embedding(x.float()).to(x.dtype)
+ grid_sizes = x.shape[2:]
+ x = x.flatten(2).transpose(1, 2)
+
+ # Per-frame time embedding
+ e = self.time_embedding(
+ sinusoidal_embedding_1d(self.freq_dim, timestep.flatten()).to(dtype=x.dtype))
+ e = e.reshape(timestep.shape[0], -1, e.shape[-1])
+ e0 = self.time_projection(e).unflatten(2, (6, self.dim))
+
+ # Text embedding (reuses crossattn_cache after first block)
+ context = self.text_embedding(context)
+
+ context_img_len = None
+ if clip_fea is not None and self.img_emb is not None:
+ context_clip = self.img_emb(clip_fea)
+ context = torch.concat([context_clip, context], dim=1)
+ context_img_len = clip_fea.shape[-2]
+
+ # RoPE for current block's temporal position
+ freqs = self.rope_encode(t, h, w, t_start=start_frame, device=x.device, dtype=x.dtype)
+
+ # Transformer blocks
+ for i, block in enumerate(self.blocks):
+ x = block(x, e=e0, freqs=freqs, context=context,
+ context_img_len=context_img_len,
+ kv_cache=kv_caches[i],
+ crossattn_cache=crossattn_caches[i])
+
+ # Head
+ x = self.head(x, e)
+
+ # Unpatchify
+ x = self.unpatchify(x, grid_sizes)
+ return x[:, :, :t, :h, :w]
+
+ def init_kv_caches(self, batch_size, max_seq_len, device, dtype):
+ """Create fresh KV caches for all layers."""
+ caches = []
+ for _ in range(self.num_layers):
+ caches.append({
+ "k": torch.zeros(batch_size, max_seq_len, self.num_heads, self.head_dim, device=device, dtype=dtype),
+ "v": torch.zeros(batch_size, max_seq_len, self.num_heads, self.head_dim, device=device, dtype=dtype),
+ "end": 0,
+ })
+ return caches
+
+ def init_crossattn_caches(self, batch_size, device, dtype):
+ """Create fresh cross-attention caches for all layers."""
+ caches = []
+ for _ in range(self.num_layers):
+ caches.append({"is_init": False})
+ return caches
+
+ def reset_kv_caches(self, kv_caches):
+ """Reset KV caches to empty (reuse allocated memory)."""
+ for cache in kv_caches:
+ cache["end"] = 0
+
+ def reset_crossattn_caches(self, crossattn_caches):
+ """Reset cross-attention caches."""
+ for cache in crossattn_caches:
+ cache["is_init"] = False
+
+ @property
+ def head_dim(self):
+ return self.dim // self.num_heads
+
+ def forward(self, x, timestep, context, clip_fea=None, time_dim_concat=None, transformer_options={}, **kwargs):
+ ar_state = transformer_options.get("ar_state")
+ if ar_state is not None:
+ bs = x.shape[0]
+ block_frames = x.shape[2]
+ t_per_frame = timestep.unsqueeze(1).expand(bs, block_frames)
+ return self.forward_block(
+ x=x, timestep=t_per_frame, context=context,
+ start_frame=ar_state["start_frame"],
+ kv_caches=ar_state["kv_caches"],
+ crossattn_caches=ar_state["crossattn_caches"],
+ clip_fea=clip_fea,
+ )
+
+ return super().forward(x, timestep, context, clip_fea=clip_fea,
+ time_dim_concat=time_dim_concat,
+ transformer_options=transformer_options, **kwargs)
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 63ee85323..f11e26ec9 100644
--- a/comfy/lora.py
+++ b/comfy/lora.py
@@ -17,6 +17,7 @@
"""
from __future__ import annotations
+import comfy.memory_management
import comfy.utils
import comfy.model_management
import comfy.model_base
@@ -96,12 +97,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
@@ -162,6 +165,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:
@@ -342,6 +352,12 @@ def model_lora_keys_unet(model, key_map={}):
key_map["base_model.model.{}".format(key_lora)] = k # Official base model loras
key_map["lycoris_{}".format(key_lora.replace(".", "_"))] = k # LyCORIS/LoKR format
+ if isinstance(model, comfy.model_base.ErnieImage):
+ for k in sdk:
+ if k.startswith("diffusion_model.") and k.endswith(".weight"):
+ key_lora = k[len("diffusion_model."):-len(".weight")]
+ key_map["transformer.{}".format(key_lora)] = k
+
return key_map
@@ -467,3 +483,17 @@ def calculate_weight(patches, weight, key, intermediate_dtype=torch.float32, ori
weight = old_weight
return weight
+
+def prefetch_prepared_value(value, allocate_buffer, stream):
+ 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)
+ 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))
+ elif isinstance(value, tuple):
+ return tuple(prefetch_prepared_value(item, allocate_buffer, stream) for item in value)
+ elif isinstance(value, list):
+ return [prefetch_prepared_value(item, allocate_buffer, stream) for item in value]
+
+ return value
diff --git a/comfy/model_base.py b/comfy/model_base.py
index 787ea1145..c22705655 100644
--- a/comfy/model_base.py
+++ b/comfy/model_base.py
@@ -42,6 +42,8 @@ import comfy.ldm.cosmos.predict2
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
@@ -52,9 +54,12 @@ import comfy.ldm.qwen_image.model
import comfy.ldm.kandinsky5.model
import comfy.ldm.anima.model
import comfy.ldm.ace.ace_step15
+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
@@ -81,6 +86,7 @@ class ModelType(Enum):
IMG_TO_IMG = 9
FLOW_COSMOS = 10
IMG_TO_IMG_FLOW = 11
+ V_PREDICTION_DDPM = 12
def model_sampling(model_config, model_type):
@@ -115,6 +121,8 @@ def model_sampling(model_config, model_type):
s = comfy.model_sampling.ModelSamplingCosmosRFlow
elif model_type == ModelType.IMG_TO_IMG_FLOW:
c = comfy.model_sampling.IMG_TO_IMG_FLOW
+ elif model_type == ModelType.V_PREDICTION_DDPM:
+ c = comfy.model_sampling.V_PREDICTION_DDPM
class ModelSampling(s, c):
pass
@@ -210,6 +218,11 @@ class BaseModel(torch.nn.Module):
if "latent_shapes" in extra_conds:
xc = utils.unpack_latents(xc, extra_conds.pop("latent_shapes"))
+ transformer_options = transformer_options.copy()
+ transformer_options["prefetch_dynamic_vbars"] = (
+ self.current_patcher is not None and self.current_patcher.is_dynamic()
+ )
+
model_output = self.diffusion_model(xc, t, context=context, control=control, transformer_options=transformer_options, **extra_conds)
if len(model_output) > 1 and not torch.is_tensor(model_output):
model_output, _ = utils.pack_latents(model_output)
@@ -1356,6 +1369,13 @@ class WAN21(BaseModel):
return out
+class WAN21_CausalAR(WAN21):
+ def __init__(self, model_config, model_type=ModelType.FLOW, device=None):
+ super(WAN21, self).__init__(model_config, model_type, device=device,
+ unet_model=comfy.ldm.wan.ar_model.CausalWanModel)
+ self.image_to_video = False
+
+
class WAN21_Vace(WAN21):
def __init__(self, model_config, model_type=ModelType.FLOW, image_to_video=False, device=None):
super(WAN21, self).__init__(model_config, model_type, device=device, unet_model=comfy.ldm.wan.model.VaceWanModel)
@@ -1582,6 +1602,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)
@@ -1632,6 +1676,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)
@@ -1979,3 +2056,59 @@ class ErnieImage(BaseModel):
class SAM3(BaseModel):
def __init__(self, model_config, model_type=ModelType.FLOW, device=None):
super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.sam3.detector.SAM3Model)
+
+class CogVideoX(BaseModel):
+ def __init__(self, model_config, model_type=ModelType.V_PREDICTION_DDPM, image_to_video=False, device=None):
+ super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.cogvideo.model.CogVideoXTransformer3DModel)
+ self.image_to_video = image_to_video
+
+ def concat_cond(self, **kwargs):
+ noise = kwargs.get("noise", None)
+ # Detect extra channels needed (e.g. 32 - 16 = 16 for ref latent)
+ extra_channels = self.diffusion_model.in_channels - noise.shape[1]
+ if extra_channels == 0:
+ return None
+
+ image = kwargs.get("concat_latent_image", None)
+ device = kwargs["device"]
+
+ if image is None:
+ shape = list(noise.shape)
+ shape[1] = extra_channels
+ return torch.zeros(shape, dtype=noise.dtype, layout=noise.layout, device=noise.device)
+
+ latent_dim = self.latent_format.latent_channels
+ image = utils.common_upscale(image.to(device), noise.shape[-1], noise.shape[-2], "bilinear", "center")
+
+ if noise.ndim == 5 and image.ndim == 5:
+ if image.shape[-3] < noise.shape[-3]:
+ image = torch.nn.functional.pad(image, (0, 0, 0, 0, 0, noise.shape[-3] - image.shape[-3]), "constant", 0)
+ elif image.shape[-3] > noise.shape[-3]:
+ image = image[:, :, :noise.shape[-3]]
+
+ for i in range(0, image.shape[1], latent_dim):
+ image[:, i:i + latent_dim] = self.process_latent_in(image[:, i:i + latent_dim])
+ image = utils.resize_to_batch_size(image, noise.shape[0])
+
+ if image.shape[1] > extra_channels:
+ image = image[:, :extra_channels]
+ elif image.shape[1] < extra_channels:
+ repeats = extra_channels // image.shape[1]
+ remainder = extra_channels % image.shape[1]
+ parts = [image] * repeats
+ if remainder > 0:
+ parts.append(image[:, :remainder])
+ image = torch.cat(parts, dim=1)
+
+ return image
+
+ def extra_conds(self, **kwargs):
+ out = super().extra_conds(**kwargs)
+ # OFS embedding (CogVideoX 1.5 I2V), default 2.0 as used by SparkVSR
+ if self.diffusion_model.ofs_proj_dim is not None:
+ ofs = kwargs.get("ofs", None)
+ if ofs is None:
+ noise = kwargs.get("noise", None)
+ ofs = torch.full((noise.shape[0],), 2.0, device=noise.device, dtype=noise.dtype)
+ out['ofs'] = comfy.conds.CONDRegular(ofs)
+ return out
diff --git a/comfy/model_detection.py b/comfy/model_detection.py
index 724a241bf..bc0b933bc 100644
--- a/comfy/model_detection.py
+++ b/comfy/model_detection.py
@@ -490,6 +490,54 @@ def detect_unet_config(state_dict, key_prefix, metadata=None):
return dit_config
+ if '{}blocks.0.norm1.linear.weight'.format(key_prefix) in state_dict_keys: # CogVideoX
+ dit_config = {}
+ dit_config["image_model"] = "cogvideox"
+
+ # Extract config from weight shapes
+ norm1_weight = state_dict['{}blocks.0.norm1.linear.weight'.format(key_prefix)]
+ time_embed_dim = norm1_weight.shape[1]
+ dim = norm1_weight.shape[0] // 6
+
+ dit_config["num_attention_heads"] = dim // 64
+ dit_config["attention_head_dim"] = 64
+ dit_config["time_embed_dim"] = time_embed_dim
+ dit_config["num_layers"] = count_blocks(state_dict_keys, '{}blocks.'.format(key_prefix) + '{}.')
+
+ # Detect in_channels from patch_embed
+ patch_proj_key = '{}patch_embed.proj.weight'.format(key_prefix)
+ if patch_proj_key in state_dict_keys:
+ w = state_dict[patch_proj_key]
+ if w.ndim == 4:
+ # Conv2d: [out, in, kh, kw] — CogVideoX 1.0
+ dit_config["in_channels"] = w.shape[1]
+ dit_config["patch_size"] = w.shape[2]
+ elif w.ndim == 2:
+ # Linear: [out, in_channels * patch_size * patch_size * patch_size_t] — CogVideoX 1.5
+ dit_config["patch_size"] = 2
+ dit_config["patch_size_t"] = 2
+ dit_config["in_channels"] = w.shape[1] // (2 * 2 * 2) # 256 // 8 = 32
+
+ text_proj_key = '{}patch_embed.text_proj.weight'.format(key_prefix)
+ if text_proj_key in state_dict_keys:
+ dit_config["text_embed_dim"] = state_dict[text_proj_key].shape[1]
+
+ # Detect OFS embedding
+ ofs_key = '{}ofs_embedding_linear_1.weight'.format(key_prefix)
+ if ofs_key in state_dict_keys:
+ dit_config["ofs_embed_dim"] = state_dict[ofs_key].shape[1]
+
+ # Detect positional embedding type
+ pos_key = '{}patch_embed.pos_embedding'.format(key_prefix)
+ if pos_key in state_dict_keys:
+ dit_config["use_learned_positional_embeddings"] = True
+ dit_config["use_rotary_positional_embeddings"] = False
+ else:
+ dit_config["use_learned_positional_embeddings"] = False
+ dit_config["use_rotary_positional_embeddings"] = True
+
+ return dit_config
+
if '{}head.modulation'.format(key_prefix) in state_dict_keys: # Wan 2.1
dit_config = {}
dit_config["image_model"] = "wan2.1"
@@ -524,6 +572,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"
@@ -570,6 +620,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"
diff --git a/comfy/model_management.py b/comfy/model_management.py
index 1a4e2f2ab..2e168f363 100644
--- a/comfy/model_management.py
+++ b/comfy/model_management.py
@@ -32,6 +32,7 @@ from contextlib import contextmanager, nullcontext
import comfy.memory_management
import comfy.utils
import comfy.quant_ops
+import comfy_aimdo.vram_buffer
from typing import TYPE_CHECKING
if TYPE_CHECKING:
@@ -118,10 +119,6 @@ if args.directml is not None:
# torch_directml.disable_tiled_resources(True)
lowvram_available = False #TODO: need to find a way to get free memory in directml before this can be enabled by default.
-try:
- import intel_extension_for_pytorch as ipex # noqa: F401
-except:
- pass
try:
_ = torch.xpu.device_count()
@@ -677,9 +674,6 @@ class LoadedModel:
real_model = self.model.model
- if is_intel_xpu() and not args.disable_ipex_optimize and 'ipex' in globals() and real_model is not None:
- with torch.no_grad():
- real_model = ipex.optimize(real_model.eval(), inplace=True, graph_mode=True, concat_linear=True)
self.real_model = weakref.ref(real_model)
self.model_finalizer = weakref.finalize(real_model, cleanup_models)
@@ -757,6 +751,7 @@ 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 = []
@@ -820,13 +815,15 @@ def load_models_gpu(models, memory_required=0, force_patch_weights=False, minimu
else:
minimum_memory_required = max(inference_memory, minimum_memory_required + extra_reserved_memory())
- models_temp = set()
+ # Order-preserving dedup. A plain set() would randomize iteration order across runs
+ models_temp = {}
for m in models:
- models_temp.add(m)
+ models_temp[m] = None
for mm in m.model_patches_models():
- models_temp.add(mm)
+ models_temp[mm] = None
- models = models_temp
+ models = list(models_temp)
+ models.reverse()
models_to_load = []
@@ -1275,6 +1272,10 @@ stream_counters = {}
STREAM_CAST_BUFFERS = {}
LARGEST_CASTED_WEIGHT = (None, 0)
+STREAM_AIMDO_CAST_BUFFERS = {}
+LARGEST_AIMDO_CASTED_WEIGHT = (None, 0)
+
+DEFAULT_AIMDO_CAST_BUFFER_RESERVATION_SIZE = 16 * 1024 ** 3
def get_cast_buffer(offload_stream, device, size, ref):
global LARGEST_CASTED_WEIGHT
@@ -1308,13 +1309,26 @@ def get_cast_buffer(offload_stream, device, size, ref):
return cast_buffer
+def get_aimdo_cast_buffer(offload_stream, device):
+ cast_buffer = STREAM_AIMDO_CAST_BUFFERS.get(offload_stream, None)
+ 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 reset_cast_buffers():
global LARGEST_CASTED_WEIGHT
+ global LARGEST_AIMDO_CASTED_WEIGHT
+
LARGEST_CASTED_WEIGHT = (None, 0)
- for offload_stream in STREAM_CAST_BUFFERS:
- offload_stream.synchronize()
+ LARGEST_AIMDO_CASTED_WEIGHT = (None, 0)
+ for offload_stream in set(STREAM_CAST_BUFFERS) | set(STREAM_AIMDO_CAST_BUFFERS):
+ if offload_stream is not None:
+ offload_stream.synchronize()
synchronize()
+
STREAM_CAST_BUFFERS.clear()
+ STREAM_AIMDO_CAST_BUFFERS.clear()
soft_empty_cache()
def get_offload_stream(device):
@@ -1674,10 +1688,7 @@ def should_use_fp16(device=None, model_params=0, prioritize_performance=True, ma
return False
if is_intel_xpu():
- if torch_version_numeric < (2, 3):
- return True
- else:
- return torch.xpu.get_device_properties(device).has_fp16
+ return torch.xpu.get_device_properties(device).has_fp16
if is_ascend_npu():
return True
@@ -1743,10 +1754,7 @@ def should_use_bf16(device=None, model_params=0, prioritize_performance=True, ma
return False
if is_intel_xpu():
- if torch_version_numeric < (2, 3):
- return True
- else:
- return torch.xpu.is_bf16_supported()
+ return torch.xpu.is_bf16_supported()
if is_ascend_npu():
return True
@@ -1877,6 +1885,7 @@ def soft_empty_cache(force=False):
if cpu_state == CPUState.MPS:
torch.mps.empty_cache()
elif is_intel_xpu():
+ torch.xpu.synchronize()
torch.xpu.empty_cache()
elif is_ascend_npu():
torch.npu.empty_cache()
diff --git a/comfy/model_patcher.py b/comfy/model_patcher.py
index 8119b4ab3..00d60ff72 100644
--- a/comfy/model_patcher.py
+++ b/comfy/model_patcher.py
@@ -27,11 +27,13 @@ import copy
from typing import Callable, Optional
import torch
+import tqdm
import comfy.float
import comfy.hooks
import comfy.lora
import comfy.model_management
+import comfy.ops
import comfy.patcher_extension
import comfy.utils
from comfy.comfy_types import UnetWrapperFunction
@@ -124,9 +126,20 @@ class LowVramPatch:
self.patches = patches
self.convert_func = convert_func # TODO: remove
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])
+ for patch in self.patches[self.key]
+ ]
+
+ def clear_prepared(self):
+ self.prepared_patches = None
def __call__(self, weight):
- return comfy.lora.calculate_weight(self.patches[self.key], weight, self.key, intermediate_dtype=weight.dtype)
+ patches = self.prepared_patches if self.prepared_patches is not None else self.patches[self.key]
+ return comfy.lora.calculate_weight(patches, weight, self.key, intermediate_dtype=weight.dtype)
LOWVRAM_PATCH_ESTIMATE_MATH_FACTOR = 2
@@ -233,6 +246,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
@@ -945,7 +989,9 @@ class ModelPatcher:
if m.comfy_patched_weights == True:
continue
- for param in params:
+ for param, param_value in params.items():
+ if hasattr(m, "comfy_cast_weights") and getattr(param_value, "is_meta", False):
+ comfy.ops.disable_weight_init._zero_init_parameter(m, param)
key = key_param_name_to_key(n, param)
self.unpin_weight(key)
self.patch_weight_to_device(key, device_to=device_to)
@@ -1568,21 +1614,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):
@@ -1758,7 +1828,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
diff --git a/comfy/model_prefetch.py b/comfy/model_prefetch.py
new file mode 100644
index 000000000..72e11dec6
--- /dev/null
+++ b/comfy/model_prefetch.py
@@ -0,0 +1,66 @@
+import comfy_aimdo.model_vbar
+import comfy.model_management
+import comfy.ops
+
+PREFETCH_QUEUES = []
+
+def cleanup_prefetched_modules(comfy_modules):
+ for s in comfy_modules:
+ prefetch = getattr(s, "_prefetch", None)
+ if prefetch is None:
+ continue
+ for param_key in ("weight", "bias"):
+ lowvram_fn = getattr(s, param_key + "_lowvram_function", None)
+ if lowvram_fn is not None:
+ lowvram_fn.clear_prepared()
+ if prefetch["signature"] is not None:
+ comfy_aimdo.model_vbar.vbar_unpin(s._v)
+ delattr(s, "_prefetch")
+
+def cleanup_prefetch_queues():
+ global PREFETCH_QUEUES
+
+ for queue in PREFETCH_QUEUES:
+ for entry in queue:
+ if entry is None or not isinstance(entry, tuple):
+ continue
+ _, prefetch_state = entry
+ comfy_modules = prefetch_state[1]
+ if comfy_modules is not None:
+ cleanup_prefetched_modules(comfy_modules)
+ PREFETCH_QUEUES = []
+
+def prefetch_queue_pop(queue, device, module):
+ if queue is None:
+ return
+
+ consumed = queue.pop(0)
+ if consumed is not None:
+ offload_stream, prefetch_state = consumed
+ if offload_stream is not None:
+ offload_stream.wait_stream(comfy.model_management.current_stream(device))
+ _, comfy_modules = prefetch_state
+ if comfy_modules is not None:
+ cleanup_prefetched_modules(comfy_modules)
+
+ prefetch = queue[0]
+ if prefetch is not None:
+ comfy_modules = []
+ for s in prefetch.modules():
+ if hasattr(s, "_v"):
+ comfy_modules.append(s)
+
+ offload_stream = comfy.ops.cast_modules_with_vbar(comfy_modules, None, device, None, True)
+ comfy.model_management.sync_stream(device, offload_stream)
+ queue[0] = (offload_stream, (prefetch, comfy_modules))
+
+def make_prefetch_queue(queue, device, transformer_options):
+ if (not transformer_options.get("prefetch_dynamic_vbars", False)
+ or comfy.model_management.NUM_STREAMS == 0
+ or comfy.model_management.is_device_cpu(device)
+ or not comfy.model_management.device_supports_non_blocking(device)):
+ return None
+
+ queue = [None] + queue + [None]
+ PREFETCH_QUEUES.append(queue)
+ return queue
diff --git a/comfy/model_sampling.py b/comfy/model_sampling.py
index 13860e6a2..5af336e76 100644
--- a/comfy/model_sampling.py
+++ b/comfy/model_sampling.py
@@ -54,6 +54,30 @@ class V_PREDICTION(EPS):
sigma = reshape_sigma(sigma, model_output.ndim)
return model_input * self.sigma_data ** 2 / (sigma ** 2 + self.sigma_data ** 2) - model_output * sigma * self.sigma_data / (sigma ** 2 + self.sigma_data ** 2) ** 0.5
+class V_PREDICTION_DDPM:
+ """CogVideoX v-prediction: model receives raw x_t (unscaled), predicts velocity v.
+ x_0 = sqrt(alpha) * x_t - sqrt(1-alpha) * v
+ = x_t / sqrt(sigma^2 + 1) - v * sigma / sqrt(sigma^2 + 1)
+ """
+ def calculate_input(self, sigma, noise):
+ return noise
+
+ def calculate_denoised(self, sigma, model_output, model_input):
+ sigma = reshape_sigma(sigma, model_output.ndim)
+ return model_input / (sigma ** 2 + 1.0) ** 0.5 - model_output * sigma / (sigma ** 2 + 1.0) ** 0.5
+
+ def noise_scaling(self, sigma, noise, latent_image, max_denoise=False):
+ sigma = reshape_sigma(sigma, noise.ndim)
+ if max_denoise:
+ noise = noise * torch.sqrt(1.0 + sigma ** 2.0)
+ else:
+ noise = noise * sigma
+ noise += latent_image
+ return noise
+
+ def inverse_noise_scaling(self, sigma, latent):
+ return latent
+
class EDM(V_PREDICTION):
def calculate_denoised(self, sigma, model_output, model_input):
sigma = reshape_sigma(sigma, model_output.ndim)
@@ -69,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)
@@ -264,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
@@ -272,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/ops.py b/comfy/ops.py
index 7a9b4b84c..eae3bd873 100644
--- a/comfy/ops.py
+++ b/comfy/ops.py
@@ -79,37 +79,68 @@ 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)
-def cast_bias_weight_with_vbar(s, dtype, device, bias_dtype, non_blocking, compute_dtype, want_requant):
+def materialize_meta_param(s, param_keys):
+ for param_key in param_keys:
+ param = getattr(s, param_key, None)
+ if param is not None and getattr(param, "is_meta", False):
+ setattr(s, param_key, torch.nn.Parameter(torch.zeros(param.shape, dtype=param.dtype), requires_grad=param.requires_grad))
- #vbar doesn't support CPU weights, but some custom nodes have weird paths
- #that might switch the layer to the CPU and expect it to work. We have to take
- #a clone conservatively as we are mmapped and some SFT files are packed misaligned
- #If you are a custom node author reading this, please move your layer to the GPU
- #or declare your ModelPatcher as CPU in the first place.
- if comfy.model_management.is_device_cpu(device):
- weight = s.weight.to(dtype=dtype, copy=True)
- if isinstance(weight, QuantizedTensor):
- weight = weight.dequantize()
- bias = None
- if s.bias is not None:
- bias = s.bias.to(dtype=bias_dtype, copy=True)
- return weight, bias, (None, None, None)
+# FIXME: add n=1 cache hit fast path
+def cast_modules_with_vbar(comfy_modules, dtype, device, bias_dtype, non_blocking):
offload_stream = None
- xfer_dest = None
+ cast_buffer = None
+ cast_buffer_offset = 0
+
+ def ensure_offload_stream(module, required_size, check_largest):
+ nonlocal offload_stream
+ nonlocal cast_buffer
+
+ if offload_stream is None:
+ offload_stream = comfy.model_management.get_offload_stream(device)
+ if offload_stream is None or not check_largest or len(comfy_modules) != 1:
+ return
+
+ current_size = 0 if cast_buffer is None else cast_buffer.size()
+ if current_size < required_size and module is comfy.model_management.LARGEST_AIMDO_CASTED_WEIGHT[0]:
+ offload_stream = comfy.model_management.get_offload_stream(device)
+ cast_buffer = None
+ if required_size > comfy.model_management.LARGEST_AIMDO_CASTED_WEIGHT[1]:
+ comfy.model_management.LARGEST_AIMDO_CASTED_WEIGHT = (module, required_size)
+
+ def get_cast_buffer(buffer_size):
+ nonlocal offload_stream
+ nonlocal cast_buffer
+ nonlocal cast_buffer_offset
+
+ if buffer_size == 0:
+ return None
+
+ if offload_stream is None:
+ return torch.empty((buffer_size,), dtype=torch.uint8, device=device)
+
+ cast_buffer = comfy.model_management.get_aimdo_cast_buffer(offload_stream, device)
+ buffer = comfy_aimdo.torch.aimdo_to_tensor(cast_buffer.get(buffer_size, cast_buffer_offset), device)
+ cast_buffer_offset += buffer_size
+ return buffer
+
+ 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)
+ prefetch = {
+ "signature": signature,
+ "resident": resident,
+ }
- signature = comfy_aimdo.model_vbar.vbar_fault(s._v)
- resident = comfy_aimdo.model_vbar.vbar_signature_compare(signature, s._v_signature)
- if signature is not None:
if resident:
- weight = s._v_weight
- bias = s._v_bias
- else:
- xfer_dest = comfy_aimdo.torch.aimdo_to_tensor(s._v, device)
+ s._prefetch = prefetch
+ continue
- if not resident:
+ materialize_meta_param(s, ["weight", "bias"])
+ xfer_dest = comfy_aimdo.torch.aimdo_to_tensor(s._v, device) if signature is not None else None
cast_geometry = comfy.memory_management.tensors_to_geometries([ s.weight, s.bias ])
cast_dest = None
+ needs_cast = False
xfer_source = [ s.weight, s.bias ]
@@ -121,22 +152,15 @@ def cast_bias_weight_with_vbar(s, dtype, device, bias_dtype, non_blocking, compu
if data is None:
continue
if data.dtype != geometry.dtype:
+ needs_cast = True
cast_dest = xfer_dest
- if cast_dest is None:
- cast_dest = torch.empty((comfy.memory_management.vram_aligned_size(cast_geometry),), dtype=torch.uint8, device=device)
xfer_dest = None
break
dest_size = comfy.memory_management.vram_aligned_size(xfer_source)
- offload_stream = comfy.model_management.get_offload_stream(device)
- if xfer_dest is None and offload_stream is not None:
- xfer_dest = comfy.model_management.get_cast_buffer(offload_stream, device, dest_size, s)
- if xfer_dest is None:
- offload_stream = comfy.model_management.get_offload_stream(device)
- xfer_dest = comfy.model_management.get_cast_buffer(offload_stream, device, dest_size, s)
+ ensure_offload_stream(s, dest_size if xfer_dest is None else 0, True)
if xfer_dest is None:
- xfer_dest = torch.empty((dest_size,), dtype=torch.uint8, device=device)
- offload_stream = None
+ xfer_dest = get_cast_buffer(dest_size)
if signature is None and pin is None:
comfy.pinned_memory.pin_memory(s)
@@ -149,27 +173,54 @@ def cast_bias_weight_with_vbar(s, dtype, device, bias_dtype, non_blocking, compu
xfer_source = [ pin ]
#send it over
comfy.model_management.cast_to_gathered(xfer_source, xfer_dest, non_blocking=non_blocking, stream=offload_stream)
- comfy.model_management.sync_stream(device, offload_stream)
- if cast_dest is not None:
+ for param_key in ("weight", "bias"):
+ lowvram_fn = getattr(s, param_key + "_lowvram_function", None)
+ if lowvram_fn is not None:
+ ensure_offload_stream(s, cast_buffer_offset, False)
+ lowvram_fn.prepare(lambda size: get_cast_buffer(size), offload_stream)
+
+ prefetch["xfer_dest"] = xfer_dest
+ prefetch["cast_dest"] = cast_dest
+ prefetch["cast_geometry"] = cast_geometry
+ prefetch["needs_cast"] = needs_cast
+ s._prefetch = prefetch
+
+ return offload_stream
+
+
+def resolve_cast_module_with_vbar(s, dtype, device, bias_dtype, compute_dtype, want_requant):
+
+ prefetch = getattr(s, "_prefetch", None)
+
+ if prefetch["resident"]:
+ weight = s._v_weight
+ bias = s._v_bias
+ else:
+ xfer_dest = prefetch["xfer_dest"]
+ if prefetch["needs_cast"]:
+ cast_dest = prefetch["cast_dest"] if prefetch["cast_dest"] is not None else torch.empty((comfy.memory_management.vram_aligned_size(prefetch["cast_geometry"]),), dtype=torch.uint8, device=device)
for pre_cast, post_cast in zip(comfy.memory_management.interpret_gathered_like([s.weight, s.bias ], xfer_dest),
- comfy.memory_management.interpret_gathered_like(cast_geometry, cast_dest)):
+ comfy.memory_management.interpret_gathered_like(prefetch["cast_geometry"], cast_dest)):
if post_cast is not None:
post_cast.copy_(pre_cast)
xfer_dest = cast_dest
- params = comfy.memory_management.interpret_gathered_like(cast_geometry, xfer_dest)
+ params = comfy.memory_management.interpret_gathered_like(prefetch["cast_geometry"], xfer_dest)
weight = params[0]
bias = params[1]
- if signature is not None:
+ if prefetch["signature"] is not None:
s._v_weight = weight
s._v_bias = bias
- s._v_signature=signature
+ s._v_signature = prefetch["signature"]
def post_cast(s, param_key, x, dtype, resident, update_weight):
lowvram_fn = getattr(s, param_key + "_lowvram_function", None)
fns = getattr(s, param_key + "_function", [])
+ if x is None:
+ return None
+
orig = x
def to_dequant(tensor, dtype):
@@ -197,18 +248,19 @@ def cast_bias_weight_with_vbar(s, dtype, device, bias_dtype, non_blocking, compu
x = f(x)
return x
- update_weight = signature is not None
+ update_weight = prefetch["signature"] is not None
+ weight = post_cast(s, "weight", weight, dtype, prefetch["resident"], update_weight)
+ if bias is not None:
+ bias = post_cast(s, "bias", bias, bias_dtype, prefetch["resident"], update_weight)
- weight = post_cast(s, "weight", weight, dtype, resident, update_weight)
- if s.bias is not None:
- bias = post_cast(s, "bias", bias, bias_dtype, resident, update_weight)
+ if prefetch["signature"] is not None:
+ prefetch["resident"] = True
- #FIXME: weird offload return protocol
- return weight, bias, (offload_stream, device if signature is not None else None, None)
+ return weight, bias
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:
@@ -222,10 +274,46 @@ def cast_bias_weight(s, input=None, dtype=None, device=None, bias_dtype=None, of
if device is None:
device = input.device
+ def format_return(result, offloadable):
+ weight, bias, offload_stream = result
+ return (weight, bias, offload_stream) if offloadable else (weight, bias)
+
non_blocking = comfy.model_management.device_supports_non_blocking(device)
if hasattr(s, "_v"):
- return cast_bias_weight_with_vbar(s, dtype, device, bias_dtype, non_blocking, compute_dtype, want_requant)
+
+ #vbar doesn't support CPU weights, but some custom nodes have weird paths
+ #that might switch the layer to the CPU and expect it to work. We have to take
+ #a clone conservatively as we are mmapped and some SFT files are packed misaligned
+ #If you are a custom node author reading this, please move your layer to the GPU
+ #or declare your ModelPatcher as CPU in the first place.
+ if comfy.model_management.is_device_cpu(device):
+ materialize_meta_param(s, ["weight", "bias"])
+ weight = s.weight.to(dtype=dtype, copy=True)
+ if isinstance(weight, QuantizedTensor):
+ weight = weight.dequantize()
+ bias = s.bias.to(dtype=bias_dtype, copy=True) if s.bias is not None else None
+ return format_return((weight, bias, (None, None, None)), offloadable)
+
+ prefetched = hasattr(s, "_prefetch")
+ offload_stream = None
+ offload_device = None
+ if not prefetched:
+ offload_stream = cast_modules_with_vbar([s], dtype, device, bias_dtype, non_blocking)
+ comfy.model_management.sync_stream(device, offload_stream)
+
+ weight, bias = resolve_cast_module_with_vbar(s, dtype, device, bias_dtype, compute_dtype, want_requant)
+
+ if not prefetched:
+ if getattr(s, "_prefetch")["signature"] is not None:
+ offload_device = device
+ for param_key in ("weight", "bias"):
+ lowvram_fn = getattr(s, param_key + "_lowvram_function", None)
+ if lowvram_fn is not None:
+ lowvram_fn.clear_prepared()
+ delattr(s, "_prefetch")
+ return format_return((weight, bias, (offload_stream, offload_device, None)), offloadable)
+
if offloadable and (device != s.weight.device or
(s.bias is not None and device != s.bias.device)):
@@ -272,11 +360,7 @@ def cast_bias_weight(s, input=None, dtype=None, device=None, bias_dtype=None, of
for f in s.weight_function:
weight = f(weight)
- if offloadable:
- return weight, bias, (offload_stream, weight_a, bias_a)
- else:
- #Legacy function signature
- return weight, bias
+ return format_return((weight, bias, (offload_stream, weight_a, bias_a)), offloadable)
def uncast_bias_weight(s, weight, bias, offload_stream):
@@ -306,6 +390,12 @@ class CastWeightBiasOp:
bias_function = []
class disable_weight_init:
+ @staticmethod
+ def _zero_init_parameter(module, name):
+ param = getattr(module, name)
+ device = None if getattr(param, "is_meta", False) else param.device
+ setattr(module, name, torch.nn.Parameter(torch.zeros(param.shape, device=device, dtype=param.dtype), requires_grad=False))
+
@staticmethod
def _lazy_load_from_state_dict(module, state_dict, prefix, local_metadata,
missing_keys, unexpected_keys, weight_shape,
@@ -472,6 +562,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
@@ -659,6 +768,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
@@ -1159,6 +1271,94 @@ def mixed_precision_ops(quant_config={}, compute_dtype=torch.bfloat16, full_prec
self._buffers[key] = fn(buf)
return self
+ class Embedding(manual_cast.Embedding):
+ 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:
+ layer_conf = json.loads(layer_conf.numpy().tobytes())
+
+ # 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:
+ self.quant_format = quant_format
+ qconfig = QUANT_ALGOS[quant_format]
+ 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]
+
+ scale_key = f"{prefix}weight_scale"
+ scale = state_dict.pop(scale_key, None)
+ if scale is not None:
+ scale = scale.float()
+ manually_loaded_keys.append(scale_key)
+
+ params = layout_cls.Params(
+ scale=scale if scale is not None else torch.ones((), dtype=torch.float32),
+ orig_dtype=MixedPrecisionOps._compute_dtype,
+ orig_shape=(self.num_embeddings, self.embedding_dim),
+ )
+ self.weight = torch.nn.Parameter(
+ QuantizedTensor(weight.to(dtype=qconfig["storage_t"]), qconfig["comfy_tensor_layout"], params),
+ requires_grad=False)
+
+ 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)
+
+ 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
+
+ def forward_comfy_cast_weights(self, input, out_dtype=None):
+ weight = self.weight
+
+ # Optimized path: lookup in fp8, dequantize only the selected rows.
+ if isinstance(weight, QuantizedTensor) and len(self.weight_function) == 0:
+ qdata, _, offload_stream = cast_bias_weight(self, device=input.device, dtype=weight.dtype, offloadable=True)
+ if isinstance(qdata, QuantizedTensor):
+ scale = qdata._params.scale
+ qdata = qdata._qdata
+ else:
+ scale = None
+
+ x = torch.nn.functional.embedding(
+ input, qdata, self.padding_idx, self.max_norm,
+ self.norm_type, self.scale_grad_by_freq, self.sparse)
+ uncast_bias_weight(self, qdata, None, offload_stream)
+ target_dtype = out_dtype if out_dtype is not None else weight._params.orig_dtype
+ x = x.to(dtype=target_dtype)
+ if scale is not None and scale != 1.0:
+ x = x * scale.to(dtype=target_dtype)
+ return x
+
+ # Fallback for non-quantized or weight_function (LoRA) case
+ return super().forward_comfy_cast_weights(input, out_dtype=out_dtype)
+
return MixedPrecisionOps
def pick_operations(weight_dtype, compute_dtype, load_device=None, disable_fast_fp8=False, fp8_optimizations=False, model_config=None):
@@ -1176,6 +1376,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/pinned_memory.py b/comfy/pinned_memory.py
index 6f142282d..6d3ba367a 100644
--- a/comfy/pinned_memory.py
+++ b/comfy/pinned_memory.py
@@ -2,7 +2,6 @@ import comfy.model_management
import comfy.memory_management
import comfy_aimdo.host_buffer
import comfy_aimdo.torch
-import psutil
from comfy.cli_args import args
@@ -12,11 +11,6 @@ def get_pin(module):
def pin_memory(module):
if module.pin_failed or args.disable_pinned_memory or get_pin(module) is not None:
return
- #FIXME: This is a RAM cache trigger event
- ram_headroom = comfy.memory_management.RAM_CACHE_HEADROOM
- #we split the difference and assume half the RAM cache headroom is for us
- if ram_headroom > 0 and psutil.virtual_memory().available < (ram_headroom * 0.5):
- comfy.memory_management.extra_ram_release(ram_headroom)
size = comfy.memory_management.vram_aligned_size([ module.weight, module.bias ])
diff --git a/comfy/quant_ops.py b/comfy/quant_ops.py
index 37e546722..b90bcfd25 100644
--- a/comfy/quant_ops.py
+++ b/comfy/quant_ops.py
@@ -1,6 +1,8 @@
import torch
import logging
+from comfy.cli_args import args
+
try:
import comfy_kitchen as ck
from comfy_kitchen.tensor import (
@@ -20,7 +22,16 @@ try:
if cuda_version < (13,):
ck.registry.disable("cuda")
logging.warning("WARNING: You need pytorch with cu130 or higher to use optimized CUDA operations.")
- ck.registry.disable("triton")
+
+ if args.enable_triton_backend:
+ try:
+ import triton
+ logging.info("Found triton %s. Enabling comfy-kitchen triton backend.", triton.__version__)
+ except ImportError as e:
+ logging.error(f"Failed to import triton, Error: {e}, the comfy-kitchen triton backend will not be available.")
+ ck.registry.disable("triton")
+ else:
+ ck.registry.disable("triton")
for k, v in ck.list_backends().items():
logging.info(f"Found comfy_kitchen backend {k}: {v}")
except ImportError as e:
diff --git a/comfy/rmsnorm.py b/comfy/rmsnorm.py
index ab7cf14fa..e54be98d6 100644
--- a/comfy/rmsnorm.py
+++ b/comfy/rmsnorm.py
@@ -3,6 +3,7 @@ import comfy.model_management
RMSNorm = torch.nn.RMSNorm
+# Note: torch's fused F.rms_norm is faster but produces slightly different output than manual implementations (rsqrt/reduction rounding).
def rms_norm(x, weight=None, eps=1e-6):
if weight is None:
return torch.nn.functional.rms_norm(x, (x.shape[-1],), eps=eps)
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 6f5447d95..bdce2f2d8 100644
--- a/comfy/sampler_helpers.py
+++ b/comfy/sampler_helpers.py
@@ -91,7 +91,8 @@ def get_additional_models(conds, dtype):
gligen += get_models_from_cond(conds[k], "gligen")
add_models += get_models_from_cond(conds[k], "additional_models")
- control_nets = set(cnets)
+ # Order-preserving dedup. A plain set() would randomize iteration order across runs
+ control_nets = list(dict.fromkeys(cnets))
inference_memory = 0
control_models = []
diff --git a/comfy/sd.py b/comfy/sd.py
index ac70abcf5..e7857bf0a 100644
--- a/comfy/sd.py
+++ b/comfy/sd.py
@@ -18,6 +18,7 @@ import comfy.ldm.wan.vae
import comfy.ldm.wan.vae2_2
import comfy.ldm.hunyuan3d.vae
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.pixel_space_convert
@@ -64,6 +65,8 @@ import comfy.text_encoders.ace15
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.model_patcher
import comfy.lora
@@ -76,7 +79,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)
@@ -88,6 +91,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
@@ -95,6 +100,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
@@ -236,7 +243,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)
@@ -421,6 +429,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"):
@@ -487,7 +502,10 @@ class VAE:
encoder_config={'target': "comfy.ldm.modules.diffusionmodules.model.Encoder", 'params': encoder_config},
decoder_config={'target': "comfy.ldm.modules.temporal_ae.VideoDecoder", 'params': decoder_config})
elif "taesd_decoder.1.weight" in sd:
- self.latent_channels = sd["taesd_decoder.1.weight"].shape[1]
+ if isinstance(metadata, dict) and "tae_latent_channels" in metadata:
+ self.latent_channels = metadata["tae_latent_channels"]
+ else:
+ self.latent_channels = sd["taesd_decoder.1.weight"].shape[1]
self.first_stage_model = comfy.taesd.taesd.TAESD(latent_channels=self.latent_channels)
elif "vquantizer.codebook.weight" in sd: #VQGan: stage a of stable cascade
self.first_stage_model = StageA()
@@ -661,6 +679,17 @@ class VAE:
self.memory_used_encode = lambda shape, dtype: (1400 * 9 * shape[-2] * shape[-1]) * model_management.dtype_size(dtype)
self.memory_used_decode = lambda shape, dtype: (3600 * 4 * shape[-2] * shape[-1] * 16 * 16) * model_management.dtype_size(dtype)
+ elif "decoder.conv_in.conv.weight" in sd and "decoder.mid_block.resnets.0.norm1.norm_layer.weight" in sd: # CogVideoX VAE
+ self.upscale_ratio = (lambda a: max(0, a * 4 - 3), 8, 8)
+ self.upscale_index_formula = (4, 8, 8)
+ self.downscale_ratio = (lambda a: max(0, math.floor((a + 3) / 4)), 8, 8)
+ self.downscale_index_formula = (4, 8, 8)
+ self.latent_dim = 3
+ self.latent_channels = sd["encoder.conv_out.conv.weight"].shape[0] // 2
+ self.first_stage_model = comfy.ldm.cogvideo.vae.AutoencoderKLCogVideoX(latent_channels=self.latent_channels)
+ self.memory_used_decode = lambda shape, dtype: (2800 * max(2, ((shape[2] - 1) * 4) + 1) * shape[3] * shape[4] * (8 * 8)) * model_management.dtype_size(dtype)
+ self.memory_used_encode = lambda shape, dtype: (1400 * max(1, shape[2]) * shape[3] * shape[4]) * model_management.dtype_size(dtype)
+ self.working_dtypes = [torch.bfloat16, torch.float16, torch.float32]
elif "decoder.conv_in.conv.weight" in sd:
ddconfig = {'double_z': True, 'z_channels': 4, 'resolution': 256, 'in_channels': 3, 'out_ch': 3, 'ch': 128, 'ch_mult': [1, 2, 4, 4], 'num_res_blocks': 2, 'attn_resolutions': [], 'dropout': 0.0}
ddconfig["conv3d"] = True
@@ -768,6 +797,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:
@@ -1223,6 +1253,7 @@ class CLIPType(Enum):
NEWBIE = 24
FLUX2 = 25
LONGCAT_IMAGE = 26
+ COGVIDEOX = 27
@@ -1271,6 +1302,9 @@ class TEModel(Enum):
QWEN35_9B = 26
QWEN35_27B = 27
MINISTRAL_3_3B = 28
+ GEMMA_4_E4B = 29
+ GEMMA_4_E2B = 30
+ GEMMA_4_31B = 31
def detect_te_model(sd):
@@ -1296,6 +1330,12 @@ def detect_te_model(sd):
return TEModel.BYT5_SMALL_GLYPH
return TEModel.T5_BASE
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
+ if 'model.layers.41.self_attn.q_norm.weight' in sd and 'model.layers.47.self_attn.q_norm.weight' not in sd:
+ return TEModel.GEMMA_4_E4B
+ if 'model.layers.34.self_attn.q_norm.weight' in sd and 'model.layers.41.self_attn.q_norm.weight' not in sd:
+ return TEModel.GEMMA_4_E2B
if 'model.layers.47.self_attn.q_norm.weight' in sd:
return TEModel.GEMMA_3_12B
if 'model.layers.0.self_attn.q_norm.weight' in sd:
@@ -1418,6 +1458,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
@@ -1435,6 +1478,13 @@ 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 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,
+ TEModel.GEMMA_4_31B: comfy.text_encoders.gemma4.Gemma4_31B}[te_model]
+ clip_target.clip = comfy.text_encoders.gemma4.gemma4_te(**llama_detect(clip_data), model_class=variant)
+ clip_target.tokenizer = variant.tokenizer
+ tokenizer_data["tokenizer_json"] = clip_data[0].get("tokenizer_json", None)
elif te_model == TEModel.GEMMA_2_2B:
clip_target.clip = comfy.text_encoders.lumina2.te(**llama_detect(clip_data))
clip_target.tokenizer = comfy.text_encoders.lumina2.LuminaTokenizer
@@ -1879,7 +1929,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 8886f32d5..1e4434fd5 100644
--- a/comfy/supported_models.py
+++ b/comfy/supported_models.py
@@ -27,6 +27,8 @@ import comfy.text_encoders.anima
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
@@ -1166,6 +1168,25 @@ class WAN21_T2V(supported_models_base.BASE):
t5_detect = comfy.text_encoders.sd3_clip.t5_xxl_detect(state_dict, "{}umt5xxl.transformer.".format(pref))
return supported_models_base.ClipTarget(comfy.text_encoders.wan.WanT5Tokenizer, comfy.text_encoders.wan.te(**t5_detect))
+class WAN21_CausalAR_T2V(WAN21_T2V):
+ unet_config = {
+ "image_model": "wan2.1",
+ "model_type": "t2v",
+ "causal_ar": True,
+ }
+
+ sampling_settings = {
+ "shift": 5.0,
+ }
+
+ def __init__(self, unet_config):
+ super().__init__(unet_config)
+ self.unet_config.pop("causal_ar", None)
+
+ def get_model(self, state_dict, prefix="", device=None):
+ return model_base.WAN21_CausalAR(self, device=device)
+
+
class WAN21_I2V(WAN21_T2V):
unet_config = {
"image_model": "wan2.1",
@@ -1293,6 +1314,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",
@@ -1380,6 +1432,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",
@@ -1832,6 +1928,158 @@ class SAM31(SAM3):
unet_config = {"image_model": "SAM31"}
-models = [LotusD, Stable_Zero123, SD15_instructpix2pix, SD15, SD20, SD21UnclipL, SD21UnclipH, SDXL_instructpix2pix, SDXLRefiner, SDXL, SSD1B, KOALA_700M, KOALA_1B, Segmind_Vega, SD_X4Upscaler, Stable_Cascade_C, Stable_Cascade_B, SV3D_u, SV3D_p, SD3, StableAudio, AuraFlow, PixArtAlpha, PixArtSigma, HunyuanDiT, HunyuanDiT1, FluxInpaint, Flux, LongCatImage, FluxSchnell, GenmoMochi, LTXV, LTXAV, HunyuanVideo15_SR_Distilled, HunyuanVideo15, HunyuanImage21Refiner, HunyuanImage21, HunyuanVideoSkyreelsI2V, HunyuanVideoI2V, HunyuanVideo, CosmosT2V, CosmosI2V, CosmosT2IPredict2, CosmosI2VPredict2, ZImagePixelSpace, ZImage, Lumina2, WAN22_T2V, WAN21_T2V, WAN21_I2V, WAN21_FunControl2V, WAN21_Vace, WAN21_Camera, WAN22_Camera, WAN22_S2V, WAN21_HuMo, WAN22_Animate, WAN21_FlowRVS, WAN21_SCAIL, Hunyuan3Dv2mini, Hunyuan3Dv2, Hunyuan3Dv2_1, HiDream, Chroma, ChromaRadiance, ACEStep, ACEStep15, Omnigen2, QwenImage, Flux2, Kandinsky5Image, Kandinsky5, Anima, RT_DETR_v4, ErnieImage, SAM3, SAM31]
+class CogVideoX_T2V(supported_models_base.BASE):
+ unet_config = {
+ "image_model": "cogvideox",
+ }
-models += [SVD_img2vid]
+ sampling_settings = {
+ "linear_start": 0.00085,
+ "linear_end": 0.012,
+ "beta_schedule": "linear",
+ "zsnr": True,
+ }
+
+ unet_extra_config = {}
+ latent_format = latent_formats.CogVideoX
+
+ supported_inference_dtypes = [torch.bfloat16, torch.float16, torch.float32]
+
+ 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:
+ 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, device=device)
+ return out
+
+ def clip_target(self, state_dict={}):
+ return supported_models_base.ClipTarget(comfy.text_encoders.cogvideo.CogVideoXT5Tokenizer, comfy.text_encoders.sd3_clip.T5XXLModel)
+
+class CogVideoX_I2V(CogVideoX_T2V):
+ unet_config = {
+ "image_model": "cogvideox",
+ "in_channels": 32,
+ }
+
+ 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
+
+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,
+ Stable_Zero123,
+ SD15_instructpix2pix,
+ SD15,
+ SD20,
+ SD21UnclipL,
+ SD21UnclipH,
+ SDXL_instructpix2pix,
+ SDXLRefiner,
+ SDXL,
+ SSD1B,
+ KOALA_700M,
+ KOALA_1B,
+ Segmind_Vega,
+ SD_X4Upscaler,
+ Stable_Cascade_C,
+ Stable_Cascade_B,
+ SV3D_u,
+ SV3D_p,
+ SD3,
+ StableAudio,
+ AuraFlow,
+ PixArtAlpha,
+ PixArtSigma,
+ HunyuanDiT,
+ HunyuanDiT1,
+ FluxInpaint,
+ Flux,
+ LongCatImage,
+ FluxSchnell,
+ GenmoMochi,
+ LTXV,
+ LTXAV,
+ HunyuanVideo15_SR_Distilled,
+ HunyuanVideo15,
+ HunyuanImage21Refiner,
+ HunyuanImage21,
+ HunyuanVideoSkyreelsI2V,
+ HunyuanVideoI2V,
+ HunyuanVideo,
+ CosmosT2V,
+ CosmosI2V,
+ CosmosT2IPredict2,
+ CosmosI2VPredict2,
+ ZImagePixelSpace,
+ ZImage,
+ Lumina2,
+ WAN22_T2V,
+ WAN21_CausalAR_T2V,
+ WAN21_T2V,
+ WAN21_I2V,
+ WAN21_FunControl2V,
+ WAN21_Vace,
+ WAN21_Camera,
+ WAN22_Camera,
+ WAN22_S2V,
+ WAN21_HuMo,
+ WAN22_Animate,
+ WAN21_FlowRVS,
+ WAN21_SCAIL,
+ WAN22_WanDancer,
+ Hunyuan3Dv2mini,
+ Hunyuan3Dv2,
+ Hunyuan3Dv2_1,
+ HiDream,
+ HiDreamO1,
+ Chroma,
+ ChromaRadiance,
+ ACEStep,
+ ACEStep15,
+ Omnigen2,
+ QwenImage,
+ Flux2,
+ Kandinsky5Image,
+ Kandinsky5,
+ Anima,
+ RT_DETR_v4,
+ ErnieImage,
+ SAM3,
+ SAM31,
+ CogVideoX_Inpaint,
+ CogVideoX_I2V,
+ CogVideoX_T2V,
+ SVD_img2vid,
+]
diff --git a/comfy/taesd/taehv.py b/comfy/taesd/taehv.py
index 6c06ce19d..696013200 100644
--- a/comfy/taesd/taehv.py
+++ b/comfy/taesd/taehv.py
@@ -7,6 +7,7 @@ from tqdm.auto import tqdm
from collections import namedtuple, deque
import comfy.ops
+import comfy.model_management
operations=comfy.ops.disable_weight_init
DecoderResult = namedtuple("DecoderResult", ("frame", "memory"))
@@ -47,11 +48,14 @@ class TGrow(nn.Module):
x = self.conv(x)
return x.reshape(-1, C, H, W)
-def apply_model_with_memblocks(model, x, parallel, show_progress_bar):
+def apply_model_with_memblocks(model, x, parallel, show_progress_bar, output_device=None,
+ patch_size=1, decode=False):
B, T, C, H, W = x.shape
if parallel:
x = x.reshape(B*T, C, H, W)
+ if not decode and patch_size > 1:
+ x = F.pixel_unshuffle(x, patch_size)
# parallel over input timesteps, iterate over blocks
for b in tqdm(model, disable=not show_progress_bar):
if isinstance(b, MemBlock):
@@ -62,20 +66,27 @@ def apply_model_with_memblocks(model, x, parallel, show_progress_bar):
x = b(x, mem)
else:
x = b(x)
- BT, C, H, W = x.shape
- T = BT // B
- x = x.view(B, T, C, H, W)
+ if decode and patch_size > 1:
+ x = F.pixel_shuffle(x, patch_size)
+ x = x.view(B, x.shape[0] // B, *x.shape[1:])
+ x = x.to(output_device)
else:
out = []
- work_queue = deque([TWorkItem(xt, 0) for t, xt in enumerate(x.reshape(B, T * C, H, W).chunk(T, dim=1))])
+ # Chunk along the time dim directly (chunks are [B,1,C,H,W] views, squeeze to [B,C,H,W] views).
+ # Avoids forcing a contiguous copy when x is non-contiguous (e.g. after movedim in encode/decode).
+ work_queue = deque([TWorkItem(xt.squeeze(1), 0) for xt in x.chunk(T, dim=1)])
progress_bar = tqdm(range(T), disable=not show_progress_bar)
mem = [None] * len(model)
while work_queue:
xt, i = work_queue.popleft()
if i == 0:
progress_bar.update(1)
+ if not decode and patch_size > 1:
+ xt = F.pixel_unshuffle(xt, patch_size)
if i == len(model):
- out.append(xt)
+ if decode and patch_size > 1:
+ xt = F.pixel_shuffle(xt, patch_size)
+ out.append(xt.to(output_device))
del xt
else:
b = model[i]
@@ -165,24 +176,20 @@ class TAEHV(nn.Module):
def encode(self, x, **kwargs):
x = x.movedim(2, 1) # [B, C, T, H, W] -> [B, T, C, H, W]
- if self.patch_size > 1:
- B, T, C, H, W = x.shape
- x = x.reshape(B * T, C, H, W)
- x = F.pixel_unshuffle(x, self.patch_size)
- x = x.reshape(B, T, C * self.patch_size ** 2, H // self.patch_size, W // self.patch_size)
if x.shape[1] % self.t_downscale != 0:
# pad at end to multiple of t_downscale
n_pad = self.t_downscale - x.shape[1] % self.t_downscale
padding = x[:, -1:].repeat_interleave(n_pad, dim=1)
x = torch.cat([x, padding], 1)
- x = apply_model_with_memblocks(self.encoder, x, self.parallel, self.show_progress_bar).movedim(2, 1)
+ x = apply_model_with_memblocks(self.encoder, x, self.parallel, self.show_progress_bar,
+ patch_size=self.patch_size).movedim(2, 1)
return self.process_out(x)
def decode(self, x, **kwargs):
x = x.unsqueeze(0) if x.ndim == 4 else x # [T, C, H, W] -> [1, T, C, H, W]
x = x.movedim(1, 2) if x.shape[1] != self.latent_channels else x # [B, T, C, H, W] or [B, C, T, H, W]
x = self.process_in(x).movedim(2, 1) # [B, C, T, H, W] -> [B, T, C, H, W]
- x = apply_model_with_memblocks(self.decoder, x, self.parallel, self.show_progress_bar)
- if self.patch_size > 1:
- x = F.pixel_shuffle(x, self.patch_size)
+ x = apply_model_with_memblocks(self.decoder, x, self.parallel, self.show_progress_bar,
+ output_device=comfy.model_management.intermediate_device(),
+ patch_size=self.patch_size, decode=True)
return x[:, self.frames_to_trim:].movedim(2, 1)
diff --git a/comfy/taesd/taesd.py b/comfy/taesd/taesd.py
index ce36f1a84..05d370209 100644
--- a/comfy/taesd/taesd.py
+++ b/comfy/taesd/taesd.py
@@ -17,32 +17,79 @@ class Clamp(nn.Module):
return torch.tanh(x / 3) * 3
class Block(nn.Module):
- def __init__(self, n_in, n_out):
+ def __init__(self, n_in: int, n_out: int, use_midblock_gn: bool = False):
super().__init__()
self.conv = nn.Sequential(conv(n_in, n_out), nn.ReLU(), conv(n_out, n_out), nn.ReLU(), conv(n_out, n_out))
self.skip = comfy.ops.disable_weight_init.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity()
self.fuse = nn.ReLU()
- def forward(self, x):
+ if not use_midblock_gn:
+ self.pool = None
+ return
+ n_gn = n_in * 4
+ self.pool = nn.Sequential(
+ comfy.ops.disable_weight_init.Conv2d(n_in, n_gn, 1, bias=False),
+ comfy.ops.disable_weight_init.GroupNorm(4, n_gn),
+ nn.ReLU(inplace=True),
+ comfy.ops.disable_weight_init.Conv2d(n_gn, n_in, 1, bias=False),
+ )
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ if self.pool is not None:
+ x = x + self.pool(x)
return self.fuse(self.conv(x) + self.skip(x))
-def Encoder(latent_channels=4):
- return nn.Sequential(
- conv(3, 64), Block(64, 64),
- conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64),
- conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64),
- conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64),
- conv(64, latent_channels),
- )
+class Encoder(nn.Sequential):
+ def __init__(self, latent_channels: int = 4, use_gn: bool = False):
+ super().__init__(
+ conv(3, 64), Block(64, 64),
+ conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64),
+ conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64),
+ conv(64, 64, stride=2, bias=False), Block(64, 64, use_gn), Block(64, 64, use_gn), Block(64, 64, use_gn),
+ conv(64, latent_channels),
+ )
+class Decoder(nn.Sequential):
+ def __init__(self, latent_channels: int = 4, use_gn: bool = False):
+ super().__init__(
+ Clamp(), conv(latent_channels, 64), nn.ReLU(),
+ Block(64, 64, use_gn), Block(64, 64, use_gn), Block(64, 64, use_gn), nn.Upsample(scale_factor=2), conv(64, 64, bias=False),
+ Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False),
+ Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False),
+ Block(64, 64), conv(64, 3),
+ )
+
+class DecoderFlux2(Decoder):
+ def __init__(self, latent_channels: int = 128, use_gn: bool = True):
+ if latent_channels != 128 or not use_gn:
+ raise ValueError("Unexpected parameters for Flux2 TAE module")
+ super().__init__(latent_channels=32, use_gn=True)
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ B, C, H, W = x.shape
+ x = (
+ x
+ .reshape(B, 32, 2, 2, H, W)
+ .permute(0, 1, 4, 2, 5, 3)
+ .reshape(B, 32, H * 2, W * 2)
+ )
+ return super().forward(x)
+
+class EncoderFlux2(Encoder):
+ def __init__(self, latent_channels: int = 128, use_gn: bool = True):
+ if latent_channels != 128 or not use_gn:
+ raise ValueError("Unexpected parameters for Flux2 TAE module")
+ super().__init__(latent_channels=32, use_gn=True)
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ result = super().forward(x)
+ B, C, H, W = result.shape
+ return (
+ result
+ .reshape(B, C, H // 2, 2, W // 2, 2)
+ .permute(0, 1, 3, 5, 2, 4)
+ .reshape(B, 128, H // 2, W // 2)
+ )
-def Decoder(latent_channels=4):
- return nn.Sequential(
- Clamp(), conv(latent_channels, 64), nn.ReLU(),
- Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False),
- Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False),
- Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False),
- Block(64, 64), conv(64, 3),
- )
class TAESD(nn.Module):
latent_magnitude = 3
@@ -51,8 +98,15 @@ class TAESD(nn.Module):
def __init__(self, encoder_path=None, decoder_path=None, latent_channels=4):
"""Initialize pretrained TAESD on the given device from the given checkpoints."""
super().__init__()
- self.taesd_encoder = Encoder(latent_channels=latent_channels)
- self.taesd_decoder = Decoder(latent_channels=latent_channels)
+ if latent_channels == 128:
+ encoder_class = EncoderFlux2
+ decoder_class = DecoderFlux2
+ else:
+ encoder_class = Encoder
+ decoder_class = Decoder
+ self.taesd_encoder = encoder_class(latent_channels=latent_channels)
+ self.taesd_decoder = decoder_class(latent_channels=latent_channels)
+
self.vae_scale = torch.nn.Parameter(torch.tensor(1.0))
self.vae_shift = torch.nn.Parameter(torch.tensor(0.0))
if encoder_path is not None:
@@ -61,19 +115,19 @@ class TAESD(nn.Module):
self.taesd_decoder.load_state_dict(comfy.utils.load_torch_file(decoder_path, safe_load=True))
@staticmethod
- def scale_latents(x):
+ def scale_latents(x: torch.Tensor) -> torch.Tensor:
"""raw latents -> [0, 1]"""
return x.div(2 * TAESD.latent_magnitude).add(TAESD.latent_shift).clamp(0, 1)
@staticmethod
- def unscale_latents(x):
+ def unscale_latents(x: torch.Tensor) -> torch.Tensor:
"""[0, 1] -> raw latents"""
return x.sub(TAESD.latent_shift).mul(2 * TAESD.latent_magnitude)
- def decode(self, x):
+ def decode(self, x: torch.Tensor) -> torch.Tensor:
x_sample = self.taesd_decoder((x - self.vae_shift) * self.vae_scale)
x_sample = x_sample.sub(0.5).mul(2)
return x_sample
- def encode(self, x):
+ def encode(self, x: torch.Tensor) -> torch.Tensor:
return (self.taesd_encoder(x * 0.5 + 0.5) / self.vae_scale) + self.vae_shift
diff --git a/comfy/text_encoders/cogvideo.py b/comfy/text_encoders/cogvideo.py
new file mode 100644
index 000000000..b97310709
--- /dev/null
+++ b/comfy/text_encoders/cogvideo.py
@@ -0,0 +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/gemma4.py b/comfy/text_encoders/gemma4.py
new file mode 100644
index 000000000..f050061ed
--- /dev/null
+++ b/comfy/text_encoders/gemma4.py
@@ -0,0 +1,1298 @@
+import torch
+import torch.nn as nn
+import numpy as np
+from dataclasses import dataclass
+import math
+
+from comfy import sd1_clip
+import comfy.model_management
+from comfy.ldm.modules.attention import optimized_attention_for_device
+from comfy.rmsnorm import rms_norm
+from comfy.text_encoders.llama import RMSNorm, MLP, BaseLlama, BaseGenerate, _make_scaled_embedding
+
+
+# Intentional minor divergences from transformers -reference implementation:
+# - Embedding sqrt(hidden_size) scale applied as a Python scalar (full precision) instead of dtype-matched buffer tensor.
+# - RMSNorm uses torch fused F.rms_norm, very slight numerical differences, but considerably faster
+# - Input image and audio resizing/resampling slightly different numerically
+
+
+GEMMA4_VISION_CONFIG = {"hidden_size": 768, "image_size": 896, "intermediate_size": 3072, "num_attention_heads": 12, "num_hidden_layers": 16, "patch_size": 16, "head_dim": 64, "rms_norm_eps": 1e-6, "position_embedding_size": 10240, "pooling_kernel_size": 3}
+GEMMA4_VISION_31B_CONFIG = {"hidden_size": 1152, "image_size": 896, "intermediate_size": 4304, "num_attention_heads": 16, "num_hidden_layers": 27, "patch_size": 16, "head_dim": 72, "rms_norm_eps": 1e-6, "position_embedding_size": 10240, "pooling_kernel_size": 3}
+GEMMA4_AUDIO_CONFIG = {"hidden_size": 1024, "num_hidden_layers": 12, "num_attention_heads": 8, "intermediate_size": 4096, "conv_kernel_size": 5, "attention_chunk_size": 12, "attention_context_left": 13, "attention_context_right": 0, "attention_logit_cap": 50.0, "output_proj_dims": 1536, "rms_norm_eps": 1e-6, "residual_weight": 0.5}
+
+@dataclass
+class Gemma4Config:
+ vocab_size: int = 262144
+ hidden_size: int = 2560
+ intermediate_size: int = 10240
+ num_hidden_layers: int = 42
+ num_attention_heads: int = 8
+ num_key_value_heads: int = 2
+ max_position_embeddings: int = 131072
+ rms_norm_eps: float = 1e-6
+ rope_theta = [1000000.0, 10000.0]
+ transformer_type: str = "gemma4"
+ head_dim = 256
+ global_head_dim = 512
+ rms_norm_add = False
+ mlp_activation = "gelu_pytorch_tanh"
+ qkv_bias = False
+ rope_dims = None
+ q_norm = "gemma3"
+ k_norm = "gemma3"
+ sliding_attention = [512, 512, 512, 512, 512, False]
+ rope_scale = None
+ partial_rotary_factor: float = 0.25
+ final_norm: bool = True
+ lm_head: bool = False
+ final_logit_softcapping: float = 30.0
+ hidden_size_per_layer_input: int = 256
+ num_kv_shared_layers: int = 18
+ use_double_wide_mlp: bool = False
+ stop_tokens = [1, 50, 106]
+ vision_config = GEMMA4_VISION_CONFIG
+ audio_config = GEMMA4_AUDIO_CONFIG
+ mm_tokens_per_image = 280
+
+@dataclass
+class Gemma4_E2B_Config(Gemma4Config):
+ hidden_size: int = 1536
+ intermediate_size: int = 6144
+ num_hidden_layers: int = 35
+ num_key_value_heads: int = 1
+ sliding_attention = [512, 512, 512, 512, False]
+ num_kv_shared_layers: int = 20
+ use_double_wide_mlp: bool = True
+
+@dataclass
+class Gemma4_31B_Config(Gemma4Config):
+ hidden_size: int = 5376
+ intermediate_size: int = 21504
+ num_hidden_layers: int = 60
+ num_attention_heads: int = 32
+ num_key_value_heads: int = 16
+ sliding_attention = [1024, 1024, 1024, 1024, 1024, False]
+ hidden_size_per_layer_input: int = 0
+ num_kv_shared_layers: int = 0
+ audio_config = None
+ vision_config = GEMMA4_VISION_31B_CONFIG
+
+
+# unfused RoPE as addcmul_ RoPE diverges from reference code
+def _apply_rotary_pos_emb(x, freqs_cis):
+ cos, sin = freqs_cis[0], freqs_cis[1]
+ half = x.shape[-1] // 2
+ out = x * cos
+ out[..., :half] -= x[..., half:] * sin[..., :half]
+ out[..., half:] += x[..., :half] * sin[..., half:]
+ return out
+
+class Gemma4Attention(nn.Module):
+ def __init__(self, config, head_dim, device=None, dtype=None, ops=None):
+ super().__init__()
+ self.num_heads = config.num_attention_heads
+ self.num_kv_heads = config.num_key_value_heads
+ self.hidden_size = config.hidden_size
+ self.head_dim = head_dim
+ self.inner_size = self.num_heads * head_dim
+
+ self.q_proj = ops.Linear(config.hidden_size, self.inner_size, bias=config.qkv_bias, device=device, dtype=dtype)
+ self.k_proj = ops.Linear(config.hidden_size, self.num_kv_heads * head_dim, bias=config.qkv_bias, device=device, dtype=dtype)
+ self.v_proj = ops.Linear(config.hidden_size, self.num_kv_heads * head_dim, bias=config.qkv_bias, device=device, dtype=dtype)
+ self.o_proj = ops.Linear(self.inner_size, config.hidden_size, bias=False, device=device, dtype=dtype)
+
+ self.q_norm = None
+ self.k_norm = None
+ if config.q_norm == "gemma3":
+ self.q_norm = RMSNorm(head_dim, eps=config.rms_norm_eps, device=device, dtype=dtype)
+ if config.k_norm == "gemma3":
+ self.k_norm = RMSNorm(head_dim, eps=config.rms_norm_eps, device=device, dtype=dtype)
+
+ def forward(
+ self,
+ hidden_states: torch.Tensor,
+ attention_mask=None,
+ freqs_cis=None,
+ past_key_value=None,
+ sliding_window=None,
+ shared_kv=None,
+ ):
+ batch_size, seq_length, _ = hidden_states.shape
+
+ xq = self.q_proj(hidden_states)
+ xq = xq.view(batch_size, seq_length, self.num_heads, self.head_dim).transpose(1, 2)
+ if self.q_norm is not None:
+ xq = self.q_norm(xq)
+
+ if shared_kv is not None:
+ xk, xv = shared_kv
+ # Apply RoPE to Q only (K already has RoPE from source layer)
+ xq = _apply_rotary_pos_emb(xq, freqs_cis)
+ present_key_value = None
+ shareable_kv = None
+ else:
+ xk = self.k_proj(hidden_states).view(batch_size, seq_length, self.num_kv_heads, self.head_dim)
+ xv = self.v_proj(hidden_states).view(batch_size, seq_length, self.num_kv_heads, self.head_dim)
+ if self.k_norm is not None:
+ xk = self.k_norm(xk)
+ xv = rms_norm(xv)
+ xk = xk.transpose(1, 2)
+ xv = xv.transpose(1, 2)
+ xq = _apply_rotary_pos_emb(xq, freqs_cis)
+ xk = _apply_rotary_pos_emb(xk, freqs_cis)
+
+ present_key_value = None
+ if past_key_value is not None:
+ cumulative_len = 0
+ if len(past_key_value) > 0:
+ past_key, past_value, cumulative_len = past_key_value
+ xk = torch.cat((past_key, xk), dim=2)
+ xv = torch.cat((past_value, xv), dim=2)
+ new_cumulative = cumulative_len + seq_length
+ if sliding_window is not None and xk.shape[2] > sliding_window - 1:
+ cache_k = xk[:, :, -(sliding_window - 1):]
+ cache_v = xv[:, :, -(sliding_window - 1):]
+ else:
+ cache_k = xk
+ cache_v = xv
+ present_key_value = (cache_k, cache_v, new_cumulative)
+
+ # KV for sharing: full xk/xv that SDPA sees (not evicted cache)
+ shareable_kv = (xk, xv)
+
+ # GQA: pass unexpanded KV with enable_gqa when no sliding mask,
+ # expand heads when sliding mask is present
+ # has to be done within SDPA itself to match the reference code, pre-scaling expansion causes numerical differences
+ expand_kv = (self.num_heads != self.num_kv_heads and
+ sliding_window is not None and
+ xk.shape[2] >= sliding_window)
+ if expand_kv:
+ 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)
+ gqa_kwargs = {} if expand_kv else ({"enable_gqa": True} if self.num_heads != self.num_kv_heads else {})
+ output = optimized_attention_for_device(xq.device, mask=attention_mask is not None, small_input=True)(xq, xk, xv, self.num_heads, mask=attention_mask, skip_reshape=True, scale=1.0, **gqa_kwargs)
+
+ return self.o_proj(output), present_key_value, shareable_kv
+
+
+class TransformerBlockGemma4(nn.Module):
+ def __init__(self, config, index, device=None, dtype=None, ops=None):
+ super().__init__()
+ if config.sliding_attention is not None:
+ self.sliding_attention = config.sliding_attention[index % len(config.sliding_attention)]
+ else:
+ self.sliding_attention = False
+
+ head_dim = config.head_dim if self.sliding_attention else config.global_head_dim
+
+ self.self_attn = Gemma4Attention(config, head_dim=head_dim, device=device, dtype=dtype, ops=ops)
+
+ num_kv_shared = config.num_kv_shared_layers
+ first_kv_shared = config.num_hidden_layers - num_kv_shared
+ mlp_size = config.intermediate_size * 2 if config.use_double_wide_mlp and index >= first_kv_shared else None
+ self.mlp = MLP(config, device=device, dtype=dtype, ops=ops, intermediate_size=mlp_size)
+
+ 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.pre_feedforward_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, device=device, dtype=dtype)
+ self.post_feedforward_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, device=device, dtype=dtype)
+
+ self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
+ if self.hidden_size_per_layer_input:
+ self.per_layer_input_gate = ops.Linear(config.hidden_size, self.hidden_size_per_layer_input, bias=False, device=device, dtype=dtype)
+ self.per_layer_projection = ops.Linear(self.hidden_size_per_layer_input, config.hidden_size, bias=False, device=device, dtype=dtype)
+ self.post_per_layer_input_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps, device=device, dtype=dtype)
+ self.register_buffer("layer_scalar", torch.ones(1, device=device, dtype=dtype))
+ else:
+ self.layer_scalar = None
+
+ def forward(self, x, attention_mask=None, freqs_cis=None, past_key_value=None, per_layer_input=None, shared_kv=None):
+ sliding_window = None
+ if self.sliding_attention:
+ sliding_window = self.sliding_attention
+ # For prefill > sliding window, add sliding window restriction to the causal mask.
+ if x.shape[1] > self.sliding_attention:
+ sw_mask = torch.zeros(x.shape[1], x.shape[1], dtype=x.dtype, device=x.device)
+ sw_mask.masked_fill_(torch.ones_like(sw_mask, dtype=torch.bool).tril_(-self.sliding_attention), torch.finfo(x.dtype).min)
+ attention_mask = attention_mask + sw_mask if attention_mask is not None else sw_mask
+ freqs_cis = freqs_cis[1]
+ else:
+ freqs_cis = freqs_cis[0]
+
+ residual = x
+ x = self.input_layernorm(x)
+ x, present_key_value, shareable_kv = self.self_attn(
+ hidden_states=x, attention_mask=attention_mask, freqs_cis=freqs_cis,
+ past_key_value=past_key_value, sliding_window=sliding_window, shared_kv=shared_kv,
+ )
+ x = self.post_attention_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
+
+ if self.hidden_size_per_layer_input and per_layer_input is not None:
+ residual = x
+ x = self.per_layer_input_gate(x)
+ x = torch.nn.functional.gelu(x, approximate="tanh")
+ x = x * per_layer_input
+ x = self.per_layer_projection(x)
+ x = self.post_per_layer_input_norm(x)
+ x = residual + x
+
+ if self.layer_scalar is not None:
+ x = x * self.layer_scalar
+
+ return x, present_key_value, shareable_kv
+
+
+class Gemma4Transformer(nn.Module):
+ def __init__(self, config, device=None, dtype=None, ops=None):
+ super().__init__()
+ self.config = config
+
+ self.embed_tokens = _make_scaled_embedding(ops, config.vocab_size, config.hidden_size, config.hidden_size ** 0.5, device, dtype)
+
+ self.layers = nn.ModuleList([
+ TransformerBlockGemma4(config, index=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) if config.final_norm else None
+
+ # Precompute RoPE inv_freq on CPU to match reference code's exact value
+ rope_angles_global = int(config.partial_rotary_factor * config.global_head_dim // 2)
+ nope_global = config.global_head_dim // 2 - rope_angles_global
+ global_inv = 1.0 / (config.rope_theta[0] ** (torch.arange(0, 2 * rope_angles_global, 2).float() / config.global_head_dim))
+ if nope_global > 0:
+ global_inv = torch.cat([global_inv, torch.zeros(nope_global)])
+ self.register_buffer("_global_inv_freq", global_inv, persistent=False)
+
+ sliding_inv = 1.0 / (config.rope_theta[1] ** (torch.arange(0, config.head_dim, 2).float() / config.head_dim))
+ self.register_buffer("_sliding_inv_freq", sliding_inv, persistent=False)
+
+ # Per-layer input mechanism
+ self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
+ if self.hidden_size_per_layer_input:
+ self.embed_tokens_per_layer = _make_scaled_embedding(ops, config.vocab_size, config.num_hidden_layers * self.hidden_size_per_layer_input, self.hidden_size_per_layer_input ** 0.5, device, dtype)
+ self.per_layer_model_projection = ops.Linear(
+ config.hidden_size, config.num_hidden_layers * self.hidden_size_per_layer_input,
+ bias=False, device=device, dtype=dtype)
+ self.per_layer_projection_norm = RMSNorm(
+ self.hidden_size_per_layer_input, eps=config.rms_norm_eps,
+ device=device, dtype=dtype)
+
+ def get_past_len(self, past_key_values):
+ for kv in past_key_values:
+ if len(kv) >= 3:
+ return kv[2]
+ return 0
+
+ def _freqs_from_inv(self, inv_freq, position_ids, device, dtype):
+ """Compute cos/sin from stored inv_freq"""
+ inv_exp = inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(device)
+ pos_exp = position_ids[:, None, :].float()
+ freqs = (inv_exp @ pos_exp).transpose(1, 2)
+ emb = torch.cat((freqs, freqs), dim=-1)
+ return emb.cos().unsqueeze(1).to(dtype), emb.sin().unsqueeze(1).to(dtype)
+
+ def compute_freqs_cis(self, position_ids, device, dtype=None):
+ global_freqs = self._freqs_from_inv(self._global_inv_freq, position_ids, device, dtype)
+ sliding_freqs = self._freqs_from_inv(self._sliding_inv_freq, position_ids, device, dtype)
+ return [global_freqs, sliding_freqs]
+
+ 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=None,
+ past_key_values=None, input_ids=None):
+ if embeds is not None:
+ x = embeds
+ else:
+ x = self.embed_tokens(x, out_dtype=dtype)
+
+ seq_len = x.shape[1]
+ past_len = 0
+ if past_key_values is not None and len(past_key_values) > 0:
+ past_len = self.get_past_len(past_key_values)
+
+ if position_ids is None:
+ position_ids = torch.arange(past_len, past_len + seq_len, device=x.device).unsqueeze(0)
+
+ freqs_cis = self.compute_freqs_cis(position_ids, x.device, dtype=x.dtype)
+
+ mask = None
+ min_val = torch.finfo(x.dtype).min
+ 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), min_val)
+
+ if seq_len > 1:
+ causal_mask = torch.zeros(past_len + seq_len, past_len + seq_len, dtype=x.dtype, device=x.device)
+ causal_mask.masked_fill_(torch.ones_like(causal_mask, dtype=torch.bool).triu_(1), min_val)
+ mask = mask + causal_mask if mask is not None else causal_mask
+
+ # Per-layer inputs
+ per_layer_inputs = None
+ if self.hidden_size_per_layer_input:
+ num_layers = self.config.num_hidden_layers
+ hpl = self.hidden_size_per_layer_input
+ per_layer_proj = self.per_layer_model_projection(x) * (1.0 / (self.config.hidden_size ** 0.5))
+ per_layer_proj = self.per_layer_projection_norm(per_layer_proj.reshape(*x.shape[:-1], num_layers, hpl))
+ if input_ids is not None and input_ids.shape[1] == x.shape[1]:
+ per_layer_emb = self.embed_tokens_per_layer(input_ids).reshape(*input_ids.shape, num_layers, hpl)
+ per_layer_inputs = (per_layer_proj + per_layer_emb) * (0.5 ** 0.5)
+ else:
+ per_layer_inputs = per_layer_proj
+
+ # KV sharing: later layers reuse KV from the last non-shared sliding/global layer
+ num_kv_shared = self.config.num_kv_shared_layers
+ first_kv_shared = self.config.num_hidden_layers - num_kv_shared if num_kv_shared > 0 else self.config.num_hidden_layers
+ shared_sliding_kv = None # KV from last non-shared sliding layer
+ shared_global_kv = None # KV from last non-shared global layer
+
+ intermediate = None
+ next_key_values = []
+ for i, layer in enumerate(self.layers):
+ past_kv = past_key_values[i] if past_key_values is not None and len(past_key_values) > 0 else None
+
+ layer_kwargs = {}
+ if per_layer_inputs is not None:
+ layer_kwargs['per_layer_input'] = per_layer_inputs[:, :, i, :]
+
+ is_sliding = hasattr(layer, 'sliding_attention') and layer.sliding_attention
+ if i >= first_kv_shared and num_kv_shared > 0:
+ shared = shared_sliding_kv if is_sliding else shared_global_kv
+ if shared is not None:
+ layer_kwargs['shared_kv'] = shared
+
+ x, current_kv, shareable_kv = layer(x=x, attention_mask=mask, freqs_cis=freqs_cis, past_key_value=past_kv, **layer_kwargs)
+
+ next_key_values.append(current_kv if current_kv is not None else ())
+
+ # Only track the last sliding/global before the sharing boundary
+ if i < first_kv_shared and shareable_kv is not None:
+ if is_sliding:
+ shared_sliding_kv = shareable_kv
+ else:
+ shared_global_kv = shareable_kv
+
+ if i == intermediate_output:
+ intermediate = x.clone()
+
+ if self.norm is not None:
+ x = self.norm(x)
+
+ if len(next_key_values) > 0:
+ return x, intermediate, next_key_values
+ return x, intermediate
+
+
+class Gemma4Base(BaseLlama, BaseGenerate, torch.nn.Module):
+ """Common base for all Gemma4 variants: text model + vision."""
+ def _init_model(self, config, dtype, device, operations):
+ self.num_layers = config.num_hidden_layers
+ self.model = Gemma4Transformer(config, device=device, dtype=dtype, ops=operations)
+ self.dtype = dtype
+ self.multi_modal_projector = Gemma4MultiModalProjector(config, dtype=dtype, device=device, ops=operations)
+ self.vision_model = Gemma4VisionEncoder(config.vision_config, dtype=dtype, device=device, ops=operations)
+
+ def logits(self, x):
+ logits = super().logits(x)
+ cap = self.model.config.final_logit_softcapping
+ if cap:
+ logits = cap * torch.tanh(logits / cap)
+ return logits
+
+ def init_kv_cache(self, batch, max_cache_len, device, execution_dtype):
+ past_key_values = []
+ for _ in range(self.model.config.num_hidden_layers):
+ past_key_values.append(())
+ return past_key_values
+
+ def preprocess_embed(self, embed, device):
+ if embed["type"] == "image":
+ image = embed.pop("data").movedim(-1, 1) # [B, H, W, C] -> [B, C, H, W]
+ max_soft_tokens = embed.get("max_soft_tokens", None)
+ vision_out = self.vision_model(image.to(device, dtype=torch.float32), max_soft_tokens=max_soft_tokens)
+ return self.multi_modal_projector(vision_out), None
+ return None, None
+
+
+class Gemma4AudioMixin:
+ """Adds audio support to a Gemma4 model."""
+ def _init_audio(self, config, dtype, device, operations):
+ self.audio_model = Gemma4AudioEncoder(config.audio_config, dtype=dtype, device=device, ops=operations)
+ self.audio_projector = Gemma4AudioProjector({"audio_output_proj_dims": config.audio_config["output_proj_dims"], "text_hidden_size": config.hidden_size, "rms_norm_eps": config.rms_norm_eps}, dtype=dtype, device=device, ops=operations)
+
+ def preprocess_embed(self, embed, device):
+ result, extra = super().preprocess_embed(embed, device)
+ if result is not None:
+ return result, extra
+ if embed["type"] == "audio":
+ audio = embed.pop("data").to(device, dtype=torch.float32)
+ audio_mask = embed.pop("mask", None)
+ if audio_mask is not None:
+ audio_mask = audio_mask.to(device)
+ audio_out = self.audio_model(audio, audio_mask=audio_mask)
+ return self.audio_projector(audio_out), None
+ return None, None
+
+
+# Vision Encoder
+
+def _compute_vision_2d_rope(head_dim, pixel_position_ids, theta=100.0, device=None):
+ """Compute 2D RoPE for vision: separate frequencies for x and y dimensions.
+
+ Args:
+ head_dim: dimension per head (e.g. 64)
+ pixel_position_ids: [batch, num_patches, 2] with (x, y) coords
+ theta: RoPE base frequency
+ Returns:
+ (cos, sin) each of shape [batch, num_patches, head_dim]
+ """
+ rotary_dim_per_axis = head_dim // 2
+ freq_indices = torch.arange(0, rotary_dim_per_axis, 2, device=device).float()
+ inv_freq = 1.0 / (theta ** (freq_indices / rotary_dim_per_axis))
+
+ all_cos, all_sin = [], []
+ for i in range(2): # x and y
+ dim_positions = pixel_position_ids[:, :, i].float() # [batch, num_patches]
+ freqs = torch.einsum('bi,j->bij', dim_positions, inv_freq.to(device)) # [batch, num_patches, rotary_dim/2]
+ emb = torch.cat([freqs, freqs], dim=-1) # [batch, num_patches, rotary_dim]
+ all_cos.append(emb.cos())
+ all_sin.append(emb.sin())
+
+ cos = torch.cat(all_cos, dim=-1).to(pixel_position_ids.device) # [batch, num_patches, head_dim]
+ sin = torch.cat(all_sin, dim=-1).to(pixel_position_ids.device)
+ return cos, sin
+
+
+def _apply_vision_2d_rope(x, freqs):
+ """Apply 2D RoPE (multidimensional) to vision query/key states.
+
+ Splits x and cos/sin into ndim=2 parts, applies 1D RoPE to each independently.
+
+ x: [batch, heads, seq, head_dim]
+ freqs: (cos, sin) each [batch, seq, head_dim]
+ """
+ cos = freqs[0].unsqueeze(1) # [batch, 1, seq, head_dim]
+ sin = freqs[1].unsqueeze(1)
+ half = x.shape[-1] // 2
+ a = _apply_rotary_pos_emb(x[..., :half], (cos[..., :half], sin[..., :half]))
+ b = _apply_rotary_pos_emb(x[..., half:], (cos[..., half:], sin[..., half:]))
+ return torch.cat([a, b], dim=-1)
+
+
+class ClippedLinear(nn.Module):
+ """Linear layer with activation clipping (from quantization-aware training).
+
+ Stores input_max/min and output_max/min as buffers loaded from checkpoint.
+ """
+ def __init__(self, in_features, out_features, bias=False, device=None, dtype=None, ops=None):
+ super().__init__()
+ self.linear = ops.Linear(in_features, out_features, bias=bias, device=device, dtype=dtype)
+ self.register_buffer('input_max', torch.tensor(float('inf'), device=device, dtype=dtype))
+ self.register_buffer('input_min', torch.tensor(float('-inf'), device=device, dtype=dtype))
+ self.register_buffer('output_max', torch.tensor(float('inf'), device=device, dtype=dtype))
+ self.register_buffer('output_min', torch.tensor(float('-inf'), device=device, dtype=dtype))
+
+ @property
+ def weight(self):
+ return self.linear.weight
+
+ def forward(self, x):
+ x = x.clamp(min=self.input_min, max=self.input_max)
+ x = self.linear(x)
+ return x.clamp_(min=self.output_min, max=self.output_max)
+
+
+class Gemma4VisionMLP(nn.Module):
+ """SwiGLU MLP matching gate_proj/up_proj/down_proj structure."""
+ def __init__(self, config, device=None, dtype=None, ops=None):
+ super().__init__()
+ hidden_size = config["hidden_size"]
+ intermediate_size = config["intermediate_size"]
+ self.gate_proj = ClippedLinear(hidden_size, intermediate_size, device=device, dtype=dtype, ops=ops)
+ self.up_proj = ClippedLinear(hidden_size, intermediate_size, device=device, dtype=dtype, ops=ops)
+ self.down_proj = ClippedLinear(intermediate_size, hidden_size, device=device, dtype=dtype, ops=ops)
+
+ def forward(self, x):
+ return self.down_proj(torch.nn.functional.gelu(self.gate_proj(x), approximate="tanh") * self.up_proj(x))
+
+
+class Gemma4VisionAttention(nn.Module):
+ def __init__(self, config, device=None, dtype=None, ops=None):
+ super().__init__()
+ self.hidden_size = config["hidden_size"]
+ self.num_heads = config["num_attention_heads"]
+ self.head_dim = config.get("head_dim", self.hidden_size // self.num_heads)
+
+ self.q_proj = ClippedLinear(self.hidden_size, self.num_heads * self.head_dim, device=device, dtype=dtype, ops=ops)
+ self.k_proj = ClippedLinear(self.hidden_size, self.num_heads * self.head_dim, device=device, dtype=dtype, ops=ops)
+ self.v_proj = ClippedLinear(self.hidden_size, self.num_heads * self.head_dim, device=device, dtype=dtype, ops=ops)
+ self.o_proj = ClippedLinear(self.num_heads * self.head_dim, self.hidden_size, device=device, dtype=dtype, ops=ops)
+
+ self.q_norm = RMSNorm(self.head_dim, eps=config["rms_norm_eps"], device=device, dtype=dtype)
+ self.k_norm = RMSNorm(self.head_dim, eps=config["rms_norm_eps"], device=device, dtype=dtype)
+
+ def forward(self, x, freqs, attention_mask=None):
+ batch_size, seq_length, _ = x.shape
+
+ xq = self.q_proj(x).view(batch_size, seq_length, self.num_heads, self.head_dim)
+ xk = self.k_proj(x).view(batch_size, seq_length, self.num_heads, self.head_dim)
+ xv = self.v_proj(x).view(batch_size, seq_length, self.num_heads, self.head_dim)
+
+ xq = self.q_norm(xq).transpose(1, 2)
+ xk = self.k_norm(xk).transpose(1, 2)
+ xv = rms_norm(xv)
+
+ xq = _apply_vision_2d_rope(xq, freqs)
+ xk = _apply_vision_2d_rope(xk, freqs)
+
+ xv = xv.to(xq.dtype).transpose(1, 2)
+
+ output = optimized_attention_for_device(xq.device, mask=attention_mask is not None, small_input=True)(xq, xk, xv, self.num_heads, mask=attention_mask, skip_reshape=True, scale=1.0)
+ return self.o_proj(output)
+
+
+class Gemma4VisionLayer(nn.Module):
+ def __init__(self, config, device=None, dtype=None, ops=None):
+ super().__init__()
+ self.self_attn = Gemma4VisionAttention(config, device=device, dtype=dtype, ops=ops)
+ self.mlp = Gemma4VisionMLP(config, device=device, dtype=dtype, ops=ops)
+ norm_kwargs = dict(eps=config["rms_norm_eps"], device=device, dtype=dtype)
+ hidden = config["hidden_size"]
+ self.input_layernorm = RMSNorm(hidden, **norm_kwargs)
+ self.post_attention_layernorm = RMSNorm(hidden, **norm_kwargs)
+ self.pre_feedforward_layernorm = RMSNorm(hidden, **norm_kwargs)
+ self.post_feedforward_layernorm = RMSNorm(hidden, **norm_kwargs)
+
+ def forward(self, x, freqs, attention_mask=None):
+ residual = x
+ x = self.input_layernorm(x)
+ x = self.self_attn(x, freqs, attention_mask=attention_mask)
+ x = self.post_attention_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 Gemma4PatchEmbedder(nn.Module):
+ """Patch embedding with learned 2D position embeddings via one-hot lookup."""
+ def __init__(self, config, device=None, dtype=None, ops=None):
+ super().__init__()
+ hidden_size = config["hidden_size"]
+ patch_size = config["patch_size"]
+ self.patch_size = patch_size
+ self.position_embedding_size = config.get("position_embedding_size", 10240)
+
+ self.input_proj = ops.Linear(3 * patch_size * patch_size, hidden_size, bias=False, device=device, dtype=dtype)
+ self.position_embedding_table = nn.Parameter(
+ torch.empty(2, self.position_embedding_size, hidden_size, device=device, dtype=dtype)
+ )
+
+ def forward(self, patches, pixel_position_ids):
+ """
+ patches: [B, num_patches, 3*patch_size²] in [0,1] range (normalized to [-1,1] inside, matching HF)
+ pixel_position_ids: [B, num_patches, 2] with (x,y) positions, (-1,-1) for padding
+ """
+ hidden_states = self.input_proj((2.0 * (patches - 0.5)).to(self.input_proj.weight.dtype))
+
+ clamped_positions = pixel_position_ids.clamp(min=0)
+ pos_table = comfy.model_management.cast_to_device(self.position_embedding_table, hidden_states.device, hidden_states.dtype)
+ position_embeddings = pos_table[0][clamped_positions[..., 0]] + pos_table[1][clamped_positions[..., 1]]
+
+ # Zero out position embeddings for padding patches (matching HF)
+ padding_positions = (pixel_position_ids == -1).all(dim=-1)
+ position_embeddings = torch.where(padding_positions.unsqueeze(-1), 0.0, position_embeddings)
+
+ return hidden_states + position_embeddings
+
+
+class Gemma4VisionEncoderLayers(nn.Module):
+ """Wrapper to produce state dict keys as encoder.layers.X.*"""
+ def __init__(self, config, dtype=None, device=None, ops=None):
+ super().__init__()
+ self.layers = nn.ModuleList([
+ Gemma4VisionLayer(config, device=device, dtype=dtype, ops=ops)
+ for _ in range(config["num_hidden_layers"])
+ ])
+
+
+class Gemma4VisionEncoder(nn.Module):
+ def __init__(self, config, dtype=None, device=None, ops=None):
+ super().__init__()
+ self.config = config
+ self.hidden_size = config["hidden_size"]
+ self.head_dim = config.get("head_dim", config["hidden_size"] // config["num_attention_heads"])
+ self.patch_size = config["patch_size"]
+ self.pooling_kernel_size = config.get("pooling_kernel_size", 3)
+ self.root_hidden_size = self.hidden_size ** 0.5
+
+ self.patch_embedder = Gemma4PatchEmbedder(config, device=device, dtype=dtype, ops=ops)
+ self.encoder = Gemma4VisionEncoderLayers(config, dtype=dtype, device=device, ops=ops)
+
+ def forward(self, pixel_values, max_soft_tokens=None):
+ """
+ pixel_values: [B, C, H, W] in [0,1] range
+ max_soft_tokens: if provided, pad to max_soft_tokens * k² total patches
+ """
+ batch_size, _, height, width = pixel_values.shape
+ ps = self.patch_size
+ k = self.pooling_kernel_size
+ patches_h, patches_w = height // ps, width // ps
+ num_patches = patches_h * patches_w
+ output_length = max_soft_tokens if max_soft_tokens is not None else num_patches // (k * k)
+ n_padding = output_length * k * k - num_patches
+
+ # Patchify and build position grid
+ patches = pixel_values.reshape(batch_size, -1, patches_h, ps, patches_w, ps)
+ patches = patches.permute(0, 2, 4, 3, 5, 1).reshape(batch_size, num_patches, -1)
+ grid_y, grid_x = torch.meshgrid(torch.arange(patches_h, device=pixel_values.device), torch.arange(patches_w, device=pixel_values.device), indexing='ij')
+ position_ids = torch.stack([grid_x.flatten(), grid_y.flatten()], dim=-1).unsqueeze(0).expand(batch_size, -1, -1)
+
+ # Append zero-pixel padding with (-1,-1) positions
+ if n_padding > 0:
+ patches = torch.cat([patches, patches.new_zeros(batch_size, n_padding, patches.shape[-1])], dim=1)
+ position_ids = torch.cat([position_ids, position_ids.new_full((batch_size, n_padding, 2), -1)], dim=1)
+
+ padding = (position_ids == -1).all(dim=-1)
+
+ # Embed, encode, pool
+ x = self.patch_embedder(patches, position_ids)
+ freqs = _compute_vision_2d_rope(self.head_dim, position_ids, device=pixel_values.device)
+ freqs = tuple(t.to(x.dtype) for t in freqs)
+ if n_padding > 0:
+ mask = padding.unsqueeze(1).unsqueeze(2).expand(-1, 1, position_ids.shape[1], -1)
+ mask = torch.zeros_like(mask, dtype=x.dtype).masked_fill_(mask, torch.finfo(x.dtype).min)
+ else:
+ mask = None
+
+ for layer in self.encoder.layers:
+ x = layer(x, freqs, attention_mask=mask)
+
+ if n_padding > 0:
+ x = x.masked_fill(padding.unsqueeze(-1), 0.0)
+
+ # Average pool by spatial position
+ clamped = position_ids.clamp(min=0)
+ max_x = clamped[:, :, 0].max(dim=-1, keepdim=True)[0] + 1
+ ki = torch.div(clamped, k, rounding_mode="floor")
+ ki = ki[:, :, 0] + (max_x // k) * ki[:, :, 1]
+ weights = torch.nn.functional.one_hot(ki.long(), output_length).float() / (k * k)
+ x = (weights.transpose(1, 2) @ x.float()).to(x.dtype)
+
+ # Strip empty output tokens
+ valid_out = ~((weights == 0).all(dim=1))
+ if valid_out.any() and not valid_out.all():
+ x = x[:, valid_out[0]] if batch_size > 1 else x[valid_out].unsqueeze(0)
+
+ return x * self.root_hidden_size
+
+
+class Gemma4RMSNormProjector(nn.Module):
+ """Shared projector: parameterless RMSNorm → linear. Used for both vision and audio."""
+ def __init__(self, in_dim, out_dim, dtype=None, device=None, ops=None):
+ super().__init__()
+ self.embedding_projection = ops.Linear(in_dim, out_dim, bias=False, device=device, dtype=dtype)
+
+ def forward(self, x):
+ return self.embedding_projection(rms_norm(x))
+
+
+class Gemma4MultiModalProjector(Gemma4RMSNormProjector):
+ def __init__(self, config, dtype=None, device=None, ops=None):
+ super().__init__(config.vision_config["hidden_size"], config.hidden_size, dtype=dtype, device=device, ops=ops)
+
+
+# Audio Encoder
+
+class Gemma4AudioConvSubsampler(nn.Module):
+ """2D convolution subsampling for audio features"""
+ def __init__(self, config, device=None, dtype=None, ops=None):
+ super().__init__()
+ eps = config["rms_norm_eps"]
+ self.layer0 = nn.ModuleDict({
+ 'conv': ops.Conv2d(1, 128, kernel_size=3, stride=2, padding=1, bias=False, device=device, dtype=dtype),
+ 'norm': ops.LayerNorm(128, eps=eps, elementwise_affine=True, bias=False, device=device, dtype=dtype),
+ })
+ self.layer1 = nn.ModuleDict({
+ 'conv': ops.Conv2d(128, 32, kernel_size=3, stride=2, padding=1, bias=False, device=device, dtype=dtype),
+ 'norm': ops.LayerNorm(32, eps=eps, elementwise_affine=True, bias=False, device=device, dtype=dtype),
+ })
+ # proj_input_dim = (128 // 4) * 32 = 1024
+ self.input_proj_linear = ops.Linear(1024, config["hidden_size"], bias=False, device=device, dtype=dtype)
+
+ def _conv_layer(self, x, layer, mask):
+ if mask is not None:
+ x = x * mask[:, None, :, None].to(x.device)
+ x = layer['conv'](x.to(layer['conv'].weight.dtype))
+ x = torch.relu(layer['norm'](x.permute(0, 2, 3, 1)).permute(0, 3, 1, 2).contiguous())
+ if mask is not None:
+ mask = mask[:, ::2]
+ return x, mask
+
+ def forward(self, x, mask=None):
+ x = x.unsqueeze(1)
+ x, mask = self._conv_layer(x, self.layer0, mask)
+ x, mask = self._conv_layer(x, self.layer1, mask)
+ batch_size, _, seq_len, _ = x.shape
+ x = x.permute(0, 2, 3, 1).contiguous().reshape(batch_size, seq_len, -1)
+ return self.input_proj_linear(x), mask
+
+
+class Gemma4AudioFeedForward(nn.Module):
+ """Conformer feed-forward with residual scaling."""
+ def __init__(self, config, device=None, dtype=None, ops=None):
+ super().__init__()
+ hidden_size = config["hidden_size"]
+ intermediate_size = config.get("intermediate_size", hidden_size * 4)
+ self.pre_layer_norm = RMSNorm(hidden_size, eps=config["rms_norm_eps"], device=device, dtype=dtype)
+ self.ffw_layer_1 = ClippedLinear(hidden_size, intermediate_size, device=device, dtype=dtype, ops=ops)
+ self.ffw_layer_2 = ClippedLinear(intermediate_size, hidden_size, device=device, dtype=dtype, ops=ops)
+ self.post_layer_norm = RMSNorm(hidden_size, eps=config["rms_norm_eps"], device=device, dtype=dtype)
+ self.post_layer_scale = config.get("residual_weight", 0.5)
+
+ def forward(self, x):
+ residual = x
+ x = self.pre_layer_norm(x)
+ x = torch.nn.functional.silu(self.ffw_layer_1(x))
+ x = self.ffw_layer_2(x)
+ x = self.post_layer_norm(x)
+ x = x * self.post_layer_scale
+ return x + residual
+
+
+class Gemma4AudioRelPositionalEncoding(nn.Module):
+ """Sinusoidal relative positional encoding for audio attention."""
+ def __init__(self, config, device=None, dtype=None):
+ super().__init__()
+ hidden_size = config["hidden_size"]
+ context_left = config.get("attention_context_left", 13)
+ context_right = config.get("attention_context_right", 0)
+ self.chunk_size = config.get("attention_chunk_size", 12)
+ self.context_size = self.chunk_size + context_left - 1 + context_right
+
+ num_timescales = hidden_size // 2
+ log_inc = math.log(10000.0) / max(num_timescales - 1, 1)
+ inv_timescales = torch.exp(torch.arange(num_timescales) * -log_inc).to(dtype=dtype).unsqueeze(0).unsqueeze(0)
+ self.register_buffer("inv_timescales", inv_timescales, persistent=False)
+
+ def forward(self, hidden_states):
+ positions = torch.arange(self.chunk_size, -1, -1, device=hidden_states.device).unsqueeze(-1)
+ scaled = positions * self.inv_timescales.to(device=hidden_states.device)
+ return torch.cat([torch.sin(scaled), torch.cos(scaled)], dim=-1).to(dtype=hidden_states.dtype)
+
+
+class Gemma4AudioAttention(nn.Module):
+ """Chunked block attention with relative position bias and softcap."""
+ def __init__(self, config, device=None, dtype=None, ops=None):
+ super().__init__()
+ self.hidden_size = config["hidden_size"]
+ self.num_heads = config["num_attention_heads"]
+ self.head_dim = self.hidden_size // self.num_heads
+ self.chunk_size = config.get("attention_chunk_size", 12)
+ self.max_past_horizon = config.get("attention_context_left", 13) - 1
+ self.max_future_horizon = config.get("attention_context_right", 0)
+ self.context_size = self.chunk_size + self.max_past_horizon + self.max_future_horizon
+
+ self.q_scale = (self.head_dim ** -0.5) / math.log(2)
+ self.k_scale = math.log(1 + math.e) / math.log(2)
+ self.register_buffer("softcap", torch.tensor(config.get("attention_logit_cap", 50.0), dtype=dtype), persistent=False)
+
+ self.q_proj = ClippedLinear(self.hidden_size, self.hidden_size, device=device, dtype=dtype, ops=ops)
+ self.k_proj = ClippedLinear(self.hidden_size, self.hidden_size, device=device, dtype=dtype, ops=ops)
+ self.v_proj = ClippedLinear(self.hidden_size, self.hidden_size, device=device, dtype=dtype, ops=ops)
+ self.post = ClippedLinear(self.hidden_size, self.hidden_size, device=device, dtype=dtype, ops=ops)
+ self.per_dim_scale = nn.Parameter(torch.empty(self.head_dim, device=device, dtype=dtype))
+ self.relative_k_proj = ops.Linear(self.hidden_size, self.hidden_size, bias=False, device=device, dtype=dtype)
+
+ def _convert_to_block(self, x):
+ B, S, H, D = x.shape
+ num_blocks = (S + self.chunk_size - 1) // self.chunk_size
+ pad = num_blocks * self.chunk_size - S
+ x = torch.nn.functional.pad(x, (0, 0, 0, 0, 0, pad))
+ return x.reshape(B, num_blocks, self.chunk_size, H, D).contiguous()
+
+ def _extract_block_context(self, x):
+ x = torch.nn.functional.pad(x, (0, 0, 0, 0, self.max_past_horizon, self.max_future_horizon + self.chunk_size - 1))
+ x = x.unfold(1, self.context_size, self.chunk_size)
+ return torch.movedim(x, -1, 2).contiguous()
+
+ def _rel_shift(self, x):
+ B, H, NB, BS, PL = x.shape
+ CS = self.context_size
+ x = torch.nn.functional.pad(x, (0, CS + 1 - PL))
+ x = x.view(B, H, NB, BS * (CS + 1))
+ x = x[..., :BS * CS]
+ return x.view(B, H, NB, BS, CS)
+
+ def _build_blocked_mask(self, seq_len, num_blocks, device, audio_mask=None):
+ """Build 5D boolean blocked attention mask (True=attend, False=mask)"""
+ q = torch.arange(seq_len, device=device)
+ dist = q[:, None] - q[None, :]
+ mask = (dist >= 0) & (dist < self.max_past_horizon)
+ if self.max_future_horizon > 0:
+ mask = mask | ((dist < 0) & ((-dist) < self.max_future_horizon))
+ if audio_mask is not None:
+ mask = mask & audio_mask[0, None, :].bool()
+ m = mask[None, None]
+ # Reshape to blocked 5D matching reference code
+ p = num_blocks * self.chunk_size - seq_len
+ m = torch.nn.functional.pad(m, (0, p, 0, p), value=False)
+ m = m.reshape(1, 1, num_blocks, self.chunk_size, -1)
+ m = torch.nn.functional.pad(m, (self.max_past_horizon, self.max_future_horizon), value=False)
+ idx = (torch.arange(num_blocks, device=device) * self.chunk_size)[:, None] + torch.arange(self.context_size, device=device)[None, :]
+ return m.gather(-1, idx[None, None, :, None, :].expand(1, 1, -1, self.chunk_size, -1))
+
+ def forward(self, x, position_embeddings=None, attn_mask=None):
+ B, S, _ = x.shape
+
+ q = self.q_proj(x).float().view(B, S, self.num_heads, self.head_dim)
+ k = self.k_proj(x).float().view(B, S, self.num_heads, self.head_dim)
+ v = self.v_proj(x).float().view(B, S, self.num_heads, self.head_dim)
+
+ q = q * self.q_scale * torch.nn.functional.softplus(self.per_dim_scale)
+ k = k * self.k_scale
+
+ q_blocks = self._convert_to_block(q)
+ k_context = self._extract_block_context(k)
+ v_context = self._extract_block_context(v)
+ num_blocks = q_blocks.shape[1]
+
+ rel_k = self.relative_k_proj(position_embeddings).view(-1, self.num_heads, self.head_dim).to(q.dtype)
+
+ queries = q_blocks.permute(0, 3, 1, 2, 4) # [B, H, NB, CS, D]
+ matrix_ac = queries @ k_context.permute(0, 3, 1, 4, 2)
+
+ queries_flat = queries.reshape(B, self.num_heads, -1, self.head_dim)
+ matrix_bd = queries_flat @ rel_k.permute(1, 2, 0)
+ matrix_bd = matrix_bd.reshape(B, self.num_heads, num_blocks, self.chunk_size, -1)
+ matrix_bd = self._rel_shift(matrix_bd)
+
+ attn_weights = matrix_ac + matrix_bd
+ attn_weights = torch.tanh(attn_weights / self.softcap) * self.softcap
+
+ # Mask out invalid positions in chunk context (matching reference's masked_fill approach)
+ if attn_mask is None:
+ attn_mask = self._build_blocked_mask(S, num_blocks, x.device)
+ attn_weights = attn_weights.masked_fill(attn_mask.logical_not(), -1e9)
+
+ attn_weights = torch.nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(v.dtype)
+ out = attn_weights @ v_context.permute(0, 3, 1, 2, 4)
+ out = out.permute(0, 2, 3, 1, 4).reshape(B, num_blocks * self.chunk_size, -1)
+ out = out[:, :S].contiguous()
+ return self.post(out.to(self.post.linear.weight.dtype))
+
+
+class Gemma4AudioLConv1d(nn.Module):
+ """Lightweight convolution with standard GLU."""
+ def __init__(self, config, device=None, dtype=None, ops=None):
+ super().__init__()
+ hidden_size = config["hidden_size"]
+ conv_kernel_size = config.get("conv_kernel_size", 5)
+ self.pre_layer_norm = RMSNorm(hidden_size, eps=config["rms_norm_eps"], device=device, dtype=dtype)
+ self.linear_start = ClippedLinear(hidden_size, hidden_size * 2, device=device, dtype=dtype, ops=ops)
+ # Causal conv: left-pad only
+ self.depthwise_conv1d = ops.Conv1d(hidden_size, hidden_size, kernel_size=conv_kernel_size, padding=0, groups=hidden_size, bias=False, device=device, dtype=dtype)
+ self.conv_left_pad = conv_kernel_size - 1 # causal: pad left by kernel-1
+ self.conv_norm = RMSNorm(hidden_size, eps=config["rms_norm_eps"], device=device, dtype=dtype)
+ self.linear_end = ClippedLinear(hidden_size, hidden_size, device=device, dtype=dtype, ops=ops)
+
+ def forward(self, x):
+ residual = x
+ x = self.pre_layer_norm(x)
+ x = self.linear_start(x)
+ x = torch.nn.functional.glu(x, dim=-1)
+ x = x.transpose(1, 2)
+ x = torch.nn.functional.pad(x, (self.conv_left_pad, 0))
+ x = self.depthwise_conv1d(x).transpose(1, 2)
+ x = self.conv_norm(x)
+ x = torch.nn.functional.silu(x)
+ x = self.linear_end(x)
+ return x + residual
+
+
+class Gemma4AudioLayer(nn.Module):
+ """Conformer block: FFN1 -> Attention -> LConv -> FFN2."""
+ def __init__(self, config, device=None, dtype=None, ops=None):
+ super().__init__()
+ self.feed_forward1 = Gemma4AudioFeedForward(config, device=device, dtype=dtype, ops=ops)
+ self.self_attn = Gemma4AudioAttention(config, device=device, dtype=dtype, ops=ops)
+ norm_kwargs = dict(eps=config["rms_norm_eps"], device=device, dtype=dtype)
+ hidden_size = config["hidden_size"]
+ self.norm_pre_attn = RMSNorm(hidden_size, **norm_kwargs)
+ self.norm_post_attn = RMSNorm(hidden_size, **norm_kwargs)
+ self.lconv1d = Gemma4AudioLConv1d(config, device=device, dtype=dtype, ops=ops)
+ self.feed_forward2 = Gemma4AudioFeedForward(config, device=device, dtype=dtype, ops=ops)
+ self.norm_out = RMSNorm(hidden_size, **norm_kwargs)
+
+ def forward(self, x, position_embeddings=None, attn_mask=None):
+ x = self.feed_forward1(x)
+
+ residual = x
+ x = self.norm_pre_attn(x)
+ x = self.self_attn(x, position_embeddings=position_embeddings, attn_mask=attn_mask)
+ x = self.norm_post_attn(x)
+ x = x + residual
+
+ x = self.lconv1d(x)
+ x = self.feed_forward2(x)
+
+ x = self.norm_out(x)
+ return x
+
+
+class Gemma4AudioEncoder(nn.Module):
+ def __init__(self, config, dtype=None, device=None, ops=None):
+ super().__init__()
+ self.hidden_size = config["hidden_size"]
+ self.output_proj_dims = config.get("output_proj_dims", 1536)
+
+ self.subsample_conv_projection = Gemma4AudioConvSubsampler(config, device=device, dtype=dtype, ops=ops)
+ self.rel_pos_enc = Gemma4AudioRelPositionalEncoding(config, device=device, dtype=dtype)
+
+ self.layers = nn.ModuleList([
+ Gemma4AudioLayer(config, device=device, dtype=dtype, ops=ops)
+ for _ in range(config["num_hidden_layers"])
+ ])
+
+ self.output_proj = ops.Linear(self.hidden_size, self.output_proj_dims, bias=True, device=device, dtype=dtype)
+
+ def forward(self, audio_features, audio_mask=None):
+ x, audio_mask = self.subsample_conv_projection(audio_features, audio_mask)
+ position_embeddings = self.rel_pos_enc(x)
+
+ # Build blocked attention mask once for all layers
+ attn_mask = self.layers[0].self_attn._build_blocked_mask(
+ x.shape[1], (x.shape[1] + self.layers[0].self_attn.chunk_size - 1) // self.layers[0].self_attn.chunk_size,
+ x.device, audio_mask=audio_mask)
+
+ for layer in self.layers:
+ x = layer(x, position_embeddings=position_embeddings, attn_mask=attn_mask)
+
+ x = self.output_proj(x)
+ return x
+
+
+class Gemma4AudioProjector(Gemma4RMSNormProjector):
+ def __init__(self, config, dtype=None, device=None, ops=None):
+ super().__init__(config.get("audio_output_proj_dims", 1536), config.get("text_hidden_size", 2560), dtype=dtype, device=device, ops=ops)
+
+
+# Tokenizer and Wrappers
+
+class Gemma4_Tokenizer():
+ tokenizer_json_data = None
+
+ def state_dict(self):
+ if self.tokenizer_json_data is not None:
+ return {"tokenizer_json": self.tokenizer_json_data}
+ return {}
+
+ def _extract_mel_spectrogram(self, waveform, sample_rate):
+ """Extract 128-bin log mel spectrogram.
+ Uses numpy for FFT/matmul/log to produce bit-identical results with reference code.
+ """
+ # Mix to mono first, then resample to 16kHz
+ if waveform.dim() > 1 and waveform.shape[0] > 1:
+ waveform = waveform.mean(dim=0, keepdim=True)
+ if waveform.dim() == 1:
+ waveform = waveform.unsqueeze(0)
+ audio = waveform.squeeze(0).float().numpy()
+ if sample_rate != 16000:
+ # Use scipy's resample_poly with a high-quality FIR filter to get as close as possible to librosa's resampling (while still not full match)
+ from scipy.signal import resample_poly, firwin
+ from math import gcd
+ g = gcd(sample_rate, 16000)
+ up, down = 16000 // g, sample_rate // g
+ L = max(up, down)
+ h = firwin(160 * L + 1, 0.96 / L, window=('kaiser', 6.5))
+ audio = resample_poly(audio, up, down, window=h).astype(np.float32)
+ n = len(audio)
+
+ # Pad to multiple of 128, build sample-level mask
+ if n % 128 != 0:
+ audio = np.pad(audio, (0, 128 - n % 128))
+ mask_raw = np.ones(len(audio), dtype=np.float32)
+ mask_raw[n:] = 0.0
+
+ # Semicausal padding: 160 zeros prepended
+ audio = np.pad(audio, (160, 0))
+ mask_raw = np.pad(mask_raw, (160, 0))
+
+ # Extract 321-sample frames via stride tricks, drop last → 320
+ nf = (len(audio) - 321) // 160 + 1
+ strides = (audio.strides[0] * 160, audio.strides[0])
+ frames = np.lib.stride_tricks.as_strided(audio, (nf, 321), strides)[..., :-1].copy()
+
+ # Periodic Hann window, FFT magnitude, mel filterbank, log
+ window = (0.5 - 0.5 * np.cos(2 * np.pi * np.arange(320) / 320)).astype(np.float32)
+ magnitude = np.abs(np.fft.rfft(frames * window, n=512, axis=-1))
+ mel_fb = self._build_mel_filterbank()
+ log_mel = np.log(np.matmul(magnitude, mel_fb) + np.float64(0.001)).astype(np.float32)
+
+ # Frame mask: valid when last sample in window is real audio
+ mask = mask_raw[np.arange(nf) * 160 + 320].astype(bool)
+ log_mel = log_mel * mask[:, None]
+ return torch.from_numpy(log_mel), torch.from_numpy(mask) # [T, 128], [T]
+
+ @staticmethod
+ def _build_mel_filterbank():
+ """Build 128-bin HTK mel filterbank [257, 128] for 512-pt FFT at 16kHz."""
+ mel_freqs = np.linspace(0.0, 2595.0 * np.log10(1.0 + 8000.0 / 700.0), 130)
+ filter_freqs = 700.0 * (10.0 ** (mel_freqs / 2595.0) - 1.0)
+ fft_freqs = np.linspace(0, 16000 // 2, 257)
+ filter_diff = np.diff(filter_freqs)
+ slopes = np.expand_dims(filter_freqs, 0) - np.expand_dims(fft_freqs, 1)
+ down_slopes = -slopes[:, :-2] / filter_diff[:-1]
+ up_slopes = slopes[:, 2:] / filter_diff[1:]
+ return np.maximum(np.zeros(1), np.minimum(down_slopes, up_slopes))
+
+ def tokenize_with_weights(self, text, return_word_ids=False, image=None, audio=None, video=None, llama_template=None, skip_template=True, thinking=False, **kwargs):
+
+ # Process audio
+ audio_features = []
+ if audio is not None:
+ waveform = audio["waveform"].squeeze(0) if hasattr(audio, "__getitem__") else audio
+ sample_rate = audio.get("sample_rate", 16000) if hasattr(audio, "get") else 16000
+ mel, mel_mask = self._extract_mel_spectrogram(waveform, sample_rate)
+ audio_features = [(mel.unsqueeze(0), mel_mask.unsqueeze(0))] # ([1, T, 128], [1, T])
+
+ # Process image/video frames
+ is_video = video is not None
+ source = video if is_video else image
+ images = []
+ if source is not None:
+ samples = source.movedim(-1, 1) # [B, C, H, W]
+ num_frames = samples.shape[0]
+
+ # Subsample video to 1fps
+ if is_video:
+ fps = kwargs.get("fps", 24)
+ step = max(1, round(fps))
+ indices = list(range(0, num_frames, step))
+ if len(indices) == 0:
+ indices = [0]
+ samples = samples[indices]
+ num_frames = len(indices)
+
+ h, w = samples.shape[2], samples.shape[3]
+ patch_size = 16
+ pooling_k = 3
+ max_soft_tokens = 70 if is_video else 280 # video uses smaller token budget per frame
+ max_patches = max_soft_tokens * pooling_k * pooling_k
+ target_px = max_patches * patch_size * patch_size
+ factor = (target_px / (h * w)) ** 0.5
+ side_mult = pooling_k * patch_size
+ target_h = max(int(factor * h // side_mult) * side_mult, side_mult)
+ target_w = max(int(factor * w // side_mult) * side_mult, side_mult)
+
+ import torchvision.transforms.functional as TVF
+ for i in range(num_frames):
+ # rescaling to match reference code
+ s = (samples[i].clamp(0, 1) * 255).to(torch.uint8) # [C, H, W] uint8
+ if target_h != h or target_w != w:
+ s = TVF.resize(s, [target_h, target_w], interpolation=TVF.InterpolationMode.BICUBIC, antialias=True)
+ s = s.float() * (1.0 / 255.0)
+ images.append({"pixels": s.unsqueeze(0).movedim(1, -1)[:, :, :, :3], "max_soft_tokens": max_soft_tokens})
+
+ if text.startswith('<|turn>'):
+ skip_template = True
+
+ if skip_template:
+ llama_text = text
+ else:
+ if llama_template is not None:
+ llama_text = llama_template.format(text)
+ else:
+ # Build template from modalities present
+ system = "<|turn>system\n<|think|>\n" if thinking else ""
+ media = ""
+ if len(images) > 0:
+ if is_video:
+ media += "\n\n"
+ for i in range(len(images)):
+ ts = f"{int(i // 60):02d}:{int(i % 60):02d}"
+ sep = "" if i == 0 else " "
+ media += f"{sep}{ts} <|image><|video|>"
+ media += "\n\n"
+ else:
+ media += "\n\n"
+ for i in range(len(images)):
+ if i > 0:
+ media += "\n\n\n\n"
+ media += "<|image><|image|>"
+ media += "\n\n"
+ if len(audio_features) > 0:
+ # Compute audio token count (always at 16kHz)
+ num_samples = int(waveform.shape[-1] * 16000 / sample_rate) if sample_rate != 16000 else waveform.shape[-1]
+ _fl = 320 # int(round(16000 * 20.0 / 1000.0))
+ _hl = 160 # int(round(16000 * 10.0 / 1000.0))
+ _nmel = (num_samples + _fl // 2 - (_fl + 1)) // _hl + 1
+ _t = _nmel
+ for _ in range(2):
+ _t = (_t + 2 - 3) // 2 + 1
+ n_audio_tokens = min(_t, 750)
+ media += "<|audio>" + "<|audio|>" * n_audio_tokens + ""
+ llama_text = f"{system}<|turn>user\n{media}{text}\n<|turn>model\n"
+
+ text_tokens = super().tokenize_with_weights(llama_text, return_word_ids)
+
+ def _replace_placeholders(token_list, token_id, embeds):
+ """Replace first placeholder with embed dict, remove remaining consecutive ones."""
+ embed_idx = 0
+ i = 0
+ while i < len(token_list):
+ if token_list[i][0] == token_id and embed_idx < len(embeds):
+ token_list[i] = (embeds[embed_idx],) + token_list[i][1:]
+ embed_idx += 1
+ i += 1
+ while i < len(token_list) and token_list[i][0] == token_id:
+ token_list.pop(i)
+ else:
+ i += 1
+
+ if len(images) > 0:
+ img_token_id = 258884 if is_video else 258880
+ img_embeds = [{"type": "image", "data": img["pixels"], "max_soft_tokens": img["max_soft_tokens"]} for img in images]
+ for r in text_tokens:
+ _replace_placeholders(r, img_token_id, img_embeds)
+
+ if len(audio_features) > 0:
+ aud_embeds = [{"type": "audio", "data": mel, "mask": mask} for mel, mask in audio_features]
+ for r in text_tokens:
+ _replace_placeholders(r, 258881, aud_embeds)
+
+ return text_tokens
+
+
+class _Gemma4Tokenizer:
+ """Tokenizer using the tokenizers (Gemma4 doesn't come with sentencepiece model)"""
+ 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())
+ 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))
+
+
+# Tokenizer
+class Gemma4SDTokenizer(Gemma4_Tokenizer, sd1_clip.SDTokenizer):
+ embedding_size = 2560
+ 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, pad_with_end=False, embedding_size=self.embedding_size, embedding_key='gemma4', tokenizer_class=_Gemma4Tokenizer, has_start_token=True, has_end_token=False, pad_to_max_length=False, max_length=99999999, min_length=1, pad_left=True, disable_weights=True, start_token=2, tokenizer_data=tokenizer_data)
+
+ def decode(self, token_ids, **kwargs):
+ text = super().decode(token_ids, skip_special_tokens=False)
+ # Translate thinking channel markers to standard / tags
+ text = text.replace("<|channel>thought\n", "\n")
+ text = text.replace("", " ")
+ # Strip remaining special tokens
+ text = text.replace("", "").replace("", "").strip()
+ return text
+
+
+class Gemma4Tokenizer(sd1_clip.SD1Tokenizer):
+ tokenizer_class = Gemma4SDTokenizer
+ def __init__(self, embedding_directory=None, tokenizer_data={}):
+ super().__init__(embedding_directory=embedding_directory, tokenizer_data=tokenizer_data, name="gemma4", tokenizer=self.tokenizer_class)
+
+
+# Model wrappers
+class Gemma4Model(sd1_clip.SDClipModel):
+ model_class = None
+ def __init__(self, device="cpu", layer="all", layer_idx=None, dtype=None, attention_mask=True, model_options={}):
+ self.dtypes = set()
+ self.dtypes.add(dtype)
+ super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config={}, dtype=dtype, special_tokens={"start": 2, "pad": 0}, layer_norm_hidden_state=False, model_class=self.model_class, enable_attention_masks=attention_mask, return_attention_masks=attention_mask, model_options=model_options)
+
+ def process_tokens(self, tokens, device):
+ embeds, _, _, _ = super().process_tokens(tokens, device)
+ return embeds
+
+ def generate(self, tokens, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed, presence_penalty=0.0):
+ if isinstance(tokens, dict):
+ tokens = next(iter(tokens.values()))
+ tokens_only = [[t[0] for t in b] for b in tokens]
+ embeds, _, _, embeds_info = sd1_clip.SDClipModel.process_tokens(self, tokens_only, self.execution_device)
+ seq_len = embeds.shape[1]
+ ids = [0] * seq_len
+ expanded_idx = 0
+ embed_map = {info["index"]: info["size"] for info in embeds_info}
+ for t in tokens_only[0]:
+ if expanded_idx in embed_map:
+ expanded_idx += embed_map[expanded_idx]
+ elif isinstance(t, int):
+ if expanded_idx < seq_len:
+ ids[expanded_idx] = t
+ expanded_idx += 1
+ else:
+ expanded_idx += 1
+ initial_token_ids = [ids]
+ input_ids = torch.tensor(initial_token_ids, device=self.execution_device)
+ return self.transformer.generate(embeds, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed, initial_tokens=initial_token_ids[0], presence_penalty=presence_penalty, initial_input_ids=input_ids)
+
+
+def gemma4_te(dtype_llama=None, llama_quantization_metadata=None, model_class=None):
+ clip_model = type('Gemma4Model_', (Gemma4Model,), {'model_class': model_class})
+ class Gemma4TEModel_(sd1_clip.SD1ClipModel):
+ def __init__(self, device="cpu", dtype=None, model_options={}):
+ if llama_quantization_metadata is not None:
+ model_options = model_options.copy()
+ model_options["quantization_metadata"] = llama_quantization_metadata
+ if dtype_llama is not None:
+ dtype = dtype_llama
+ super().__init__(device=device, dtype=dtype, name="gemma4", clip_model=clip_model, model_options=model_options)
+ return Gemma4TEModel_
+
+
+# Variants
+
+def _make_variant(config_cls):
+ audio = config_cls.audio_config is not None
+ bases = (Gemma4AudioMixin, Gemma4Base) if audio else (Gemma4Base,)
+ class Variant(*bases):
+ def __init__(self, config_dict, dtype, device, operations):
+ super().__init__()
+ self._init_model(config_cls(**config_dict), dtype, device, operations)
+ if audio:
+ self._init_audio(self.model.config, dtype, device, operations)
+ embedding_size = config_cls.hidden_size
+ if embedding_size != Gemma4SDTokenizer.embedding_size:
+ tok_cls = type('T', (Gemma4SDTokenizer,), {'embedding_size': embedding_size})
+ class Tokenizer(Gemma4Tokenizer):
+ tokenizer_class = tok_cls
+ Variant.tokenizer = Tokenizer
+ else:
+ Variant.tokenizer = Gemma4Tokenizer
+ return Variant
+
+Gemma4_E4B = _make_variant(Gemma4Config)
+Gemma4_E2B = _make_variant(Gemma4_E2B_Config)
+Gemma4_31B = _make_variant(Gemma4_31B_Config)
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 6ea8e36b1..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 :]))
@@ -521,7 +532,7 @@ class Attention(nn.Module):
else:
present_key_value = (xk, xv, index + num_tokens)
- if sliding_window is not None and xk.shape[2] > sliding_window:
+ if sliding_window is not None and xk.shape[2] > sliding_window and seq_length == 1:
xk = xk[:, :, -sliding_window:]
xv = xv[:, :, -sliding_window:]
attention_mask = attention_mask[..., -sliding_window:] if attention_mask is not None else None
@@ -533,12 +544,12 @@ class Attention(nn.Module):
return self.o_proj(output), present_key_value
class MLP(nn.Module):
- def __init__(self, config: Llama2Config, device=None, dtype=None, ops: Any = None):
+ def __init__(self, config: Llama2Config, device=None, dtype=None, ops: Any = None, intermediate_size=None):
super().__init__()
- ops = ops or nn
- self.gate_proj = ops.Linear(config.hidden_size, config.intermediate_size, bias=False, device=device, dtype=dtype)
- self.up_proj = ops.Linear(config.hidden_size, config.intermediate_size, bias=False, device=device, dtype=dtype)
- self.down_proj = ops.Linear(config.intermediate_size, config.hidden_size, bias=False, device=device, dtype=dtype)
+ intermediate_size = intermediate_size or config.intermediate_size
+ self.gate_proj = ops.Linear(config.hidden_size, intermediate_size, bias=False, device=device, dtype=dtype)
+ self.up_proj = ops.Linear(config.hidden_size, intermediate_size, bias=False, device=device, dtype=dtype)
+ self.down_proj = ops.Linear(intermediate_size, config.hidden_size, bias=False, device=device, dtype=dtype)
if config.mlp_activation == "silu":
self.activation = torch.nn.functional.silu
elif config.mlp_activation == "gelu_pytorch_tanh":
@@ -647,24 +658,25 @@ class TransformerBlockGemma2(nn.Module):
return x, present_key_value
+def _make_scaled_embedding(ops, vocab_size, hidden_size, scale, device, dtype):
+ class ScaledEmbedding(ops.Embedding):
+ def forward(self, input_ids, out_dtype=None):
+ return super().forward(input_ids, out_dtype=out_dtype) * scale
+ return ScaledEmbedding(vocab_size, hidden_size, device=device, dtype=dtype)
+
+
class Llama2_(nn.Module):
def __init__(self, config, device=None, dtype=None, ops=None):
super().__init__()
self.config = config
self.vocab_size = config.vocab_size
- self.embed_tokens = ops.Embedding(
- config.vocab_size,
- config.hidden_size,
- device=device,
- dtype=dtype
- )
if self.config.transformer_type == "gemma2" or self.config.transformer_type == "gemma3":
transformer = TransformerBlockGemma2
- self.normalize_in = True
+ self.embed_tokens = _make_scaled_embedding(ops, config.vocab_size, config.hidden_size, config.hidden_size ** 0.5, device, dtype)
else:
transformer = TransformerBlock
- self.normalize_in = False
+ self.embed_tokens = ops.Embedding(config.vocab_size, config.hidden_size, device=device, dtype=dtype)
self.layers = nn.ModuleList([
transformer(config, index=i, device=device, dtype=dtype, ops=ops)
@@ -688,17 +700,15 @@ 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):
+ 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):
if embeds is not None:
x = embeds
else:
x = self.embed_tokens(x, out_dtype=dtype)
- if self.normalize_in:
- x *= self.config.hidden_size ** 0.5
-
seq_len = x.shape[1]
past_len = 0
if past_key_values is not None and len(past_key_values) > 0:
@@ -850,7 +860,7 @@ class BaseGenerate:
torch.empty([batch, model_config.num_key_value_heads, max_cache_len, model_config.head_dim], device=device, dtype=execution_dtype), 0))
return past_key_values
- def generate(self, embeds=None, do_sample=True, max_length=256, temperature=1.0, top_k=50, top_p=0.9, min_p=0.0, repetition_penalty=1.0, seed=42, stop_tokens=None, initial_tokens=[], execution_dtype=None, min_tokens=0, presence_penalty=0.0):
+ def generate(self, embeds=None, do_sample=True, max_length=256, temperature=1.0, top_k=50, top_p=0.9, min_p=0.0, repetition_penalty=1.0, seed=42, stop_tokens=None, initial_tokens=[], execution_dtype=None, min_tokens=0, presence_penalty=0.0, initial_input_ids=None):
device = embeds.device
if stop_tokens is None:
@@ -875,14 +885,16 @@ class BaseGenerate:
pbar = comfy.utils.ProgressBar(max_length)
# Generation loop
+ current_input_ids = initial_input_ids
for step in tqdm(range(max_length), desc="Generating tokens"):
- x, _, past_key_values = self.model.forward(None, embeds=embeds, attention_mask=None, past_key_values=past_key_values)
+ x, _, past_key_values = self.model.forward(None, embeds=embeds, attention_mask=None, past_key_values=past_key_values, input_ids=current_input_ids)
logits = self.logits(x)[:, -1]
next_token = self.sample_token(logits, temperature, top_k, top_p, min_p, repetition_penalty, initial_tokens + generated_token_ids, generator, do_sample=do_sample, presence_penalty=presence_penalty)
token_id = next_token[0].item()
generated_token_ids.append(token_id)
embeds = self.model.embed_tokens(next_token).to(execution_dtype)
+ current_input_ids = next_token if initial_input_ids is not None else None
pbar.update(1)
if token_id in stop_tokens:
diff --git a/comfy/text_encoders/lt.py b/comfy/text_encoders/lt.py
index 5aee1f4c0..bc5cbae28 100644
--- a/comfy/text_encoders/lt.py
+++ b/comfy/text_encoders/lt.py
@@ -93,8 +93,7 @@ class Gemma3_12BModel(sd1_clip.SDClipModel):
def generate(self, tokens, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed, presence_penalty):
tokens_only = [[t[0] for t in b] for b in tokens]
- embeds, _, _, embeds_info = self.process_tokens(tokens_only, self.execution_device)
- comfy.utils.normalize_image_embeddings(embeds, embeds_info, self.transformer.model.config.hidden_size ** 0.5)
+ embeds, _, _, _ = self.process_tokens(tokens_only, self.execution_device)
return self.transformer.generate(embeds, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed, stop_tokens=[106], presence_penalty=presence_penalty) # 106 is
class DualLinearProjection(torch.nn.Module):
diff --git a/comfy/text_encoders/lumina2.py b/comfy/text_encoders/lumina2.py
index 01ebdfabe..b1f1dbb9f 100644
--- a/comfy/text_encoders/lumina2.py
+++ b/comfy/text_encoders/lumina2.py
@@ -50,8 +50,7 @@ class Gemma3_4B_Vision_Model(sd1_clip.SDClipModel):
super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config={}, dtype=dtype, special_tokens={"start": 2, "pad": 0}, layer_norm_hidden_state=False, model_class=comfy.text_encoders.llama.Gemma3_4B_Vision, enable_attention_masks=attention_mask, return_attention_masks=attention_mask, model_options=model_options)
def process_tokens(self, tokens, device):
- embeds, _, _, embeds_info = super().process_tokens(tokens, device)
- comfy.utils.normalize_image_embeddings(embeds, embeds_info, self.transformer.model.config.hidden_size ** 0.5)
+ embeds, _, _, _ = super().process_tokens(tokens, device)
return embeds
class LuminaModel(sd1_clip.SD1ClipModel):
diff --git a/comfy/text_encoders/qwen35.py b/comfy/text_encoders/qwen35.py
index ce9b07464..416ce9d18 100644
--- a/comfy/text_encoders/qwen35.py
+++ b/comfy/text_encoders/qwen35.py
@@ -408,8 +408,6 @@ class Qwen35Transformer(Llama2_):
nn.Module.__init__(self)
self.config = config
self.vocab_size = config.vocab_size
- self.normalize_in = False
-
self.embed_tokens = ops.Embedding(config.vocab_size, config.hidden_size, device=device, dtype=dtype)
self.layers = nn.ModuleList([
Qwen35TransformerBlock(config, index=i, device=device, dtype=dtype, ops=ops)
@@ -453,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):
@@ -653,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)
@@ -763,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|>'):
@@ -774,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/utils.py b/comfy/utils.py
index 78c491b98..66682690a 100644
--- a/comfy/utils.py
+++ b/comfy/utils.py
@@ -1164,12 +1164,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 +1202,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 +1396,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
@@ -1446,10 +1452,3 @@ def deepcopy_list_dict(obj, memo=None):
memo[obj_id] = res
return res
-def normalize_image_embeddings(embeds, embeds_info, scale_factor):
- """Normalize image embeddings to match text embedding scale"""
- for info in embeds_info:
- if info.get("type") == "image":
- start_idx = info["index"]
- end_idx = start_idx + info["size"]
- embeds[:, start_idx:end_idx, :] /= scale_factor
diff --git a/comfy_api/feature_flags.py b/comfy_api/feature_flags.py
index 9f6918315..adb5a3144 100644
--- a/comfy_api/feature_flags.py
+++ b/comfy_api/feature_flags.py
@@ -5,12 +5,95 @@ This module handles capability negotiation between frontend and backend,
allowing graceful protocol evolution while maintaining backward compatibility.
"""
-from typing import Any
+import logging
+from typing import Any, TypedDict
from comfy.cli_args import args
+
+class FeatureFlagInfo(TypedDict):
+ type: str
+ default: Any
+ description: str
+
+
+# Registry of known CLI-settable feature flags.
+# Launchers can query this via --list-feature-flags to discover valid flags.
+CLI_FEATURE_FLAG_REGISTRY: dict[str, FeatureFlagInfo] = {
+ "show_signin_button": {
+ "type": "bool",
+ "default": False,
+ "description": "Show the sign-in button in the frontend even when not signed in",
+ },
+}
+
+
+def _coerce_bool(v: str) -> bool:
+ """Strict bool coercion: only 'true'/'false' (case-insensitive).
+
+ Anything else raises ValueError so the caller can warn and drop the flag,
+ rather than silently treating typos like 'ture' or 'yes' as False.
+ """
+ lower = v.lower()
+ if lower == "true":
+ return True
+ if lower == "false":
+ return False
+ raise ValueError(f"expected 'true' or 'false', got {v!r}")
+
+
+_COERCE_FNS: dict[str, Any] = {
+ "bool": _coerce_bool,
+ "int": lambda v: int(v),
+ "float": lambda v: float(v),
+}
+
+
+def _coerce_flag_value(key: str, raw_value: str) -> Any:
+ """Coerce a raw string value using the registry type, or keep as string.
+
+ Returns the raw string if the key is unregistered or the type is unknown.
+ Raises ValueError/TypeError if the key is registered with a known type but
+ the value cannot be coerced; callers are expected to warn and drop the flag.
+ """
+ info = CLI_FEATURE_FLAG_REGISTRY.get(key)
+ if info is None:
+ return raw_value
+ coerce = _COERCE_FNS.get(info["type"])
+ if coerce is None:
+ return raw_value
+ return coerce(raw_value)
+
+
+def _parse_cli_feature_flags() -> dict[str, Any]:
+ """Parse --feature-flag key=value pairs from CLI args into a dict.
+
+ Items without '=' default to the value 'true' (bare flag form).
+ Flags whose value cannot be coerced to the registered type are dropped
+ with a warning, so a typo like '--feature-flag some_bool=ture' does not
+ silently take effect as the wrong value.
+ """
+ result: dict[str, Any] = {}
+ for item in getattr(args, "feature_flag", []):
+ key, sep, raw_value = item.partition("=")
+ key = key.strip()
+ if not key:
+ continue
+ if not sep:
+ raw_value = "true"
+ try:
+ result[key] = _coerce_flag_value(key, raw_value.strip())
+ except (ValueError, TypeError) as e:
+ info = CLI_FEATURE_FLAG_REGISTRY.get(key, {})
+ logging.warning(
+ "Could not coerce --feature-flag %s=%r to %s (%s); dropping flag.",
+ key, raw_value.strip(), info.get("type", "?"), e,
+ )
+ return result
+
+
# Default server capabilities
-SERVER_FEATURE_FLAGS: dict[str, Any] = {
+_CORE_FEATURE_FLAGS: dict[str, Any] = {
"supports_preview_metadata": True,
"max_upload_size": args.max_upload_size * 1024 * 1024, # Convert MB to bytes
"extension": {"manager": {"supports_v4": True}},
@@ -18,6 +101,11 @@ SERVER_FEATURE_FLAGS: dict[str, Any] = {
"assets": args.enable_assets,
}
+# CLI-provided flags cannot overwrite core flags
+_cli_flags = {k: v for k, v in _parse_cli_feature_flags().items() if k not in _CORE_FEATURE_FLAGS}
+
+SERVER_FEATURE_FLAGS: dict[str, Any] = {**_CORE_FEATURE_FLAGS, **_cli_flags}
+
def get_connection_feature(
sockets_metadata: dict[str, dict[str, Any]],
diff --git a/comfy_api/input/__init__.py b/comfy_api/input/__init__.py
index 16d4acfd1..dc33533cc 100644
--- a/comfy_api/input/__init__.py
+++ b/comfy_api/input/__init__.py
@@ -9,6 +9,7 @@ from comfy_api.latest._input import (
CurveInput,
MonotoneCubicCurve,
LinearCurve,
+ RangeInput,
)
__all__ = [
@@ -21,4 +22,5 @@ __all__ = [
"CurveInput",
"MonotoneCubicCurve",
"LinearCurve",
+ "RangeInput",
]
diff --git a/comfy_api/latest/_input/__init__.py b/comfy_api/latest/_input/__init__.py
index 05cd3d40a..f0229717e 100644
--- a/comfy_api/latest/_input/__init__.py
+++ b/comfy_api/latest/_input/__init__.py
@@ -1,5 +1,6 @@
from .basic_types import ImageInput, AudioInput, MaskInput, LatentInput
from .curve_types import CurvePoint, CurveInput, MonotoneCubicCurve, LinearCurve
+from .range_types import RangeInput
from .video_types import VideoInput
__all__ = [
@@ -12,4 +13,5 @@ __all__ = [
"CurveInput",
"MonotoneCubicCurve",
"LinearCurve",
+ "RangeInput",
]
diff --git a/comfy_api/latest/_input/range_types.py b/comfy_api/latest/_input/range_types.py
new file mode 100644
index 000000000..f4c5cb290
--- /dev/null
+++ b/comfy_api/latest/_input/range_types.py
@@ -0,0 +1,70 @@
+from __future__ import annotations
+
+import logging
+import math
+import numpy as np
+
+logger = logging.getLogger(__name__)
+
+
+class RangeInput:
+ """Represents a levels/range adjustment: input range [min, max] with
+ optional midpoint (gamma control).
+
+ Generates a 1D LUT identical to GIMP's levels mapping:
+ 1. Normalize input to [0, 1] using [min, max]
+ 2. Apply gamma correction: pow(value, 1/gamma)
+ 3. Clamp to [0, 1]
+
+ The midpoint field is a position in [0, 1] representing where the
+ midtone falls within [min, max]. It maps to gamma via:
+ gamma = -log2(midpoint)
+ So midpoint=0.5 → gamma=1.0 (linear).
+ """
+
+ def __init__(self, min_val: float, max_val: float, midpoint: float | None = None):
+ self.min_val = min_val
+ self.max_val = max_val
+ self.midpoint = midpoint
+
+ @staticmethod
+ def from_raw(data) -> RangeInput:
+ if isinstance(data, RangeInput):
+ return data
+ if isinstance(data, dict):
+ return RangeInput(
+ min_val=float(data.get("min", 0.0)),
+ max_val=float(data.get("max", 1.0)),
+ midpoint=float(data["midpoint"]) if data.get("midpoint") is not None else None,
+ )
+ raise TypeError(f"Cannot convert {type(data)} to RangeInput")
+
+ def to_lut(self, size: int = 256) -> np.ndarray:
+ """Generate a float64 lookup table mapping [0, 1] input through this
+ levels adjustment.
+
+ The LUT maps normalized input values (0..1) to output values (0..1),
+ matching the GIMP levels formula.
+ """
+ xs = np.linspace(0.0, 1.0, size, dtype=np.float64)
+
+ in_range = self.max_val - self.min_val
+ if abs(in_range) < 1e-10:
+ return np.where(xs >= self.min_val, 1.0, 0.0).astype(np.float64)
+
+ # Normalize: map [min, max] → [0, 1]
+ result = (xs - self.min_val) / in_range
+ result = np.clip(result, 0.0, 1.0)
+
+ # Gamma correction from midpoint
+ if self.midpoint is not None and self.midpoint > 0 and self.midpoint != 0.5:
+ gamma = max(-math.log2(self.midpoint), 0.001)
+ inv_gamma = 1.0 / gamma
+ mask = result > 0
+ result[mask] = np.power(result[mask], inv_gamma)
+
+ return result
+
+ def __repr__(self) -> str:
+ mid = f", midpoint={self.midpoint}" if self.midpoint is not None else ""
+ return f"RangeInput(min={self.min_val}, max={self.max_val}{mid})"
diff --git a/comfy_api/latest/_input_impl/video_types.py b/comfy_api/latest/_input_impl/video_types.py
index 1b4993aa7..942278d88 100644
--- a/comfy_api/latest/_input_impl/video_types.py
+++ b/comfy_api/latest/_input_impl/video_types.py
@@ -12,6 +12,7 @@ import numpy as np
import math
import torch
from .._util import VideoContainer, VideoCodec, VideoComponents
+import logging
def container_to_output_format(container_format: str | None) -> str | None:
@@ -238,64 +239,125 @@ class VideoFromFile(VideoInput):
start_time = max(self._get_raw_duration() + self.__start_time, 0)
else:
start_time = self.__start_time
+
# Get video frames
frames = []
+ audio_frames = []
+ alphas = None
start_pts = int(start_time / video_stream.time_base)
end_pts = int((start_time + self.__duration) / video_stream.time_base)
- container.seek(start_pts, stream=video_stream)
- for frame in container.decode(video_stream):
- if frame.pts < start_pts:
- continue
- if self.__duration and frame.pts >= end_pts:
- break
- img = frame.to_ndarray(format='rgb24') # shape: (H, W, 3)
- img = torch.from_numpy(img) / 255.0 # shape: (H, W, 3)
- frames.append(img)
- images = torch.stack(frames) if len(frames) > 0 else torch.zeros(0, 3, 0, 0)
+ if start_pts != 0:
+ container.seek(start_pts, stream=video_stream)
+
+ image_format = 'gbrpf32le'
+ process_image_format = lambda a: a
+ audio = None
+
+ streams = [video_stream]
+ has_first_audio_frame = False
+ checked_alpha = False
+
+ # Default to False so we decode until EOF if duration is 0
+ video_done = False
+ audio_done = True
+
+ if len(container.streams.audio):
+ audio_stream = container.streams.audio[-1]
+ streams += [audio_stream]
+ resampler = av.audio.resampler.AudioResampler(format='fltp')
+ audio_done = False
+
+ for packet in container.demux(*streams):
+ if video_done and audio_done:
+ break
+
+ if packet.stream.type == "video":
+ if video_done:
+ continue
+ try:
+ for frame in packet.decode():
+ if frame.pts < start_pts:
+ continue
+ if self.__duration and frame.pts >= end_pts:
+ video_done = True
+ break
+
+ if not checked_alpha:
+ alpha_channel = False
+ for comp in frame.format.components:
+ if comp.is_alpha or frame.format.name == "pal8":
+ alphas = []
+ alpha_channel = True
+ break
+ if frame.format.name in ("yuvj420p", "yuvj422p", "yuvj444p", "rgb24", "rgba", "pal8"):
+ process_image_format = lambda a: a.float() / 255.0
+ if alpha_channel:
+ image_format = 'rgba'
+ else:
+ image_format = 'rgb24'
+ else:
+ process_image_format = lambda a: a
+ if alpha_channel:
+ image_format = 'gbrapf32le'
+ else:
+ image_format = 'gbrpf32le'
+
+ checked_alpha = True
+
+ img = frame.to_ndarray(format=image_format) # shape: (H, W, 4)
+ if frame.rotation != 0:
+ k = int(round(frame.rotation // 90))
+ img = np.rot90(img, k=k, axes=(0, 1)).copy()
+ if alphas is None:
+ frames.append(torch.from_numpy(img))
+ else:
+ frames.append(torch.from_numpy(img[..., :-1]))
+ alphas.append(torch.from_numpy(img[..., -1:]))
+ except av.error.InvalidDataError:
+ logging.info("pyav decode error")
+
+ elif packet.stream.type == "audio":
+ if audio_done:
+ continue
+
+ aframes = itertools.chain.from_iterable(
+ map(resampler.resample, packet.decode())
+ )
+ for frame in aframes:
+ if self.__duration and frame.time > start_time + self.__duration:
+ audio_done = True
+ break
+
+ if not has_first_audio_frame:
+ offset_seconds = start_time - frame.pts * audio_stream.time_base
+ to_skip = max(0, int(offset_seconds * audio_stream.sample_rate))
+ if to_skip < frame.samples:
+ has_first_audio_frame = True
+ audio_frames.append(frame.to_ndarray()[..., to_skip:])
+ else:
+ audio_frames.append(frame.to_ndarray())
+
+ images = process_image_format(torch.stack(frames)) if len(frames) > 0 else torch.zeros(0, 0, 0, 3)
+ if alphas is not None:
+ alphas = process_image_format(torch.stack(alphas)) if len(alphas) > 0 else torch.zeros(0, 0, 0, 1)
# Get frame rate
frame_rate = Fraction(video_stream.average_rate) if video_stream.average_rate else Fraction(1)
- # Get audio if available
- audio = None
- container.seek(start_pts, stream=video_stream)
- # Use last stream for consistency
- if len(container.streams.audio):
- audio_stream = container.streams.audio[-1]
- audio_frames = []
- resample = av.audio.resampler.AudioResampler(format='fltp').resample
- frames = itertools.chain.from_iterable(
- map(resample, container.decode(audio_stream))
- )
+ if len(audio_frames) > 0:
+ audio_data = np.concatenate(audio_frames, axis=1) # shape: (channels, total_samples)
+ if self.__duration:
+ audio_data = audio_data[..., :int(self.__duration * audio_stream.sample_rate)]
- has_first_frame = False
- for frame in frames:
- offset_seconds = start_time - frame.pts * audio_stream.time_base
- to_skip = max(0, int(offset_seconds * audio_stream.sample_rate))
- if to_skip < frame.samples:
- has_first_frame = True
- break
- if has_first_frame:
- audio_frames.append(frame.to_ndarray()[..., to_skip:])
-
- for frame in frames:
- if self.__duration and frame.time > start_time + self.__duration:
- break
- audio_frames.append(frame.to_ndarray()) # shape: (channels, samples)
- if len(audio_frames) > 0:
- audio_data = np.concatenate(audio_frames, axis=1) # shape: (channels, total_samples)
- if self.__duration:
- audio_data = audio_data[..., :int(self.__duration * audio_stream.sample_rate)]
-
- audio_tensor = torch.from_numpy(audio_data).unsqueeze(0) # shape: (1, channels, total_samples)
- audio = AudioInput({
- "waveform": audio_tensor,
- "sample_rate": int(audio_stream.sample_rate) if audio_stream.sample_rate else 1,
- })
+ audio_tensor = torch.from_numpy(audio_data).unsqueeze(0) # shape: (1, channels, total_samples)
+ audio = AudioInput({
+ "waveform": audio_tensor,
+ "sample_rate": int(audio_stream.sample_rate) if audio_stream.sample_rate else 1,
+ })
metadata = container.metadata
- return VideoComponents(images=images, audio=audio, frame_rate=frame_rate, metadata=metadata)
+ return VideoComponents(images=images, alpha=alphas, audio=audio, frame_rate=frame_rate, metadata=metadata)
def get_components(self) -> VideoComponents:
if isinstance(self.__file, io.BytesIO):
diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py
index fdeffea2d..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
@@ -395,7 +396,6 @@ class Combo(ComfyTypeIO):
@comfytype(io_type="COMBO")
class MultiCombo(ComfyTypeI):
'''Multiselect Combo input (dropdown for selecting potentially more than one value).'''
- # TODO: something is wrong with the serialization, frontend does not recognize it as multiselect
Type = list[str]
class Input(Combo.Input):
def __init__(self, id: str, options: list[str], display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
@@ -408,12 +408,14 @@ class MultiCombo(ComfyTypeI):
self.default: list[str]
def as_dict(self):
- to_return = super().as_dict() | prune_dict({
- "multi_select": self.multiselect,
- "placeholder": self.placeholder,
- "chip": self.chip,
+ # Frontend expects `multi_select` to be an object config (not a boolean).
+ # Keep top-level `multiselect` from Combo.Input for backwards compatibility.
+ return super().as_dict() | prune_dict({
+ "multi_select": prune_dict({
+ "placeholder": self.placeholder,
+ "chip": self.chip,
+ }),
})
- return to_return
@comfytype(io_type="IMAGE")
class Image(ComfyTypeIO):
@@ -613,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:
@@ -1266,6 +1273,43 @@ class Histogram(ComfyTypeIO):
Type = list[int]
+@comfytype(io_type="RANGE")
+class Range(ComfyTypeIO):
+ from comfy_api.input import RangeInput
+ if TYPE_CHECKING:
+ Type = RangeInput
+
+ class Input(WidgetInput):
+ def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None,
+ socketless: bool=True, default: dict=None,
+ display: str=None,
+ gradient_stops: list=None,
+ show_midpoint: bool=None,
+ midpoint_scale: str=None,
+ value_min: float=None,
+ value_max: float=None,
+ advanced: bool=None):
+ super().__init__(id, display_name, optional, tooltip, None, default, socketless, None, None, None, None, advanced)
+ if default is None:
+ self.default = {"min": 0.0, "max": 1.0}
+ self.display = display
+ self.gradient_stops = gradient_stops
+ self.show_midpoint = show_midpoint
+ self.midpoint_scale = midpoint_scale
+ self.value_min = value_min
+ self.value_max = value_max
+
+ def as_dict(self):
+ return super().as_dict() | prune_dict({
+ "display": self.display,
+ "gradient_stops": self.gradient_stops,
+ "show_midpoint": self.show_midpoint,
+ "midpoint_scale": self.midpoint_scale,
+ "value_min": self.value_min,
+ "value_max": self.value_max,
+ })
+
+
DYNAMIC_INPUT_LOOKUP: dict[str, Callable[[dict[str, Any], dict[str, Any], tuple[str, dict[str, Any]], str, list[str] | None], None]] = {}
def register_dynamic_input_func(io_type: str, func: Callable[[dict[str, Any], dict[str, Any], tuple[str, dict[str, Any]], str, list[str] | None], None]):
DYNAMIC_INPUT_LOOKUP[io_type] = func
@@ -2219,6 +2263,7 @@ __all__ = [
"ModelPatch",
"ClipVision",
"ClipVisionOutput",
+ "BackgroundRemoval",
"AudioEncoder",
"AudioEncoderOutput",
"StyleModel",
@@ -2276,5 +2321,6 @@ __all__ = [
"BoundingBox",
"Curve",
"Histogram",
+ "Range",
"NodeReplace",
]
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 fd3b5a510..c92477f08 100644
--- a/comfy_api/latest/_util/video_types.py
+++ b/comfy_api/latest/_util/video_types.py
@@ -3,7 +3,7 @@ from dataclasses import dataclass
from enum import Enum
from fractions import Fraction
from typing import Optional
-from .._input import ImageInput, AudioInput
+from .._input import ImageInput, AudioInput, MaskInput
class VideoCodec(str, Enum):
AUTO = "auto"
@@ -48,5 +48,4 @@ class VideoComponents:
frame_rate: Fraction
audio: Optional[AudioInput] = None
metadata: Optional[dict] = None
-
-
+ alpha: Optional[MaskInput] = None
diff --git a/comfy_api_nodes/apis/anthropic.py b/comfy_api_nodes/apis/anthropic.py
new file mode 100644
index 000000000..6cac537ea
--- /dev/null
+++ b/comfy_api_nodes/apis/anthropic.py
@@ -0,0 +1,75 @@
+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 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)
+
+
+class AnthropicResponseTextBlock(BaseModel):
+ type: Literal["text"] = "text"
+ text: str = Field(...)
+
+
+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[AnthropicResponseTextBlock] | 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/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 eafabbefe..03f4c445b 100644
--- a/comfy_api_nodes/apis/bytedance.py
+++ b/comfy_api_nodes/apis/bytedance.py
@@ -157,6 +157,11 @@ class SeedanceCreateAssetResponse(BaseModel):
asset_id: str = Field(...)
+class SeedanceVirtualLibraryCreateAssetRequest(BaseModel):
+ url: str = Field(..., description="Publicly accessible URL of the image asset to upload.")
+ hash: str = Field(..., description="Dedup key. Re-submitting the same hash returns the existing asset id.")
+
+
# Dollars per 1K tokens, keyed by (model_id, has_video_input).
SEEDANCE2_PRICE_PER_1K_TOKENS = {
("dreamina-seedance-2-0-260128", False): 0.007,
@@ -193,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/luma.py b/comfy_api_nodes/apis/luma.py
index 632c4ab96..8c6db2022 100644
--- a/comfy_api_nodes/apis/luma.py
+++ b/comfy_api_nodes/apis/luma.py
@@ -1,15 +1,12 @@
from __future__ import annotations
-
-import torch
-
from enum import Enum
from typing import Optional, Union
+import torch
from pydantic import BaseModel, Field, confloat
-
class LumaIO:
LUMA_REF = "LUMA_REF"
LUMA_CONCEPTS = "LUMA_CONCEPTS"
@@ -183,13 +180,13 @@ class LumaAssets(BaseModel):
class LumaImageRef(BaseModel):
- '''Used for image gen'''
+ """Used for image gen"""
url: str = Field(..., description='The URL of the image reference')
weight: confloat(ge=0.0, le=1.0) = Field(..., description='The weight of the image reference')
class LumaImageReference(BaseModel):
- '''Used for video gen'''
+ """Used for video gen"""
type: Optional[str] = Field('image', description='Input type, defaults to image')
url: str = Field(..., description='The URL of the image')
@@ -251,3 +248,32 @@ class LumaGeneration(BaseModel):
assets: Optional[LumaAssets] = Field(None, description='The assets of the generation')
model: str = Field(..., description='The model used for the generation')
request: Union[LumaGenerationRequest, LumaImageGenerationRequest] = Field(..., description="The request used for the generation")
+
+
+class Luma2ImageRef(BaseModel):
+ url: str | None = None
+ data: str | None = None
+ media_type: str | None = None
+
+
+class Luma2GenerationRequest(BaseModel):
+ prompt: str = Field(..., min_length=1, max_length=6000)
+ model: str | None = None
+ type: str | None = None
+ aspect_ratio: str | None = None
+ style: str | None = None
+ output_format: str | None = None
+ web_search: bool | None = None
+ image_ref: list[Luma2ImageRef] | None = None
+ source: Luma2ImageRef | None = None
+
+
+class Luma2Generation(BaseModel):
+ id: str | None = None
+ type: str | None = None
+ state: str | None = None
+ model: str | None = None
+ created_at: str | None = None
+ output: list[LumaImageReference] | None = None
+ failure_reason: str | None = None
+ failure_code: str | None = None
diff --git a/comfy_api_nodes/apis/moonvalley.py b/comfy_api_nodes/apis/moonvalley.py
deleted file mode 100644
index 7ec7a4ade..000000000
--- a/comfy_api_nodes/apis/moonvalley.py
+++ /dev/null
@@ -1,152 +0,0 @@
-from enum import Enum
-from typing import Optional, Dict, Any
-
-from pydantic import BaseModel, Field, StrictBytes
-
-
-class MoonvalleyPromptResponse(BaseModel):
- error: Optional[Dict[str, Any]] = None
- frame_conditioning: Optional[Dict[str, Any]] = None
- id: Optional[str] = None
- inference_params: Optional[Dict[str, Any]] = None
- meta: Optional[Dict[str, Any]] = None
- model_params: Optional[Dict[str, Any]] = None
- output_url: Optional[str] = None
- prompt_text: Optional[str] = None
- status: Optional[str] = None
-
-
-class MoonvalleyTextToVideoInferenceParams(BaseModel):
- add_quality_guidance: Optional[bool] = Field(
- True, description='Whether to add quality guidance'
- )
- caching_coefficient: Optional[float] = Field(
- 0.3, description='Caching coefficient for optimization'
- )
- caching_cooldown: Optional[int] = Field(
- 3, description='Number of caching cooldown steps'
- )
- caching_warmup: Optional[int] = Field(
- 3, description='Number of caching warmup steps'
- )
- clip_value: Optional[float] = Field(
- 3, description='CLIP value for generation control'
- )
- conditioning_frame_index: Optional[int] = Field(
- 0, description='Index of the conditioning frame'
- )
- cooldown_steps: Optional[int] = Field(
- 75, description='Number of cooldown steps (calculated based on num_frames)'
- )
- fps: Optional[int] = Field(
- 24, description='Frames per second of the generated video'
- )
- guidance_scale: Optional[float] = Field(
- 10, description='Guidance scale for generation control'
- )
- height: Optional[int] = Field(
- 1080, description='Height of the generated video in pixels'
- )
- negative_prompt: Optional[str] = Field(None, description='Negative prompt text')
- num_frames: Optional[int] = Field(64, description='Number of frames to generate')
- seed: Optional[int] = Field(
- None, description='Random seed for generation (default: random)'
- )
- shift_value: Optional[float] = Field(
- 3, description='Shift value for generation control'
- )
- steps: Optional[int] = Field(80, description='Number of denoising steps')
- use_guidance_schedule: Optional[bool] = Field(
- True, description='Whether to use guidance scheduling'
- )
- use_negative_prompts: Optional[bool] = Field(
- False, description='Whether to use negative prompts'
- )
- use_timestep_transform: Optional[bool] = Field(
- True, description='Whether to use timestep transformation'
- )
- warmup_steps: Optional[int] = Field(
- 0, description='Number of warmup steps (calculated based on num_frames)'
- )
- width: Optional[int] = Field(
- 1920, description='Width of the generated video in pixels'
- )
-
-
-class MoonvalleyTextToVideoRequest(BaseModel):
- image_url: Optional[str] = None
- inference_params: Optional[MoonvalleyTextToVideoInferenceParams] = None
- prompt_text: Optional[str] = None
- webhook_url: Optional[str] = None
-
-
-class MoonvalleyUploadFileRequest(BaseModel):
- file: Optional[StrictBytes] = None
-
-
-class MoonvalleyUploadFileResponse(BaseModel):
- access_url: Optional[str] = None
-
-
-class MoonvalleyVideoToVideoInferenceParams(BaseModel):
- add_quality_guidance: Optional[bool] = Field(
- True, description='Whether to add quality guidance'
- )
- caching_coefficient: Optional[float] = Field(
- 0.3, description='Caching coefficient for optimization'
- )
- caching_cooldown: Optional[int] = Field(
- 3, description='Number of caching cooldown steps'
- )
- caching_warmup: Optional[int] = Field(
- 3, description='Number of caching warmup steps'
- )
- clip_value: Optional[float] = Field(
- 3, description='CLIP value for generation control'
- )
- conditioning_frame_index: Optional[int] = Field(
- 0, description='Index of the conditioning frame'
- )
- cooldown_steps: Optional[int] = Field(
- 36, description='Number of cooldown steps (calculated based on num_frames)'
- )
- guidance_scale: Optional[float] = Field(
- 15, description='Guidance scale for generation control'
- )
- negative_prompt: Optional[str] = Field(None, description='Negative prompt text')
- seed: Optional[int] = Field(
- None, description='Random seed for generation (default: random)'
- )
- shift_value: Optional[float] = Field(
- 3, description='Shift value for generation control'
- )
- steps: Optional[int] = Field(80, description='Number of denoising steps')
- use_guidance_schedule: Optional[bool] = Field(
- True, description='Whether to use guidance scheduling'
- )
- use_negative_prompts: Optional[bool] = Field(
- False, description='Whether to use negative prompts'
- )
- use_timestep_transform: Optional[bool] = Field(
- True, description='Whether to use timestep transformation'
- )
- warmup_steps: Optional[int] = Field(
- 24, description='Number of warmup steps (calculated based on num_frames)'
- )
-
-
-class ControlType(str, Enum):
- motion_control = 'motion_control'
- pose_control = 'pose_control'
-
-
-class MoonvalleyVideoToVideoRequest(BaseModel):
- control_type: ControlType = Field(
- ..., description='Supported types for video control'
- )
- inference_params: Optional[MoonvalleyVideoToVideoInferenceParams] = None
- prompt_text: str = Field(..., description='Describes the video to generate')
- video_url: str = Field(..., description='Url to control video')
- webhook_url: Optional[str] = Field(
- None, description='Optional webhook URL for notifications'
- )
diff --git a/comfy_api_nodes/apis/openai.py b/comfy_api_nodes/apis/openai.py
index b85ef252b..bee75d639 100644
--- a/comfy_api_nodes/apis/openai.py
+++ b/comfy_api_nodes/apis/openai.py
@@ -56,14 +56,14 @@ class ModelResponseProperties(BaseModel):
instructions: str | None = Field(None)
max_output_tokens: int | None = Field(None)
model: str | None = Field(None)
- temperature: float | None = Field(1, description="Controls randomness in the response", ge=0.0, le=2.0)
+ temperature: float | None = Field(None, description="Controls randomness in the response", ge=0.0, le=2.0)
top_p: float | None = Field(
- 1,
+ None,
description="Controls diversity of the response via nucleus sampling",
ge=0.0,
le=1.0,
)
- truncation: str | None = Field("disabled", description="Allowed values: 'auto' or 'disabled'")
+ truncation: str | None = Field(None, description="Allowed values: 'auto' or 'disabled'")
class ResponseProperties(BaseModel):
diff --git a/comfy_api_nodes/apis/topaz.py b/comfy_api_nodes/apis/topaz.py
index a9e6235a7..f91980e3d 100644
--- a/comfy_api_nodes/apis/topaz.py
+++ b/comfy_api_nodes/apis/topaz.py
@@ -1,4 +1,4 @@
-from typing import Optional, Union
+from typing import Optional
from pydantic import BaseModel, Field
@@ -72,8 +72,11 @@ class VideoEnhancementFilter(BaseModel):
grain: Optional[float] = Field(None, description="Grain after AI model processing")
grainSize: Optional[float] = Field(None, description="Size of generated grain")
recoverOriginalDetailValue: Optional[float] = Field(None, description="Source details into the output video")
- creativity: Optional[str] = Field(None, description="Creativity level(high, low) for slc-1 only")
+ creativity: float | str | None = Field(None, description="slc-1/slp-2.5: enum (low/middle/high). ast-2: decimal 0.0-1.0.")
isOptimizedMode: Optional[bool] = Field(None, description="Set to true for Starlight Creative (slc-1) only")
+ prompt: str | None = Field(None, description="Descriptive scene prompt (ast-2 only)")
+ sharp: float | None = Field(None, description="ast-2 pre-enhance sharpness")
+ realism: float | None = Field(None, description="ast-2 realism control")
class OutputInformationVideo(BaseModel):
@@ -90,7 +93,7 @@ class Overrides(BaseModel):
class CreateVideoRequest(BaseModel):
source: CreateVideoRequestSource = Field(...)
- filters: list[Union[VideoFrameInterpolationFilter, VideoEnhancementFilter]] = Field(...)
+ filters: list[VideoFrameInterpolationFilter | VideoEnhancementFilter] = Field(...)
output: OutputInformationVideo = Field(...)
overrides: Overrides = Field(Overrides(isPaidDiffusion=True))
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/apis/wan.py b/comfy_api_nodes/apis/wan.py
index 44b65e4f6..c64acae97 100644
--- a/comfy_api_nodes/apis/wan.py
+++ b/comfy_api_nodes/apis/wan.py
@@ -118,7 +118,7 @@ class Wan27ReferenceVideoInputField(BaseModel):
class Wan27ReferenceVideoParametersField(BaseModel):
resolution: str = Field(...)
ratio: str | None = Field(None)
- duration: int = Field(5, ge=2, le=10)
+ duration: int = Field(5, ge=2, le=15)
watermark: bool = Field(False)
seed: int = Field(..., ge=0, le=2147483647)
@@ -157,7 +157,7 @@ class Wan27VideoEditInputField(BaseModel):
class Wan27VideoEditParametersField(BaseModel):
resolution: str = Field(...)
ratio: str | None = Field(None)
- duration: int = Field(0)
+ duration: int | None = Field(0)
audio_setting: str = Field("auto")
watermark: bool = Field(False)
seed: int = Field(..., ge=0, le=2147483647)
diff --git a/comfy_api_nodes/nodes_anthropic.py b/comfy_api_nodes/nodes_anthropic.py
new file mode 100644
index 000000000..28dd70d4e
--- /dev/null
+++ b/comfy_api_nodes/nodes_anthropic.py
@@ -0,0 +1,245 @@
+"""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,
+ AnthropicRole,
+ AnthropicTextContent,
+)
+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",
+}
+
+
+def _claude_model_inputs():
+ return [
+ IO.Int.Input(
+ "max_tokens",
+ default=16000,
+ min=32,
+ max=32000,
+ tooltip="Maximum number of tokens to generate before stopping.",
+ 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.",
+ advanced=True,
+ ),
+ ]
+
+
+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 ""
+ return "\n".join(block.text for block in response.content if 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()) 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["max_tokens"]
+ temperature = None if model_label == "Opus 4.7" else model["temperature"]
+
+ 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,
+ ),
+ 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 de192c5ac..d6b479336 100644
--- a/comfy_api_nodes/nodes_bytedance.py
+++ b/comfy_api_nodes/nodes_bytedance.py
@@ -1,3 +1,4 @@
+import hashlib
import logging
import math
import re
@@ -9,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,
@@ -20,6 +24,7 @@ from comfy_api_nodes.apis.bytedance import (
SeedanceCreateAssetResponse,
SeedanceCreateVisualValidateSessionResponse,
SeedanceGetVisualValidateSessionResponse,
+ SeedanceVirtualLibraryCreateAssetRequest,
Seedream4Options,
Seedream4TaskCreationRequest,
TaskAudioContent,
@@ -66,6 +71,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}
@@ -271,6 +282,30 @@ async def _wait_for_asset_active(cls: type[IO.ComfyNode], asset_id: str, group_i
)
+async def _seedance_virtual_library_upload_image_asset(
+ cls: type[IO.ComfyNode],
+ image: torch.Tensor,
+ *,
+ wait_label: str = "Uploading image",
+) -> str:
+ """Upload an image into the caller's per-customer Seedance virtual library."""
+ public_url = await upload_image_to_comfyapi(cls, image, wait_label=wait_label)
+ normalized = image.detach().cpu().contiguous().to(torch.float32)
+ digest = hashlib.sha256()
+ digest.update(str(tuple(normalized.shape)).encode("utf-8"))
+ digest.update(b"\0")
+ digest.update(normalized.numpy().tobytes())
+ image_hash = digest.hexdigest()
+ create_resp = await sync_op(
+ cls,
+ ApiEndpoint(path="/proxy/seedance/virtual-library/assets", method="POST"),
+ response_model=SeedanceCreateAssetResponse,
+ data=SeedanceVirtualLibraryCreateAssetRequest(url=public_url, hash=image_hash),
+ )
+ await _wait_for_asset_active(cls, create_resp.asset_id, group_id="virtual-library")
+ return f"asset://{create_resp.asset_id}"
+
+
def _seedance2_price_extractor(model_id: str, has_video_input: bool):
"""Returns a price_extractor closure for Seedance 2.0 poll_op."""
rate = SEEDANCE2_PRICE_PER_1K_TOKENS.get((model_id, has_video_input))
@@ -536,6 +571,7 @@ class ByteDanceSeedreamNode(IO.ComfyNode):
)
""",
),
+ is_deprecated=True,
)
@classmethod
@@ -625,6 +661,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
@@ -1245,7 +1501,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",
@@ -1261,6 +1517,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(
@@ -1377,7 +1634,6 @@ class ByteDance2TextToVideoNode(IO.ComfyNode):
status_extractor=lambda r: r.status,
price_extractor=_seedance2_price_extractor(model_id, has_video_input=False),
poll_interval=9,
- max_poll_attempts=180,
)
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
@@ -1395,8 +1651,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.",
),
@@ -1507,7 +1769,9 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode):
if first_frame_asset_id:
first_frame_url = image_assets[first_frame_asset_id]
else:
- first_frame_url = await upload_image_to_comfyapi(cls, first_frame, wait_label="Uploading first frame.")
+ first_frame_url = await _seedance_virtual_library_upload_image_asset(
+ cls, first_frame, wait_label="Uploading first frame."
+ )
content: list[TaskTextContent | TaskImageContent] = [
TaskTextContent(text=model["prompt"]),
@@ -1527,7 +1791,9 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode):
content.append(
TaskImageContent(
image_url=TaskImageContentUrl(
- url=await upload_image_to_comfyapi(cls, last_frame, wait_label="Uploading last frame.")
+ url=await _seedance_virtual_library_upload_image_asset(
+ cls, last_frame, wait_label="Uploading last frame."
+ )
),
role="last_frame",
),
@@ -1555,14 +1821,13 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode):
status_extractor=lambda r: r.status,
price_extractor=_seedance2_price_extractor(model_id, has_video_input=False),
poll_interval=9,
- max_poll_attempts=180,
)
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(
@@ -1640,8 +1905,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.",
),
@@ -1805,9 +2076,9 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
content.append(
TaskImageContent(
image_url=TaskImageContentUrl(
- url=await upload_image_to_comfyapi(
+ url=await _seedance_virtual_library_upload_image_asset(
cls,
- image=reference_images[key],
+ reference_images[key],
wait_label=f"Uploading image {i}",
),
),
@@ -1877,7 +2148,6 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
status_extractor=lambda r: r.status,
price_extractor=_seedance2_price_extractor(model_id, has_video_input=has_video_input),
poll_interval=9,
- max_poll_attempts=180,
)
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
@@ -2065,6 +2335,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_hitpaw.py b/comfy_api_nodes/nodes_hitpaw.py
index 488080a74..bca5170e4 100644
--- a/comfy_api_nodes/nodes_hitpaw.py
+++ b/comfy_api_nodes/nodes_hitpaw.py
@@ -178,7 +178,6 @@ class HitPawGeneralImageEnhance(IO.ComfyNode):
status_extractor=lambda x: x.data.status,
price_extractor=lambda x: request_price,
poll_interval=10.0,
- max_poll_attempts=480,
)
return IO.NodeOutput(await download_url_to_image_tensor(final_response.data.res_url))
@@ -324,7 +323,6 @@ class HitPawVideoEnhance(IO.ComfyNode):
status_extractor=lambda x: x.data.status,
price_extractor=lambda x: request_price,
poll_interval=10.0,
- max_poll_attempts=320,
)
return IO.NodeOutput(await download_url_to_video_output(final_response.data.res_url))
diff --git a/comfy_api_nodes/nodes_kling.py b/comfy_api_nodes/nodes_kling.py
index 709b3726c..ef647b20b 100644
--- a/comfy_api_nodes/nodes_kling.py
+++ b/comfy_api_nodes/nodes_kling.py
@@ -2788,11 +2788,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_luma.py b/comfy_api_nodes/nodes_luma.py
index 9ed6cd299..d92a7c382 100644
--- a/comfy_api_nodes/nodes_luma.py
+++ b/comfy_api_nodes/nodes_luma.py
@@ -1,10 +1,11 @@
-from typing import Optional
-
import torch
from typing_extensions import override
-from comfy_api.latest import IO, ComfyExtension
+from comfy_api.latest import IO, ComfyExtension, Input
from comfy_api_nodes.apis.luma import (
+ Luma2Generation,
+ Luma2GenerationRequest,
+ Luma2ImageRef,
LumaAspectRatio,
LumaCharacterRef,
LumaConceptChain,
@@ -30,6 +31,7 @@ from comfy_api_nodes.util import (
download_url_to_video_output,
poll_op,
sync_op,
+ upload_image_to_comfyapi,
upload_images_to_comfyapi,
validate_string,
)
@@ -212,9 +214,9 @@ class LumaImageGenerationNode(IO.ComfyNode):
aspect_ratio: str,
seed,
style_image_weight: float,
- image_luma_ref: Optional[LumaReferenceChain] = None,
- style_image: Optional[torch.Tensor] = None,
- character_image: Optional[torch.Tensor] = None,
+ image_luma_ref: LumaReferenceChain | None = None,
+ style_image: torch.Tensor | None = None,
+ character_image: torch.Tensor | None = None,
) -> IO.NodeOutput:
validate_string(prompt, strip_whitespace=True, min_length=3)
# handle image_luma_ref
@@ -434,7 +436,7 @@ class LumaTextToVideoGenerationNode(IO.ComfyNode):
duration: str,
loop: bool,
seed,
- luma_concepts: Optional[LumaConceptChain] = None,
+ luma_concepts: LumaConceptChain | None = None,
) -> IO.NodeOutput:
validate_string(prompt, strip_whitespace=False, min_length=3)
duration = duration if model != LumaVideoModel.ray_1_6 else None
@@ -533,7 +535,6 @@ class LumaImageToVideoGenerationNode(IO.ComfyNode):
],
is_api_node=True,
price_badge=PRICE_BADGE_VIDEO,
-
)
@classmethod
@@ -644,6 +645,293 @@ PRICE_BADGE_VIDEO = IO.PriceBadge(
)
+def _luma2_uni1_common_inputs(max_image_refs: int) -> list:
+ return [
+ IO.Combo.Input(
+ "style",
+ options=["auto", "manga"],
+ default="auto",
+ tooltip="Style preset. 'auto' picks based on the prompt; "
+ "'manga' applies a manga/anime aesthetic and requires a portrait "
+ "aspect ratio (2:3, 9:16, 1:2, 1:3).",
+ ),
+ IO.Boolean.Input(
+ "web_search",
+ default=False,
+ tooltip="Search the web for visual references before generating.",
+ ),
+ IO.Autogrow.Input(
+ "image_ref",
+ template=IO.Autogrow.TemplateNames(
+ IO.Image.Input("image"),
+ names=[f"image_{i}" for i in range(1, max_image_refs + 1)],
+ min=0,
+ ),
+ optional=True,
+ tooltip=f"Up to {max_image_refs} reference images for style/content guidance.",
+ ),
+ ]
+
+
+async def _luma2_upload_image_refs(
+ cls: type[IO.ComfyNode],
+ refs: dict | None,
+ max_count: int,
+) -> list[Luma2ImageRef] | None:
+ if not refs:
+ return None
+ out: list[Luma2ImageRef] = []
+ for key in refs:
+ url = await upload_image_to_comfyapi(cls, refs[key])
+ out.append(Luma2ImageRef(url=url))
+ if len(out) > max_count:
+ raise ValueError(f"Maximum {max_count} reference images are allowed.")
+ return out or None
+
+
+async def _luma2_submit_and_poll(
+ cls: type[IO.ComfyNode],
+ request: Luma2GenerationRequest,
+) -> Input.Image:
+ initial = await sync_op(
+ cls,
+ ApiEndpoint(path="/proxy/luma_2/generations", method="POST"),
+ response_model=Luma2Generation,
+ data=request,
+ )
+ if not initial.id:
+ raise RuntimeError("Luma 2 API did not return a generation id.")
+ final = await poll_op(
+ cls,
+ ApiEndpoint(path=f"/proxy/luma_2/generations/{initial.id}", method="GET"),
+ response_model=Luma2Generation,
+ status_extractor=lambda r: r.state,
+ progress_extractor=lambda r: None,
+ )
+ if not final.output:
+ msg = final.failure_reason or "no output returned"
+ raise RuntimeError(f"Luma 2 generation failed: {msg}")
+ url = final.output[0].url
+ if not url:
+ raise RuntimeError("Luma 2 generation completed without an output URL.")
+ return await download_url_to_image_tensor(url)
+
+
+class LumaImageNode(IO.ComfyNode):
+
+ @classmethod
+ def define_schema(cls) -> IO.Schema:
+ return IO.Schema(
+ node_id="LumaImageNode2",
+ display_name="Luma UNI-1 Image",
+ category="api node/image/Luma",
+ description="Generate images from text using the Luma UNI-1 model.",
+ inputs=[
+ IO.String.Input(
+ "prompt",
+ multiline=True,
+ default="",
+ tooltip="Text description of the desired image. 1–6000 characters.",
+ ),
+ IO.DynamicCombo.Input(
+ "model",
+ options=[
+ IO.DynamicCombo.Option(
+ "uni-1",
+ [
+ IO.Combo.Input(
+ "aspect_ratio",
+ options=[
+ "auto",
+ "3:1",
+ "2:1",
+ "16:9",
+ "3:2",
+ "1:1",
+ "2:3",
+ "9:16",
+ "1:2",
+ "1:3",
+ ],
+ default="auto",
+ tooltip="Output image aspect ratio. 'auto' lets "
+ "the model pick based on the prompt.",
+ ),
+ *_luma2_uni1_common_inputs(max_image_refs=9),
+ ],
+ ),
+ IO.DynamicCombo.Option(
+ "uni-1-max",
+ [
+ IO.Combo.Input(
+ "aspect_ratio",
+ options=[
+ "auto",
+ "3:1",
+ "2:1",
+ "16:9",
+ "3:2",
+ "1:1",
+ "2:3",
+ "9:16",
+ "1:2",
+ "1:3",
+ ],
+ default="auto",
+ tooltip="Output image aspect ratio. 'auto' lets "
+ "the model pick based on the prompt.",
+ ),
+ *_luma2_uni1_common_inputs(max_image_refs=9),
+ ],
+ ),
+ ],
+ tooltip="Model to use for generation.",
+ ),
+ 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.",
+ ),
+ ],
+ 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"], input_groups=["model.image_ref"]),
+ expr="""
+ (
+ $m := widgets.model;
+ $refs := $lookup(inputGroups, "model.image_ref");
+ $base := $m = "uni-1-max" ? 0.1 : 0.0404;
+ {"type":"usd","usd": $round($base + 0.003 * $refs, 4)}
+ )
+ """,
+ ),
+ )
+
+ @classmethod
+ async def execute(
+ cls,
+ prompt: str,
+ model: dict,
+ seed: int,
+ ) -> IO.NodeOutput:
+ validate_string(prompt, min_length=1, max_length=6000)
+ aspect_ratio = model["aspect_ratio"]
+ style = model["style"]
+ allowed_manga_ratios = {"2:3", "9:16", "1:2", "1:3"}
+ if style == "manga" and aspect_ratio != "auto" and aspect_ratio not in allowed_manga_ratios:
+ raise ValueError(
+ f"'manga' style requires a portrait aspect ratio "
+ f"({', '.join(sorted(allowed_manga_ratios))}) or 'auto'; got '{aspect_ratio}'."
+ )
+ request = Luma2GenerationRequest(
+ prompt=prompt,
+ model=model["model"],
+ type="image",
+ aspect_ratio=aspect_ratio if aspect_ratio != "auto" else None,
+ style=style if style != "auto" else None,
+ output_format="png",
+ web_search=model["web_search"],
+ image_ref=await _luma2_upload_image_refs(cls, model.get("image_ref"), max_count=9),
+ )
+ return IO.NodeOutput(await _luma2_submit_and_poll(cls, request))
+
+
+class LumaImageEditNode(IO.ComfyNode):
+
+ @classmethod
+ def define_schema(cls) -> IO.Schema:
+ return IO.Schema(
+ node_id="LumaImageEditNode2",
+ display_name="Luma UNI-1 Image Edit",
+ category="api node/image/Luma",
+ description="Edit an existing image with a text prompt using the Luma UNI-1 model.",
+ inputs=[
+ IO.Image.Input(
+ "source",
+ tooltip="Source image to edit.",
+ ),
+ IO.String.Input(
+ "prompt",
+ multiline=True,
+ default="",
+ tooltip="Description of the desired edit. 1–6000 characters.",
+ ),
+ IO.DynamicCombo.Input(
+ "model",
+ options=[
+ IO.DynamicCombo.Option(
+ "uni-1",
+ _luma2_uni1_common_inputs(max_image_refs=8),
+ ),
+ IO.DynamicCombo.Option(
+ "uni-1-max",
+ _luma2_uni1_common_inputs(max_image_refs=8),
+ ),
+ ],
+ tooltip="Model to use for editing.",
+ ),
+ 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.",
+ ),
+ ],
+ 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"], input_groups=["model.image_ref"]),
+ expr="""
+ (
+ $m := widgets.model;
+ $refs := $lookup(inputGroups, "model.image_ref");
+ $base := $m = "uni-1-max" ? 0.103 : 0.0434;
+ {"type":"usd","usd": $round($base + 0.003 * $refs, 4)}
+ )
+ """,
+ ),
+ )
+
+ @classmethod
+ async def execute(
+ cls,
+ source: Input.Image,
+ prompt: str,
+ model: dict,
+ seed: int,
+ ) -> IO.NodeOutput:
+ validate_string(prompt, min_length=1, max_length=6000)
+ request = Luma2GenerationRequest(
+ prompt=prompt,
+ model=model["model"],
+ type="image_edit",
+ source=Luma2ImageRef(url=await upload_image_to_comfyapi(cls, source)),
+ style=model["style"] if model["style"] != "auto" else None,
+ output_format="png",
+ web_search=model["web_search"],
+ image_ref=await _luma2_upload_image_refs(cls, model.get("image_ref"), max_count=8),
+ )
+ return IO.NodeOutput(await _luma2_submit_and_poll(cls, request))
+
+
class LumaExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
@@ -654,6 +942,8 @@ class LumaExtension(ComfyExtension):
LumaImageToVideoGenerationNode,
LumaReferenceNode,
LumaConceptsNode,
+ LumaImageNode,
+ LumaImageEditNode,
]
diff --git a/comfy_api_nodes/nodes_magnific.py b/comfy_api_nodes/nodes_magnific.py
index 0f53208d4..38b881fea 100644
--- a/comfy_api_nodes/nodes_magnific.py
+++ b/comfy_api_nodes/nodes_magnific.py
@@ -230,7 +230,6 @@ class MagnificImageUpscalerCreativeNode(IO.ComfyNode):
status_extractor=lambda x: x.status,
price_extractor=lambda _: price_usd,
poll_interval=10.0,
- max_poll_attempts=480,
)
return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0]))
@@ -391,7 +390,6 @@ class MagnificImageUpscalerPreciseV2Node(IO.ComfyNode):
status_extractor=lambda x: x.status,
price_extractor=lambda _: price_usd,
poll_interval=10.0,
- max_poll_attempts=480,
)
return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0]))
@@ -541,7 +539,6 @@ class MagnificImageStyleTransferNode(IO.ComfyNode):
response_model=TaskResponse,
status_extractor=lambda x: x.status,
poll_interval=10.0,
- max_poll_attempts=480,
)
return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0]))
@@ -782,7 +779,6 @@ class MagnificImageRelightNode(IO.ComfyNode):
response_model=TaskResponse,
status_extractor=lambda x: x.status,
poll_interval=10.0,
- max_poll_attempts=480,
)
return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0]))
@@ -924,7 +920,6 @@ class MagnificImageSkinEnhancerNode(IO.ComfyNode):
response_model=TaskResponse,
status_extractor=lambda x: x.status,
poll_interval=10.0,
- max_poll_attempts=480,
)
return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0]))
diff --git a/comfy_api_nodes/nodes_moonvalley.py b/comfy_api_nodes/nodes_moonvalley.py
deleted file mode 100644
index 78a230529..000000000
--- a/comfy_api_nodes/nodes_moonvalley.py
+++ /dev/null
@@ -1,534 +0,0 @@
-import logging
-
-from typing_extensions import override
-
-from comfy_api.latest import IO, ComfyExtension, Input
-from comfy_api_nodes.apis.moonvalley import (
- MoonvalleyPromptResponse,
- MoonvalleyTextToVideoInferenceParams,
- MoonvalleyTextToVideoRequest,
- MoonvalleyVideoToVideoInferenceParams,
- MoonvalleyVideoToVideoRequest,
-)
-from comfy_api_nodes.util import (
- ApiEndpoint,
- download_url_to_video_output,
- poll_op,
- sync_op,
- trim_video,
- upload_images_to_comfyapi,
- upload_video_to_comfyapi,
- validate_container_format_is_mp4,
- validate_image_dimensions,
- validate_string,
-)
-
-API_UPLOADS_ENDPOINT = "/proxy/moonvalley/uploads"
-API_PROMPTS_ENDPOINT = "/proxy/moonvalley/prompts"
-API_VIDEO2VIDEO_ENDPOINT = "/proxy/moonvalley/prompts/video-to-video"
-API_TXT2VIDEO_ENDPOINT = "/proxy/moonvalley/prompts/text-to-video"
-API_IMG2VIDEO_ENDPOINT = "/proxy/moonvalley/prompts/image-to-video"
-
-MIN_WIDTH = 300
-MIN_HEIGHT = 300
-
-MAX_WIDTH = 10000
-MAX_HEIGHT = 10000
-
-MIN_VID_WIDTH = 300
-MIN_VID_HEIGHT = 300
-
-MAX_VID_WIDTH = 10000
-MAX_VID_HEIGHT = 10000
-
-MAX_VIDEO_SIZE = 1024 * 1024 * 1024 # 1 GB max for in-memory video processing
-
-MOONVALLEY_MAREY_MAX_PROMPT_LENGTH = 5000
-
-
-def is_valid_task_creation_response(response: MoonvalleyPromptResponse) -> bool:
- """Verifies that the initial response contains a task ID."""
- return bool(response.id)
-
-
-def validate_task_creation_response(response) -> None:
- if not is_valid_task_creation_response(response):
- error_msg = f"Moonvalley Marey API: Initial request failed. Code: {response.code}, Message: {response.message}, Data: {response}"
- logging.error(error_msg)
- raise RuntimeError(error_msg)
-
-
-def validate_video_to_video_input(video: Input.Video) -> Input.Video:
- """
- Validates and processes video input for Moonvalley Video-to-Video generation.
-
- Args:
- video: Input video to validate
-
- Returns:
- Validated and potentially trimmed video
-
- Raises:
- ValueError: If video doesn't meet requirements
- MoonvalleyApiError: If video duration is too short
- """
- width, height = _get_video_dimensions(video)
- _validate_video_dimensions(width, height)
- validate_container_format_is_mp4(video)
-
- return _validate_and_trim_duration(video)
-
-
-def _get_video_dimensions(video: Input.Video) -> tuple[int, int]:
- """Extracts video dimensions with error handling."""
- try:
- return video.get_dimensions()
- except Exception as e:
- logging.error("Error getting dimensions of video: %s", e)
- raise ValueError(f"Cannot get video dimensions: {e}") from e
-
-
-def _validate_video_dimensions(width: int, height: int) -> None:
- """Validates video dimensions meet Moonvalley V2V requirements."""
- supported_resolutions = {
- (1920, 1080),
- (1080, 1920),
- (1152, 1152),
- (1536, 1152),
- (1152, 1536),
- }
-
- if (width, height) not in supported_resolutions:
- supported_list = ", ".join([f"{w}x{h}" for w, h in sorted(supported_resolutions)])
- raise ValueError(f"Resolution {width}x{height} not supported. Supported: {supported_list}")
-
-
-def _validate_and_trim_duration(video: Input.Video) -> Input.Video:
- """Validates video duration and trims to 5 seconds if needed."""
- duration = video.get_duration()
- _validate_minimum_duration(duration)
- return _trim_if_too_long(video, duration)
-
-
-def _validate_minimum_duration(duration: float) -> None:
- """Ensures video is at least 5 seconds long."""
- if duration < 5:
- raise ValueError("Input video must be at least 5 seconds long.")
-
-
-def _trim_if_too_long(video: Input.Video, duration: float) -> Input.Video:
- """Trims video to 5 seconds if longer."""
- if duration > 5:
- return trim_video(video, 5)
- return video
-
-
-def parse_width_height_from_res(resolution: str):
- # Accepts a string like "16:9 (1920 x 1080)" and returns width, height as a dict
- res_map = {
- "16:9 (1920 x 1080)": {"width": 1920, "height": 1080},
- "9:16 (1080 x 1920)": {"width": 1080, "height": 1920},
- "1:1 (1152 x 1152)": {"width": 1152, "height": 1152},
- "4:3 (1536 x 1152)": {"width": 1536, "height": 1152},
- "3:4 (1152 x 1536)": {"width": 1152, "height": 1536},
- # "21:9 (2560 x 1080)": {"width": 2560, "height": 1080},
- }
- return res_map.get(resolution, {"width": 1920, "height": 1080})
-
-
-def parse_control_parameter(value):
- control_map = {
- "Motion Transfer": "motion_control",
- "Canny": "canny_control",
- "Pose Transfer": "pose_control",
- "Depth": "depth_control",
- }
- return control_map.get(value, control_map["Motion Transfer"])
-
-
-async def get_response(cls: type[IO.ComfyNode], task_id: str) -> MoonvalleyPromptResponse:
- return await poll_op(
- cls,
- ApiEndpoint(path=f"{API_PROMPTS_ENDPOINT}/{task_id}"),
- response_model=MoonvalleyPromptResponse,
- status_extractor=lambda r: (r.status if r and r.status else None),
- poll_interval=16.0,
- max_poll_attempts=240,
- )
-
-
-class MoonvalleyImg2VideoNode(IO.ComfyNode):
-
- @classmethod
- def define_schema(cls) -> IO.Schema:
- return IO.Schema(
- node_id="MoonvalleyImg2VideoNode",
- display_name="Moonvalley Marey Image to Video",
- category="api node/video/Moonvalley Marey",
- description="Moonvalley Marey Image to Video Node",
- inputs=[
- IO.Image.Input(
- "image",
- tooltip="The reference image used to generate the video",
- ),
- IO.String.Input(
- "prompt",
- multiline=True,
- ),
- IO.String.Input(
- "negative_prompt",
- multiline=True,
- default=" gopro, bright, contrast, static, overexposed, vignette, "
- "artifacts, still, noise, texture, scanlines, videogame, 360 camera, VR, transition, "
- "flare, saturation, distorted, warped, wide angle, saturated, vibrant, glowing, "
- "cross dissolve, cheesy, ugly hands, mutated hands, mutant, disfigured, extra fingers, "
- "blown out, horrible, blurry, worst quality, bad, dissolve, melt, fade in, fade out, "
- "wobbly, weird, low quality, plastic, stock footage, video camera, boring",
- tooltip="Negative prompt text",
- ),
- IO.Combo.Input(
- "resolution",
- options=[
- "16:9 (1920 x 1080)",
- "9:16 (1080 x 1920)",
- "1:1 (1152 x 1152)",
- "4:3 (1536 x 1152)",
- "3:4 (1152 x 1536)",
- # "21:9 (2560 x 1080)",
- ],
- default="16:9 (1920 x 1080)",
- tooltip="Resolution of the output video",
- ),
- IO.Float.Input(
- "prompt_adherence",
- default=4.5,
- min=1.0,
- max=20.0,
- step=1.0,
- tooltip="Guidance scale for generation control",
- ),
- IO.Int.Input(
- "seed",
- default=9,
- min=0,
- max=4294967295,
- step=1,
- display_mode=IO.NumberDisplay.number,
- tooltip="Random seed value",
- control_after_generate=True,
- ),
- IO.Int.Input(
- "steps",
- default=80,
- min=75, # steps should be greater or equal to cooldown_steps(75) + warmup_steps(0)
- max=100,
- step=1,
- tooltip="Number of denoising steps",
- ),
- ],
- outputs=[IO.Video.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(),
- expr="""{"type":"usd","usd": 1.5}""",
- ),
- )
-
- @classmethod
- async def execute(
- cls,
- image: Input.Image,
- prompt: str,
- negative_prompt: str,
- resolution: str,
- prompt_adherence: float,
- seed: int,
- steps: int,
- ) -> IO.NodeOutput:
- validate_image_dimensions(image, min_width=300, min_height=300, max_height=MAX_HEIGHT, max_width=MAX_WIDTH)
- validate_string(prompt, min_length=1, max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
- validate_string(negative_prompt, field_name="negative_prompt", max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
- width_height = parse_width_height_from_res(resolution)
-
- inference_params = MoonvalleyTextToVideoInferenceParams(
- negative_prompt=negative_prompt,
- steps=steps,
- seed=seed,
- guidance_scale=prompt_adherence,
- width=width_height["width"],
- height=width_height["height"],
- use_negative_prompts=True,
- )
-
- # Get MIME type from tensor - assuming PNG format for image tensors
- mime_type = "image/png"
- image_url = (await upload_images_to_comfyapi(cls, image, max_images=1, mime_type=mime_type))[0]
- task_creation_response = await sync_op(
- cls,
- endpoint=ApiEndpoint(path=API_IMG2VIDEO_ENDPOINT, method="POST"),
- response_model=MoonvalleyPromptResponse,
- data=MoonvalleyTextToVideoRequest(
- image_url=image_url, prompt_text=prompt, inference_params=inference_params
- ),
- )
- validate_task_creation_response(task_creation_response)
- final_response = await get_response(cls, task_creation_response.id)
- video = await download_url_to_video_output(final_response.output_url)
- return IO.NodeOutput(video)
-
-
-class MoonvalleyVideo2VideoNode(IO.ComfyNode):
-
- @classmethod
- def define_schema(cls) -> IO.Schema:
- return IO.Schema(
- node_id="MoonvalleyVideo2VideoNode",
- display_name="Moonvalley Marey Video to Video",
- category="api node/video/Moonvalley Marey",
- description="",
- inputs=[
- IO.String.Input(
- "prompt",
- multiline=True,
- tooltip="Describes the video to generate",
- ),
- IO.String.Input(
- "negative_prompt",
- multiline=True,
- default=" gopro, bright, contrast, static, overexposed, vignette, "
- "artifacts, still, noise, texture, scanlines, videogame, 360 camera, VR, transition, "
- "flare, saturation, distorted, warped, wide angle, saturated, vibrant, glowing, "
- "cross dissolve, cheesy, ugly hands, mutated hands, mutant, disfigured, extra fingers, "
- "blown out, horrible, blurry, worst quality, bad, dissolve, melt, fade in, fade out, "
- "wobbly, weird, low quality, plastic, stock footage, video camera, boring",
- tooltip="Negative prompt text",
- ),
- IO.Int.Input(
- "seed",
- default=9,
- min=0,
- max=4294967295,
- step=1,
- display_mode=IO.NumberDisplay.number,
- tooltip="Random seed value",
- control_after_generate=False,
- ),
- IO.Video.Input(
- "video",
- tooltip="The reference video used to generate the output video. Must be at least 5 seconds long. "
- "Videos longer than 5s will be automatically trimmed. Only MP4 format supported.",
- ),
- IO.Combo.Input(
- "control_type",
- options=["Motion Transfer", "Pose Transfer"],
- default="Motion Transfer",
- optional=True,
- ),
- IO.Int.Input(
- "motion_intensity",
- default=100,
- min=0,
- max=100,
- step=1,
- tooltip="Only used if control_type is 'Motion Transfer'",
- optional=True,
- ),
- IO.Int.Input(
- "steps",
- default=60,
- min=60, # steps should be greater or equal to cooldown_steps(36) + warmup_steps(24)
- max=100,
- step=1,
- display_mode=IO.NumberDisplay.number,
- tooltip="Number of inference steps",
- ),
- ],
- outputs=[IO.Video.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(),
- expr="""{"type":"usd","usd": 2.25}""",
- ),
- )
-
- @classmethod
- async def execute(
- cls,
- prompt: str,
- negative_prompt: str,
- seed: int,
- video: Input.Video | None = None,
- control_type: str = "Motion Transfer",
- motion_intensity: int | None = 100,
- steps=60,
- prompt_adherence=4.5,
- ) -> IO.NodeOutput:
- validated_video = validate_video_to_video_input(video)
- video_url = await upload_video_to_comfyapi(cls, validated_video)
- validate_string(prompt, min_length=1, max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
- validate_string(negative_prompt, field_name="negative_prompt", max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
-
- # Only include motion_intensity for Motion Transfer
- control_params = {}
- if control_type == "Motion Transfer" and motion_intensity is not None:
- control_params["motion_intensity"] = motion_intensity
-
- inference_params = MoonvalleyVideoToVideoInferenceParams(
- negative_prompt=negative_prompt,
- seed=seed,
- control_params=control_params,
- steps=steps,
- guidance_scale=prompt_adherence,
- )
-
- task_creation_response = await sync_op(
- cls,
- endpoint=ApiEndpoint(path=API_VIDEO2VIDEO_ENDPOINT, method="POST"),
- response_model=MoonvalleyPromptResponse,
- data=MoonvalleyVideoToVideoRequest(
- control_type=parse_control_parameter(control_type),
- video_url=video_url,
- prompt_text=prompt,
- inference_params=inference_params,
- ),
- )
- validate_task_creation_response(task_creation_response)
- final_response = await get_response(cls, task_creation_response.id)
- return IO.NodeOutput(await download_url_to_video_output(final_response.output_url))
-
-
-class MoonvalleyTxt2VideoNode(IO.ComfyNode):
-
- @classmethod
- def define_schema(cls) -> IO.Schema:
- return IO.Schema(
- node_id="MoonvalleyTxt2VideoNode",
- display_name="Moonvalley Marey Text to Video",
- category="api node/video/Moonvalley Marey",
- description="",
- inputs=[
- IO.String.Input(
- "prompt",
- multiline=True,
- ),
- IO.String.Input(
- "negative_prompt",
- multiline=True,
- default=" gopro, bright, contrast, static, overexposed, vignette, "
- "artifacts, still, noise, texture, scanlines, videogame, 360 camera, VR, transition, "
- "flare, saturation, distorted, warped, wide angle, saturated, vibrant, glowing, "
- "cross dissolve, cheesy, ugly hands, mutated hands, mutant, disfigured, extra fingers, "
- "blown out, horrible, blurry, worst quality, bad, dissolve, melt, fade in, fade out, "
- "wobbly, weird, low quality, plastic, stock footage, video camera, boring",
- tooltip="Negative prompt text",
- ),
- IO.Combo.Input(
- "resolution",
- options=[
- "16:9 (1920 x 1080)",
- "9:16 (1080 x 1920)",
- "1:1 (1152 x 1152)",
- "4:3 (1536 x 1152)",
- "3:4 (1152 x 1536)",
- "21:9 (2560 x 1080)",
- ],
- default="16:9 (1920 x 1080)",
- tooltip="Resolution of the output video",
- ),
- IO.Float.Input(
- "prompt_adherence",
- default=4.0,
- min=1.0,
- max=20.0,
- step=1.0,
- tooltip="Guidance scale for generation control",
- ),
- IO.Int.Input(
- "seed",
- default=9,
- min=0,
- max=4294967295,
- step=1,
- display_mode=IO.NumberDisplay.number,
- control_after_generate=True,
- tooltip="Random seed value",
- ),
- IO.Int.Input(
- "steps",
- default=80,
- min=75, # steps should be greater or equal to cooldown_steps(75) + warmup_steps(0)
- max=100,
- step=1,
- tooltip="Inference steps",
- ),
- ],
- outputs=[IO.Video.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(),
- expr="""{"type":"usd","usd": 1.5}""",
- ),
- )
-
- @classmethod
- async def execute(
- cls,
- prompt: str,
- negative_prompt: str,
- resolution: str,
- prompt_adherence: float,
- seed: int,
- steps: int,
- ) -> IO.NodeOutput:
- validate_string(prompt, min_length=1, max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
- validate_string(negative_prompt, field_name="negative_prompt", max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
- width_height = parse_width_height_from_res(resolution)
-
- inference_params = MoonvalleyTextToVideoInferenceParams(
- negative_prompt=negative_prompt,
- steps=steps,
- seed=seed,
- guidance_scale=prompt_adherence,
- num_frames=128,
- width=width_height["width"],
- height=width_height["height"],
- )
-
- task_creation_response = await sync_op(
- cls,
- endpoint=ApiEndpoint(path=API_TXT2VIDEO_ENDPOINT, method="POST"),
- response_model=MoonvalleyPromptResponse,
- data=MoonvalleyTextToVideoRequest(prompt_text=prompt, inference_params=inference_params),
- )
- validate_task_creation_response(task_creation_response)
- final_response = await get_response(cls, task_creation_response.id)
- return IO.NodeOutput(await download_url_to_video_output(final_response.output_url))
-
-
-class MoonvalleyExtension(ComfyExtension):
- @override
- async def get_node_list(self) -> list[type[IO.ComfyNode]]:
- return [
- MoonvalleyImg2VideoNode,
- MoonvalleyTxt2VideoNode,
- MoonvalleyVideo2VideoNode,
- ]
-
-
-async def comfy_entrypoint() -> MoonvalleyExtension:
- return MoonvalleyExtension()
diff --git a/comfy_api_nodes/nodes_openai.py b/comfy_api_nodes/nodes_openai.py
index bbb758068..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,
@@ -39,16 +40,18 @@ STARTING_POINT_ID_PATTERN = r""
class SupportedOpenAIModel(str, Enum):
- o4_mini = "o4-mini"
- o1 = "o1"
- o3 = "o3"
- o1_pro = "o1-pro"
- gpt_4_1 = "gpt-4.1"
- gpt_4_1_mini = "gpt-4.1-mini"
- gpt_4_1_nano = "gpt-4.1-nano"
+ gpt_5_5_pro = "gpt-5.5-pro"
+ gpt_5_5 = "gpt-5.5"
gpt_5 = "gpt-5"
gpt_5_mini = "gpt-5-mini"
gpt_5_nano = "gpt-5-nano"
+ gpt_4_1 = "gpt-4.1"
+ gpt_4_1_mini = "gpt-4.1-mini"
+ gpt_4_1_nano = "gpt-4.1-nano"
+ o4_mini = "o4-mini"
+ o3 = "o3"
+ o1_pro = "o1-pro"
+ o1 = "o1"
async def validate_and_cast_response(response, timeout: int = None) -> torch.Tensor:
@@ -370,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",
@@ -415,8 +419,9 @@ class OpenAIGPTImage1(IO.ComfyNode):
"1152x2048",
"3840x2160",
"2160x3840",
+ "Custom",
],
- tooltip="Image size",
+ tooltip="Image size. Select 'Custom' to use the custom width and height (GPT Image 2 only).",
optional=True,
),
IO.Int.Input(
@@ -445,6 +450,24 @@ class OpenAIGPTImage1(IO.ComfyNode):
default="gpt-image-2",
optional=True,
),
+ 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 (GPT Image 2 only).",
+ optional=True,
+ ),
+ 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 (GPT Image 2 only).",
+ optional=True,
+ ),
],
outputs=[
IO.Image.Output(),
@@ -471,9 +494,9 @@ class OpenAIGPTImage1(IO.ComfyNode):
"high": [0.133, 0.22]
},
"gpt-image-2": {
- "low": [0.0048, 0.012],
- "medium": [0.041, 0.112],
- "high": [0.165, 0.43]
+ "low": [0.0048, 0.019],
+ "medium": [0.041, 0.168],
+ "high": [0.165, 0.67]
}
};
$range := $lookup($lookup($ranges, widgets.model), widgets.quality);
@@ -503,6 +526,8 @@ class OpenAIGPTImage1(IO.ComfyNode):
mask: Input.Image | None = None,
n: int = 1,
size: str = "1024x1024",
+ custom_width: int = 1024,
+ custom_height: int = 1024,
model: str = "gpt-image-1",
) -> IO.NodeOutput:
validate_string(prompt, strip_whitespace=False)
@@ -510,7 +535,25 @@ class OpenAIGPTImage1(IO.ComfyNode):
if mask is not None and image is None:
raise ValueError("Cannot use a mask without an input image")
- if model in ("gpt-image-1", "gpt-image-1.5"):
+ if size == "Custom":
+ if model != "gpt-image-2":
+ raise ValueError("Custom resolution is only supported by GPT Image 2 model")
+ 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}"
+ elif model in ("gpt-image-1", "gpt-image-1.5"):
if size not in ("auto", "1024x1024", "1024x1536", "1536x1024"):
raise ValueError(f"Resolution {size} is only supported by GPT Image 2 model")
@@ -599,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.
@@ -700,6 +1053,16 @@ class OpenAIChatNode(IO.ComfyNode):
"usd": [0.002, 0.008],
"format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" }
}
+ : $contains($m, "gpt-5.5-pro") ? {
+ "type": "list_usd",
+ "usd": [0.03, 0.18],
+ "format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" }
+ }
+ : $contains($m, "gpt-5.5") ? {
+ "type": "list_usd",
+ "usd": [0.005, 0.03],
+ "format": { "approximate": true, "separator": "-", "suffix": " per 1K tokens" }
+ }
: $contains($m, "gpt-5-nano") ? {
"type": "list_usd",
"usd": [0.00005, 0.0004],
@@ -948,6 +1311,7 @@ class OpenAIExtension(ComfyExtension):
OpenAIDalle2,
OpenAIDalle3,
OpenAIGPTImage1,
+ OpenAIGPTImageNodeV2,
OpenAIChatNode,
OpenAIInputFiles,
OpenAIChatConfig,
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_sora.py b/comfy_api_nodes/nodes_sora.py
index afc18bb25..c1d485188 100644
--- a/comfy_api_nodes/nodes_sora.py
+++ b/comfy_api_nodes/nodes_sora.py
@@ -33,9 +33,13 @@ class OpenAIVideoSora2(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="OpenAIVideoSora2",
- display_name="OpenAI Sora - Video",
+ display_name="OpenAI Sora - Video (DEPRECATED)",
category="api node/video/Sora",
- description="OpenAI video and audio generation.",
+ description=(
+ "OpenAI video and audio generation.\n\n"
+ "DEPRECATION NOTICE: OpenAI will stop serving the Sora v2 API in September 2026. "
+ "This node will be removed from ComfyUI at that time."
+ ),
inputs=[
IO.Combo.Input(
"model",
diff --git a/comfy_api_nodes/nodes_topaz.py b/comfy_api_nodes/nodes_topaz.py
index b18b31af1..e79c16d3c 100644
--- a/comfy_api_nodes/nodes_topaz.py
+++ b/comfy_api_nodes/nodes_topaz.py
@@ -36,11 +36,15 @@ from comfy_api_nodes.util import (
)
UPSCALER_MODELS_MAP = {
+ "Astra 2": "ast-2",
"Starlight (Astra) Fast": "slf-1",
"Starlight (Astra) Creative": "slc-1",
"Starlight Precise 2.5": "slp-2.5",
}
+AST2_MAX_FRAMES = 9000
+AST2_MAX_FRAMES_WITH_PROMPT = 450
+
class TopazImageEnhance(IO.ComfyNode):
@classmethod
@@ -230,13 +234,20 @@ class TopazVideoEnhance(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="TopazVideoEnhance",
- display_name="Topaz Video Enhance",
+ display_name="Topaz Video Enhance (Legacy)",
category="api node/video/Topaz",
description="Breathe new life into video with powerful upscaling and recovery technology.",
inputs=[
IO.Video.Input("video"),
IO.Boolean.Input("upscaler_enabled", default=True),
- IO.Combo.Input("upscaler_model", options=list(UPSCALER_MODELS_MAP.keys())),
+ IO.Combo.Input(
+ "upscaler_model",
+ options=[
+ "Starlight (Astra) Fast",
+ "Starlight (Astra) Creative",
+ "Starlight Precise 2.5",
+ ],
+ ),
IO.Combo.Input("upscaler_resolution", options=["FullHD (1080p)", "4K (2160p)"]),
IO.Combo.Input(
"upscaler_creativity",
@@ -304,6 +315,7 @@ class TopazVideoEnhance(IO.ComfyNode):
IO.Hidden.unique_id,
],
is_api_node=True,
+ is_deprecated=True,
)
@classmethod
@@ -453,7 +465,350 @@ class TopazVideoEnhance(IO.ComfyNode):
progress_extractor=lambda x: getattr(x, "progress", 0),
price_extractor=lambda x: (x.estimates.cost[0] * 0.08 if x.estimates and x.estimates.cost[0] else None),
poll_interval=10.0,
- max_poll_attempts=320,
+ )
+ return IO.NodeOutput(await download_url_to_video_output(final_response.download.url))
+
+
+class TopazVideoEnhanceV2(IO.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return IO.Schema(
+ node_id="TopazVideoEnhanceV2",
+ display_name="Topaz Video Enhance",
+ category="api node/video/Topaz",
+ description="Breathe new life into video with powerful upscaling and recovery technology.",
+ inputs=[
+ IO.Video.Input("video"),
+ IO.DynamicCombo.Input(
+ "upscaler_model",
+ options=[
+ IO.DynamicCombo.Option(
+ "Astra 2",
+ [
+ IO.Combo.Input("upscaler_resolution", options=["FullHD (1080p)", "4K (2160p)"]),
+ IO.Float.Input(
+ "creativity",
+ default=0.5,
+ min=0.0,
+ max=1.0,
+ step=0.1,
+ display_mode=IO.NumberDisplay.slider,
+ tooltip="Creative strength of the upscale.",
+ ),
+ IO.String.Input(
+ "prompt",
+ multiline=True,
+ default="",
+ tooltip="Optional descriptive (not instructive) scene prompt."
+ f"Capping input at {AST2_MAX_FRAMES_WITH_PROMPT} frames (~15s @ 30fps) when set.",
+ ),
+ IO.Float.Input(
+ "sharp",
+ default=0.5,
+ min=0.0,
+ max=1.0,
+ step=0.01,
+ display_mode=IO.NumberDisplay.slider,
+ tooltip="Pre-enhance sharpness: "
+ "0.0=Gaussian blur, 0.5=passthrough (default), 1.0=USM sharpening.",
+ advanced=True,
+ ),
+ IO.Float.Input(
+ "realism",
+ default=0.0,
+ min=0.0,
+ max=1.0,
+ step=0.01,
+ display_mode=IO.NumberDisplay.slider,
+ tooltip="Pulls output toward photographic realism."
+ "Leave at 0 for the model default.",
+ advanced=True,
+ ),
+ ],
+ ),
+ IO.DynamicCombo.Option(
+ "Starlight (Astra) Fast",
+ [IO.Combo.Input("upscaler_resolution", options=["FullHD (1080p)", "4K (2160p)"]),],
+ ),
+ IO.DynamicCombo.Option(
+ "Starlight (Astra) Creative",
+ [
+ IO.Combo.Input("upscaler_resolution", options=["FullHD (1080p)", "4K (2160p)"]),
+ IO.Combo.Input(
+ "creativity",
+ options=["low", "middle", "high"],
+ default="low",
+ tooltip="Creative strength of the upscale.",
+ ),
+ ],
+ ),
+ IO.DynamicCombo.Option(
+ "Starlight Precise 2.5",
+ [IO.Combo.Input("upscaler_resolution", options=["FullHD (1080p)", "4K (2160p)"])],
+ ),
+ IO.DynamicCombo.Option("Disabled", []),
+ ],
+ ),
+ IO.DynamicCombo.Input(
+ "interpolation_model",
+ options=[
+ IO.DynamicCombo.Option("Disabled", []),
+ IO.DynamicCombo.Option(
+ "apo-8",
+ [
+ IO.Int.Input(
+ "interpolation_frame_rate",
+ default=60,
+ min=15,
+ max=240,
+ display_mode=IO.NumberDisplay.number,
+ tooltip="Output frame rate.",
+ ),
+ IO.Int.Input(
+ "interpolation_slowmo",
+ default=1,
+ min=1,
+ max=16,
+ display_mode=IO.NumberDisplay.number,
+ tooltip="Slow-motion factor applied to the input video. "
+ "For example, 2 makes the output twice as slow and doubles the duration.",
+ advanced=True,
+ ),
+ IO.Boolean.Input(
+ "interpolation_duplicate",
+ default=False,
+ tooltip="Analyze the input for duplicate frames and remove them.",
+ advanced=True,
+ ),
+ IO.Float.Input(
+ "interpolation_duplicate_threshold",
+ default=0.01,
+ min=0.001,
+ max=0.1,
+ step=0.001,
+ display_mode=IO.NumberDisplay.number,
+ tooltip="Detection sensitivity for duplicate frames.",
+ advanced=True,
+ ),
+ ],
+ ),
+ ],
+ ),
+ IO.Combo.Input(
+ "dynamic_compression_level",
+ options=["Low", "Mid", "High"],
+ default="Low",
+ tooltip="CQP level.",
+ optional=True,
+ ),
+ ],
+ outputs=[
+ IO.Video.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=[
+ "upscaler_model",
+ "upscaler_model.upscaler_resolution",
+ "interpolation_model",
+ ]),
+ expr="""
+ (
+ $model := $lookup(widgets, "upscaler_model");
+ $res := $lookup(widgets, "upscaler_model.upscaler_resolution");
+ $interp := $lookup(widgets, "interpolation_model");
+ $is4k := $contains($res, "4k");
+ $hasInterp := $interp != "disabled";
+ $rates := {
+ "starlight (astra) fast": {"hd": 0.43, "uhd": 0.85},
+ "starlight precise 2.5": {"hd": 0.70, "uhd": 1.54},
+ "astra 2": {"hd": 1.72, "uhd": 2.85},
+ "starlight (astra) creative": {"hd": 2.25, "uhd": 3.99}
+ };
+ $surcharge := $is4k ? 0.28 : 0.14;
+ $entry := $lookup($rates, $model);
+ $base := $is4k ? $entry.uhd : $entry.hd;
+ $hi := $base + ($hasInterp ? $surcharge : 0);
+ $model = "disabled"
+ ? {"type":"text","text":"Interpolation only"}
+ : ($hasInterp
+ ? {"type":"text","text":"~" & $string($base) & "–" & $string($hi) & " credits/src frame"}
+ : {"type":"text","text":"~" & $string($base) & " credits/src frame"})
+ )
+ """,
+ ),
+ )
+
+ @classmethod
+ async def execute(
+ cls,
+ video: Input.Video,
+ upscaler_model: dict,
+ interpolation_model: dict,
+ dynamic_compression_level: str = "Low",
+ ) -> IO.NodeOutput:
+ upscaler_choice = upscaler_model["upscaler_model"]
+ interpolation_choice = interpolation_model["interpolation_model"]
+ if upscaler_choice == "Disabled" and interpolation_choice == "Disabled":
+ raise ValueError("There is nothing to do: both upscaling and interpolation are disabled.")
+ validate_container_format_is_mp4(video)
+ src_width, src_height = video.get_dimensions()
+ src_frame_rate = int(video.get_frame_rate())
+ duration_sec = video.get_duration()
+ src_video_stream = video.get_stream_source()
+ target_width = src_width
+ target_height = src_height
+ target_frame_rate = src_frame_rate
+ filters = []
+ if upscaler_choice != "Disabled":
+ if "1080p" in upscaler_model["upscaler_resolution"]:
+ target_pixel_p = 1080
+ max_long_side = 1920
+ else:
+ target_pixel_p = 2160
+ max_long_side = 3840
+ ar = src_width / src_height
+ if src_width >= src_height:
+ # Landscape or Square; Attempt to set height to target (e.g., 2160), calculate width
+ target_height = target_pixel_p
+ target_width = int(target_height * ar)
+ # Check if width exceeds standard bounds (for ultra-wide e.g., 21:9 ARs)
+ if target_width > max_long_side:
+ target_width = max_long_side
+ target_height = int(target_width / ar)
+ else:
+ # Portrait; Attempt to set width to target (e.g., 2160), calculate height
+ target_width = target_pixel_p
+ target_height = int(target_width / ar)
+ # Check if height exceeds standard bounds
+ if target_height > max_long_side:
+ target_height = max_long_side
+ target_width = int(target_height * ar)
+ if target_width % 2 != 0:
+ target_width += 1
+ if target_height % 2 != 0:
+ target_height += 1
+ model_id = UPSCALER_MODELS_MAP[upscaler_choice]
+ if model_id == "slc-1":
+ filters.append(
+ VideoEnhancementFilter(
+ model=model_id,
+ creativity=upscaler_model["creativity"],
+ isOptimizedMode=True,
+ )
+ )
+ elif model_id == "ast-2":
+ n_frames = video.get_frame_count()
+ ast2_prompt = (upscaler_model["prompt"] or "").strip()
+ if ast2_prompt and n_frames > AST2_MAX_FRAMES_WITH_PROMPT:
+ raise ValueError(
+ f"Astra 2 with a prompt is limited to {AST2_MAX_FRAMES_WITH_PROMPT} input frames "
+ f"(~15s @ 30fps); video has {n_frames}. Clear the prompt or shorten the clip."
+ )
+ if n_frames > AST2_MAX_FRAMES:
+ raise ValueError(f"Astra 2 is limited to {AST2_MAX_FRAMES} input frames; video has {n_frames}.")
+ realism = upscaler_model["realism"]
+ filters.append(
+ VideoEnhancementFilter(
+ model=model_id,
+ creativity=upscaler_model["creativity"],
+ prompt=(ast2_prompt or None),
+ sharp=upscaler_model["sharp"],
+ realism=(realism if realism > 0 else None),
+ )
+ )
+ else:
+ filters.append(VideoEnhancementFilter(model=model_id))
+ if interpolation_choice != "Disabled":
+ target_frame_rate = interpolation_model["interpolation_frame_rate"]
+ filters.append(
+ VideoFrameInterpolationFilter(
+ model=interpolation_choice,
+ slowmo=interpolation_model["interpolation_slowmo"],
+ fps=interpolation_model["interpolation_frame_rate"],
+ duplicate=interpolation_model["interpolation_duplicate"],
+ duplicate_threshold=interpolation_model["interpolation_duplicate_threshold"],
+ ),
+ )
+ initial_res = await sync_op(
+ cls,
+ ApiEndpoint(path="/proxy/topaz/video/", method="POST"),
+ response_model=CreateVideoResponse,
+ data=CreateVideoRequest(
+ source=CreateVideoRequestSource(
+ container="mp4",
+ size=get_fs_object_size(src_video_stream),
+ duration=int(duration_sec),
+ frameCount=video.get_frame_count(),
+ frameRate=src_frame_rate,
+ resolution=Resolution(width=src_width, height=src_height),
+ ),
+ filters=filters,
+ output=OutputInformationVideo(
+ resolution=Resolution(width=target_width, height=target_height),
+ frameRate=target_frame_rate,
+ audioCodec="AAC",
+ audioTransfer="Copy",
+ dynamicCompressionLevel=dynamic_compression_level,
+ ),
+ ),
+ wait_label="Creating task",
+ final_label_on_success="Task created",
+ )
+ upload_res = await sync_op(
+ cls,
+ ApiEndpoint(
+ path=f"/proxy/topaz/video/{initial_res.requestId}/accept",
+ method="PATCH",
+ ),
+ response_model=VideoAcceptResponse,
+ wait_label="Preparing upload",
+ final_label_on_success="Upload started",
+ )
+ if len(upload_res.urls) > 1:
+ raise NotImplementedError(
+ "Large files are not currently supported. Please open an issue in the ComfyUI repository."
+ )
+ async with aiohttp.ClientSession(headers={"Content-Type": "video/mp4"}) as session:
+ if isinstance(src_video_stream, BytesIO):
+ src_video_stream.seek(0)
+ async with session.put(upload_res.urls[0], data=src_video_stream, raise_for_status=True) as res:
+ upload_etag = res.headers["Etag"]
+ else:
+ with builtins.open(src_video_stream, "rb") as video_file:
+ async with session.put(upload_res.urls[0], data=video_file, raise_for_status=True) as res:
+ upload_etag = res.headers["Etag"]
+ await sync_op(
+ cls,
+ ApiEndpoint(
+ path=f"/proxy/topaz/video/{initial_res.requestId}/complete-upload",
+ method="PATCH",
+ ),
+ response_model=VideoCompleteUploadResponse,
+ data=VideoCompleteUploadRequest(
+ uploadResults=[
+ VideoCompleteUploadRequestPart(
+ partNum=1,
+ eTag=upload_etag,
+ ),
+ ],
+ ),
+ wait_label="Finalizing upload",
+ final_label_on_success="Upload completed",
+ )
+ final_response = await poll_op(
+ cls,
+ ApiEndpoint(path=f"/proxy/topaz/video/{initial_res.requestId}/status"),
+ response_model=VideoStatusResponse,
+ status_extractor=lambda x: x.status,
+ progress_extractor=lambda x: getattr(x, "progress", 0),
+ price_extractor=lambda x: (x.estimates.cost[0] * 0.08 if x.estimates and x.estimates.cost[0] else None),
+ poll_interval=10.0,
)
return IO.NodeOutput(await download_url_to_video_output(final_response.download.url))
@@ -464,6 +819,7 @@ class TopazExtension(ComfyExtension):
return [
TopazImageEnhance,
TopazVideoEnhance,
+ TopazVideoEnhanceV2,
]
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/nodes_vidu.py b/comfy_api_nodes/nodes_vidu.py
index f04407eb5..8d90cefeb 100644
--- a/comfy_api_nodes/nodes_vidu.py
+++ b/comfy_api_nodes/nodes_vidu.py
@@ -38,7 +38,7 @@ async def execute_task(
cls: type[IO.ComfyNode],
vidu_endpoint: str,
payload: TaskCreationRequest | TaskExtendCreationRequest | TaskMultiFrameCreationRequest,
- max_poll_attempts: int = 320,
+ max_poll_attempts: int = 480,
) -> list[TaskResult]:
task_creation_response = await sync_op(
cls,
@@ -1097,7 +1097,6 @@ class ViduExtendVideoNode(IO.ComfyNode):
video_url=await upload_video_to_comfyapi(cls, video, wait_label="Uploading video"),
images=[image_url] if image_url else None,
),
- max_poll_attempts=480,
)
return IO.NodeOutput(await download_url_to_video_output(results[0].url))
diff --git a/comfy_api_nodes/nodes_wan.py b/comfy_api_nodes/nodes_wan.py
index d1470894a..68061bb5c 100644
--- a/comfy_api_nodes/nodes_wan.py
+++ b/comfy_api_nodes/nodes_wan.py
@@ -818,7 +818,6 @@ class WanReferenceVideoApi(IO.ComfyNode):
response_model=VideoTaskStatusResponse,
status_extractor=lambda x: x.output.task_status,
poll_interval=6,
- max_poll_attempts=280,
)
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
@@ -1646,6 +1645,557 @@ class Wan2ReferenceVideoApi(IO.ComfyNode):
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
+class HappyHorseTextToVideoApi(IO.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return IO.Schema(
+ node_id="HappyHorseTextToVideoApi",
+ display_name="HappyHorse Text to Video",
+ category="api node/video/Wan",
+ description="Generates a video based on a text prompt using the HappyHorse model.",
+ inputs=[
+ IO.DynamicCombo.Input(
+ "model",
+ options=[
+ IO.DynamicCombo.Option(
+ "happyhorse-1.0-t2v",
+ [
+ IO.String.Input(
+ "prompt",
+ multiline=True,
+ default="",
+ tooltip="Prompt describing the elements and visual features. "
+ "Supports English and Chinese.",
+ ),
+ IO.Combo.Input(
+ "resolution",
+ options=["720P", "1080P"],
+ ),
+ IO.Combo.Input(
+ "ratio",
+ options=["16:9", "9:16", "1:1", "4:3", "3:4"],
+ ),
+ IO.Int.Input(
+ "duration",
+ default=5,
+ min=3,
+ max=15,
+ step=1,
+ 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="Seed to use for generation.",
+ ),
+ IO.Boolean.Input(
+ "watermark",
+ default=False,
+ tooltip="Whether to add an AI-generated watermark to the result.",
+ advanced=True,
+ ),
+ ],
+ outputs=[
+ IO.Video.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.duration"]),
+ expr="""
+ (
+ $res := $lookup(widgets, "model.resolution");
+ $dur := $lookup(widgets, "model.duration");
+ $ppsTable := { "720p": 0.14, "1080p": 0.24 };
+ $pps := $lookup($ppsTable, $res);
+ { "type": "usd", "usd": $pps * $dur }
+ )
+ """,
+ ),
+ )
+
+ @classmethod
+ async def execute(
+ cls,
+ model: dict,
+ seed: int,
+ watermark: bool,
+ ):
+ validate_string(model["prompt"], strip_whitespace=False, min_length=1)
+ initial_response = await sync_op(
+ cls,
+ ApiEndpoint(
+ path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
+ method="POST",
+ ),
+ response_model=TaskCreationResponse,
+ data=Wan27Text2VideoTaskCreationRequest(
+ model=model["model"],
+ input=Text2VideoInputField(
+ prompt=model["prompt"],
+ negative_prompt=None,
+ ),
+ parameters=Wan27Text2VideoParametersField(
+ resolution=model["resolution"],
+ ratio=model["ratio"],
+ duration=model["duration"],
+ seed=seed,
+ watermark=watermark,
+ ),
+ ),
+ )
+ if not initial_response.output:
+ raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
+ response = await poll_op(
+ cls,
+ ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
+ response_model=VideoTaskStatusResponse,
+ status_extractor=lambda x: x.output.task_status,
+ poll_interval=7,
+ )
+ return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
+
+
+class HappyHorseImageToVideoApi(IO.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return IO.Schema(
+ node_id="HappyHorseImageToVideoApi",
+ display_name="HappyHorse Image to Video",
+ category="api node/video/Wan",
+ description="Generate a video from a first-frame image using the HappyHorse model.",
+ inputs=[
+ IO.DynamicCombo.Input(
+ "model",
+ options=[
+ IO.DynamicCombo.Option(
+ "happyhorse-1.0-i2v",
+ [
+ IO.String.Input(
+ "prompt",
+ multiline=True,
+ default="",
+ tooltip="Prompt describing the elements and visual features. "
+ "Supports English and Chinese.",
+ ),
+ IO.Combo.Input(
+ "resolution",
+ options=["720P", "1080P"],
+ ),
+ IO.Int.Input(
+ "duration",
+ default=5,
+ min=3,
+ max=15,
+ step=1,
+ display_mode=IO.NumberDisplay.number,
+ ),
+ ],
+ ),
+ ],
+ ),
+ IO.Image.Input(
+ "first_frame",
+ tooltip="First frame image. The output aspect ratio is derived from this image.",
+ ),
+ 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 result.",
+ advanced=True,
+ ),
+ ],
+ outputs=[
+ IO.Video.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.duration"]),
+ expr="""
+ (
+ $res := $lookup(widgets, "model.resolution");
+ $dur := $lookup(widgets, "model.duration");
+ $ppsTable := { "720p": 0.14, "1080p": 0.24 };
+ $pps := $lookup($ppsTable, $res);
+ { "type": "usd", "usd": $pps * $dur }
+ )
+ """,
+ ),
+ )
+
+ @classmethod
+ async def execute(
+ cls,
+ model: dict,
+ first_frame: Input.Image,
+ seed: int,
+ watermark: bool,
+ ):
+ media = [
+ Wan27MediaItem(
+ type="first_frame",
+ url=await upload_image_to_comfyapi(cls, image=first_frame),
+ )
+ ]
+ initial_response = await sync_op(
+ cls,
+ ApiEndpoint(
+ path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
+ method="POST",
+ ),
+ response_model=TaskCreationResponse,
+ data=Wan27ImageToVideoTaskCreationRequest(
+ model=model["model"],
+ input=Wan27ImageToVideoInputField(
+ prompt=model["prompt"] or None,
+ negative_prompt=None,
+ media=media,
+ ),
+ parameters=Wan27ImageToVideoParametersField(
+ resolution=model["resolution"],
+ duration=model["duration"],
+ seed=seed,
+ watermark=watermark,
+ ),
+ ),
+ )
+ if not initial_response.output:
+ raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
+ response = await poll_op(
+ cls,
+ ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
+ response_model=VideoTaskStatusResponse,
+ status_extractor=lambda x: x.output.task_status,
+ poll_interval=7,
+ )
+ return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
+
+
+class HappyHorseVideoEditApi(IO.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return IO.Schema(
+ node_id="HappyHorseVideoEditApi",
+ display_name="HappyHorse Video Edit",
+ category="api node/video/Wan",
+ description="Edit a video using text instructions or reference images with the HappyHorse model. "
+ "Output duration is 3-15s and matches the input video; inputs longer than 15s are truncated.",
+ inputs=[
+ IO.DynamicCombo.Input(
+ "model",
+ options=[
+ IO.DynamicCombo.Option(
+ "happyhorse-1.0-video-edit",
+ [
+ IO.String.Input(
+ "prompt",
+ multiline=True,
+ default="",
+ tooltip="Editing instructions or style transfer requirements.",
+ ),
+ IO.Combo.Input(
+ "resolution",
+ options=["720P", "1080P"],
+ ),
+ IO.Combo.Input(
+ "ratio",
+ options=["16:9", "9:16", "1:1", "4:3", "3:4"],
+ tooltip="Aspect ratio. If not changed, approximates the input video ratio.",
+ ),
+ IO.Autogrow.Input(
+ "reference_images",
+ template=IO.Autogrow.TemplateNames(
+ IO.Image.Input("reference_image"),
+ names=[
+ "image1",
+ "image2",
+ "image3",
+ "image4",
+ "image5",
+ ],
+ min=0,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ IO.Video.Input(
+ "video",
+ tooltip="The video to edit.",
+ ),
+ 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 result.",
+ advanced=True,
+ ),
+ ],
+ outputs=[
+ IO.Video.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"]),
+ expr="""
+ (
+ $res := $lookup(widgets, "model.resolution");
+ $ppsTable := { "720p": 0.14, "1080p": 0.24 };
+ $pps := $lookup($ppsTable, $res);
+ { "type": "usd", "usd": $pps, "format": { "suffix": "/second" } }
+ )
+ """,
+ ),
+ )
+
+ @classmethod
+ async def execute(
+ cls,
+ model: dict,
+ video: Input.Video,
+ seed: int,
+ watermark: bool,
+ ):
+ validate_string(model["prompt"], strip_whitespace=False, min_length=1)
+ validate_video_duration(video, min_duration=3, max_duration=60)
+ media = [Wan27MediaItem(type="video", url=await upload_video_to_comfyapi(cls, video))]
+ reference_images = model.get("reference_images", {})
+ for key in reference_images:
+ media.append(
+ Wan27MediaItem(
+ type="reference_image", url=await upload_image_to_comfyapi(cls, image=reference_images[key])
+ )
+ )
+ initial_response = await sync_op(
+ cls,
+ ApiEndpoint(
+ path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
+ method="POST",
+ ),
+ response_model=TaskCreationResponse,
+ data=Wan27VideoEditTaskCreationRequest(
+ model=model["model"],
+ input=Wan27VideoEditInputField(prompt=model["prompt"], media=media),
+ parameters=Wan27VideoEditParametersField(
+ resolution=model["resolution"],
+ ratio=model["ratio"],
+ duration=None,
+ watermark=watermark,
+ seed=seed,
+ ),
+ ),
+ )
+ if not initial_response.output:
+ raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
+ response = await poll_op(
+ cls,
+ ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
+ response_model=VideoTaskStatusResponse,
+ status_extractor=lambda x: x.output.task_status,
+ poll_interval=7,
+ )
+ return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
+
+
+class HappyHorseReferenceVideoApi(IO.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return IO.Schema(
+ node_id="HappyHorseReferenceVideoApi",
+ display_name="HappyHorse Reference to Video",
+ category="api node/video/Wan",
+ description="Generate a video featuring a person or object from reference materials with the HappyHorse "
+ "model. Supports single-character performances and multi-character interactions.",
+ inputs=[
+ IO.DynamicCombo.Input(
+ "model",
+ options=[
+ IO.DynamicCombo.Option(
+ "happyhorse-1.0-r2v",
+ [
+ IO.String.Input(
+ "prompt",
+ multiline=True,
+ default="",
+ tooltip="Prompt describing the video. Use identifiers such as 'character1' and "
+ "'character2' to refer to the reference characters.",
+ ),
+ IO.Combo.Input(
+ "resolution",
+ options=["720P", "1080P"],
+ ),
+ IO.Combo.Input(
+ "ratio",
+ options=["16:9", "9:16", "1:1", "4:3", "3:4"],
+ ),
+ IO.Int.Input(
+ "duration",
+ default=5,
+ min=3,
+ max=15,
+ step=1,
+ display_mode=IO.NumberDisplay.number,
+ ),
+ IO.Autogrow.Input(
+ "reference_images",
+ template=IO.Autogrow.TemplateNames(
+ IO.Image.Input("reference_image"),
+ names=[
+ "image1",
+ "image2",
+ "image3",
+ "image4",
+ "image5",
+ "image6",
+ "image7",
+ "image8",
+ "image9",
+ ],
+ min=1,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ 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 result.",
+ advanced=True,
+ ),
+ ],
+ outputs=[
+ IO.Video.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.duration"]),
+ expr="""
+ (
+ $res := $lookup(widgets, "model.resolution");
+ $dur := $lookup(widgets, "model.duration");
+ $ppsTable := { "720p": 0.14, "1080p": 0.24 };
+ $pps := $lookup($ppsTable, $res);
+ { "type": "usd", "usd": $pps * $dur }
+ )
+ """,
+ ),
+ )
+
+ @classmethod
+ async def execute(
+ cls,
+ model: dict,
+ seed: int,
+ watermark: bool,
+ ):
+ validate_string(model["prompt"], strip_whitespace=False, min_length=1)
+ media = []
+ reference_images = model.get("reference_images", {})
+ for key in reference_images:
+ media.append(
+ Wan27MediaItem(
+ type="reference_image",
+ url=await upload_image_to_comfyapi(cls, image=reference_images[key]),
+ )
+ )
+ if not media:
+ raise ValueError("At least one reference reference image must be provided.")
+
+ initial_response = await sync_op(
+ cls,
+ ApiEndpoint(
+ path="/proxy/wan/api/v1/services/aigc/video-generation/video-synthesis",
+ method="POST",
+ ),
+ response_model=TaskCreationResponse,
+ data=Wan27ReferenceVideoTaskCreationRequest(
+ model=model["model"],
+ input=Wan27ReferenceVideoInputField(
+ prompt=model["prompt"],
+ negative_prompt=None,
+ media=media,
+ ),
+ parameters=Wan27ReferenceVideoParametersField(
+ resolution=model["resolution"],
+ ratio=model["ratio"],
+ duration=model["duration"],
+ watermark=watermark,
+ seed=seed,
+ ),
+ ),
+ )
+ if not initial_response.output:
+ raise Exception(f"An unknown error occurred: {initial_response.code} - {initial_response.message}")
+ response = await poll_op(
+ cls,
+ ApiEndpoint(path=f"/proxy/wan/api/v1/tasks/{initial_response.output.task_id}"),
+ response_model=VideoTaskStatusResponse,
+ status_extractor=lambda x: x.output.task_status,
+ poll_interval=7,
+ )
+ return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
+
+
class WanApiExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
@@ -1660,6 +2210,10 @@ class WanApiExtension(ComfyExtension):
Wan2VideoContinuationApi,
Wan2VideoEditApi,
Wan2ReferenceVideoApi,
+ HappyHorseTextToVideoApi,
+ HappyHorseImageToVideoApi,
+ HappyHorseVideoEditApi,
+ HappyHorseReferenceVideoApi,
]
diff --git a/comfy_api_nodes/nodes_wavespeed.py b/comfy_api_nodes/nodes_wavespeed.py
index c59fafd3b..65e45f60a 100644
--- a/comfy_api_nodes/nodes_wavespeed.py
+++ b/comfy_api_nodes/nodes_wavespeed.py
@@ -84,7 +84,6 @@ class WavespeedFlashVSRNode(IO.ComfyNode):
response_model=TaskResultResponse,
status_extractor=lambda x: "failed" if x.data is None else x.data.status,
poll_interval=10.0,
- max_poll_attempts=480,
)
if final_response.code != 200:
raise ValueError(
@@ -156,7 +155,6 @@ class WavespeedImageUpscaleNode(IO.ComfyNode):
response_model=TaskResultResponse,
status_extractor=lambda x: "failed" if x.data is None else x.data.status,
poll_interval=10.0,
- max_poll_attempts=480,
)
if final_response.code != 200:
raise ValueError(
diff --git a/comfy_api_nodes/util/client.py b/comfy_api_nodes/util/client.py
index b0cf97ae4..052301c33 100644
--- a/comfy_api_nodes/util/client.py
+++ b/comfy_api_nodes/util/client.py
@@ -19,6 +19,8 @@ from comfy import utils
from comfy_api.latest import IO
from server import PromptServer
+from comfy.deploy_environment import get_deploy_environment
+
from . import request_logger
from ._helpers import (
default_base_url,
@@ -148,7 +150,7 @@ async def poll_op(
queued_statuses: list[str | int] | None = None,
data: BaseModel | None = None,
poll_interval: float = 5.0,
- max_poll_attempts: int = 160,
+ max_poll_attempts: int = 480,
timeout_per_poll: float = 120.0,
max_retries_per_poll: int = 10,
retry_delay_per_poll: float = 1.0,
@@ -254,7 +256,7 @@ async def poll_op_raw(
queued_statuses: list[str | int] | None = None,
data: dict[str, Any] | BaseModel | None = None,
poll_interval: float = 5.0,
- max_poll_attempts: int = 160,
+ max_poll_attempts: int = 480,
timeout_per_poll: float = 120.0,
max_retries_per_poll: int = 10,
retry_delay_per_poll: float = 1.0,
@@ -486,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
@@ -624,6 +646,7 @@ async def _request_base(cfg: _RequestConfig, expect_binary: bool):
payload_headers = {"Accept": "*/*"} if expect_binary else {"Accept": "application/json"}
if not parsed_url.scheme and not parsed_url.netloc: # is URL relative?
payload_headers.update(get_auth_header(cfg.node_cls))
+ payload_headers["Comfy-Env"] = get_deploy_environment()
if cfg.endpoint.headers:
payload_headers.update(cfg.endpoint.headers)
diff --git a/comfy_execution/caching.py b/comfy_execution/caching.py
index f9c913bdb..ba1e8bc84 100644
--- a/comfy_execution/caching.py
+++ b/comfy_execution/caching.py
@@ -5,6 +5,7 @@ import psutil
import time
import torch
from typing import Sequence, Mapping, Dict
+from comfy.model_patcher import ModelPatcher
from comfy_execution.graph import DynamicPrompt
from abc import ABC, abstractmethod
@@ -523,13 +524,15 @@ class RAMPressureCache(LRUCache):
self.timestamps[self.cache_key_set.get_data_key(node_id)] = time.time()
super().set_local(node_id, value)
- def ram_release(self, target):
+ def ram_release(self, target, free_active=False):
if psutil.virtual_memory().available >= target:
return
clean_list = []
for key, cache_entry in self.cache.items():
+ if not free_active and self.used_generation[key] == self.generation:
+ continue
oom_score = RAM_CACHE_OLD_WORKFLOW_OOM_MULTIPLIER ** (self.generation - self.used_generation[key])
ram_usage = RAM_CACHE_DEFAULT_RAM_USAGE
@@ -542,6 +545,9 @@ class RAMPressureCache(LRUCache):
scan_list_for_ram_usage(output)
elif isinstance(output, torch.Tensor) and output.device.type == 'cpu':
ram_usage += output.numel() * output.element_size()
+ elif isinstance(output, ModelPatcher) and self.used_generation[key] != self.generation:
+ #old ModelPatchers are the first to go
+ ram_usage = 1e30
scan_list_for_ram_usage(cache_entry.outputs)
oom_score *= ram_usage
diff --git a/comfy_extras/frame_interpolation_models/film_net.py b/comfy_extras/frame_interpolation_models/film_net.py
index cf4f6e1e1..36bc79dc3 100644
--- a/comfy_extras/frame_interpolation_models/film_net.py
+++ b/comfy_extras/frame_interpolation_models/film_net.py
@@ -199,6 +199,9 @@ class FILMNet(nn.Module):
def get_dtype(self):
return self.extract.extract_sublevels.convs[0][0].conv.weight.dtype
+ def memory_used_forward(self, shape, dtype):
+ return 1700 * shape[1] * shape[2] * dtype.itemsize
+
def _build_warp_grids(self, H, W, device):
"""Pre-compute warp grids for all pyramid levels."""
if (H, W) in self._warp_grids:
diff --git a/comfy_extras/frame_interpolation_models/ifnet.py b/comfy_extras/frame_interpolation_models/ifnet.py
index 03cb34c50..ad6edbec9 100644
--- a/comfy_extras/frame_interpolation_models/ifnet.py
+++ b/comfy_extras/frame_interpolation_models/ifnet.py
@@ -74,6 +74,9 @@ class IFNet(nn.Module):
def get_dtype(self):
return self.encode.cnn0.weight.dtype
+ def memory_used_forward(self, shape, dtype):
+ return 300 * shape[1] * shape[2] * dtype.itemsize
+
def _build_warp_grids(self, H, W, device):
if (H, W) in self._warp_grids:
return
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
new file mode 100644
index 000000000..1a15facfa
--- /dev/null
+++ b/comfy_extras/nodes_ar_video.py
@@ -0,0 +1,136 @@
+"""
+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
+from typing_extensions import override
+
+import comfy.model_management
+import comfy.samplers
+import comfy.utils
+from comfy_api.latest import ComfyExtension, io
+
+
+class EmptyARVideoLatent(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="EmptyARVideoLatent",
+ category="latent/video",
+ inputs=[
+ 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.Latent.Output(display_name="LATENT"),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, width, height, length, batch_size) -> io.NodeOutput:
+ 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({"samples": latent})
+
+
+class SamplerARVideo(io.ComfyNode):
+ """Sampler for autoregressive video models (Causal Forcing, Self-Forcing).
+
+ All AR-loop parameters are owned by this node so they live in the workflow.
+ Add new widgets here as the AR sampler grows new options.
+ """
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="SamplerARVideo",
+ display_name="Sampler AR Video",
+ category="sampling/samplers",
+ inputs=[
+ io.Int.Input(
+ "num_frame_per_block",
+ default=1, min=1, max=64,
+ tooltip="Frames per autoregressive block. 1 = framewise, "
+ "3 = chunkwise. Must match the checkpoint's training mode.",
+ ),
+ ],
+ outputs=[io.Sampler.Output()],
+ )
+
+ @classmethod
+ def execute(cls, num_frame_per_block) -> io.NodeOutput:
+ extra_options = {
+ "num_frame_per_block": num_frame_per_block,
+ }
+ 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,
+ ]
+
+
+async def comfy_entrypoint() -> ARVideoExtension:
+ return ARVideoExtension()
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..2d6b3c7ea 100644
--- a/comfy_extras/nodes_audio.py
+++ b/comfy_extras/nodes_audio.py
@@ -33,7 +33,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 +82,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 +173,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 +202,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 +232,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 +260,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 +307,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 +402,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 +448,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 +482,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"]
@@ -537,6 +561,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"]
@@ -584,6 +614,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 +630,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]
@@ -645,6 +684,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 +769,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_compositing.py b/comfy_extras/nodes_compositing.py
index 3bc9fccb3..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"),
@@ -202,14 +202,11 @@ class JoinImageWithAlpha(io.ComfyNode):
@classmethod
def execute(cls, image: torch.Tensor, alpha: torch.Tensor) -> io.NodeOutput:
- batch_size = min(len(image), len(alpha))
- out_images = []
-
- alpha = 1.0 - resize_mask(alpha, image.shape[1:])
- for i in range(batch_size):
- out_images.append(torch.cat((image[i][:,:,:3], alpha[i].unsqueeze(2)), dim=2))
-
- return io.NodeOutput(torch.stack(out_images))
+ batch_size = max(len(image), len(alpha))
+ 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))
class CompositingExtension(ComfyExtension):
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 0e43f2e44..f7ca833dc 100644
--- a/comfy_extras/nodes_context_windows.py
+++ b/comfy_extras/nodes_context_windows.py
@@ -10,7 +10,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."),
@@ -29,6 +29,7 @@ class ContextWindowsManualNode(io.ComfyNode):
io.Boolean.Input("freenoise", default=False, tooltip="Whether to apply FreeNoise noise shuffling, improves window blending."),
io.String.Input("cond_retain_index_list", default="", tooltip="List of latent indices to retain in the conditioning tensors for each window, for example setting this to '0' will use the initial start image for each window."),
io.Boolean.Input("split_conds_to_windows", default=False, tooltip="Whether to split multiple conditionings (created by ConditionCombine) to each window based on region index."),
+ io.Boolean.Input("causal_window_fix", default=True, tooltip="Whether to add a causal fix frame to non-0-indexed context windows."),
],
outputs=[
io.Model.Output(tooltip="The model with context windows applied during sampling."),
@@ -38,7 +39,7 @@ class ContextWindowsManualNode(io.ComfyNode):
@classmethod
def execute(cls, model: io.Model.Type, context_length: int, context_overlap: int, context_schedule: str, context_stride: int, closed_loop: bool, fuse_method: str, dim: int, freenoise: bool,
- cond_retain_index_list: list[int]=[], split_conds_to_windows: bool=False) -> io.Model:
+ cond_retain_index_list: list[int]=[], split_conds_to_windows: bool=False, causal_window_fix: bool=True) -> io.Model:
model = model.clone()
model.model_options["context_handler"] = comfy.context_windows.IndexListContextHandler(
context_schedule=comfy.context_windows.get_matching_context_schedule(context_schedule),
@@ -50,7 +51,8 @@ class ContextWindowsManualNode(io.ComfyNode):
dim=dim,
freenoise=freenoise,
cond_retain_index_list=cond_retain_index_list,
- split_conds_to_windows=split_conds_to_windows
+ split_conds_to_windows=split_conds_to_windows,
+ causal_window_fix=causal_window_fix,
)
# make memory usage calculation only take into account the context window latents
comfy.context_windows.create_prepare_sampling_wrapper(model)
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_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_frame_interpolation.py b/comfy_extras/nodes_frame_interpolation.py
index a3b00d36e..9dd34cfb8 100644
--- a/comfy_extras/nodes_frame_interpolation.py
+++ b/comfy_extras/nodes_frame_interpolation.py
@@ -37,7 +37,7 @@ class FrameInterpolationModelLoader(io.ComfyNode):
model = cls._detect_and_load(sd)
dtype = torch.float16 if model_management.should_use_fp16(model_management.get_torch_device()) else torch.float32
model.eval().to(dtype)
- patcher = comfy.model_patcher.ModelPatcher(
+ patcher = comfy.model_patcher.CoreModelPatcher(
model,
load_device=model_management.get_torch_device(),
offload_device=model_management.unet_offload_device(),
@@ -78,7 +78,7 @@ class FrameInterpolate(io.ComfyNode):
return io.Schema(
node_id="FrameInterpolate",
display_name="Frame Interpolate",
- category="image/video",
+ category="video",
search_aliases=["rife", "film", "frame interpolation", "slow motion", "interpolate frames", "vfi"],
inputs=[
FrameInterpolationModel.Input("interp_model"),
@@ -98,16 +98,13 @@ class FrameInterpolate(io.ComfyNode):
if num_frames < 2 or multiplier < 2:
return io.NodeOutput(images)
- model_management.load_model_gpu(interp_model)
device = interp_model.load_device
dtype = interp_model.model_dtype()
inference_model = interp_model.model
-
- # Free VRAM for inference activations (model weights + ~20x a single frame's worth)
- H, W = images.shape[1], images.shape[2]
- activation_mem = H * W * 3 * images.element_size() * 20
- model_management.free_memory(activation_mem, device)
+ activation_mem = inference_model.memory_used_forward(images.shape, dtype)
+ model_management.load_models_gpu([interp_model], memory_required=activation_mem)
align = getattr(inference_model, "pad_align", 1)
+ H, W = images.shape[1], images.shape[2]
# Prepare a single padded frame on device for determining output dimensions
def prepare_frame(idx):
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 df0c3e4b1..403eb855b 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,6 +419,7 @@ class VoxelToMeshBasic(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="VoxelToMeshBasic",
+ display_name="Voxel to Mesh (Basic)",
category="3d",
inputs=[
IO.Voxel.Input("voxel"),
@@ -443,7 +439,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,6 +451,7 @@ class VoxelToMesh(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="VoxelToMesh",
+ display_name="Voxel to Mesh",
category="3d",
inputs=[
IO.Voxel.Input("voxel"),
@@ -479,206 +478,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(' IO.NodeOutput:
- full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory())
- results = []
-
- metadata = {}
- if not args.disable_metadata:
- if cls.hidden.prompt is not None:
- metadata["prompt"] = json.dumps(cls.hidden.prompt)
- if cls.hidden.extra_pnginfo is not None:
- for x in cls.hidden.extra_pnginfo:
- metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
-
- if isinstance(mesh, Types.File3D):
- # Handle File3D input - save BytesIO data to output folder
- ext = mesh.format or "glb"
- f = f"{filename}_{counter:05}_.{ext}"
- mesh.save_to(os.path.join(full_output_folder, f))
- results.append({
- "filename": f,
- "subfolder": subfolder,
- "type": "output"
- })
- else:
- # Handle Mesh input - save vertices and faces as GLB
- for i in range(mesh.vertices.shape[0]):
- f = f"{filename}_{counter:05}_.glb"
- save_glb(mesh.vertices[i], mesh.faces[i], os.path.join(full_output_folder, f), metadata)
- results.append({
- "filename": f,
- "subfolder": subfolder,
- "type": "output"
- })
- counter += 1
- return IO.NodeOutput(ui={"3d": results})
-
-
class Hunyuan3dExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
@@ -689,7 +495,6 @@ class Hunyuan3dExtension(ComfyExtension):
VAEDecodeHunyuan3D,
VoxelToMeshBasic,
VoxelToMesh,
- SaveGLB,
]
diff --git a/comfy_extras/nodes_hypernetwork.py b/comfy_extras/nodes_hypernetwork.py
index 2a6a87a81..44a9c6f97 100644
--- a/comfy_extras/nodes_hypernetwork.py
+++ b/comfy_extras/nodes_hypernetwork.py
@@ -102,6 +102,7 @@ class HypernetworkLoader(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="HypernetworkLoader",
+ display_name="Load Hypernetwork",
category="loaders",
inputs=[
IO.Model.Input("model"),
diff --git a/comfy_extras/nodes_image_compare.py b/comfy_extras/nodes_image_compare.py
index 3d943be67..58af9ae82 100644
--- a/comfy_extras/nodes_image_compare.py
+++ b/comfy_extras/nodes_image_compare.py
@@ -11,7 +11,7 @@ class ImageCompare(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="ImageCompare",
- display_name="Image Compare",
+ display_name="Compare Images",
description="Compares two images side by side with a slider.",
category="image",
essentials_category="Image Tools",
diff --git a/comfy_extras/nodes_images.py b/comfy_extras/nodes_images.py
index a77f0641f..6326c5be8 100644
--- a/comfy_extras/nodes_images.py
+++ b/comfy_extras/nodes_images.py
@@ -24,7 +24,7 @@ class ImageCrop(IO.ComfyNode):
return IO.Schema(
node_id="ImageCrop",
search_aliases=["trim"],
- display_name="Image Crop (Deprecated)",
+ display_name="Crop Image (DEPRECATED)",
category="image/transform",
is_deprecated=True,
essentials_category="Image Tools",
@@ -56,7 +56,7 @@ class ImageCropV2(IO.ComfyNode):
return IO.Schema(
node_id="ImageCropV2",
search_aliases=["trim"],
- display_name="Image Crop",
+ display_name="Crop Image",
category="image/transform",
essentials_category="Image Tools",
has_intermediate_output=True,
@@ -109,6 +109,7 @@ class RepeatImageBatch(IO.ComfyNode):
return IO.Schema(
node_id="RepeatImageBatch",
search_aliases=["duplicate image", "clone image"],
+ display_name="Repeat Image Batch",
category="image/batch",
inputs=[
IO.Image.Input("image"),
@@ -131,10 +132,11 @@ class ImageFromBatch(IO.ComfyNode):
return IO.Schema(
node_id="ImageFromBatch",
search_aliases=["select image", "pick from batch", "extract image"],
+ display_name="Get Image from Batch",
category="image/batch",
inputs=[
IO.Image.Input("image"),
- IO.Int.Input("batch_index", default=0, min=0, max=4095),
+ IO.Int.Input("batch_index", default=0, min=-MAX_RESOLUTION, max=MAX_RESOLUTION),
IO.Int.Input("length", default=1, min=1, max=4096),
],
outputs=[IO.Image.Output()],
@@ -143,7 +145,9 @@ class ImageFromBatch(IO.ComfyNode):
@classmethod
def execute(cls, image, batch_index, length) -> IO.NodeOutput:
s_in = image
- batch_index = min(s_in.shape[0] - 1, batch_index)
+ if batch_index < 0:
+ batch_index += s_in.shape[0]
+ batch_index = max(0, min(s_in.shape[0] - 1, batch_index))
length = min(s_in.shape[0] - batch_index, length)
s = s_in[batch_index:batch_index + length].clone()
return IO.NodeOutput(s)
@@ -157,7 +161,8 @@ class ImageAddNoise(IO.ComfyNode):
return IO.Schema(
node_id="ImageAddNoise",
search_aliases=["film grain"],
- category="image",
+ display_name="Add Noise to Image",
+ category="image/filters",
inputs=[
IO.Image.Input("image"),
IO.Int.Input(
@@ -189,7 +194,8 @@ class SaveAnimatedWEBP(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="SaveAnimatedWEBP",
- category="image/animation",
+ display_name="Save Animated WEBP",
+ category="image",
inputs=[
IO.Image.Input("images"),
IO.String.Input("filename_prefix", default="ComfyUI"),
@@ -226,7 +232,8 @@ class SaveAnimatedPNG(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="SaveAnimatedPNG",
- category="image/animation",
+ display_name="Save Animated PNG",
+ category="image",
inputs=[
IO.Image.Input("images"),
IO.String.Input("filename_prefix", default="ComfyUI"),
@@ -259,7 +266,7 @@ class ImageStitch(IO.ComfyNode):
return IO.Schema(
node_id="ImageStitch",
search_aliases=["combine images", "join images", "concatenate images", "side by side"],
- display_name="Image Stitch",
+ display_name="Stitch Images",
description="Stitches image2 to image1 in the specified direction.\n"
"If image2 is not provided, returns image1 unchanged.\n"
"Optional spacing can be added between images.",
@@ -434,6 +441,7 @@ class ResizeAndPadImage(IO.ComfyNode):
return IO.Schema(
node_id="ResizeAndPadImage",
search_aliases=["fit to size"],
+ display_name="Resize And Pad Image",
category="image/transform",
inputs=[
IO.Image.Input("image"),
@@ -485,8 +493,9 @@ class SaveSVGNode(IO.ComfyNode):
return IO.Schema(
node_id="SaveSVGNode",
search_aliases=["export vector", "save vector graphics"],
+ display_name="Save SVG",
description="Save SVG files on disk.",
- category="image/save",
+ category="image",
inputs=[
IO.SVG.Input("svg"),
IO.String.Input(
@@ -591,7 +600,7 @@ class ImageRotate(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="ImageRotate",
- display_name="Image Rotate",
+ display_name="Rotate Image",
search_aliases=["turn", "flip orientation"],
category="image/transform",
essentials_category="Image Tools",
@@ -624,6 +633,7 @@ class ImageFlip(IO.ComfyNode):
return IO.Schema(
node_id="ImageFlip",
search_aliases=["mirror", "reflect"],
+ display_name="Flip Image",
category="image/transform",
inputs=[
IO.Image.Input("image"),
@@ -650,6 +660,7 @@ class ImageScaleToMaxDimension(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="ImageScaleToMaxDimension",
+ display_name="Scale Image to Max Dimension",
category="image/upscaling",
inputs=[
IO.Image.Input("image"),
@@ -709,7 +720,7 @@ class SplitImageToTileList(IO.ComfyNode):
def get_grid_coords(width, height, tile_width, tile_height, overlap):
coords = []
stride_x = round(max(tile_width * 0.25, tile_width - overlap))
- stride_y = round(max(tile_width * 0.25, tile_height - overlap))
+ stride_y = round(max(tile_height * 0.25, tile_height - overlap))
y = 0
while y < height:
diff --git a/comfy_extras/nodes_lora_extract.py b/comfy_extras/nodes_lora_extract.py
index 975f90f45..bcd249c29 100644
--- a/comfy_extras/nodes_lora_extract.py
+++ b/comfy_extras/nodes_lora_extract.py
@@ -91,7 +91,7 @@ class LoraSave(io.ComfyNode):
node_id="LoraSave",
search_aliases=["export lora"],
display_name="Extract and Save Lora",
- category="_for_testing",
+ category="experimental",
inputs=[
io.String.Input("filename_prefix", default="loras/ComfyUI_extracted_lora"),
io.Int.Input("rank", default=8, min=1, max=4096, step=1, advanced=True),
diff --git a/comfy_extras/nodes_lt.py b/comfy_extras/nodes_lt.py
index 19d8a387f..51cf7951f 100644
--- a/comfy_extras/nodes_lt.py
+++ b/comfy_extras/nodes_lt.py
@@ -14,6 +14,49 @@ from typing_extensions import override
from comfy.ldm.lightricks.symmetric_patchifier import SymmetricPatchifier, latent_to_pixel_coords
from comfy_api.latest import ComfyExtension, io
+ICLoRAParameters = io.Custom("IC_LORA_PARAMETERS")
+
+
+class GetICLoRAParameters(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="GetICLoRAParameters",
+ display_name="Get IC-LoRA Parameters",
+ description="Extracts IC-LoRA parameters from the safetensors metadata of a LoRA-loaded "
+ "model and outputs them for LTXVAddGuide (eg. reference_downscale_factor).",
+ category="conditioning/video_models",
+ search_aliases=["ic-lora", "ic lora", "iclora", "downscale factor", "reference downscale"],
+ inputs=[
+ io.Model.Input(
+ "iclora_model",
+ tooltip="Direct output from a LoRA Loader for the specific IC-LoRA "
+ "from which to extract the metadata.",
+ ),
+ ],
+ outputs=[
+ ICLoRAParameters.Output(
+ "iclora_parameters",
+ tooltip="IC-LoRA parameters extracted from the LoRA metadata "
+ "(eg. reference_downscale_factor). Connect to LTXVAddGuide "
+ "if the LoRA requires special handling of the guides.",
+ ),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, iclora_model) -> io.NodeOutput:
+ metadata = iclora_model.get_attachment("lora_metadata")
+ factor = 1
+ if metadata:
+ try:
+ factor = max(1, round(float(metadata.get("reference_downscale_factor", 1))))
+ except (TypeError, ValueError):
+ factor = 1
+ parameters = {"reference_downscale_factor": factor}
+ return io.NodeOutput(parameters)
+
+
class EmptyLTXVLatentVideo(io.ComfyNode):
@classmethod
def define_schema(cls):
@@ -34,7 +77,7 @@ class EmptyLTXVLatentVideo(io.ComfyNode):
@classmethod
def execute(cls, width, height, length, batch_size=1) -> io.NodeOutput:
latent = torch.zeros([batch_size, 128, ((length - 1) // 8) + 1, height // 32, width // 32], device=comfy.model_management.intermediate_device())
- return io.NodeOutput({"samples": latent})
+ return io.NodeOutput({"samples": latent, "downscale_ratio_spacial": 32})
generate = execute # TODO: remove
@@ -106,12 +149,12 @@ class LTXVImgToVideoInplace(io.ComfyNode):
if bypass:
return (latent,)
- samples = latent["samples"]
+ samples = latent["samples"].clone()
_, height_scale_factor, width_scale_factor = (
vae.downscale_index_formula
)
- batch, _, latent_frames, latent_height, latent_width = samples.shape
+ _, _, _, latent_height, latent_width = samples.shape
width = latent_width * width_scale_factor
height = latent_height * height_scale_factor
@@ -124,11 +167,7 @@ class LTXVImgToVideoInplace(io.ComfyNode):
samples[:, :, :t.shape[2]] = t
- conditioning_latent_frames_mask = torch.ones(
- (batch, 1, latent_frames, 1, 1),
- dtype=torch.float32,
- device=samples.device,
- )
+ conditioning_latent_frames_mask = get_noise_mask(latent)
conditioning_latent_frames_mask[:, :, :t.shape[2]] = 1.0 - strength
return io.NodeOutput({"samples": samples, "noise_mask": conditioning_latent_frames_mask})
@@ -136,7 +175,7 @@ class LTXVImgToVideoInplace(io.ComfyNode):
generate = execute # TODO: remove
-def _append_guide_attention_entry(positive, negative, pre_filter_count, latent_shape, strength=1.0):
+def _append_guide_attention_entry(positive, negative, pre_filter_count, latent_shape, strength=1.0, attention_mask=None):
"""Append a guide_attention_entry to both positive and negative conditioning.
Each entry tracks one guide reference for per-reference attention control.
@@ -145,9 +184,10 @@ def _append_guide_attention_entry(positive, negative, pre_filter_count, latent_s
new_entry = {
"pre_filter_count": pre_filter_count,
"strength": strength,
- "pixel_mask": None,
+ "pixel_mask": attention_mask.unsqueeze(0).unsqueeze(0) if attention_mask is not None else None, # reshape to (1, 1, F, H, W)
"latent_shape": latent_shape,
}
+
results = []
for cond in (positive, negative):
# Read existing entries from this specific conditioning
@@ -157,8 +197,7 @@ def _append_guide_attention_entry(positive, negative, pre_filter_count, latent_s
if found is not None:
existing = found
break
- # Shallow copy and append (no deepcopy needed — entries contain
- # only scalars and None for pixel_mask at this call site).
+ # Shallow copy only and append (pixel_mask is never mutated).
entries = [*existing, new_entry]
results.append(node_helpers.conditioning_set_values(
cond, {"guide_attention_entries": entries}
@@ -223,7 +262,21 @@ class LTXVAddGuide(io.ComfyNode):
"For videos with 9+ frames, frame_idx must be divisible by 8, otherwise it will be rounded "
"down to the nearest multiple of 8. Negative values are counted from the end of the video.",
),
- io.Float.Input("strength", default=1.0, min=0.0, max=1.0, step=0.01),
+ io.Float.Input("strength", default=1.0, min=0.0, max=10.0, step=0.01),
+ io.Mask.Input(
+ "attention_mask",
+ optional=True,
+ tooltip="Optional pixel-space spatial mask. Controls per-region "
+ "conditioning influence via self-attention, multiplied by strength.",
+ ),
+ ICLoRAParameters.Input(
+ "iclora_parameters",
+ optional=True,
+ tooltip="Optional IC-LoRA parameters from a Get IC-LoRA Parameters node. "
+ "Used for adjusting guide processing as required by certain IC-LoRAs "
+ "(eg. those with a reference_downscale_factor > 1). "
+ "When chained, each LTXVAddGuide uses only the parameters connected to it.",
+ ),
],
outputs=[
io.Conditioning.Output(display_name="positive"),
@@ -233,14 +286,41 @@ class LTXVAddGuide(io.ComfyNode):
)
@classmethod
- def encode(cls, vae, latent_width, latent_height, images, scale_factors):
+ def encode(cls, vae, latent_width, latent_height, images, scale_factors, latent_downscale_factor=1):
time_scale_factor, width_scale_factor, height_scale_factor = scale_factors
images = images[:(images.shape[0] - 1) // time_scale_factor * time_scale_factor + 1]
- pixels = comfy.utils.common_upscale(images.movedim(-1, 1), latent_width * width_scale_factor, latent_height * height_scale_factor, "bilinear", crop="disabled").movedim(1, -1)
+ target_width = int(latent_width * width_scale_factor / latent_downscale_factor)
+ target_height = int(latent_height * height_scale_factor / latent_downscale_factor)
+ pixels = comfy.utils.common_upscale(images.movedim(-1, 1), target_width, target_height, "bilinear", crop="center").movedim(1, -1)
encode_pixels = pixels[:, :, :, :3]
t = vae.encode(encode_pixels)
return encode_pixels, t
+ @classmethod
+ def dilate_latent(cls, guide_latent, latent_downscale_factor):
+ if latent_downscale_factor <= 1:
+ return guide_latent, None
+ scale = int(latent_downscale_factor)
+ dilated_shape = guide_latent.shape[:3] + (guide_latent.shape[3] * scale, guide_latent.shape[4] * scale)
+ dilated = torch.zeros(dilated_shape, device=guide_latent.device, dtype=guide_latent.dtype)
+ dilated[..., ::scale, ::scale] = guide_latent
+ dilated_mask = torch.full(
+ (dilated.shape[0], 1, dilated.shape[2], dilated.shape[3], dilated.shape[4]),
+ -1.0, device=guide_latent.device, dtype=guide_latent.dtype,
+ )
+ dilated_mask[..., ::scale, ::scale] = 1.0
+ return dilated, dilated_mask
+
+ @classmethod
+ def get_reference_downscale_factor(cls, iclora_parameters):
+ if not iclora_parameters:
+ return 1
+ try:
+ factor = max(1, round(float(iclora_parameters.get("reference_downscale_factor", 1))))
+ except (TypeError, ValueError):
+ factor = 1
+ return factor
+
@classmethod
def get_latent_index(cls, cond, latent_length, guide_length, frame_idx, scale_factors):
time_scale_factor, _, _ = scale_factors
@@ -302,7 +382,7 @@ class LTXVAddGuide(io.ComfyNode):
else:
mask = torch.full(
(noise_mask.shape[0], 1, guiding_latent.shape[2], noise_mask.shape[3], noise_mask.shape[4]),
- 1.0 - strength,
+ max(0.0, 1.0 - strength), # clamp here to amplify only via the attention mask
dtype=noise_mask.dtype,
device=noise_mask.device,
)
@@ -322,7 +402,7 @@ class LTXVAddGuide(io.ComfyNode):
mask = torch.full(
(noise_mask.shape[0], 1, cond_length, 1, 1),
- 1.0 - strength,
+ max(0.0, 1.0 - strength), # clamp here to amplify only via the attention mask
dtype=noise_mask.dtype,
device=noise_mask.device,
)
@@ -336,13 +416,43 @@ class LTXVAddGuide(io.ComfyNode):
return latent_image, noise_mask
@classmethod
- def execute(cls, positive, negative, vae, latent, image, frame_idx, strength) -> io.NodeOutput:
+ def execute(cls, positive, negative, vae, latent, image, frame_idx, strength, attention_mask=None, iclora_parameters=None) -> io.NodeOutput:
scale_factors = vae.downscale_index_formula
latent_image = latent["samples"]
noise_mask = get_noise_mask(latent)
_, _, latent_length, latent_height, latent_width = latent_image.shape
- image, t = cls.encode(vae, latent_width, latent_height, image, scale_factors)
+
+ latent_downscale_factor = cls.get_reference_downscale_factor(iclora_parameters)
+ if latent_downscale_factor > 1:
+ if latent_width % latent_downscale_factor != 0 or latent_height % latent_downscale_factor != 0:
+ raise ValueError(
+ f"Latent spatial size {latent_width}x{latent_height} must be divisible by "
+ f"reference_downscale_factor {latent_downscale_factor} from the IC-LoRA parameters."
+ )
+
+ # For mid-video multi-frame guides, prepend+strip a throwaway first frame so the VAE's "first latent = 1 pixel frame" asymmetry lands on the discarded slot
+ time_scale_factor = scale_factors[0]
+ num_frames_to_keep = ((image.shape[0] - 1) // time_scale_factor) * time_scale_factor + 1
+ resolved_frame_idx = frame_idx
+ if frame_idx < 0:
+ _, num_keyframes = get_keyframe_idxs(positive)
+ resolved_frame_idx = max((latent_length - num_keyframes - 1) * time_scale_factor + 1 + frame_idx, 0)
+ causal_fix = resolved_frame_idx == 0 or num_frames_to_keep == 1
+
+ if not causal_fix:
+ image = torch.cat([image[:1], image], dim=0)
+
+ image, t = cls.encode(vae, latent_width, latent_height, image, scale_factors, latent_downscale_factor)
+
+ if not causal_fix:
+ t = t[:, :, 1:, :, :]
+ image = image[1:]
+
+ guide_latent_shape = list(t.shape[2:]) # pre-dilation [F, H, W] for spatial-mask downsampling
+ guide_mask = None
+ if latent_downscale_factor > 1:
+ t, guide_mask = cls.dilate_latent(t, latent_downscale_factor)
frame_idx, latent_idx = cls.get_latent_index(positive, latent_length, len(image), frame_idx, scale_factors)
assert latent_idx + t.shape[2] <= latent_length, "Conditioning frames exceed the length of the latent sequence."
@@ -356,13 +466,16 @@ class LTXVAddGuide(io.ComfyNode):
t,
strength,
scale_factors,
+ guide_mask=guide_mask,
+ latent_downscale_factor=latent_downscale_factor,
+ causal_fix=causal_fix,
)
# Track this guide for per-reference attention control.
pre_filter_count = t.shape[2] * t.shape[3] * t.shape[4]
- guide_latent_shape = list(t.shape[2:]) # [F, H, W]
positive, negative = _append_guide_attention_entry(
positive, negative, pre_filter_count, guide_latent_shape, strength=strength,
+ attention_mask=attention_mask,
)
return io.NodeOutput(positive, negative, {"samples": latent_image, "noise_mask": noise_mask})
@@ -488,7 +601,7 @@ class LTXVScheduler(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="LTXVScheduler",
- category="sampling/custom_sampling/schedulers",
+ category="sampling/schedulers",
inputs=[
io.Int.Input("steps", default=20, min=1, max=10000),
io.Float.Input("max_shift", default=2.05, min=0.0, max=100.0, step=0.01),
@@ -594,7 +707,8 @@ class LTXVPreprocess(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="LTXVPreprocess",
- category="image",
+ display_name="LTXV Preprocess",
+ category="video/preprocessors",
inputs=[
io.Image.Input("image"),
io.Int.Input(
@@ -779,6 +893,7 @@ class LtxvExtension(ComfyExtension):
ModelSamplingLTXV,
LTXVConditioning,
LTXVScheduler,
+ GetICLoRAParameters,
LTXVAddGuide,
LTXVPreprocess,
LTXVCropGuides,
diff --git a/comfy_extras/nodes_lt_audio.py b/comfy_extras/nodes_lt_audio.py
index 15d8a497e..3369e3a2a 100644
--- a/comfy_extras/nodes_lt_audio.py
+++ b/comfy_extras/nodes_lt_audio.py
@@ -147,7 +147,6 @@ class LTXVEmptyLatentAudio(io.ComfyNode):
z_channels = audio_vae.latent_channels
audio_freq = audio_vae.first_stage_model.latent_frequency_bins
- sampling_rate = int(audio_vae.first_stage_model.sample_rate)
num_audio_latents = audio_vae.first_stage_model.num_of_latents_from_frames(frames_number, frame_rate)
@@ -159,7 +158,6 @@ class LTXVEmptyLatentAudio(io.ComfyNode):
return io.NodeOutput(
{
"samples": audio_latents,
- "sample_rate": sampling_rate,
"type": "audio",
}
)
diff --git a/comfy_extras/nodes_mahiro.py b/comfy_extras/nodes_mahiro.py
index a25226e6d..7bd5f6652 100644
--- a/comfy_extras/nodes_mahiro.py
+++ b/comfy_extras/nodes_mahiro.py
@@ -11,7 +11,7 @@ class Mahiro(io.ComfyNode):
return io.Schema(
node_id="Mahiro",
display_name="Positive-Biased Guidance",
- category="_for_testing",
+ category="experimental",
description="Modify the guidance to scale more on the 'direction' of the positive prompt rather than the difference between the negative prompt.",
inputs=[
io.Model.Input("model"),
diff --git a/comfy_extras/nodes_mask.py b/comfy_extras/nodes_mask.py
index c44602597..d15f1f4e7 100644
--- a/comfy_extras/nodes_mask.py
+++ b/comfy_extras/nodes_mask.py
@@ -2,6 +2,7 @@ import numpy as np
import scipy.ndimage
import torch
import comfy.utils
+import comfy.model_management
import node_helpers
from typing_extensions import override
from comfy_api.latest import ComfyExtension, IO, UI
@@ -45,6 +46,7 @@ def composite(destination, source, x, y, mask = None, multiplier = 8, resize_sou
destination[..., top:bottom, left:right] = source_portion + destination_portion
return destination
+
class LatentCompositeMasked(IO.ComfyNode):
@classmethod
def define_schema(cls):
@@ -79,8 +81,9 @@ class ImageCompositeMasked(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="ImageCompositeMasked",
- search_aliases=["paste image", "overlay", "layer"],
- category="image",
+ search_aliases=["overlay", "layer", "paste image", "images composition"],
+ display_name="Image Composite Masked",
+ category="image/compositing",
inputs=[
IO.Image.Input("destination"),
IO.Image.Input("source"),
@@ -109,7 +112,7 @@ class MaskToImage(IO.ComfyNode):
node_id="MaskToImage",
search_aliases=["convert mask"],
display_name="Convert Mask to Image",
- category="mask",
+ category="image/mask",
inputs=[
IO.Mask.Input("mask"),
],
@@ -131,7 +134,7 @@ class ImageToMask(IO.ComfyNode):
node_id="ImageToMask",
search_aliases=["extract channel", "channel to mask"],
display_name="Convert Image to Mask",
- category="mask",
+ category="image/mask",
inputs=[
IO.Image.Input("image"),
IO.Combo.Input("channel", options=["red", "green", "blue", "alpha"]),
@@ -154,7 +157,8 @@ class ImageColorToMask(IO.ComfyNode):
return IO.Schema(
node_id="ImageColorToMask",
search_aliases=["color keying", "chroma key"],
- category="mask",
+ display_name="Convert Image Color to Mask",
+ category="image/mask",
inputs=[
IO.Image.Input("image"),
IO.Int.Input("color", default=0, min=0, max=0xFFFFFF, step=1, display_mode=IO.NumberDisplay.number),
@@ -177,7 +181,8 @@ class SolidMask(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="SolidMask",
- category="mask",
+ display_name="Create Solid Mask",
+ category="image/mask",
inputs=[
IO.Float.Input("value", default=1.0, min=0.0, max=1.0, step=0.01),
IO.Int.Input("width", default=512, min=1, max=nodes.MAX_RESOLUTION, step=1),
@@ -188,7 +193,7 @@ class SolidMask(IO.ComfyNode):
@classmethod
def execute(cls, value, width, height) -> IO.NodeOutput:
- out = torch.full((1, height, width), value, dtype=torch.float32, device="cpu")
+ out = torch.full((1, height, width), value, dtype=torch.float32, device=comfy.model_management.intermediate_device())
return IO.NodeOutput(out)
solid = execute # TODO: remove
@@ -200,7 +205,8 @@ class InvertMask(IO.ComfyNode):
return IO.Schema(
node_id="InvertMask",
search_aliases=["reverse mask", "flip mask"],
- category="mask",
+ display_name="Invert Mask",
+ category="image/mask",
inputs=[
IO.Mask.Input("mask"),
],
@@ -221,7 +227,8 @@ class CropMask(IO.ComfyNode):
return IO.Schema(
node_id="CropMask",
search_aliases=["cut mask", "extract mask region", "mask slice"],
- category="mask",
+ display_name="Crop Mask",
+ category="image/mask",
inputs=[
IO.Mask.Input("mask"),
IO.Int.Input("x", default=0, min=0, max=nodes.MAX_RESOLUTION, step=1),
@@ -246,8 +253,9 @@ class MaskComposite(IO.ComfyNode):
def define_schema(cls):
return IO.Schema(
node_id="MaskComposite",
- search_aliases=["combine masks", "blend masks", "layer masks"],
- category="mask",
+ search_aliases=["combine masks", "blend masks", "layer masks", "masks composition"],
+ display_name="Combine Masks",
+ category="image/mask",
inputs=[
IO.Mask.Input("destination"),
IO.Mask.Input("source"),
@@ -262,6 +270,7 @@ class MaskComposite(IO.ComfyNode):
def execute(cls, destination, source, x, y, operation) -> IO.NodeOutput:
output = destination.reshape((-1, destination.shape[-2], destination.shape[-1])).clone()
source = source.reshape((-1, source.shape[-2], source.shape[-1]))
+ source = source.to(output.device)
left, top = (x, y,)
right, bottom = (min(left + source.shape[-1], destination.shape[-1]), min(top + source.shape[-2], destination.shape[-2]))
@@ -296,7 +305,8 @@ class FeatherMask(IO.ComfyNode):
return IO.Schema(
node_id="FeatherMask",
search_aliases=["soft edge mask", "blur mask edges", "gradient mask edge"],
- category="mask",
+ display_name="Feather Mask",
+ category="image/mask",
inputs=[
IO.Mask.Input("mask"),
IO.Int.Input("left", default=0, min=0, max=nodes.MAX_RESOLUTION, step=1),
@@ -322,7 +332,7 @@ class FeatherMask(IO.ComfyNode):
for x in range(right):
feather_rate = (x + 1) / right
- output[:, :, -x] *= feather_rate
+ output[:, :, -(x + 1)] *= feather_rate
for y in range(top):
feather_rate = (y + 1) / top
@@ -330,7 +340,7 @@ class FeatherMask(IO.ComfyNode):
for y in range(bottom):
feather_rate = (y + 1) / bottom
- output[:, -y, :] *= feather_rate
+ output[:, -(y + 1), :] *= feather_rate
return IO.NodeOutput(output)
@@ -344,7 +354,7 @@ class GrowMask(IO.ComfyNode):
node_id="GrowMask",
search_aliases=["expand mask", "shrink mask"],
display_name="Grow Mask",
- category="mask",
+ category="image/mask",
inputs=[
IO.Mask.Input("mask"),
IO.Int.Input("expand", default=0, min=-nodes.MAX_RESOLUTION, max=nodes.MAX_RESOLUTION, step=1),
@@ -374,14 +384,14 @@ class GrowMask(IO.ComfyNode):
expand_mask = execute # TODO: remove
-
class ThresholdMask(IO.ComfyNode):
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="ThresholdMask",
search_aliases=["binary mask"],
- category="mask",
+ display_name="Threshold Mask",
+ category="image/mask",
inputs=[
IO.Mask.Input("mask"),
IO.Float.Input("value", default=0.5, min=0.0, max=1.0, step=0.01),
@@ -407,7 +417,7 @@ class MaskPreview(IO.ComfyNode):
node_id="MaskPreview",
search_aliases=["show mask", "view mask", "inspect mask", "debug mask"],
display_name="Preview Mask",
- category="mask",
+ category="image/mask",
description="Saves the input images to your ComfyUI output directory.",
inputs=[
IO.Mask.Input("mask"),
diff --git a/comfy_extras/nodes_math.py b/comfy_extras/nodes_math.py
index 6417bacf1..6030ee9d8 100644
--- a/comfy_extras/nodes_math.py
+++ b/comfy_extras/nodes_math.py
@@ -63,14 +63,14 @@ class MathExpressionNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
autogrow = io.Autogrow.TemplateNames(
- input=io.MultiType.Input("value", [io.Float, io.Int]),
+ input=io.MultiType.Input("value", [io.Float, io.Int, io.Boolean]),
names=list(string.ascii_lowercase),
min=1,
)
return io.Schema(
node_id="ComfyMathExpression",
display_name="Math Expression",
- category="math",
+ category="logic",
search_aliases=[
"expression", "formula", "calculate", "calculator",
"eval", "math",
@@ -82,6 +82,7 @@ class MathExpressionNode(io.ComfyNode):
outputs=[
io.Float.Output(display_name="FLOAT"),
io.Int.Output(display_name="INT"),
+ io.Boolean.Output(display_name="BOOL"),
],
)
@@ -97,7 +98,7 @@ class MathExpressionNode(io.ComfyNode):
result = simple_eval(expression, names=context, functions=MATH_FUNCTIONS)
# bool check must come first because bool is a subclass of int in Python
- if isinstance(result, bool) or not isinstance(result, (int, float)):
+ if not isinstance(result, (int, float)):
raise ValueError(
f"Math Expression '{expression}' must evaluate to a numeric result, "
f"got {type(result).__name__}: {result!r}"
@@ -106,7 +107,7 @@ class MathExpressionNode(io.ComfyNode):
raise ValueError(
f"Math Expression '{expression}' produced a non-finite result: {result}"
)
- return io.NodeOutput(float(result), int(result))
+ return io.NodeOutput(float(result), int(result), bool(result))
class MathExtension(ComfyExtension):
diff --git a/comfy_extras/nodes_model_advanced.py b/comfy_extras/nodes_model_advanced.py
index 8bf6a1afa..b27ac1296 100644
--- a/comfy_extras/nodes_model_advanced.py
+++ b/comfy_extras/nodes_model_advanced.py
@@ -134,8 +134,11 @@ class ModelSamplingSD3:
class ModelSamplingAdvanced(sampling_base, sampling_type):
pass
+ original = m.get_model_object("model_sampling")
model_sampling = ModelSamplingAdvanced(model.model.model_config)
model_sampling.set_parameters(shift=shift, multiplier=multiplier)
+ if hasattr(original, "noise_scale"):
+ model_sampling.set_noise_scale(original.noise_scale)
m.add_object_patch("model_sampling", model_sampling)
return (m, )
@@ -300,6 +303,29 @@ class RescaleCFG:
m.set_model_sampler_cfg_function(rescale_cfg)
return (m, )
+class ModelNoiseScale:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required": { "model": ("MODEL",),
+ "noise_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 64.0, "step": 0.01,
+ "tooltip": "Absolute training noise scale. For example HiDream-O1 base: 8.0, dev: 7.5."}),
+ }}
+
+ RETURN_TYPES = ("MODEL",)
+ FUNCTION = "patch"
+
+ CATEGORY = "advanced/model"
+
+ def patch(self, model, noise_scale):
+ m = model.clone()
+ original = m.get_model_object("model_sampling")
+ ms = type(original)(m.model.model_config)
+ ms.set_parameters(shift=original.shift, multiplier=original.multiplier)
+ ms.set_noise_scale(noise_scale)
+ m.add_object_patch("model_sampling", ms)
+ return (m, )
+
+
class ModelComputeDtype:
SEARCH_ALIASES = ["model precision", "change dtype"]
@classmethod
@@ -327,6 +353,7 @@ NODE_CLASS_MAPPINGS = {
"ModelSamplingSD3": ModelSamplingSD3,
"ModelSamplingAuraFlow": ModelSamplingAuraFlow,
"ModelSamplingFlux": ModelSamplingFlux,
+ "ModelNoiseScale": ModelNoiseScale,
"RescaleCFG": RescaleCFG,
"ModelComputeDtype": ModelComputeDtype,
}
diff --git a/comfy_extras/nodes_model_merging.py b/comfy_extras/nodes_model_merging.py
index 5384ed531..b6b29e34a 100644
--- a/comfy_extras/nodes_model_merging.py
+++ b/comfy_extras/nodes_model_merging.py
@@ -276,8 +276,8 @@ class CLIPSave:
for x in extra_pnginfo:
metadata[x] = json.dumps(extra_pnginfo[x])
- comfy.model_management.load_models_gpu([clip.load_model()], force_patch_weights=True)
- clip_sd = clip.get_sd()
+ clip.load_model()
+ clip_sd = clip.state_dict_for_saving()
for prefix in ["clip_l.", "clip_g.", "clip_h.", "t5xxl.", "pile_t5xl.", "mt5xl.", "umt5xxl.", "t5base.", "gemma2_2b.", "llama.", "hydit_clip.", ""]:
k = list(filter(lambda a: a.startswith(prefix), clip_sd.keys()))
diff --git a/comfy_extras/nodes_moge.py b/comfy_extras/nodes_moge.py
new file mode 100644
index 000000000..d9a08ebc7
--- /dev/null
+++ b/comfy_extras/nodes_moge.py
@@ -0,0 +1,406 @@
+"""ComfyUI nodes for the native MoGe (Monocular Geometry Estimation) integration."""
+
+from __future__ import annotations
+
+import torch
+
+import comfy.utils
+import folder_paths
+from comfy_api.latest import ComfyExtension, Types, io
+from typing_extensions import override
+
+from comfy.ldm.moge.model import MoGeModel
+from comfy.ldm.moge.geometry import triangulate_grid_mesh
+from comfy.ldm.moge.panorama import get_panorama_cameras, split_panorama_image, merge_panorama_depth, spherical_uv_to_directions, _uv_grid
+import comfy.model_management
+from tqdm.auto import tqdm
+
+MoGeModelType = io.Custom("MOGE_MODEL")
+MoGeGeometry = io.Custom("MOGE_GEOMETRY")
+
+
+# MOGE_GEOMETRY is a dict with these optional keys (absent when the upstream model didn't produce them):
+# "points": torch.Tensor (B, H, W, 3)
+# "depth": torch.Tensor (B, H, W)
+# "intrinsics": torch.Tensor (B, 3, 3) -- perspective only
+# "mask": torch.Tensor (B, H, W) bool
+# "normal": torch.Tensor (B, H, W, 3) -- v2 only
+# "image": torch.Tensor (B, H, W, 3) in [0, 1], CPU (always present)
+
+
+def _turbo(x: torch.Tensor) -> torch.Tensor:
+ """Anton Mikhailov polynomial approximation of the turbo colormap."""
+ x = x.clamp(0.0, 1.0)
+ x2 = x * x
+ x3 = x2 * x
+ x4 = x2 * x2
+ x5 = x4 * x
+ r = 0.13572138 + 4.61539260*x - 42.66032258*x2 + 132.13108234*x3 - 152.94239396*x4 + 59.28637943*x5
+ g = 0.09140261 + 2.19418839*x + 4.84296658*x2 - 14.18503333*x3 + 4.27729857*x4 + 2.82956604*x5
+ b = 0.10667330 + 12.64194608*x - 60.58204836*x2 + 110.36276771*x3 - 89.90310912*x4 + 27.34824973*x5
+ return torch.stack([r, g, b], dim=-1).clamp(0.0, 1.0)
+
+
+def _normals_from_points(points: torch.Tensor) -> torch.Tensor:
+ """Camera-space surface normals from a (B, H, W, 3) point map (v1 fallback)."""
+ finite = torch.isfinite(points).all(dim=-1)
+ pts = torch.where(finite.unsqueeze(-1), points, torch.zeros_like(points))
+ dx = pts[..., :, 2:, :] - pts[..., :, :-2, :]
+ dy = pts[..., 2:, :, :] - pts[..., :-2, :, :]
+ dx = torch.nn.functional.pad(dx.permute(0, 3, 1, 2), (1, 1, 0, 0)).permute(0, 2, 3, 1)
+ dy = torch.nn.functional.pad(dy.permute(0, 3, 1, 2), (0, 0, 1, 1)).permute(0, 2, 3, 1)
+ # dy x dx (not dx x dy) so the result is outward-facing in OpenCV (Y-down flips the right-hand rule), matching v2's predicted normals.
+ n = torch.cross(dy, dx, dim=-1)
+ n = torch.nn.functional.normalize(n, dim=-1)
+ return torch.where(finite.unsqueeze(-1), n, torch.zeros_like(n))
+
+
+def _normalize_disparity(depth: torch.Tensor) -> torch.Tensor:
+ """Per-batch normalize 1/depth to [0, 1] using 0.1/99.9 percentile clipping."""
+ out = torch.zeros_like(depth)
+ for i in range(depth.shape[0]):
+ d = depth[i]
+ valid = torch.isfinite(d) & (d > 0)
+ if not valid.any():
+ continue
+ disp = torch.where(valid, 1.0 / d.clamp_min(1e-6), torch.zeros_like(d))
+ disp_valid = disp[valid]
+ lo = torch.quantile(disp_valid, 0.001)
+ hi = torch.quantile(disp_valid, 0.999)
+ scale = (hi - lo).clamp_min(1e-6)
+ norm = ((disp - lo) / scale).clamp(0.0, 1.0)
+ out[i] = torch.where(valid, norm, torch.zeros_like(norm))
+ return out
+
+
+class LoadMoGeModel(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="LoadMoGeModel",
+ display_name="Load MoGe Model",
+ category="loaders",
+ inputs=[
+ io.Combo.Input("model_name", options=folder_paths.get_filename_list("geometry_estimation")),
+ ],
+ outputs=[MoGeModelType.Output()],
+ )
+
+ @classmethod
+ def execute(cls, model_name) -> io.NodeOutput:
+ path = folder_paths.get_full_path_or_raise("geometry_estimation", model_name)
+ sd = comfy.utils.load_torch_file(path, safe_load=True)
+ return io.NodeOutput(MoGeModel(sd))
+
+
+class MoGePanoramaInference(io.ComfyNode):
+ """Equirectangular panorama inference: split into 12 perspective views, run
+ MoGe at fov_x=90 on each, merge via multi-scale Poisson + gradient solve.
+ v2's predicted normals and metric scale are ignored (per-view scales would not align across seams).
+ """
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="MoGePanoramaInference",
+ display_name="MoGe Panorama Inference",
+ category="image/geometry_estimation",
+ inputs=[
+ MoGeModelType.Input("moge_model"),
+ io.Image.Input("image", tooltip="Equirectangular panorama (any aspect)."),
+ io.Int.Input("resolution_level", default=9, min=0, max=9,
+ tooltip="Per-view detail (0 = fastest, 9 = most detailed)."),
+ io.Int.Input("split_resolution", default=512, min=256, max=1024,
+ tooltip="Resolution of each perspective split."),
+ io.Int.Input("merge_resolution", default=1920, min=256, max=8192,
+ tooltip="Long-side resolution of the merged equirect distance map."),
+ io.Int.Input("batch_size", default=4, min=1, max=12,
+ tooltip="Views per inference batch (12 splits total)."),
+ ],
+ outputs=[MoGeGeometry.Output(display_name="moge_geometry")],
+ )
+
+ @classmethod
+ def execute(cls, moge_model, image, resolution_level, split_resolution, merge_resolution, batch_size) -> io.NodeOutput:
+
+ if image.shape[0] != 1:
+ raise ValueError(f"MoGePanoramaInference takes a single image (got batch of {image.shape[0]})")
+
+ image = image[..., :3]
+ H, W = int(image.shape[1]), int(image.shape[2])
+ scale = min(merge_resolution / max(H, W), 1.0)
+ merge_h, merge_w = max(int(H * scale), 32), max(int(W * scale), 32)
+
+ extrinsics, intrinsics = get_panorama_cameras()
+
+ comfy.model_management.load_model_gpu(moge_model.patcher)
+ device = moge_model.load_device
+ img_chw = image[0].movedim(-1, -3).to(device=device, dtype=moge_model.dtype)
+ splits = split_panorama_image(img_chw, extrinsics, intrinsics, split_resolution)
+
+ n_views = splits.shape[0]
+
+ # Weight each lsmr solve by 4^level so the final-resolution solve doesn't leave the bar idle.
+ merge_levels: list[tuple[int, int]] = []
+ w_, h_ = merge_w, merge_h
+ while True:
+ merge_levels.append((w_, h_))
+ if max(w_, h_) <= 256:
+ break
+ w_, h_ = w_ // 2, h_ // 2
+ merge_levels.reverse()
+
+ solve_weight = {wh: 4 ** i for i, wh in enumerate(merge_levels)}
+ n_merge_view_units = n_views * len(merge_levels)
+ n_merge_solve_units = sum(solve_weight.values())
+
+ pbar = comfy.utils.ProgressBar(n_views + n_merge_view_units + n_merge_solve_units)
+ done = 0
+
+ distance_maps: list = []
+ masks: list = []
+ with tqdm(total=n_views, desc="MoGe panorama inference") as tq:
+ for i in range(0, n_views, batch_size):
+ batch = splits[i:i + batch_size]
+ # apply_metric_scale=False: per-view scales would not align across overlap seams.
+ result = moge_model.infer(batch, resolution_level=resolution_level,
+ fov_x=90.0, force_projection=True,
+ apply_mask=False, apply_metric_scale=False)
+ distance_maps.extend(list(result["points"].float().norm(dim=-1).cpu().numpy()))
+ masks.extend(list(result["mask"].cpu().numpy()))
+ n = batch.shape[0]
+ done += n
+ pbar.update_absolute(done)
+ tq.update(n)
+
+ with tqdm(total=n_merge_view_units + n_merge_solve_units, desc="MoGe panorama merge: views") as tq:
+ def _on_merge_view():
+ nonlocal done
+ done += 1
+ pbar.update_absolute(done)
+ tq.update(1)
+
+ def _on_solve_start(w, h):
+ tq.set_description(f"MoGe panorama merge: solving {w}x{h}")
+
+ def _on_solve_end(w, h):
+ nonlocal done
+ weight = solve_weight[(w, h)]
+ done += weight
+ pbar.update_absolute(done)
+ tq.update(weight)
+ tq.set_description("MoGe panorama merge: views")
+
+ pano_depth, pano_mask = merge_panorama_depth(
+ merge_w, merge_h, distance_maps, masks, list(extrinsics), intrinsics,
+ on_view=_on_merge_view, on_solve_start=_on_solve_start, on_solve_end=_on_solve_end)
+
+ pano_depth = torch.from_numpy(pano_depth)
+ pano_mask = torch.from_numpy(pano_mask)
+
+ if (merge_h, merge_w) != (H, W):
+ pano_depth = torch.nn.functional.interpolate(pano_depth[None, None], size=(H, W), mode="bilinear", align_corners=False).squeeze()
+ pano_mask = torch.nn.functional.interpolate(pano_mask[None, None].float(), size=(H, W), mode="nearest").squeeze() > 0
+
+ # Pixels uncovered by any view's predicted foreground are unconstrained in the lsmr solve and stay at log_depth=0 (depth=1)
+ if pano_mask.any() and not pano_mask.all():
+ far = torch.quantile(pano_depth[pano_mask], 0.95) * 5.0
+ pano_depth = torch.where(pano_mask, pano_depth, far)
+
+ directions = torch.from_numpy(spherical_uv_to_directions(_uv_grid(H, W)))
+ points = (directions * pano_depth[..., None]).unsqueeze(0)
+ depth = pano_depth.unsqueeze(0)
+ mask = pano_mask.unsqueeze(0)
+
+ # Points stay in MoGe spherical coords; MoGePointMapToMesh applies the spherical->glTF rotation after triangulation
+ moge_geometry = {"points": points, "depth": depth, "mask": mask, "image": image.cpu()}
+ return io.NodeOutput(moge_geometry)
+
+
+class MoGeInference(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="MoGeInference",
+ display_name="MoGe Inference",
+ category="image/geometry_estimation",
+ inputs=[
+ MoGeModelType.Input("moge_model"),
+ io.Image.Input("image"),
+ io.Int.Input("resolution_level", default=9, min=0, max=9,
+ tooltip="0 = fastest, 9 = most detail."),
+ io.Float.Input("fov_x_degrees", default=0.0, min=0.0, max=170.0, step=0.1, advanced=True,
+ tooltip="Horizontal field of view of the source camera. Sets the focal length used to unproject the depth map into 3D. 0 = auto-recover from the predicted points."),
+ io.Int.Input("batch_size", default=4, min=1, max=64,
+ tooltip="Images per inference call. Lower if you OOM on a long video / image set."),
+ io.Boolean.Input("force_projection", default=True, advanced=True),
+ io.Boolean.Input("apply_mask", default=True, advanced=True,
+ tooltip="Set masked-out (sky / invalid) pixels to inf in points and depth so meshing culls them. Disable to keep the raw predicted geometry everywhere; the mask is still returned separately."),
+ ],
+ outputs=[MoGeGeometry.Output(display_name="moge_geometry")],
+ )
+
+ @classmethod
+ def execute(cls, moge_model, image, resolution_level, fov_x_degrees, batch_size, force_projection, apply_mask) -> io.NodeOutput:
+
+ image = image[..., :3]
+ bchw = image.movedim(-1, -3).contiguous()
+ B = bchw.shape[0]
+ fov = None if fov_x_degrees <= 0 else float(fov_x_degrees)
+
+ pbar = comfy.utils.ProgressBar(B)
+ chunks: list[dict] = []
+ with tqdm(total=B, desc="MoGe inference") as tq:
+ for i in range(0, B, batch_size):
+ chunk = bchw[i:i + batch_size]
+ chunks.append(moge_model.infer(chunk, resolution_level=resolution_level, fov_x=fov,
+ force_projection=force_projection, apply_mask=apply_mask))
+ pbar.update_absolute(min(i + batch_size, B))
+ tq.update(chunk.shape[0])
+
+ def stack(field):
+ vals = [c[field] for c in chunks if field in c]
+ return torch.cat(vals, dim=0) if vals else None
+
+ moge_geometry = {"image": image.cpu()}
+ for field in ("points", "depth", "intrinsics", "mask", "normal"):
+ v = stack(field)
+ if v is not None:
+ moge_geometry[field] = v
+ return io.NodeOutput(moge_geometry)
+
+
+class MoGeRender(io.ComfyNode):
+ """Render a visualization or mask from a MOGE_GEOMETRY packet."""
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="MoGeRender",
+ display_name="MoGe Render",
+ category="image/geometry_estimation",
+ inputs=[
+ MoGeGeometry.Input("moge_geometry"),
+ io.Combo.Input("output", options=["depth", "depth_colored", "normal_opengl", "normal_directx", "mask"], default="depth",
+ tooltip="DirectX vs OpenGL controls the normal-map green-channel convention. DirectX: green = -Y down (Unreal). OpenGL: green = +Y up (Blender, Substance, Unity, glTF)."),
+ ],
+ outputs=[io.Image.Output()],
+ )
+
+ @classmethod
+ def execute(cls, moge_geometry, output) -> io.NodeOutput:
+ is_normal = output in ("normal_directx", "normal_opengl")
+ opengl = output.endswith("_opengl")
+
+ # Pick the input tensor for the chosen mode and validate availability.
+ if output in ("depth", "depth_colored"):
+ if "depth" not in moge_geometry:
+ raise ValueError("moge_geometry has no depth output.")
+ src = moge_geometry["depth"]
+ elif is_normal:
+ if "normal" in moge_geometry:
+ src = moge_geometry["normal"]
+ elif "points" in moge_geometry:
+ src = moge_geometry["points"]
+ else:
+ raise ValueError("moge_geometry has neither normals nor points to derive normals from.")
+ elif output == "mask":
+ if "mask" not in moge_geometry:
+ raise ValueError("moge_geometry has no mask output.")
+ src = moge_geometry["mask"]
+ else:
+ raise ValueError(f"Unknown output mode: {output}")
+
+ B = src.shape[0]
+ pbar = comfy.utils.ProgressBar(B)
+ out: list[torch.Tensor] = []
+ with tqdm(total=B, desc=f"MoGe render: {output}") as tq:
+ for i in range(B):
+ slc = src[i:i + 1].float()
+ if output in ("depth", "depth_colored"):
+ d = _normalize_disparity(slc)
+ out.append(_turbo(d) if output == "depth_colored"
+ else d.unsqueeze(-1).expand(*d.shape, 3).contiguous())
+ elif is_normal:
+ n = slc if "normal" in moge_geometry else _normals_from_points(slc)
+ # MoGe is OpenCV (Z+ into scene); normal-map convention is Z+ out of surface, so flip Z.
+ y_sign = -1.0 if opengl else 1.0
+ n = n * n.new_tensor([1.0, y_sign, -1.0])
+ out.append((n * 0.5 + 0.5).clamp(0.0, 1.0))
+ elif output == "mask":
+ out.append(slc.unsqueeze(-1).expand(*slc.shape, 3).contiguous())
+ pbar.update_absolute(i + 1)
+ tq.update(1)
+ result = torch.cat(out, dim=0).to(device=comfy.model_management.intermediate_device(), dtype=comfy.model_management.intermediate_dtype())
+ return io.NodeOutput(result)
+
+
+class MoGePointMapToMesh(io.ComfyNode):
+ """Triangulate one image of a MoGe point map into a Types.MESH (UVs + texture)."""
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="MoGePointMapToMesh",
+ display_name="MoGe Point Map to Mesh",
+ category="image/geometry_estimation",
+ inputs=[
+ MoGeGeometry.Input("moge_geometry"),
+ io.Int.Input("batch_index", default=0, min=0, max=4096,
+ tooltip="Which image of a batched MoGe geometry to mesh. Per-image vertex counts "
+ "differ, so batches can't be stacked into a single MESH."),
+ io.Int.Input("decimation", default=1, min=1, max=8,
+ tooltip="Vertex stride; 1 = full resolution."),
+ io.Float.Input("discontinuity_threshold", default=0.04, min=0.0, max=1.0, step=0.01,
+ tooltip="Drop pixels whose 3x3 depth span exceeds this fraction. 0 = off."),
+ io.Boolean.Input("texture", default=True,
+ tooltip="Carry the source image through as the baseColor texture."),
+ ],
+ outputs=[io.Mesh.Output()],
+ )
+
+ @classmethod
+ def execute(cls, moge_geometry, batch_index, decimation, discontinuity_threshold, texture) -> io.NodeOutput:
+ if "points" not in moge_geometry:
+ raise ValueError("moge_geometry has no points output.")
+ points = moge_geometry["points"]
+ B = points.shape[0]
+ if batch_index >= B:
+ raise ValueError(f"batch_index {batch_index} out of range; moge_geometry has batch size {B}.")
+
+ # Pass depth so the rtol edge check sees radial depth -- for panoramas
+ # points[..., 2] = cos(phi)*r goes negative below the equator and the rtol clamp would drop the bottom half.
+ edge_depth = moge_geometry["depth"][batch_index] if "depth" in moge_geometry else None
+ verts, faces, uvs = triangulate_grid_mesh(
+ points[batch_index], decimation=decimation,
+ discontinuity_threshold=discontinuity_threshold, depth=edge_depth,
+ )
+ if verts.shape[0] == 0 or faces.shape[0] == 0:
+ raise ValueError("MoGe produced an empty mesh; try discontinuity_threshold=0 or apply_mask=False.")
+
+ if "intrinsics" not in moge_geometry:
+ # Panorama: rotate MoGe spherical (Z up) -> glTF (Y up, Z back), correct for inside-the-sphere viewing)
+ verts = verts[:, [1, 2, 0]].contiguous()
+ else:
+ # Perspective MoGe (X right, Y down, Z forward) -> glTF; face flip keeps winding CCW after the Y/Z flip.
+ verts = verts * torch.tensor([1.0, -1.0, -1.0], dtype=verts.dtype)
+ faces = faces[:, [0, 2, 1]].contiguous()
+
+ tex = moge_geometry["image"][batch_index:batch_index + 1] if texture else None
+ mesh = Types.MESH(
+ vertices=verts.unsqueeze(0),
+ faces=faces.unsqueeze(0),
+ uvs=uvs.unsqueeze(0),
+ texture=tex,
+ )
+ return io.NodeOutput(mesh)
+
+
+class MoGeExtension(ComfyExtension):
+ @override
+ async def get_node_list(self) -> list[type[io.ComfyNode]]:
+ return [LoadMoGeModel, MoGeInference, MoGePanoramaInference, MoGeRender, MoGePointMapToMesh]
+
+
+async def comfy_entrypoint() -> MoGeExtension:
+ return MoGeExtension()
diff --git a/comfy_extras/nodes_morphology.py b/comfy_extras/nodes_morphology.py
index 4ab2fb7e8..0142040dd 100644
--- a/comfy_extras/nodes_morphology.py
+++ b/comfy_extras/nodes_morphology.py
@@ -13,8 +13,8 @@ class Morphology(io.ComfyNode):
return io.Schema(
node_id="Morphology",
search_aliases=["erode", "dilate"],
- display_name="ImageMorphology",
- category="image/postprocessing",
+ display_name="Apply Morphology",
+ category="image/filters",
inputs=[
io.Image.Input("image"),
io.Combo.Input(
@@ -59,7 +59,8 @@ class ImageRGBToYUV(io.ComfyNode):
return io.Schema(
node_id="ImageRGBToYUV",
search_aliases=["color space conversion"],
- category="image/batch",
+ display_name="Image RGB to YUV",
+ category="image/color",
inputs=[
io.Image.Input("image"),
],
@@ -81,7 +82,8 @@ class ImageYUVToRGB(io.ComfyNode):
return io.Schema(
node_id="ImageYUVToRGB",
search_aliases=["color space conversion"],
- category="image/batch",
+ display_name="Image YUV to RGB",
+ category="image/color",
inputs=[
io.Image.Input("Y"),
io.Image.Input("U"),
diff --git a/comfy_extras/nodes_nop.py b/comfy_extras/nodes_nop.py
index 953061bcb..f9c1357c3 100644
--- a/comfy_extras/nodes_nop.py
+++ b/comfy_extras/nodes_nop.py
@@ -13,7 +13,7 @@ class wanBlockSwap(io.ComfyNode):
return io.Schema(
node_id="wanBlockSwap",
category="",
- description="NOP",
+ description="Intercept wanBlockSwap custom node that causes major instability and make it no-op.",
inputs=[
io.Model.Input("model"),
],
diff --git a/comfy_extras/nodes_number_convert.py b/comfy_extras/nodes_number_convert.py
index cac7e736d..e38a33c15 100644
--- a/comfy_extras/nodes_number_convert.py
+++ b/comfy_extras/nodes_number_convert.py
@@ -20,8 +20,8 @@ class NumberConvertNode(io.ComfyNode):
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="ComfyNumberConvert",
- display_name="Number Convert",
- category="math",
+ display_name="Convert Number",
+ category="utils",
search_aliases=[
"int to float", "float to int", "number convert",
"int2float", "float2int", "cast", "parse number",
diff --git a/comfy_extras/nodes_optimalsteps.py b/comfy_extras/nodes_optimalsteps.py
index 73f0104d8..5beeaa7db 100644
--- a/comfy_extras/nodes_optimalsteps.py
+++ b/comfy_extras/nodes_optimalsteps.py
@@ -31,7 +31,7 @@ class OptimalStepsScheduler(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="OptimalStepsScheduler",
- category="sampling/custom_sampling/schedulers",
+ category="sampling/schedulers",
inputs=[
io.Combo.Input("model_type", options=["FLUX", "Wan", "Chroma"]),
io.Int.Input("steps", default=20, min=3, max=1000),
diff --git a/comfy_extras/nodes_perpneg.py b/comfy_extras/nodes_perpneg.py
index ed1467de9..a7a72d1bc 100644
--- a/comfy_extras/nodes_perpneg.py
+++ b/comfy_extras/nodes_perpneg.py
@@ -24,8 +24,8 @@ class PerpNeg(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="PerpNeg",
- display_name="Perp-Neg (DEPRECATED by PerpNegGuider)",
- category="_for_testing",
+ display_name="Perp-Neg (DEPRECATED by Perp-Neg Guider)",
+ category="experimental",
inputs=[
io.Model.Input("model"),
io.Conditioning.Input("empty_conditioning"),
@@ -127,7 +127,8 @@ class PerpNegGuider(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="PerpNegGuider",
- category="_for_testing",
+ display_name="Perp-Neg Guider",
+ category="experimental",
inputs=[
io.Model.Input("model"),
io.Conditioning.Input("positive"),
diff --git a/comfy_extras/nodes_photomaker.py b/comfy_extras/nodes_photomaker.py
index 228183c07..8a2248572 100644
--- a/comfy_extras/nodes_photomaker.py
+++ b/comfy_extras/nodes_photomaker.py
@@ -123,7 +123,7 @@ class PhotoMakerLoader(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="PhotoMakerLoader",
- category="_for_testing/photomaker",
+ category="experimental/photomaker",
inputs=[
io.Combo.Input("photomaker_model_name", options=folder_paths.get_filename_list("photomaker")),
],
@@ -149,7 +149,7 @@ class PhotoMakerEncode(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="PhotoMakerEncode",
- category="_for_testing/photomaker",
+ category="experimental/photomaker",
inputs=[
io.Photomaker.Input("photomaker"),
io.Image.Input("image"),
diff --git a/comfy_extras/nodes_post_processing.py b/comfy_extras/nodes_post_processing.py
index c932b747a..a25db277c 100644
--- a/comfy_extras/nodes_post_processing.py
+++ b/comfy_extras/nodes_post_processing.py
@@ -20,8 +20,9 @@ class Blend(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="ImageBlend",
- display_name="Image Blend",
- category="image/postprocessing",
+ search_aliases=["mix images"],
+ display_name="Blend Images",
+ category="image/filters",
essentials_category="Image Tools",
inputs=[
io.Image.Input("image1"),
@@ -79,8 +80,8 @@ class Blur(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="ImageBlur",
- display_name="Image Blur",
- category="image/postprocessing",
+ display_name="Blur Image",
+ category="image/filters",
inputs=[
io.Image.Input("image"),
io.Int.Input("blur_radius", default=1, min=1, max=31, step=1),
@@ -115,7 +116,8 @@ class Quantize(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="ImageQuantize",
- category="image/postprocessing",
+ display_name="Quantize Image",
+ category="image/filters",
inputs=[
io.Image.Input("image"),
io.Int.Input("colors", default=256, min=1, max=256, step=1),
@@ -180,7 +182,8 @@ class Sharpen(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="ImageSharpen",
- category="image/postprocessing",
+ display_name="Sharpen Image",
+ category="image/filters",
inputs=[
io.Image.Input("image"),
io.Int.Input("sharpen_radius", default=1, min=1, max=31, step=1, advanced=True),
@@ -224,6 +227,7 @@ class ImageScaleToTotalPixels(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="ImageScaleToTotalPixels",
+ display_name="Scale Image to Total Pixels",
category="image/upscaling",
inputs=[
io.Image.Input("image"),
@@ -434,7 +438,7 @@ class ResizeImageMaskNode(io.ComfyNode):
node_id="ResizeImageMaskNode",
display_name="Resize Image/Mask",
description="Resize an image or mask using various scaling methods.",
- category="transform",
+ category="image/transform",
search_aliases=["resize", "resize image", "resize mask", "scale", "scale image", "scale mask", "image resize", "change size", "dimensions", "shrink", "enlarge"],
inputs=[
io.MatchType.Input("input", template=template),
@@ -564,11 +568,11 @@ def batch_latents(latents: list[dict[str, torch.Tensor]]) -> dict[str, torch.Ten
class BatchImagesNode(io.ComfyNode):
@classmethod
def define_schema(cls):
- autogrow_template = io.Autogrow.TemplatePrefix(io.Image.Input("image"), prefix="image", min=2, max=50)
+ autogrow_template = io.Autogrow.TemplatePrefix(io.Image.Input("image"), prefix="image", min=1, max=50)
return io.Schema(
node_id="BatchImagesNode",
display_name="Batch Images",
- category="image",
+ category="image/batch",
essentials_category="Image Tools",
search_aliases=["batch", "image batch", "batch images", "combine images", "merge images", "stack images"],
inputs=[
@@ -586,12 +590,12 @@ class BatchImagesNode(io.ComfyNode):
class BatchMasksNode(io.ComfyNode):
@classmethod
def define_schema(cls):
- autogrow_template = io.Autogrow.TemplatePrefix(io.Mask.Input("mask"), prefix="mask", min=2, max=50)
+ autogrow_template = io.Autogrow.TemplatePrefix(io.Mask.Input("mask"), prefix="mask", min=1, max=50)
return io.Schema(
node_id="BatchMasksNode",
search_aliases=["combine masks", "stack masks", "merge masks"],
display_name="Batch Masks",
- category="mask",
+ category="image/mask",
inputs=[
io.Autogrow.Input("masks", template=autogrow_template)
],
@@ -607,7 +611,7 @@ class BatchMasksNode(io.ComfyNode):
class BatchLatentsNode(io.ComfyNode):
@classmethod
def define_schema(cls):
- autogrow_template = io.Autogrow.TemplatePrefix(io.Latent.Input("latent"), prefix="latent", min=2, max=50)
+ autogrow_template = io.Autogrow.TemplatePrefix(io.Latent.Input("latent"), prefix="latent", min=1, max=50)
return io.Schema(
node_id="BatchLatentsNode",
search_aliases=["combine latents", "stack latents", "merge latents"],
@@ -666,12 +670,13 @@ class ColorTransfer(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="ColorTransfer",
- category="image/postprocessing",
+ display_name="Transfer Color",
+ category="image/filters",
description="Match the colors of one image to another using various algorithms.",
search_aliases=["color match", "color grading", "color correction", "match colors", "color transform", "mkl", "reinhard", "histogram"],
inputs=[
io.Image.Input("image_target", tooltip="Image(s) to apply the color transform to."),
- io.Image.Input("image_ref", optional=True, tooltip="Reference image(s) to match colors to. If not provided, processing is skipped"),
+ io.Image.Input("image_ref", tooltip="Reference image(s) to match colors to."),
io.Combo.Input("method", options=['reinhard_lab', 'mkl_lab', 'histogram'],),
io.DynamicCombo.Input("source_stats",
tooltip="per_frame: each frame matched to image_ref individually. uniform: pool stats across all source frames as baseline, match to image_ref. target_frame: use one chosen frame as the baseline for the transform to image_ref, applied uniformly to all frames (preserves relative differences)",
diff --git a/comfy_extras/nodes_preview_any.py b/comfy_extras/nodes_preview_any.py
index 0a1558f2b..17e25d514 100644
--- a/comfy_extras/nodes_preview_any.py
+++ b/comfy_extras/nodes_preview_any.py
@@ -1,5 +1,6 @@
import json
from comfy.comfy_types.node_typing import IO
+import torch
# Preview Any - original implement from
# https://github.com/rgthree/rgthree-comfy/blob/main/py/display_any.py
@@ -19,6 +20,7 @@ class PreviewAny():
SEARCH_ALIASES = ["show output", "inspect", "debug", "print value", "show text"]
def main(self, source=None):
+ torch.set_printoptions(edgeitems=6)
value = 'None'
if isinstance(source, str):
value = source
@@ -33,6 +35,7 @@ class PreviewAny():
except Exception:
value = 'source exists, but could not be serialized.'
+ torch.set_printoptions()
return {"ui": {"text": (value,)}, "result": (value,)}
NODE_CLASS_MAPPINGS = {
diff --git a/comfy_extras/nodes_primitive.py b/comfy_extras/nodes_primitive.py
index 9c2e98758..33373266b 100644
--- a/comfy_extras/nodes_primitive.py
+++ b/comfy_extras/nodes_primitive.py
@@ -9,7 +9,8 @@ class String(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="PrimitiveString",
- display_name="String",
+ search_aliases=["text", "string", "text box", "prompt"],
+ display_name="Text String",
category="utils/primitive",
inputs=[
io.String.Input("value"),
@@ -27,7 +28,8 @@ class StringMultiline(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="PrimitiveStringMultiline",
- display_name="String (Multiline)",
+ search_aliases=["text", "string", "text multiline", "string multiline", "text box", "prompt"],
+ display_name="Text String (Multiline)",
category="utils/primitive",
essentials_category="Basics",
inputs=[
@@ -49,7 +51,7 @@ class Int(io.ComfyNode):
display_name="Int",
category="utils/primitive",
inputs=[
- io.Int.Input("value", min=-sys.maxsize, max=sys.maxsize, control_after_generate=True),
+ io.Int.Input("value", min=-sys.maxsize, max=sys.maxsize, control_after_generate=io.ControlAfterGenerate.fixed),
],
outputs=[io.Int.Output()],
)
diff --git a/comfy_extras/nodes_qwen.py b/comfy_extras/nodes_qwen.py
index 6894367be..fde8fac9a 100644
--- a/comfy_extras/nodes_qwen.py
+++ b/comfy_extras/nodes_qwen.py
@@ -116,7 +116,7 @@ class EmptyQwenImageLayeredLatentImage(io.ComfyNode):
inputs=[
io.Int.Input("width", default=640, min=16, max=nodes.MAX_RESOLUTION, step=16),
io.Int.Input("height", default=640, min=16, max=nodes.MAX_RESOLUTION, step=16),
- io.Int.Input("layers", default=3, min=0, max=nodes.MAX_RESOLUTION, step=1, advanced=True),
+ io.Int.Input("layers", default=3, min=0, max=nodes.MAX_RESOLUTION, step=1),
io.Int.Input("batch_size", default=1, min=1, max=4096),
],
outputs=[
diff --git a/comfy_extras/nodes_rtdetr.py b/comfy_extras/nodes_rtdetr.py
index 7feaf3ab3..e5a9b3902 100644
--- a/comfy_extras/nodes_rtdetr.py
+++ b/comfy_extras/nodes_rtdetr.py
@@ -15,7 +15,7 @@ class RTDETR_detect(io.ComfyNode):
return io.Schema(
node_id="RTDETR_detect",
display_name="RT-DETR Detect",
- category="detection/",
+ category="image/detection",
search_aliases=["bbox", "bounding box", "object detection", "coco"],
inputs=[
io.Model.Input("model", display_name="model"),
@@ -71,7 +71,7 @@ class DrawBBoxes(io.ComfyNode):
return io.Schema(
node_id="DrawBBoxes",
display_name="Draw BBoxes",
- category="detection/",
+ category="image/detection",
search_aliases=["bbox", "bounding box", "object detection", "rt_detr", "visualize detections", "coco"],
inputs=[
io.Image.Input("image", optional=True),
diff --git a/comfy_extras/nodes_sag.py b/comfy_extras/nodes_sag.py
index d9c47851c..9dbf1b6f9 100644
--- a/comfy_extras/nodes_sag.py
+++ b/comfy_extras/nodes_sag.py
@@ -113,7 +113,7 @@ class SelfAttentionGuidance(io.ComfyNode):
return io.Schema(
node_id="SelfAttentionGuidance",
display_name="Self-Attention Guidance",
- category="_for_testing",
+ category="experimental",
inputs=[
io.Model.Input("model"),
io.Float.Input("scale", default=0.5, min=-2.0, max=5.0, step=0.01),
diff --git a/comfy_extras/nodes_sam3.py b/comfy_extras/nodes_sam3.py
index 5cf92ccb3..daac52f9b 100644
--- a/comfy_extras/nodes_sam3.py
+++ b/comfy_extras/nodes_sam3.py
@@ -93,7 +93,7 @@ class SAM3_Detect(io.ComfyNode):
return io.Schema(
node_id="SAM3_Detect",
display_name="SAM3 Detect",
- category="detection/",
+ category="image/detection",
search_aliases=["sam3", "segment anything", "open vocabulary", "text detection", "segment"],
inputs=[
io.Model.Input("model", display_name="model"),
@@ -265,15 +265,15 @@ class SAM3_VideoTrack(io.ComfyNode):
return io.Schema(
node_id="SAM3_VideoTrack",
display_name="SAM3 Video Track",
- category="detection/",
+ category="image/detection",
search_aliases=["sam3", "video", "track", "propagate"],
inputs=[
io.Image.Input("images", display_name="images", tooltip="Video frames as batched images"),
io.Model.Input("model", display_name="model"),
io.Mask.Input("initial_mask", display_name="initial_mask", optional=True, tooltip="Mask(s) for the first frame to track (one per object)"),
io.Conditioning.Input("conditioning", display_name="conditioning", optional=True, tooltip="Text conditioning for detecting new objects during tracking"),
- io.Float.Input("detection_threshold", display_name="detection_threshold", default=0.5, min=0.0, max=1.0, step=0.01, tooltip="Score threshold for text-prompted detection"),
- io.Int.Input("max_objects", display_name="max_objects", default=0, min=0, tooltip="Max tracked objects (0=unlimited). Initial masks count toward this limit."),
+ io.Float.Input("detection_threshold", display_name="detection_threshold", default=0.5, min=0.0, max=1.0, step=0.01, tooltip="Score threshold for text-prompted detection."),
+ io.Int.Input("max_objects", display_name="max_objects", default=4, min=0, max=64, tooltip="Max tracked objects. Initial masks count toward this limit. 0 uses the internal cap of 64."),
io.Int.Input("detect_interval", display_name="detect_interval", default=1, min=1, tooltip="Run detection every N frames (1=every frame). Higher values save compute."),
],
outputs=[
@@ -290,8 +290,7 @@ class SAM3_VideoTrack(io.ComfyNode):
dtype = model.model.get_dtype()
sam3_model = model.model.diffusion_model
- frames = images[..., :3].movedim(-1, 1)
- frames_in = comfy.utils.common_upscale(frames, 1008, 1008, "bilinear", crop="disabled").to(device=device, dtype=dtype)
+ frames_in = images[..., :3].movedim(-1, 1)
init_masks = None
if initial_mask is not None:
@@ -308,7 +307,7 @@ class SAM3_VideoTrack(io.ComfyNode):
result = sam3_model.forward_video(
images=frames_in, initial_masks=init_masks, pbar=pbar, text_prompts=text_prompts,
new_det_thresh=detection_threshold, max_objects=max_objects,
- detect_interval=detect_interval)
+ detect_interval=detect_interval, target_device=device, target_dtype=dtype)
result["orig_size"] = (H, W)
return io.NodeOutput(result)
@@ -321,7 +320,7 @@ class SAM3_TrackPreview(io.ComfyNode):
return io.Schema(
node_id="SAM3_TrackPreview",
display_name="SAM3 Track Preview",
- category="detection/",
+ category="image/detection",
inputs=[
SAM3TrackData.Input("track_data", display_name="track_data"),
io.Image.Input("images", display_name="images", optional=True),
@@ -449,14 +448,18 @@ class SAM3_TrackPreview(io.ComfyNode):
cx = (bool_masks * grid_x).sum(dim=(-1, -2)) // area
has = area > 1
scores = track_data.get("scores", [])
+ label_scale = max(3, H // 240) # Scale font with resolutio
+ size_caps = (area.float().sqrt() / 15).clamp_(min=1).long().tolist() #cap per-object so the number doesn't dwarf small masks
for obj_idx in range(N_obj):
if has[obj_idx]:
_cx, _cy = int(cx[obj_idx]), int(cy[obj_idx])
color = cls.COLORS[obj_idx % len(cls.COLORS)]
- SAM3_TrackPreview._draw_number_gpu(frame_gpu, obj_idx, _cx, _cy, color)
+ obj_scale = min(label_scale, size_caps[obj_idx])
+ score_scale = max(1, obj_scale * 2 // 3)
+ SAM3_TrackPreview._draw_number_gpu(frame_gpu, obj_idx, _cx, _cy, color, scale=obj_scale)
if obj_idx < len(scores) and scores[obj_idx] < 1.0:
SAM3_TrackPreview._draw_number_gpu(frame_gpu, int(scores[obj_idx] * 100),
- _cx, _cy + 5 * 3 + 3, color, scale=2)
+ _cx, _cy + 5 * obj_scale + 3, color, scale=score_scale)
frame_cpu.copy_(frame_gpu.clamp_(0, 1).mul_(255).byte())
else:
frame_cpu.copy_(frame.clamp_(0, 1).mul_(255).byte())
@@ -475,7 +478,7 @@ class SAM3_TrackToMask(io.ComfyNode):
return io.Schema(
node_id="SAM3_TrackToMask",
display_name="SAM3 Track to Mask",
- category="detection/",
+ category="image/detection",
inputs=[
SAM3TrackData.Input("track_data", display_name="track_data"),
io.String.Input("object_indices", display_name="object_indices", default="",
@@ -507,9 +510,10 @@ class SAM3_TrackToMask(io.ComfyNode):
if not indices:
return io.NodeOutput(torch.zeros(N, H, W, device=comfy.model_management.intermediate_device()))
- selected = packed[:, indices]
- binary = unpack_masks(selected) # [N, len(indices), Hm, Wm] bool
- union = binary.any(dim=1, keepdim=True).float()
+ union_packed = packed[:, indices[0]].clone()
+ for i in indices[1:]:
+ union_packed |= packed[:, i]
+ union = unpack_masks(union_packed).unsqueeze(1).float() # [N, 1, Hm, Wm]
mask_out = F.interpolate(union, size=(H, W), mode="bilinear", align_corners=False)[:, 0]
return io.NodeOutput(mask_out)
diff --git a/comfy_extras/nodes_save_3d.py b/comfy_extras/nodes_save_3d.py
new file mode 100644
index 000000000..c03524246
--- /dev/null
+++ b/comfy_extras/nodes_save_3d.py
@@ -0,0 +1,396 @@
+"""Save-side 3D nodes: mesh packing/slicing helpers + GLB writer + SaveGLB node."""
+
+import json
+import logging
+import os
+import struct
+from io import BytesIO
+
+import numpy as np
+from PIL import Image
+import torch
+from typing_extensions import override
+
+import folder_paths
+from comfy.cli_args import args
+from comfy_api.latest import ComfyExtension, IO, Types
+
+
+def pack_variable_mesh_batch(vertices, faces, colors=None, uvs=None, texture=None):
+ # Pack lists of (Nᵢ, *) vertex/face/color/uv tensors into padded batched tensors,
+ # stashing per-item lengths as runtime attrs so consumers can recover the real slice.
+ # colors and uvs are 1:1 with vertices, so they're padded to max_vertices and read with vertex_counts.
+ # texture is (B, H, W, 3) — passed through unchanged
+ batch_size = len(vertices)
+ max_vertices = max(v.shape[0] for v in vertices)
+ max_faces = max(f.shape[0] for f in faces)
+
+ packed_vertices = vertices[0].new_zeros((batch_size, max_vertices, vertices[0].shape[1]))
+ packed_faces = faces[0].new_zeros((batch_size, max_faces, faces[0].shape[1]))
+ vertex_counts = torch.tensor([v.shape[0] for v in vertices], device=vertices[0].device, dtype=torch.int64)
+ face_counts = torch.tensor([f.shape[0] for f in faces], device=faces[0].device, dtype=torch.int64)
+
+ for i, (v, f) in enumerate(zip(vertices, faces)):
+ packed_vertices[i, :v.shape[0]] = v
+ packed_faces[i, :f.shape[0]] = f
+
+ packed_colors = None
+ if colors is not None:
+ packed_colors = colors[0].new_zeros((batch_size, max_vertices, colors[0].shape[1]))
+ for i, c in enumerate(colors):
+ assert c.shape[0] == vertices[i].shape[0], (
+ f"vertex_colors[{i}] has {c.shape[0]} entries, expected {vertices[i].shape[0]} (1:1 with vertices)"
+ )
+ packed_colors[i, :c.shape[0]] = c
+
+ packed_uvs = None
+ if uvs is not None:
+ packed_uvs = uvs[0].new_zeros((batch_size, max_vertices, uvs[0].shape[1]))
+ for i, u in enumerate(uvs):
+ assert u.shape[0] == vertices[i].shape[0], (
+ f"uvs[{i}] has {u.shape[0]} entries, expected {vertices[i].shape[0]} (1:1 with vertices)"
+ )
+ packed_uvs[i, :u.shape[0]] = u
+
+ return Types.MESH(packed_vertices, packed_faces,
+ uvs=packed_uvs, vertex_colors=packed_colors, texture=texture,
+ vertex_counts=vertex_counts, face_counts=face_counts)
+
+
+def get_mesh_batch_item(mesh, index):
+ # Returns (vertices, faces, colors, uvs) for batch index, slicing to real lengths
+ # if the mesh carries per-item counts (variable-size batch).
+ v_colors = getattr(mesh, "vertex_colors", None)
+ v_uvs = getattr(mesh, "uvs", None)
+ if getattr(mesh, "vertex_counts", None) is not None:
+ vertex_count = int(mesh.vertex_counts[index].item())
+ face_count = int(mesh.face_counts[index].item())
+ vertices = mesh.vertices[index, :vertex_count]
+ faces = mesh.faces[index, :face_count]
+ colors = v_colors[index, :vertex_count] if v_colors is not None else None
+ uvs = v_uvs[index, :vertex_count] if v_uvs is not None else None
+ return vertices, faces, colors, uvs
+
+ colors = v_colors[index] if v_colors is not None else None
+ uvs = v_uvs[index] if v_uvs is not None else None
+ return mesh.vertices[index], mesh.faces[index], colors, uvs
+
+
+def save_glb(vertices, faces, filepath, metadata=None,
+ uvs=None, vertex_colors=None, texture_image=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)
+ metadata: dict - Optional asset.extras metadata
+ uvs: torch.Tensor of shape (N, 2) - Optional per-vertex texture coordinates
+ vertex_colors: torch.Tensor of shape (N, 3) or (N, 4) - Optional per-vertex colors in [0, 1]
+ texture_image: PIL.Image - Optional baseColor texture, embedded as PNG
+ """
+
+ # Convert tensors to numpy arrays
+ vertices_np = vertices.cpu().numpy().astype(np.float32)
+ faces_signed = faces.cpu().numpy().astype(np.int64)
+ uvs_np = uvs.cpu().numpy().astype(np.float32) if uvs is not None else None
+ colors_np = vertex_colors.cpu().numpy().astype(np.float32) if vertex_colors is not None else None
+ if colors_np is not None:
+ colors_np = np.clip(colors_np, 0.0, 1.0)
+
+ n_verts = vertices_np.shape[0]
+ if n_verts == 0:
+ raise ValueError("save_glb: vertices is empty")
+ if faces_signed.size > 0:
+ fmin = int(faces_signed.min())
+ fmax = int(faces_signed.max())
+ if fmin < 0 or fmax >= n_verts:
+ raise ValueError(
+ f"save_glb: face index out of range [0, {n_verts}): min={fmin}, max={fmax}"
+ )
+ if uvs_np is not None and uvs_np.shape[0] != n_verts:
+ raise ValueError(
+ f"save_glb: uvs has {uvs_np.shape[0]} entries but vertex count is {n_verts}"
+ )
+ if colors_np is not None and colors_np.shape[0] != n_verts:
+ raise ValueError(
+ f"save_glb: vertex_colors has {colors_np.shape[0]} entries but vertex count is {n_verts}"
+ )
+ faces_np = faces_signed.astype(np.uint32)
+ texture_png_bytes = None
+ if texture_image is not None:
+ buf = BytesIO()
+ texture_image.save(buf, format="PNG")
+ texture_png_bytes = buf.getvalue()
+
+ vertices_buffer = vertices_np.tobytes()
+ indices_buffer = faces_np.tobytes()
+ uvs_buffer = uvs_np.tobytes() if uvs_np is not None else b""
+ colors_buffer = colors_np.tobytes() if colors_np is not None else b""
+ texture_buffer = texture_png_bytes if texture_png_bytes is not None else b""
+
+ 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)
+ uvs_buffer_padded = pad_to_4_bytes(uvs_buffer)
+ colors_buffer_padded = pad_to_4_bytes(colors_buffer)
+ texture_buffer_padded = pad_to_4_bytes(texture_buffer)
+
+ buffer_data = b"".join([
+ vertices_buffer_padded,
+ indices_buffer_padded,
+ uvs_buffer_padded,
+ colors_buffer_padded,
+ texture_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)
+ uvs_byte_offset = indices_byte_offset + len(indices_buffer_padded)
+ colors_byte_offset = uvs_byte_offset + len(uvs_buffer_padded)
+ texture_byte_offset = colors_byte_offset + len(colors_buffer_padded)
+
+ buffer_views = [
+ {
+ "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"
+ }
+ ]
+ primitive_attributes = {"POSITION": 0}
+
+ if uvs_np is not None and len(uvs_np) > 0:
+ buffer_views.append({
+ "buffer": 0,
+ "byteOffset": uvs_byte_offset,
+ "byteLength": len(uvs_buffer),
+ "target": 34962
+ })
+ accessor_idx = len(accessors)
+ accessors.append({
+ "bufferView": len(buffer_views) - 1,
+ "byteOffset": 0,
+ "componentType": 5126,
+ "count": len(uvs_np),
+ "type": "VEC2",
+ })
+ primitive_attributes["TEXCOORD_0"] = accessor_idx
+
+ if colors_np is not None and len(colors_np) > 0:
+ buffer_views.append({
+ "buffer": 0,
+ "byteOffset": colors_byte_offset,
+ "byteLength": len(colors_buffer),
+ "target": 34962
+ })
+ accessor_idx = len(accessors)
+ accessors.append({
+ "bufferView": len(buffer_views) - 1,
+ "byteOffset": 0,
+ "componentType": 5126,
+ "count": len(colors_np),
+ "type": "VEC3" if colors_np.shape[1] == 3 else "VEC4",
+ })
+ primitive_attributes["COLOR_0"] = accessor_idx
+
+ primitive = {
+ "attributes": primitive_attributes,
+ "indices": 1,
+ "mode": 4 # TRIANGLES
+ }
+
+ images = []
+ textures = []
+ samplers = []
+ materials = []
+ if texture_png_bytes is not None and "TEXCOORD_0" in primitive_attributes:
+ buffer_views.append({
+ "buffer": 0,
+ "byteOffset": texture_byte_offset,
+ "byteLength": len(texture_buffer),
+ })
+ images.append({"bufferView": len(buffer_views) - 1, "mimeType": "image/png"})
+ samplers.append({"magFilter": 9729, "minFilter": 9729, "wrapS": 33071, "wrapT": 33071})
+ textures.append({"source": 0, "sampler": 0})
+ materials.append({
+ "pbrMetallicRoughness": {
+ "baseColorTexture": {"index": 0, "texCoord": 0},
+ "metallicFactor": 0.0,
+ "roughnessFactor": 1.0,
+ },
+ "doubleSided": True,
+ })
+ primitive["material"] = 0
+
+ gltf = {
+ "asset": {"version": "2.0", "generator": "ComfyUI"},
+ "buffers": [{"byteLength": len(buffer_data)}],
+ "bufferViews": buffer_views,
+ "accessors": accessors,
+ "meshes": [{"primitives": [primitive]}],
+ "nodes": [{"mesh": 0}],
+ "scenes": [{"nodes": [0]}],
+ "scene": 0,
+ }
+ if images:
+ gltf["images"] = images
+ if samplers:
+ gltf["samplers"] = samplers
+ if textures:
+ gltf["textures"] = textures
+ if materials:
+ gltf["materials"] = materials
+
+ if metadata:
+ 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 (a 4-byte ASCII magic identifier 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(' IO.NodeOutput:
+ full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory())
+ results = []
+
+ metadata = {}
+ if not args.disable_metadata:
+ if cls.hidden.prompt is not None:
+ metadata["prompt"] = json.dumps(cls.hidden.prompt)
+ if cls.hidden.extra_pnginfo is not None:
+ for x in cls.hidden.extra_pnginfo:
+ metadata[x] = json.dumps(cls.hidden.extra_pnginfo[x])
+
+ if isinstance(mesh, Types.File3D):
+ # Handle File3D input - save BytesIO data to output folder
+ ext = mesh.format or "glb"
+ f = f"{filename}_{counter:05}_.{ext}"
+ mesh.save_to(os.path.join(full_output_folder, f))
+ results.append({
+ "filename": f,
+ "subfolder": subfolder,
+ "type": "output"
+ })
+ counter += 1
+ else:
+ # Handle Mesh input - save vertices and faces as GLB; carry optional UVs / colors / texture.
+ texture_b = getattr(mesh, "texture", None)
+ texture_np = None
+ if texture_b is not None:
+ texture_np = (texture_b.clamp(0.0, 1.0).cpu().numpy() * 255).astype(np.uint8)
+ assert texture_np.ndim == 4 and texture_np.shape[-1] == 3, (
+ f"texture must be (B, H, W, 3) RGB, got shape {tuple(texture_np.shape)}"
+ )
+ for i in range(mesh.vertices.shape[0]):
+ vertices_i, faces_i, v_colors, uvs_i = get_mesh_batch_item(mesh, i)
+ if vertices_i.shape[0] == 0 or faces_i.shape[0] == 0:
+ logging.warning(f"SaveGLB: skipping empty mesh at batch index {i}")
+ continue
+ tex_img = Image.fromarray(texture_np[i], mode="RGB") if texture_np is not None else None
+ f = f"{filename}_{counter:05}_.glb"
+ save_glb(vertices_i, faces_i, os.path.join(full_output_folder, f), metadata,
+ uvs=uvs_i,
+ vertex_colors=v_colors,
+ texture_image=tex_img)
+ results.append({
+ "filename": f,
+ "subfolder": subfolder,
+ "type": "output"
+ })
+ counter += 1
+ return IO.NodeOutput(ui={"3d": results})
+
+
+class Save3DExtension(ComfyExtension):
+ @override
+ async def get_node_list(self) -> list[type[IO.ComfyNode]]:
+ return [SaveGLB]
+
+
+async def comfy_entrypoint() -> Save3DExtension:
+ return Save3DExtension()
diff --git a/comfy_extras/nodes_sd3.py b/comfy_extras/nodes_sd3.py
index c43844a1a..6655c1ba7 100644
--- a/comfy_extras/nodes_sd3.py
+++ b/comfy_extras/nodes_sd3.py
@@ -54,7 +54,7 @@ class EmptySD3LatentImage(io.ComfyNode):
@classmethod
def execute(cls, width, height, batch_size=1) -> io.NodeOutput:
- latent = torch.zeros([batch_size, 16, height // 8, width // 8], device=comfy.model_management.intermediate_device())
+ latent = torch.zeros([batch_size, 16, height // 8, width // 8], device=comfy.model_management.intermediate_device(), dtype=comfy.model_management.intermediate_dtype())
return io.NodeOutput({"samples": latent, "downscale_ratio_spacial": 8})
generate = execute # TODO: remove
diff --git a/comfy_extras/nodes_sdpose.py b/comfy_extras/nodes_sdpose.py
index 7d54967d5..20d459b00 100644
--- a/comfy_extras/nodes_sdpose.py
+++ b/comfy_extras/nodes_sdpose.py
@@ -353,7 +353,8 @@ class SDPoseDrawKeypoints(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="SDPoseDrawKeypoints",
- category="image/preprocessors",
+ display_name="SDPose Draw Keypoints",
+ category="image/detection",
search_aliases=["openpose", "pose detection", "preprocessor", "keypoints", "pose"],
inputs=[
io.Custom("POSE_KEYPOINT").Input("keypoints"),
@@ -421,7 +422,8 @@ class SDPoseKeypointExtractor(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="SDPoseKeypointExtractor",
- category="image/preprocessors",
+ display_name="SDPose Keypoint Extractor",
+ category="image/detection",
search_aliases=["openpose", "pose detection", "preprocessor", "keypoints", "sdpose"],
description="Extract pose keypoints from images using the SDPose model: https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
inputs=[
@@ -459,27 +461,23 @@ class SDPoseKeypointExtractor(io.ComfyNode):
total_images = image.shape[0]
captured_feat = None
- model_h = int(head.heatmap_size[0]) * 4 # e.g. 192 * 4 = 768
- model_w = int(head.heatmap_size[1]) * 4 # e.g. 256 * 4 = 1024
+ model_w = int(head.heatmap_size[0]) * 4 # 192 * 4 = 768
+ model_h = int(head.heatmap_size[1]) * 4 # 256 * 4 = 1024
def _resize_to_model(imgs):
- """Aspect-preserving resize + zero-pad BHWC images to (model_h, model_w). Returns (resized_bhwc, scale, pad_top, pad_left)."""
+ """Stretch BHWC images to (model_h, model_w), model expects no aspect preservation."""
h, w = imgs.shape[-3], imgs.shape[-2]
- scale = min(model_h / h, model_w / w)
- sh, sw = int(round(h * scale)), int(round(w * scale))
- pt, pl = (model_h - sh) // 2, (model_w - sw) // 2
+ method = "area" if (model_h <= h and model_w <= w) else "bilinear"
chw = imgs.permute(0, 3, 1, 2).float()
- scaled = comfy.utils.common_upscale(chw, sw, sh, upscale_method="bilinear", crop="disabled")
- padded = torch.zeros(scaled.shape[0], scaled.shape[1], model_h, model_w, dtype=scaled.dtype, device=scaled.device)
- padded[:, :, pt:pt + sh, pl:pl + sw] = scaled
- return padded.permute(0, 2, 3, 1), scale, pt, pl
+ scaled = comfy.utils.common_upscale(chw, model_w, model_h, upscale_method=method, crop="disabled")
+ return scaled.permute(0, 2, 3, 1), model_w / w, model_h / h
- def _remap_keypoints(kp, scale, pad_top, pad_left, offset_x=0, offset_y=0):
+ def _remap_keypoints(kp, scale_x, scale_y, offset_x=0, offset_y=0):
"""Remap keypoints from model space back to original image space."""
kp = kp.copy() if isinstance(kp, np.ndarray) else np.array(kp, dtype=np.float32)
invalid = kp[..., 0] < 0
- kp[..., 0] = (kp[..., 0] - pad_left) / scale + offset_x
- kp[..., 1] = (kp[..., 1] - pad_top) / scale + offset_y
+ kp[..., 0] = kp[..., 0] / scale_x + offset_x
+ kp[..., 1] = kp[..., 1] / scale_y + offset_y
kp[invalid] = -1
return kp
@@ -529,18 +527,18 @@ class SDPoseKeypointExtractor(io.ComfyNode):
continue
crop = img[:, y1:y2, x1:x2, :] # (1, crop_h, crop_w, C)
- crop_resized, scale, pad_top, pad_left = _resize_to_model(crop)
+ crop_resized, sx, sy = _resize_to_model(crop)
latent_crop = vae.encode(crop_resized)
kp_batch, sc_batch = _run_on_latent(latent_crop)
- kp = _remap_keypoints(kp_batch[0], scale, pad_top, pad_left, x1, y1)
+ kp = _remap_keypoints(kp_batch[0], sx, sy, x1, y1)
img_keypoints.append(kp)
img_scores.append(sc_batch[0])
else:
- img_resized, scale, pad_top, pad_left = _resize_to_model(img)
+ img_resized, sx, sy = _resize_to_model(img)
latent_img = vae.encode(img_resized)
kp_batch, sc_batch = _run_on_latent(latent_img)
- img_keypoints.append(_remap_keypoints(kp_batch[0], scale, pad_top, pad_left))
+ img_keypoints.append(_remap_keypoints(kp_batch[0], sx, sy))
img_scores.append(sc_batch[0])
all_keypoints.append(img_keypoints)
@@ -549,12 +547,12 @@ class SDPoseKeypointExtractor(io.ComfyNode):
else: # full-image mode, batched
for batch_start in tqdm(range(0, total_images, batch_size), desc="Extracting keypoints"):
- batch_resized, scale, pad_top, pad_left = _resize_to_model(image[batch_start:batch_start + batch_size])
+ batch_resized, sx, sy = _resize_to_model(image[batch_start:batch_start + batch_size])
latent_batch = vae.encode(batch_resized)
kp_batch, sc_batch = _run_on_latent(latent_batch)
for kp, sc in zip(kp_batch, sc_batch):
- all_keypoints.append([_remap_keypoints(kp, scale, pad_top, pad_left)])
+ all_keypoints.append([_remap_keypoints(kp, sx, sy)])
all_scores.append([sc])
pbar.update(len(kp_batch))
@@ -599,7 +597,8 @@ class SDPoseFaceBBoxes(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="SDPoseFaceBBoxes",
- category="image/preprocessors",
+ display_name="SDPose Face Bounding Boxes",
+ category="image/detection",
search_aliases=["face bbox", "face bounding box", "pose", "keypoints"],
inputs=[
io.Custom("POSE_KEYPOINT").Input("keypoints"),
@@ -656,7 +655,8 @@ class CropByBBoxes(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="CropByBBoxes",
- category="image/preprocessors",
+ display_name="Crop By Bounding Boxes",
+ category="image/transform",
search_aliases=["crop", "face crop", "bbox crop", "pose", "bounding box"],
description="Crop and resize regions from the input image batch based on provided bounding boxes.",
inputs=[
@@ -727,13 +727,13 @@ class CropByBBoxes(io.ComfyNode):
scale = min(output_width / crop_w, output_height / crop_h)
scaled_w = int(round(crop_w * scale))
scaled_h = int(round(crop_h * scale))
- scaled = comfy.utils.common_upscale(crop_chw, scaled_w, scaled_h, upscale_method="bilinear", crop="disabled")
+ scaled = comfy.utils.common_upscale(crop_chw, scaled_w, scaled_h, upscale_method="area", crop="disabled")
pad_left = (output_width - scaled_w) // 2
pad_top = (output_height - scaled_h) // 2
resized = torch.zeros(1, num_ch, output_height, output_width, dtype=image.dtype, device=image.device)
resized[:, :, pad_top:pad_top + scaled_h, pad_left:pad_left + scaled_w] = scaled
else: # "stretch"
- resized = comfy.utils.common_upscale(crop_chw, output_width, output_height, upscale_method="bilinear", crop="disabled")
+ resized = comfy.utils.common_upscale(crop_chw, output_width, output_height, upscale_method="area", crop="disabled")
crops.append(resized)
if not crops:
diff --git a/comfy_extras/nodes_stable_cascade.py b/comfy_extras/nodes_stable_cascade.py
index 8c1aebca9..0dc6c9fcd 100644
--- a/comfy_extras/nodes_stable_cascade.py
+++ b/comfy_extras/nodes_stable_cascade.py
@@ -119,7 +119,7 @@ class StableCascade_SuperResolutionControlnet(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="StableCascade_SuperResolutionControlnet",
- category="_for_testing/stable_cascade",
+ category="experimental/stable_cascade",
is_experimental=True,
inputs=[
io.Image.Input("image"),
diff --git a/comfy_extras/nodes_string.py b/comfy_extras/nodes_string.py
index 604076c4e..97485c8c5 100644
--- a/comfy_extras/nodes_string.py
+++ b/comfy_extras/nodes_string.py
@@ -1,18 +1,49 @@
import re
import json
+import string
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io
+class StringFormat(io.ComfyNode):
+ @classmethod
+ def define_schema(cls) -> io.Schema:
+ autogrow = io.Autogrow.TemplateNames(
+ input=io.AnyType.Input("value"),
+ names=list(string.ascii_lowercase),
+ min=0,
+ )
+ return io.Schema(
+ node_id="StringFormat",
+ display_name="Format Text",
+ category="text",
+ search_aliases=["string", "format"],
+ description="Same as Python's string format method. Supports all of Python's format options and features.",
+ inputs=[
+ io.Autogrow.Input("values", template=autogrow),
+ io.String.Input("f_string", default="{a}", multiline=True),
+ ],
+ outputs=[
+ io.String.Output(),
+ ],
+ )
+
+ @classmethod
+ def execute(
+ cls, values: io.Autogrow.Type, f_string: str
+ ) -> io.NodeOutput:
+ return io.NodeOutput(f_string.format(**values))
+
+
class StringConcatenate(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringConcatenate",
- display_name="Text Concatenate",
- category="utils/string",
- search_aliases=["Concatenate", "text concat", "join text", "merge text", "combine strings", "concat", "concatenate", "append text", "combine text", "string"],
+ search_aliases=["concatenate", "text concat", "join text", "merge text", "combine strings", "string concat", "append text", "combine text"],
+ display_name="Concatenate Text",
+ category="text",
inputs=[
io.String.Input("string_a", multiline=True),
io.String.Input("string_b", multiline=True),
@@ -33,9 +64,9 @@ class StringSubstring(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="StringSubstring",
- search_aliases=["Substring", "extract text", "text portion"],
- display_name="Text Substring",
- category="utils/string",
+ search_aliases=["substring", "extract text", "text portion"],
+ display_name="Substring",
+ category="text",
inputs=[
io.String.Input("string", multiline=True),
io.Int.Input("start"),
@@ -58,7 +89,7 @@ class StringLength(io.ComfyNode):
node_id="StringLength",
search_aliases=["character count", "text size", "string length"],
display_name="Text Length",
- category="utils/string",
+ category="text",
inputs=[
io.String.Input("string", multiline=True),
],
@@ -77,9 +108,9 @@ class CaseConverter(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="CaseConverter",
- search_aliases=["Case Converter", "text case", "uppercase", "lowercase", "capitalize"],
- display_name="Text Case Converter",
- category="utils/string",
+ search_aliases=["case converter", "text case", "uppercase", "lowercase", "capitalize"],
+ display_name="Convert Text Case",
+ category="text",
inputs=[
io.String.Input("string", multiline=True),
io.Combo.Input("mode", options=["UPPERCASE", "lowercase", "Capitalize", "Title Case"]),
@@ -110,9 +141,9 @@ class StringTrim(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="StringTrim",
- search_aliases=["Trim", "clean whitespace", "remove whitespace", "strip"],
- display_name="Text Trim",
- category="utils/string",
+ search_aliases=["trim", "clean whitespace", "remove whitespace", "remove spaces","strip"],
+ display_name="Trim Text",
+ category="text",
inputs=[
io.String.Input("string", multiline=True),
io.Combo.Input("mode", options=["Both", "Left", "Right"]),
@@ -141,9 +172,9 @@ class StringReplace(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="StringReplace",
- search_aliases=["Replace", "find and replace", "substitute", "swap text"],
- display_name="Text Replace",
- category="utils/string",
+ search_aliases=["replace", "find and replace", "substitute", "swap text"],
+ display_name="Replace Text",
+ category="text",
inputs=[
io.String.Input("string", multiline=True),
io.String.Input("find", multiline=True),
@@ -164,9 +195,9 @@ class StringContains(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="StringContains",
- search_aliases=["Contains", "text includes", "string includes"],
- display_name="Text Contains",
- category="utils/string",
+ search_aliases=["contains", "text includes", "string includes"],
+ display_name="Contains Text",
+ category="text",
inputs=[
io.String.Input("string", multiline=True),
io.String.Input("substring", multiline=True),
@@ -192,9 +223,9 @@ class StringCompare(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="StringCompare",
- search_aliases=["Compare", "text match", "string equals", "starts with", "ends with"],
- display_name="Text Compare",
- category="utils/string",
+ search_aliases=["compare", "text match", "string equals", "starts with", "ends with"],
+ display_name="Compare Text",
+ category="text",
inputs=[
io.String.Input("string_a", multiline=True),
io.String.Input("string_b", multiline=True),
@@ -228,9 +259,9 @@ class RegexMatch(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="RegexMatch",
- search_aliases=["Regex Match", "regex", "pattern match", "text contains", "string match"],
- display_name="Text Match",
- category="utils/string",
+ search_aliases=["regex match", "regex", "pattern match", "text contains", "string match"],
+ display_name="Match Text",
+ category="text",
inputs=[
io.String.Input("string", multiline=True),
io.String.Input("regex_pattern", multiline=True),
@@ -269,9 +300,9 @@ class RegexExtract(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="RegexExtract",
- search_aliases=["Regex Extract", "regex", "pattern extract", "text parser", "parse text"],
- display_name="Text Extract Substring",
- category="utils/string",
+ search_aliases=["regex extract", "regex", "pattern extract", "text parser", "parse text"],
+ display_name="Extract Text",
+ category="text",
inputs=[
io.String.Input("string", multiline=True),
io.String.Input("regex_pattern", multiline=True),
@@ -344,9 +375,9 @@ class RegexReplace(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="RegexReplace",
- search_aliases=["Regex Replace", "regex", "pattern replace", "regex replace", "substitution"],
- display_name="Text Replace (Regex)",
- category="utils/string",
+ search_aliases=["regex replace", "regex", "pattern replace", "substitution"],
+ display_name="Replace Text (Regex)",
+ category="text",
description="Find and replace text using regex patterns.",
inputs=[
io.String.Input("string", multiline=True),
@@ -381,8 +412,8 @@ class JsonExtractString(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="JsonExtractString",
- display_name="Extract String from JSON",
- category="utils/string",
+ display_name="Extract Text from JSON",
+ category="text",
search_aliases=["json", "extract json", "parse json", "json value", "read json"],
inputs=[
io.String.Input("json_string", multiline=True),
@@ -413,6 +444,7 @@ class StringExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
+ StringFormat,
StringConcatenate,
StringSubstring,
StringLength,
diff --git a/comfy_extras/nodes_textgen.py b/comfy_extras/nodes_textgen.py
index 1f46d820f..d52faf815 100644
--- a/comfy_extras/nodes_textgen.py
+++ b/comfy_extras/nodes_textgen.py
@@ -26,12 +26,15 @@ class TextGenerate(io.ComfyNode):
return io.Schema(
node_id="TextGenerate",
- category="textgen",
+ display_name="Generate Text",
+ category="text",
search_aliases=["LLM", "gemma"],
inputs=[
io.Clip.Input("clip"),
io.String.Input("prompt", multiline=True, dynamic_prompts=True, default=""),
io.Image.Input("image", optional=True),
+ io.Image.Input("video", optional=True, tooltip="Video frames as image batch. Assumed to be 24 FPS; subsampled to 1 FPS internally."),
+ io.Audio.Input("audio", optional=True),
io.Int.Input("max_length", default=256, min=1, max=2048),
io.DynamicCombo.Input("sampling_mode", options=sampling_options, display_name="Sampling Mode"),
io.Boolean.Input("thinking", optional=True, default=False, tooltip="Operate in thinking mode if the model supports it."),
@@ -43,9 +46,9 @@ class TextGenerate(io.ComfyNode):
)
@classmethod
- def execute(cls, clip, prompt, max_length, sampling_mode, image=None, thinking=False, use_default_template=True) -> io.NodeOutput:
+ def execute(cls, clip, prompt, max_length, sampling_mode, image=None, thinking=False, use_default_template=True, video=None, audio=None) -> io.NodeOutput:
- tokens = clip.tokenize(prompt, image=image, skip_template=not use_default_template, min_length=1, thinking=thinking)
+ tokens = clip.tokenize(prompt, image=image, skip_template=not use_default_template, min_length=1, thinking=thinking, video=video, audio=audio)
# Get sampling parameters from dynamic combo
do_sample = sampling_mode.get("sampling_mode") == "on"
@@ -70,7 +73,8 @@ class TextGenerate(io.ComfyNode):
seed=seed
)
- generated_text = clip.decode(generated_ids, skip_special_tokens=True)
+ generated_text = clip.decode(generated_ids)
+
return io.NodeOutput(generated_text)
@@ -154,6 +158,7 @@ class TextGenerateLTX2Prompt(TextGenerate):
parent_schema = super().define_schema()
return io.Schema(
node_id="TextGenerateLTX2Prompt",
+ display_name="Generate LTX2 Prompt",
category=parent_schema.category,
inputs=parent_schema.inputs,
outputs=parent_schema.outputs,
@@ -161,12 +166,12 @@ class TextGenerateLTX2Prompt(TextGenerate):
)
@classmethod
- def execute(cls, clip, prompt, max_length, sampling_mode, image=None, thinking=False, use_default_template=True) -> io.NodeOutput:
+ def execute(cls, clip, prompt, max_length, sampling_mode, image=None, thinking=False, use_default_template=True, video=None, audio=None) -> io.NodeOutput:
if image is None:
formatted_prompt = f"system\n{LTX2_T2V_SYSTEM_PROMPT.strip()}\nuser\nUser Raw Input Prompt: {prompt}.\nmodel\n"
else:
formatted_prompt = f"system\n{LTX2_I2V_SYSTEM_PROMPT.strip()}\nuser\n\n\n\nUser Raw Input Prompt: {prompt}.\nmodel\n"
- return super().execute(clip, formatted_prompt, max_length, sampling_mode, image, thinking, use_default_template)
+ return super().execute(clip, formatted_prompt, max_length, sampling_mode, image=image, thinking=thinking, use_default_template=use_default_template, video=video, audio=audio)
class TextgenExtension(ComfyExtension):
diff --git a/comfy_extras/nodes_torch_compile.py b/comfy_extras/nodes_torch_compile.py
index c9e2e0026..d4506b1a9 100644
--- a/comfy_extras/nodes_torch_compile.py
+++ b/comfy_extras/nodes_torch_compile.py
@@ -10,7 +10,7 @@ class TorchCompileModel(io.ComfyNode):
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="TorchCompileModel",
- category="_for_testing",
+ category="experimental",
inputs=[
io.Model.Input("model"),
io.Combo.Input(
diff --git a/comfy_extras/nodes_train.py b/comfy_extras/nodes_train.py
index 0616dfc2d..e9871369b 100644
--- a/comfy_extras/nodes_train.py
+++ b/comfy_extras/nodes_train.py
@@ -1361,7 +1361,7 @@ class SaveLoRA(io.ComfyNode):
node_id="SaveLoRA",
search_aliases=["export lora"],
display_name="Save LoRA Weights",
- category="loaders",
+ category="advanced/model_merging",
is_experimental=True,
is_output_node=True,
inputs=[
diff --git a/comfy_extras/nodes_video.py b/comfy_extras/nodes_video.py
index 5c096c232..78a2a28f8 100644
--- a/comfy_extras/nodes_video.py
+++ b/comfy_extras/nodes_video.py
@@ -17,7 +17,8 @@ class SaveWEBM(io.ComfyNode):
return io.Schema(
node_id="SaveWEBM",
search_aliases=["export webm"],
- category="image/video",
+ display_name="Save WEBM",
+ category="video",
is_experimental=True,
inputs=[
io.Image.Input("images"),
@@ -72,7 +73,7 @@ class SaveVideo(io.ComfyNode):
node_id="SaveVideo",
search_aliases=["export video"],
display_name="Save Video",
- category="image/video",
+ category="video",
essentials_category="Basics",
description="Saves the input images to your ComfyUI output directory.",
inputs=[
@@ -121,7 +122,8 @@ class CreateVideo(io.ComfyNode):
node_id="CreateVideo",
search_aliases=["images to video"],
display_name="Create Video",
- category="image/video",
+ category="video",
+ essentials_category="Video Tools",
description="Create a video from images.",
inputs=[
io.Image.Input("images", tooltip="The images to create a video from."),
@@ -146,7 +148,7 @@ class GetVideoComponents(io.ComfyNode):
node_id="GetVideoComponents",
search_aliases=["extract frames", "split video", "video to images", "demux"],
display_name="Get Video Components",
- category="image/video",
+ category="video",
description="Extracts all components from a video: frames, audio, and framerate.",
inputs=[
io.Video.Input("video", tooltip="The video to extract components from."),
@@ -174,7 +176,7 @@ class LoadVideo(io.ComfyNode):
node_id="LoadVideo",
search_aliases=["import video", "open video", "video file"],
display_name="Load Video",
- category="image/video",
+ category="video",
essentials_category="Basics",
inputs=[
io.Combo.Input("file", options=sorted(files), upload=io.UploadType.video),
@@ -216,7 +218,7 @@ class VideoSlice(io.ComfyNode):
"frame load cap",
"start time",
],
- category="image/video",
+ category="video",
essentials_category="Video Tools",
inputs=[
io.Video.Input("video"),
diff --git a/comfy_extras/nodes_video_model.py b/comfy_extras/nodes_video_model.py
index a3b148d7d..b0d0390ca 100644
--- a/comfy_extras/nodes_video_model.py
+++ b/comfy_extras/nodes_video_model.py
@@ -15,7 +15,7 @@ class ImageOnlyCheckpointLoader:
RETURN_TYPES = ("MODEL", "CLIP_VISION", "VAE")
FUNCTION = "load_checkpoint"
- CATEGORY = "loaders/video_models"
+ CATEGORY = "loaders"
def load_checkpoint(self, ckpt_name, output_vae=True, output_clip=True):
ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)
@@ -128,7 +128,7 @@ class VideoLinearCFGGuidance:
RETURN_TYPES = ("MODEL",)
FUNCTION = "patch"
- CATEGORY = "sampling/video_models"
+ CATEGORY = "sampling/guiders"
def patch(self, model, min_cfg):
def linear_cfg(args):
@@ -152,7 +152,7 @@ class VideoTriangleCFGGuidance:
RETURN_TYPES = ("MODEL",)
FUNCTION = "patch"
- CATEGORY = "sampling/video_models"
+ CATEGORY = "sampling/guiders"
def patch(self, model, min_cfg):
def linear_cfg(args):
@@ -221,6 +221,8 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
- "ImageOnlyCheckpointLoader": "Image Only Checkpoint Loader (img2vid model)",
+ "ImageOnlyCheckpointLoader": "Load Checkpoint Image Only (img2vid model)",
"ImageOnlyCheckpointLoaderDevice": "Image Only Checkpoint Loader (Device)",
+ "VideoLinearCFGGuidance": "Video Linear CFG Guidance",
+ "VideoTriangleCFGGuidance": "Video Triangle CFG Guidance",
}
diff --git a/comfy_extras/nodes_void.py b/comfy_extras/nodes_void.py
new file mode 100644
index 000000000..be724371a
--- /dev/null
+++ b/comfy_extras/nodes_void.py
@@ -0,0 +1,484 @@
+import logging
+
+import torch
+
+import comfy
+import comfy.model_management
+import comfy.model_patcher
+import comfy.samplers
+import comfy.utils
+import folder_paths
+import node_helpers
+import nodes
+from comfy.utils import model_trange as trange
+from comfy_api.latest import ComfyExtension, io
+from torchvision.models.optical_flow import raft_large
+from typing_extensions import override
+
+
+from comfy_extras.void_noise_warp import RaftOpticalFlow, get_noise_from_video
+
+OpticalFlow = io.Custom("OPTICAL_FLOW")
+
+TEMPORAL_COMPRESSION = 4
+PATCH_SIZE_T = 2
+
+
+def _valid_void_length(length: int) -> int:
+ """Round ``length`` down to a value that produces an even latent_t.
+
+ VOID / CogVideoX-Fun-V1.5 uses patch_size_t=2, so the VAE-encoded latent
+ must have an even temporal dimension. If latent_t is odd, the transformer
+ pad_to_patch_size circular-wraps an extra latent frame onto the end; after
+ the post-transformer crop the last real latent frame has been influenced
+ by the wrapped phantom frame, producing visible jitter and "disappearing"
+ subjects near the end of the decoded video. Rounding down fixes this.
+ """
+ latent_t = ((length - 1) // TEMPORAL_COMPRESSION) + 1
+ if latent_t % PATCH_SIZE_T == 0:
+ return length
+ # Round latent_t down to the nearest multiple of PATCH_SIZE_T, then invert
+ # the ((length - 1) // TEMPORAL_COMPRESSION) + 1 formula. Floor at 1 frame
+ # so we never return a non-positive length.
+ target_latent_t = max(PATCH_SIZE_T, (latent_t // PATCH_SIZE_T) * PATCH_SIZE_T)
+ return (target_latent_t - 1) * TEMPORAL_COMPRESSION + 1
+
+
+class OpticalFlowLoader(io.ComfyNode):
+ """Load an optical flow model from ``models/optical_flow/``.
+
+ Only torchvision's RAFT-large format is recognized today (the model used
+ by VOIDWarpedNoise). The checkpoint must be placed under
+ ``models/optical_flow/`` — ComfyUI never downloads optical-flow weights
+ at runtime.
+ """
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="OpticalFlowLoader",
+ display_name="Load Optical Flow Model",
+ category="loaders",
+ inputs=[
+ io.Combo.Input(
+ "model_name",
+ options=folder_paths.get_filename_list("optical_flow"),
+ tooltip=(
+ "Optical flow model to load. Files must be placed in the "
+ "'optical_flow' folder. Today only torchvision's "
+ "raft_large.pth is supported."
+ ),
+ ),
+ ],
+ outputs=[
+ OpticalFlow.Output(),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, model_name) -> io.NodeOutput:
+
+ model_path = folder_paths.get_full_path_or_raise("optical_flow", model_name)
+ sd = comfy.utils.load_torch_file(model_path, safe_load=True)
+
+ has_raft_keys = (
+ any(k.startswith("feature_encoder.") for k in sd)
+ and any(k.startswith("context_encoder.") for k in sd)
+ and any(k.startswith("update_block.") for k in sd)
+ )
+ if not has_raft_keys:
+ raise ValueError(
+ "Unrecognized optical flow model format: expected a torchvision "
+ "RAFT-large state dict with 'feature_encoder.', 'context_encoder.' "
+ "and 'update_block.' prefixes."
+ )
+
+ model = raft_large(weights=None, progress=False)
+ model.load_state_dict(sd)
+ model.eval().to(torch.float32)
+
+ patcher = comfy.model_patcher.ModelPatcher(
+ model,
+ load_device=comfy.model_management.get_torch_device(),
+ offload_device=comfy.model_management.unet_offload_device(),
+ )
+ return io.NodeOutput(patcher)
+
+
+class VOIDQuadmaskPreprocess(io.ComfyNode):
+ """Preprocess a quadmask video for VOID inpainting.
+
+ Quantizes mask values to four semantic levels, inverts, and normalizes:
+ 0 -> primary object to remove
+ 63 -> overlap of primary + affected
+ 127 -> affected region (interactions)
+ 255 -> background (keep)
+
+ After inversion and normalization, the output mask has values in [0, 1]
+ with four discrete levels: 1.0 (remove), ~0.75, ~0.50, 0.0 (keep).
+ """
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="VOIDQuadmaskPreprocess",
+ display_name="VOID Quadmask Preprocessor",
+ category="image/mask",
+ inputs=[
+ io.Mask.Input("mask"),
+ io.Int.Input("dilate_width", default=0, min=0, max=50, step=1,
+ tooltip="Dilation radius for the primary mask region (0 = no dilation)"),
+ ],
+ outputs=[
+ io.Mask.Output(display_name="quadmask"),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, mask, dilate_width=0) -> io.NodeOutput:
+ m = mask.clone()
+
+ if m.max() <= 1.0:
+ m = m * 255.0
+
+ if dilate_width > 0 and m.ndim >= 3:
+ binary = (m < 128).float()
+ kernel_size = dilate_width * 2 + 1
+ if binary.ndim == 3:
+ binary = binary.unsqueeze(1)
+ dilated = torch.nn.functional.max_pool2d(
+ binary, kernel_size=kernel_size, stride=1, padding=dilate_width
+ )
+ if dilated.ndim == 4:
+ dilated = dilated.squeeze(1)
+ m = torch.where(dilated > 0.5, torch.zeros_like(m), m)
+
+ m = torch.where(m <= 31, torch.zeros_like(m), m)
+ m = torch.where((m > 31) & (m <= 95), torch.full_like(m, 63), m)
+ m = torch.where((m > 95) & (m <= 191), torch.full_like(m, 127), m)
+ m = torch.where(m > 191, torch.full_like(m, 255), m)
+
+ m = (255.0 - m) / 255.0
+
+ return io.NodeOutput(m)
+
+
+class VOIDInpaintConditioning(io.ComfyNode):
+ """Build VOID inpainting conditioning for CogVideoX.
+
+ Encodes the processed quadmask and masked source video through the VAE,
+ producing a 32-channel concat conditioning (16ch mask + 16ch masked video)
+ that gets concatenated with the 16ch noise latent by the model.
+ """
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="VOIDInpaintConditioning",
+ category="conditioning/video_models",
+ inputs=[
+ io.Conditioning.Input("positive"),
+ io.Conditioning.Input("negative"),
+ io.Vae.Input("vae"),
+ io.Image.Input("video", tooltip="Source video frames [T, H, W, 3]"),
+ io.Mask.Input("quadmask", tooltip="Preprocessed quadmask from VOIDQuadmaskPreprocess [T, H, W]"),
+ io.Int.Input("width", default=672, min=16, max=nodes.MAX_RESOLUTION, step=8),
+ io.Int.Input("height", default=384, min=16, max=nodes.MAX_RESOLUTION, step=8),
+ io.Int.Input("length", default=45, min=1, max=nodes.MAX_RESOLUTION, step=1,
+ tooltip="Number of pixel frames to process. For CogVideoX-Fun-V1.5 "
+ "(patch_size_t=2), latent_t must be even — lengths that "
+ "produce odd latent_t are rounded down (e.g. 49 → 45)."),
+ io.Int.Input("batch_size", default=1, min=1, max=64),
+ ],
+ outputs=[
+ io.Conditioning.Output(display_name="positive"),
+ io.Conditioning.Output(display_name="negative"),
+ io.Latent.Output(display_name="latent"),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, positive, negative, vae, video, quadmask,
+ width, height, length, batch_size) -> io.NodeOutput:
+
+ adjusted_length = _valid_void_length(length)
+ if adjusted_length != length:
+ logging.warning(
+ "VOIDInpaintConditioning: rounding length %d down to %d so that "
+ "latent_t is even (required by CogVideoX-Fun-V1.5 patch_size_t=2). "
+ "Using odd latent_t causes the last frame to be corrupted by "
+ "circular padding.", length, adjusted_length,
+ )
+ length = adjusted_length
+
+ latent_t = ((length - 1) // TEMPORAL_COMPRESSION) + 1
+ latent_h = height // 8
+ latent_w = width // 8
+
+ vid = video[:length]
+ vid = comfy.utils.common_upscale(
+ vid.movedim(-1, 1), width, height, "bilinear", "center"
+ ).movedim(1, -1)
+
+ qm = quadmask[:length]
+ if qm.ndim == 3:
+ qm = qm.unsqueeze(-1)
+ qm = comfy.utils.common_upscale(
+ qm.movedim(-1, 1), width, height, "bilinear", "center"
+ ).movedim(1, -1)
+ if qm.ndim == 4 and qm.shape[-1] == 1:
+ qm = qm.squeeze(-1)
+
+ mask_condition = qm
+ if mask_condition.ndim == 3:
+ mask_condition_3ch = mask_condition.unsqueeze(-1).expand(-1, -1, -1, 3)
+ else:
+ mask_condition_3ch = mask_condition
+
+ inverted_mask_3ch = 1.0 - mask_condition_3ch
+ masked_video = vid[:, :, :, :3] * (1.0 - mask_condition_3ch)
+
+ mask_latents = vae.encode(inverted_mask_3ch)
+ masked_video_latents = vae.encode(masked_video)
+
+ def _match_temporal(lat, target_t):
+ if lat.shape[2] > target_t:
+ return lat[:, :, :target_t]
+ elif lat.shape[2] < target_t:
+ pad = target_t - lat.shape[2]
+ return torch.cat([lat, lat[:, :, -1:].repeat(1, 1, pad, 1, 1)], dim=2)
+ return lat
+
+ mask_latents = _match_temporal(mask_latents, latent_t)
+ masked_video_latents = _match_temporal(masked_video_latents, latent_t)
+
+ inpaint_latents = torch.cat([mask_latents, masked_video_latents], dim=1)
+
+ # No explicit scaling needed here: the model's CogVideoX.concat_cond()
+ # applies process_latent_in (×latent_format.scale_factor) to each 16-ch
+ # block of the stored conditioning. For 5b-class checkpoints (incl. the
+ # VOID/CogVideoX-Fun-V1.5 inpainting model) that scale_factor is auto-
+ # selected as 0.7 in supported_models.CogVideoX_T2V, which matches the
+ # diffusers vae/config.json scaling_factor VOID was trained with.
+
+ positive = node_helpers.conditioning_set_values(
+ positive, {"concat_latent_image": inpaint_latents}
+ )
+ negative = node_helpers.conditioning_set_values(
+ negative, {"concat_latent_image": inpaint_latents}
+ )
+
+ noise_latent = torch.zeros(
+ [batch_size, 16, latent_t, latent_h, latent_w],
+ device=comfy.model_management.intermediate_device()
+ )
+
+ return io.NodeOutput(positive, negative, {"samples": noise_latent})
+
+
+class VOIDWarpedNoise(io.ComfyNode):
+ """Generate optical-flow warped noise for VOID Pass 2 refinement.
+
+ Takes the Pass 1 output video and produces temporally-correlated noise
+ by warping Gaussian noise along optical flow vectors. This noise is used
+ as the initial latent for Pass 2, resulting in better temporal consistency.
+ """
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="VOIDWarpedNoise",
+ category="latent/video",
+ inputs=[
+ OpticalFlow.Input(
+ "optical_flow",
+ tooltip="Optical flow model from OpticalFlowLoader (RAFT-large).",
+ ),
+ io.Image.Input("video", tooltip="Pass 1 output video frames [T, H, W, 3]"),
+ io.Int.Input("width", default=672, min=16, max=nodes.MAX_RESOLUTION, step=8),
+ io.Int.Input("height", default=384, min=16, max=nodes.MAX_RESOLUTION, step=8),
+ io.Int.Input("length", default=45, min=1, max=nodes.MAX_RESOLUTION, step=1,
+ tooltip="Number of pixel frames. Rounded down to make latent_t "
+ "even (patch_size_t=2 requirement), e.g. 49 → 45."),
+ io.Int.Input("batch_size", default=1, min=1, max=64),
+ ],
+ outputs=[
+ io.Latent.Output(display_name="warped_noise"),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, optical_flow, video, width, height, length, batch_size) -> io.NodeOutput:
+
+ adjusted_length = _valid_void_length(length)
+ if adjusted_length != length:
+ logging.warning(
+ "VOIDWarpedNoise: rounding length %d down to %d so that "
+ "latent_t is even (required by CogVideoX-Fun-V1.5 patch_size_t=2).",
+ length, adjusted_length,
+ )
+ length = adjusted_length
+
+ latent_t = ((length - 1) // TEMPORAL_COMPRESSION) + 1
+ latent_h = height // 8
+ latent_w = width // 8
+
+ # RAFT + noise warp is real compute, not an "intermediate" buffer, so
+ # we want the actual torch device (CUDA/MPS). The final latent is
+ # moved back to intermediate_device() before returning to match the
+ # rest of the ComfyUI pipeline.
+ device = comfy.model_management.get_torch_device()
+
+ comfy.model_management.load_model_gpu(optical_flow)
+ raft = RaftOpticalFlow(optical_flow.model, device=device)
+
+ vid = video[:length].to(device)
+ vid = comfy.utils.common_upscale(
+ vid.movedim(-1, 1), width, height, "bilinear", "center"
+ ).movedim(1, -1)
+ vid_uint8 = (vid.clamp(0, 1) * 255).to(torch.uint8)
+
+ FRAME = 2**-1
+ FLOW = 2**3
+ LATENT_SCALE = 8
+
+ warped = get_noise_from_video(
+ vid_uint8,
+ raft,
+ noise_channels=16,
+ resize_frames=FRAME,
+ resize_flow=FLOW,
+ downscale_factor=round(FRAME * FLOW) * LATENT_SCALE,
+ device=device,
+ )
+
+ if warped.shape[0] != latent_t:
+ indices = torch.linspace(0, warped.shape[0] - 1, latent_t,
+ device=device).long()
+ warped = warped[indices]
+
+ if warped.shape[1] != latent_h or warped.shape[2] != latent_w:
+ # (T, H, W, C) → (T, C, H, W) → bilinear resize → back
+ warped = warped.permute(0, 3, 1, 2)
+ warped = torch.nn.functional.interpolate(
+ warped, size=(latent_h, latent_w),
+ mode="bilinear", align_corners=False,
+ )
+ warped = warped.permute(0, 2, 3, 1)
+
+ # (T, H, W, C) → (B, C, T, H, W)
+ warped_tensor = warped.permute(3, 0, 1, 2).unsqueeze(0)
+ if batch_size > 1:
+ warped_tensor = warped_tensor.repeat(batch_size, 1, 1, 1, 1)
+
+ warped_tensor = warped_tensor.to(comfy.model_management.intermediate_device())
+ return io.NodeOutput({"samples": warped_tensor})
+
+
+class Noise_FromLatent:
+ """Wraps a pre-computed LATENT tensor as a NOISE source."""
+ def __init__(self, latent_dict):
+ self.seed = 0
+ self._samples = latent_dict["samples"]
+
+ def generate_noise(self, input_latent):
+ return self._samples.clone().cpu()
+
+
+class VOIDWarpedNoiseSource(io.ComfyNode):
+ """Convert a LATENT (e.g. from VOIDWarpedNoise) into a NOISE source
+ for use with SamplerCustomAdvanced."""
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="VOIDWarpedNoiseSource",
+ category="sampling/noise",
+ inputs=[
+ io.Latent.Input("warped_noise",
+ tooltip="Warped noise latent from VOIDWarpedNoise"),
+ ],
+ outputs=[io.Noise.Output()],
+ )
+
+ @classmethod
+ def execute(cls, warped_noise) -> io.NodeOutput:
+ return io.NodeOutput(Noise_FromLatent(warped_noise))
+
+
+class VOID_DDIM(comfy.samplers.Sampler):
+ """DDIM sampler for VOID inpainting models.
+
+ VOID was trained with the diffusers CogVideoXDDIMScheduler which operates in
+ alpha-space (input std ≈ 1). The standard KSampler applies noise_scaling that
+ multiplies by sqrt(1+sigma^2) ≈ 4500x, which is incompatible with VOID's
+ training. This sampler skips noise_scaling and implements the DDIM update rule
+ directly using sigma-to-alpha conversion.
+ """
+
+ def sample(self, model_wrap, sigmas, extra_args, callback, noise, latent_image=None, denoise_mask=None, disable_pbar=False):
+ x = noise.to(torch.float32)
+ model_options = extra_args.get("model_options", {})
+ seed = extra_args.get("seed", None)
+ s_in = x.new_ones([x.shape[0]])
+
+ for i in trange(len(sigmas) - 1, disable=disable_pbar):
+ sigma = sigmas[i]
+ sigma_next = sigmas[i + 1]
+
+ denoised = model_wrap(x, sigma * s_in, model_options=model_options, seed=seed)
+
+ if callback is not None:
+ callback(i, denoised, x, len(sigmas) - 1)
+
+ if sigma_next == 0:
+ x = denoised
+ else:
+ alpha_t = 1.0 / (1.0 + sigma ** 2)
+ alpha_prev = 1.0 / (1.0 + sigma_next ** 2)
+
+ pred_eps = (x - (alpha_t ** 0.5) * denoised) / (1.0 - alpha_t) ** 0.5
+ x = (alpha_prev ** 0.5) * denoised + (1.0 - alpha_prev) ** 0.5 * pred_eps
+
+ return x
+
+
+class VOIDSampler(io.ComfyNode):
+ """VOID DDIM sampler for use with SamplerCustom / SamplerCustomAdvanced.
+
+ Required for VOID inpainting models. Implements the same DDIM loop that VOID
+ was trained with (diffusers CogVideoXDDIMScheduler), without the noise_scaling
+ that the standard KSampler applies. Use with RandomNoise or VOIDWarpedNoiseSource.
+ """
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="VOIDSampler",
+ category="sampling/samplers",
+ inputs=[],
+ outputs=[io.Sampler.Output()],
+ )
+
+ @classmethod
+ def execute(cls) -> io.NodeOutput:
+ return io.NodeOutput(VOID_DDIM())
+
+ get_sampler = execute
+
+
+class VOIDExtension(ComfyExtension):
+ @override
+ async def get_node_list(self) -> list[type[io.ComfyNode]]:
+ return [
+ OpticalFlowLoader,
+ VOIDQuadmaskPreprocess,
+ VOIDInpaintConditioning,
+ VOIDWarpedNoise,
+ VOIDWarpedNoiseSource,
+ VOIDSampler,
+ ]
+
+
+async def comfy_entrypoint() -> VOIDExtension:
+ return VOIDExtension()
diff --git a/comfy_extras/nodes_wandancer.py b/comfy_extras/nodes_wandancer.py
new file mode 100644
index 000000000..fc005ed4c
--- /dev/null
+++ b/comfy_extras/nodes_wandancer.py
@@ -0,0 +1,971 @@
+import math
+import nodes
+import node_helpers
+import torch
+import torchaudio
+import comfy.model_management
+import comfy.utils
+import numpy as np
+import logging
+from typing_extensions import override
+from comfy_api.latest import ComfyExtension, io
+
+import scipy.signal
+import scipy.ndimage
+import scipy.fft
+import scipy.sparse
+
+# Audio Processing Functions - Derived from librosa (https://github.com/librosa/librosa)
+# Copyright (c) 2013--2023, librosa development team.
+
+def mel_to_hz(mels, htk=False):
+ """Convert mel to Hz (slaney)"""
+ mels = np.asanyarray(mels)
+ if htk:
+ return 700.0 * (10.0 ** (mels / 2595.0) - 1.0)
+ f_min = 0.0
+ f_sp = 200.0 / 3
+ freqs = f_min + f_sp * mels
+ min_log_hz = 1000.0
+ min_log_mel = (min_log_hz - f_min) / f_sp
+ logstep = np.log(6.4) / 27.0
+ if mels.ndim:
+ log_t = mels >= min_log_mel
+ freqs[log_t] = min_log_hz * np.exp(logstep * (mels[log_t] - min_log_mel))
+ elif mels >= min_log_mel:
+ freqs = min_log_hz * np.exp(logstep * (mels - min_log_mel))
+ return freqs
+
+def hz_to_mel(frequencies, htk=False):
+ """Convert Hz to mel (slaney)"""
+ frequencies = np.asanyarray(frequencies)
+ if htk:
+ return 2595.0 * np.log10(1.0 + frequencies / 700.0)
+ f_min = 0.0
+ f_sp = 200.0 / 3
+ mels = (frequencies - f_min) / f_sp
+ min_log_hz = 1000.0
+ min_log_mel = (min_log_hz - f_min) / f_sp
+ logstep = np.log(6.4) / 27.0
+ if frequencies.ndim:
+ log_t = frequencies >= min_log_hz
+ mels[log_t] = min_log_mel + np.log(frequencies[log_t] / min_log_hz) / logstep
+ elif frequencies >= min_log_hz:
+ mels = min_log_mel + np.log(frequencies / min_log_hz) / logstep
+ return mels
+
+def compute_cqt(y, sr=22050, hop_length=512, fmin=None, n_bins=84, bins_per_octave=12, tuning=0.0):
+ """Compute Constant-Q Transform (CQT) spectrogram."""
+
+ def _relative_bandwidth(freqs):
+ bpo = np.empty_like(freqs)
+ logf = np.log2(freqs)
+ bpo[0] = 1.0 / (logf[1] - logf[0])
+ bpo[-1] = 1.0 / (logf[-1] - logf[-2])
+ bpo[1:-1] = 2.0 / (logf[2:] - logf[:-2])
+ return (2.0 ** (2.0 / bpo) - 1.0) / (2.0 ** (2.0 / bpo) + 1.0)
+
+ def _wavelet_lengths(freqs, sr, filter_scale, alpha):
+ Q = float(filter_scale) / alpha
+ return Q * sr / freqs # shape (n_bins,) floats
+
+ def _build_wavelet(freqs_oct, sr, filter_scale, alpha_oct):
+ lengths = _wavelet_lengths(freqs_oct, sr, filter_scale, alpha_oct)
+ filters = []
+ for ilen, freq in zip(lengths, freqs_oct):
+ t = np.arange(int(-ilen // 2), int(ilen // 2), dtype=float)
+ sig = (np.cos(t * 2 * np.pi * freq / sr)
+ + 1j * np.sin(t * 2 * np.pi * freq / sr)).astype(np.complex64)
+ sig *= scipy.signal.get_window('hann', len(sig), fftbins=True)
+ l1 = np.sum(np.abs(sig))
+ tiny = np.finfo(np.float32).tiny
+ sig /= max(l1, tiny)
+ filters.append(sig)
+ max_len = max(lengths)
+ n_fft = int(2.0 ** np.ceil(np.log2(max_len)))
+ out = np.zeros((len(filters), n_fft), dtype=np.complex64)
+ for k, f in enumerate(filters):
+ lpad = int((n_fft - len(f)) // 2)
+ out[k, lpad: lpad + len(f)] = f
+ return out, lengths
+
+ def _resample_half(y):
+ ratio = 0.5
+ n_samples = int(np.ceil(len(y) * ratio))
+ # Kaiser-windowed FIR matches librosa/soxr more closely than scipy's default Hamming filter
+ L = 2
+ h = scipy.signal.firwin(160 * L + 1, 0.96 / L, window=('kaiser', 6.5))
+ y_hat = scipy.signal.resample_poly(y.astype(np.float32), 1, 2, window=h)
+ if len(y_hat) > n_samples:
+ y_hat = y_hat[:n_samples]
+ elif len(y_hat) < n_samples:
+ y_hat = np.pad(y_hat, (0, n_samples - len(y_hat)))
+ y_hat /= np.sqrt(ratio)
+ return y_hat.astype(np.float32)
+
+ def _sparsify_rows(x, quantile=0.01):
+ mags = np.abs(x)
+ norms = np.sum(mags, axis=1, keepdims=True)
+ norms = np.where(norms == 0, 1.0, norms)
+ mag_sort = np.sort(mags, axis=1)
+ cumulative_mag = np.cumsum(mag_sort / norms, axis=1)
+ threshold_idx = np.argmin(cumulative_mag < quantile, axis=1)
+ x_sparse = scipy.sparse.lil_matrix(x.shape, dtype=x.dtype)
+ for i, j in enumerate(threshold_idx):
+ idx = np.where(mags[i] >= mag_sort[i, j])
+ x_sparse[i, idx] = x[i, idx]
+ return x_sparse.tocsr()
+
+ if fmin is None:
+ fmin = 32.70319566257483 # C1 note frequency
+
+ fmin = fmin * (2.0 ** (tuning / bins_per_octave))
+ freqs = fmin * (2.0 ** (np.arange(n_bins) / bins_per_octave))
+
+ alpha = _relative_bandwidth(freqs)
+ lengths = _wavelet_lengths(freqs, float(sr), 1, alpha)
+
+ n_octaves = int(np.ceil(float(n_bins) / bins_per_octave))
+ n_filters = min(bins_per_octave, n_bins)
+
+ cqt_resp = []
+ my_y = y.astype(np.float32)
+ my_sr = float(sr)
+ my_hop = int(hop_length)
+
+ for i in range(n_octaves):
+ if i == 0:
+ sl = slice(-n_filters, None)
+ else:
+ sl = slice(-n_filters * (i + 1), -n_filters * i)
+
+ freqs_oct = freqs[sl]
+ alpha_oct = alpha[sl]
+
+ basis, basis_lengths = _build_wavelet(freqs_oct, my_sr, 1, alpha_oct)
+ n_fft_oct = basis.shape[1]
+
+ # Frequency-domain normalisation
+ basis = basis.astype(np.complex64)
+ basis *= basis_lengths[:, np.newaxis] / float(n_fft_oct)
+ fft_basis = scipy.fft.fft(basis, n=n_fft_oct, axis=1)[:, :(n_fft_oct // 2) + 1]
+ fft_basis = _sparsify_rows(fft_basis, quantile=0.01)
+ fft_basis = fft_basis * np.sqrt(sr / my_sr)
+
+ y_pad = np.pad(my_y, int(n_fft_oct // 2), mode='constant')
+ n_frames = 1 + (len(y_pad) - n_fft_oct) // my_hop
+ frames = np.lib.stride_tricks.as_strided(
+ y_pad,
+ shape=(n_fft_oct, n_frames),
+ strides=(y_pad.strides[0], y_pad.strides[0] * my_hop),
+ )
+ stft_result = scipy.fft.rfft(frames, axis=0)
+ cqt_resp.append(fft_basis.dot(stft_result))
+
+ if my_hop % 2 == 0:
+ my_hop //= 2
+ my_sr /= 2.0
+ my_y = _resample_half(my_y)
+
+ max_col = min(c.shape[-1] for c in cqt_resp)
+ cqt_out = np.empty((n_bins, max_col), dtype=np.complex64)
+ end = n_bins
+ for c_i in cqt_resp:
+ n_oct = c_i.shape[0]
+ if end < n_oct:
+ cqt_out[:end, :] = c_i[-end:, :max_col]
+ else:
+ cqt_out[end - n_oct:end, :] = c_i[:, :max_col]
+ end -= n_oct
+
+ cqt_out /= np.sqrt(lengths)[:, np.newaxis]
+ return np.abs(cqt_out).astype(np.float32)
+
+
+def cq_to_chroma_mapping(n_input, bins_per_octave=12, n_chroma=12, fmin=None):
+ """Map CQT bins to chroma bins."""
+
+ if fmin is None:
+ fmin = 32.70319566257483 # C1 note frequency
+
+ n_merge = bins_per_octave / n_chroma
+ cq_to_ch = np.repeat(np.eye(n_chroma), int(n_merge), axis=1)
+ cq_to_ch = np.roll(cq_to_ch, -int(n_merge // 2), axis=1)
+ n_octaves = int(np.ceil(n_input / bins_per_octave))
+ cq_to_ch = np.tile(cq_to_ch, n_octaves)[:, :n_input]
+
+ midi_0 = np.mod(12 * np.log2(fmin / 440.0) + 69, 12)
+ roll = int(np.round(midi_0 * (n_chroma / 12.0)))
+ cq_to_ch = np.roll(cq_to_ch, roll, axis=0)
+
+ return cq_to_ch.astype(np.float32)
+
+
+def _parabolic_interpolation(S, axis=-2):
+ """Compute parabolic interpolation shift for peak refinement."""
+ S_next = np.roll(S, -1, axis=axis)
+ S_prev = np.roll(S, 1, axis=axis)
+
+ a = S_next + S_prev - 2 * S
+ b = (S_next - S_prev) / 2.0
+
+ shifts = np.zeros_like(S)
+ valid = np.abs(b) < np.abs(a)
+ shifts[valid] = -b[valid] / a[valid]
+
+ if axis == -2 or axis == S.ndim - 2:
+ shifts[0, :] = 0
+ shifts[-1, :] = 0
+ elif axis == 0:
+ shifts[0, ...] = 0
+ shifts[-1, ...] = 0
+
+ return shifts
+
+
+def _localmax(S, axis=-2):
+ """Find local maxima along an axis."""
+
+ S_prev = np.roll(S, 1, axis=axis)
+ S_next = np.roll(S, -1, axis=axis)
+
+ local_max = (S > S_prev) & (S >= S_next)
+
+ if axis == -2 or axis == S.ndim - 2:
+ local_max[-1, :] = S[-1, :] > S[-2, :]
+ # First element is never a local max (strict inequality with previous)
+ local_max[0, :] = False
+ elif axis == 0:
+ local_max[-1, ...] = S[-1, ...] > S[-2, ...]
+ local_max[0, ...] = False
+
+ return local_max
+
+
+def piptrack(y=None, sr=22050, S=None, n_fft=2048, hop_length=512,
+ fmin=150.0, fmax=4000.0, threshold=0.1):
+ """Pitch tracking on thresholded parabolically-interpolated STFT."""
+
+ # Compute STFT if not provided
+ if S is None:
+ if y is None:
+ raise ValueError("Either y or S must be provided")
+
+ fft_window = scipy.signal.get_window('hann', n_fft, fftbins=True)
+ if len(fft_window) < n_fft:
+ lpad = int((n_fft - len(fft_window)) // 2)
+ fft_window = np.pad(fft_window, (lpad, int(n_fft - len(fft_window) - lpad)), mode='constant')
+ fft_window = fft_window.reshape((-1, 1))
+
+ y_pad = np.pad(y, int(n_fft // 2), mode='constant')
+ n_frames = 1 + (len(y_pad) - n_fft) // hop_length
+ frames = np.lib.stride_tricks.as_strided(
+ y_pad,
+ shape=(n_fft, n_frames),
+ strides=(y_pad.strides[0], y_pad.strides[0] * hop_length)
+ )
+
+ S = scipy.fft.rfft((fft_window * frames).astype(np.float32), axis=0)
+
+ S = np.abs(S)
+
+ fmin = max(fmin, 0)
+ fmax = min(fmax, float(sr) / 2)
+
+ fft_freqs = np.fft.rfftfreq(S.shape[0] * 2 - 2, 1.0 / sr)
+ if len(fft_freqs) > S.shape[0]:
+ fft_freqs = fft_freqs[:S.shape[0]]
+
+ shift = _parabolic_interpolation(S, axis=0)
+ avg = np.gradient(S, axis=0)
+ dskew = 0.5 * avg * shift
+
+ pitches = np.zeros_like(S)
+ mags = np.zeros_like(S)
+
+ freq_mask = (fmin <= fft_freqs) & (fft_freqs < fmax)
+ freq_mask = freq_mask.reshape(-1, 1)
+
+ ref_value = threshold * np.max(S, axis=0, keepdims=True)
+ local_max = _localmax(S * (S > ref_value), axis=0)
+ idx = np.nonzero(freq_mask & local_max)
+
+ pitches[idx] = (idx[0] + shift[idx]) * float(sr) / (S.shape[0] * 2 - 2)
+ mags[idx] = S[idx] + dskew[idx]
+
+ return pitches, mags
+
+
+def hz_to_octs(frequencies, tuning=0.0, bins_per_octave=12):
+ """Convert frequencies (Hz) to octave numbers."""
+
+ A440 = 440.0 * 2.0 ** (tuning / bins_per_octave)
+ octs = np.log2(np.asanyarray(frequencies) / (float(A440) / 16))
+ return octs
+
+
+def pitch_tuning(frequencies, resolution=0.01, bins_per_octave=12):
+ """Estimate tuning offset from a collection of pitches."""
+
+ frequencies = np.atleast_1d(frequencies)
+ frequencies = frequencies[frequencies > 0]
+
+ if not np.any(frequencies):
+ return 0.0
+
+ residual = np.mod(bins_per_octave * hz_to_octs(frequencies, tuning=0.0,
+ bins_per_octave=bins_per_octave), 1.0)
+ residual[residual >= 0.5] -= 1.0
+
+ bins = np.linspace(-0.5, 0.5, int(np.ceil(1.0 / resolution)) + 1)
+ counts, tuning = np.histogram(residual, bins)
+ tuning_est = tuning[np.argmax(counts)]
+ return tuning_est
+
+
+def estimate_tuning(y, sr=22050, bins_per_octave=12):
+ """Estimate global tuning deviation from 12-TET."""
+ n_fft = 2048
+ hop_length = 512
+
+ if len(y) < n_fft:
+ return 0.0
+
+ pitch, mag = piptrack(y=y, sr=sr, n_fft=n_fft, hop_length=hop_length,
+ fmin=150.0, fmax=4000.0, threshold=0.1)
+
+ pitch_mask = pitch > 0
+
+ if not pitch_mask.any():
+ return 0.0
+
+ threshold = np.median(mag[pitch_mask])
+ valid_pitches = pitch[(mag >= threshold) & pitch_mask]
+
+ if len(valid_pitches) == 0:
+ return 0.0
+
+ tuning = pitch_tuning(valid_pitches, resolution=0.01, bins_per_octave=bins_per_octave)
+
+ return float(tuning)
+
+
+def compute_chroma_cens(y, sr=22050, hop_length=512, n_chroma=12,
+ n_octaves=7, bins_per_octave=36,
+ win_len_smooth=41, norm=2):
+ """Compute Chroma Energy Normalized Statistics (CENS) features."""
+
+ tuning = estimate_tuning(y, sr, bins_per_octave=bins_per_octave)
+
+ fmin = 32.70319566257483 # C1 note frequency
+ n_bins = n_octaves * bins_per_octave
+ cqt_mag = compute_cqt(y, sr=sr, hop_length=hop_length,
+ fmin=fmin, n_bins=n_bins,
+ bins_per_octave=bins_per_octave,
+ tuning=tuning)
+
+ chroma_map = cq_to_chroma_mapping(n_bins, bins_per_octave=bins_per_octave,
+ n_chroma=n_chroma, fmin=fmin)
+ chroma = np.dot(chroma_map, cqt_mag)
+
+ threshold = np.finfo(chroma.dtype).tiny
+ chroma_sum = np.sum(np.abs(chroma), axis=0, keepdims=True)
+ chroma_sum = np.maximum(chroma_sum, threshold)
+ chroma = chroma / chroma_sum
+
+ quant_steps = [0.4, 0.2, 0.1, 0.05]
+ quant_weights = [0.25, 0.25, 0.25, 0.25]
+ chroma_quant = np.zeros_like(chroma)
+ for step, weight in zip(quant_steps, quant_weights):
+ chroma_quant += (chroma > step) * weight
+
+ if win_len_smooth is not None and win_len_smooth > 0:
+ win = scipy.signal.get_window('hann', win_len_smooth + 2, fftbins=False)
+ win /= np.sum(win)
+ win = win.reshape(1, -1)
+ chroma_smooth = scipy.ndimage.convolve(chroma_quant, win, mode='constant')
+ else:
+ chroma_smooth = chroma_quant
+
+ if norm == 2:
+ threshold = np.finfo(chroma_smooth.dtype).tiny
+ chroma_norm = np.sqrt(np.sum(chroma_smooth ** 2, axis=0, keepdims=True))
+ chroma_norm = np.maximum(chroma_norm, threshold)
+ chroma_smooth = chroma_smooth / chroma_norm
+ elif norm == np.inf:
+ threshold = np.finfo(chroma_smooth.dtype).tiny
+ chroma_norm = np.max(np.abs(chroma_smooth), axis=0, keepdims=True)
+ chroma_norm = np.maximum(chroma_norm, threshold)
+ chroma_smooth = chroma_smooth / chroma_norm
+
+ return chroma_smooth
+
+
+def _create_mel_filterbank(sr, n_fft, n_mels=128, fmin=0.0, fmax=None):
+ """Create mel-scale filterbank matrix."""
+ if fmax is None:
+ fmax = sr / 2.0
+ mel_basis = np.zeros((n_mels, int(1 + n_fft // 2)), dtype=np.float32)
+ fftfreqs = np.fft.rfftfreq(n=n_fft, d=1.0 / sr)
+ min_mel = hz_to_mel(fmin)
+ max_mel = hz_to_mel(fmax)
+ mels = np.linspace(min_mel, max_mel, n_mels + 2)
+ mel_f = mel_to_hz(mels)
+ fdiff = np.diff(mel_f)
+ ramps = np.subtract.outer(mel_f, fftfreqs)
+
+ for i in range(n_mels):
+ lower = -ramps[i] / fdiff[i]
+ upper = ramps[i + 2] / fdiff[i + 1]
+ mel_basis[i] = np.maximum(0, np.minimum(lower, upper))
+
+ enorm = 2.0 / (mel_f[2:n_mels + 2] - mel_f[:n_mels])
+ mel_basis *= enorm[:, np.newaxis]
+ return mel_basis
+
+
+def _compute_mel_spectrogram(data, sr, n_fft=2048, hop_length=512, n_mels=128):
+ """Compute mel spectrogram from audio signal."""
+ fft_window = scipy.signal.get_window('hann', n_fft, fftbins=True)
+ if len(fft_window) < n_fft:
+ lpad = int((n_fft - len(fft_window)) // 2)
+ fft_window = np.pad(fft_window, (lpad, int(n_fft - len(fft_window) - lpad)), mode='constant')
+
+ fft_window = fft_window.reshape((-1, 1))
+ data_padded = np.pad(data, int(n_fft // 2), mode='constant')
+ n_frames = 1 + (len(data_padded) - n_fft) // hop_length
+ shape = (n_fft, n_frames)
+ strides = (data_padded.strides[0], data_padded.strides[0] * hop_length)
+ frames = np.lib.stride_tricks.as_strided(data_padded, shape=shape, strides=strides)
+
+ stft_result = scipy.fft.rfft(fft_window * frames, axis=0).astype(np.complex64)
+ power_spec = np.abs(stft_result) ** 2
+
+ mel_basis = _create_mel_filterbank(sr, n_fft, n_mels=n_mels, fmin=0.0, fmax=sr / 2.0)
+ mel_spec = np.dot(mel_basis, power_spec)
+ return mel_spec.astype(np.float32)
+
+
+def quick_tempo_estimate(audio_np, sr, start_bpm=120.0, std_bpm=1.0, hop_length=512):
+ """Estimate tempo using autocorrelation tempogram."""
+
+ if len(audio_np) < hop_length * 10:
+ logging.warning("Audio too short for tempo estimation, returning default BPM of 120.0")
+ return 120.0
+
+ n_fft = 2048
+ mel_S = _compute_mel_spectrogram(audio_np, sr, n_fft=n_fft, hop_length=hop_length, n_mels=128)
+ log_mel_S = 10.0 * np.log10(np.maximum(1e-10, mel_S))
+
+ lag = 1
+ S_diff = log_mel_S[:, lag:] - log_mel_S[:, :-lag]
+ S_onset = np.maximum(0.0, S_diff)
+ onset_env_pre = np.mean(S_onset, axis=0)
+ pad_width = lag + n_fft // (2 * hop_length)
+ onset_env = np.pad(onset_env_pre, (pad_width, 0), mode='constant')
+ onset_env = onset_env[:mel_S.shape[1]]
+
+ return estimate_tempo_from_onset(onset_env, sr, hop_length, start_bpm, std_bpm, max_tempo=320.0)
+
+
+def estimate_tempo_from_onset(onset_env, sr, hop_length, start_bpm=120.0, std_bpm=1.0, max_tempo=320.0):
+ """Estimate tempo from onset strength envelope using autocorrelation tempogram."""
+ if len(onset_env) < 20:
+ return 120.0
+
+ ac_size = 8.0
+ win_length = int(np.round(ac_size * sr / hop_length))
+ win_length = min(win_length, len(onset_env))
+
+ pad_width = win_length // 2
+ onset_padded = np.pad(onset_env, (pad_width, pad_width), mode='linear_ramp', end_values=(0, 0))
+
+ n_frames = len(onset_env)
+ shape = (win_length, n_frames)
+ strides = (onset_padded.strides[0], onset_padded.strides[0])
+ frames = np.lib.stride_tricks.as_strided(onset_padded, shape=shape, strides=strides)
+
+ hann_window = scipy.signal.get_window('hann', win_length, fftbins=True)
+ windowed_frames = frames * hann_window[:, np.newaxis]
+
+ tempogram = np.zeros((win_length, n_frames))
+ for i in range(n_frames):
+ frame = windowed_frames[:, i]
+ n_pad = scipy.fft.next_fast_len(2 * len(frame) - 1)
+ fft_result = scipy.fft.rfft(frame, n=n_pad)
+ powspec = np.abs(fft_result) ** 2
+ ac = scipy.fft.irfft(powspec, n=n_pad)
+ tempogram[:, i] = ac[:win_length]
+
+ ac_max = np.max(np.abs(tempogram), axis=0)
+ mask = ac_max > 0
+ tempogram[:, mask] /= ac_max[mask]
+
+ tempogram_mean = np.mean(tempogram, axis=1)
+ tempogram_mean = np.maximum(tempogram_mean, 0)
+
+ bpms = np.zeros(win_length, dtype=np.float64)
+ bpms[0] = np.inf
+ bpms[1:] = 60.0 * sr / (hop_length * np.arange(1.0, win_length))
+
+ logprior = -0.5 * ((np.log2(bpms) - np.log2(start_bpm)) / std_bpm) ** 2
+
+ if max_tempo is not None:
+ max_idx = int(np.argmax(bpms < max_tempo))
+ if max_idx > 0:
+ logprior[:max_idx] = -np.inf
+
+ weighted = np.log1p(1e6 * tempogram_mean) + logprior
+ best_idx = int(np.argmax(weighted[1:])) + 1
+ tempo = bpms[best_idx]
+
+ return tempo
+
+
+def detect_onset_peaks(onset_env, sr=22050, hop_length=512, pre_max=0.03, post_max=0.0,
+ pre_avg=0.10, post_avg=0.10, wait=0.03, delta=0.07):
+ """Detect onset peaks using peak picking algorithm."""
+
+ onset_normalized = onset_env - np.min(onset_env)
+ onset_max = np.max(onset_normalized)
+ if onset_max > 0:
+ onset_normalized = onset_normalized / onset_max
+
+ pre_max_frames = int(pre_max * sr / hop_length)
+ post_max_frames = int(post_max * sr / hop_length) + 1
+ pre_avg_frames = int(pre_avg * sr / hop_length)
+ post_avg_frames = int(post_avg * sr / hop_length) + 1
+ wait_frames = int(wait * sr / hop_length)
+
+ peaks = np.zeros(len(onset_normalized), dtype=bool)
+ peaks[0] = (onset_normalized[0] >= np.max(onset_normalized[:min(post_max_frames, len(onset_normalized))]))
+ peaks[0] &= (onset_normalized[0] >= np.mean(onset_normalized[:min(post_avg_frames, len(onset_normalized))]) + delta)
+
+ if peaks[0]:
+ n = wait_frames + 1
+ else:
+ n = 1
+
+ while n < len(onset_normalized):
+ maxn = np.max(onset_normalized[max(0, n - pre_max_frames):min(n + post_max_frames, len(onset_normalized))])
+ peaks[n] = (onset_normalized[n] == maxn)
+
+ if not peaks[n]:
+ n += 1
+ continue
+
+ avgn = np.mean(onset_normalized[max(0, n - pre_avg_frames):min(n + post_avg_frames, len(onset_normalized))])
+ peaks[n] &= (onset_normalized[n] >= avgn + delta)
+
+ if not peaks[n]:
+ n += 1
+ continue
+
+ n += wait_frames + 1
+
+ return np.flatnonzero(peaks).astype(np.int32)
+
+
+def track_beats(onset_env, tempo, sr, hop_length, tightness=100, trim=True):
+ """Track beats using dynamic programming."""
+
+ frame_rate = sr / hop_length
+ frames_per_beat = np.round(frame_rate * 60.0 / tempo)
+
+ if frames_per_beat <= 0 or len(onset_env) < 2:
+ return np.array([], dtype=np.int32)
+
+ onset_std = np.std(onset_env, ddof=1)
+ if onset_std > 0:
+ onset_normalized = onset_env / onset_std
+ else:
+ onset_normalized = onset_env
+
+ window_range = np.arange(-frames_per_beat, frames_per_beat + 1)
+ window = np.exp(-0.5 * (window_range * 32.0 / frames_per_beat) ** 2)
+
+ localscore = scipy.signal.convolve(onset_normalized, window, mode='same')
+
+ backlink = np.full(len(localscore), -1, dtype=np.int32)
+ cumscore = np.zeros(len(localscore), dtype=np.float64)
+
+ score_thresh = 0.01 * localscore.max()
+ first_beat = True
+
+ backlink[0] = -1
+ cumscore[0] = localscore[0]
+
+ fpb = int(frames_per_beat)
+
+ for i in range(1, len(localscore)):
+ score_i = localscore[i]
+ best_score = -np.inf
+ beat_location = -1
+
+ search_start = int(i - np.round(fpb / 2.0))
+ search_end = int(i - 2 * fpb - 1)
+
+ for loc in range(search_start, search_end, -1):
+ if loc < 0:
+ break
+
+ score = cumscore[loc] - tightness * (np.log(i - loc) - np.log(fpb)) ** 2
+
+ if score > best_score:
+ best_score = score
+ beat_location = loc
+
+ if beat_location >= 0:
+ cumscore[i] = score_i + best_score
+ else:
+ cumscore[i] = score_i
+
+ if first_beat and score_i < score_thresh:
+ backlink[i] = -1
+ else:
+ backlink[i] = beat_location
+ first_beat = False
+
+ local_max_mask = np.zeros(len(cumscore), dtype=bool)
+
+ local_max_mask[0] = False
+
+ for i in range(1, len(cumscore) - 1):
+ local_max_mask[i] = (cumscore[i] > cumscore[i-1]) and (cumscore[i] >= cumscore[i+1])
+
+ if len(cumscore) > 1:
+ local_max_mask[-1] = cumscore[-1] > cumscore[-2]
+
+ if np.any(local_max_mask):
+ median_max = np.median(cumscore[local_max_mask])
+ threshold = 0.5 * median_max
+
+ tail = -1
+ for i in range(len(cumscore) - 1, -1, -1):
+ if local_max_mask[i] and cumscore[i] >= threshold:
+ tail = i
+ break
+ else:
+ tail = len(cumscore) - 1
+
+ beats = np.zeros(len(localscore), dtype=bool)
+ n = tail
+ visited = set()
+ while n >= 0 and n not in visited:
+ beats[n] = True
+ visited.add(n)
+ n = backlink[n]
+
+ if trim and np.any(beats):
+ beat_positions = np.flatnonzero(beats)
+
+ beat_localscores = localscore[beat_positions]
+
+ w = np.hanning(5)
+ smooth_boe_full = np.convolve(beat_localscores, w)
+ smooth_boe = smooth_boe_full[len(w)//2 : len(localscore) + len(w)//2]
+
+ threshold = 0.5 * np.sqrt(np.mean(smooth_boe ** 2))
+
+ start_frame = 0
+ while start_frame < len(localscore) and localscore[start_frame] <= threshold:
+ beats[start_frame] = False
+ start_frame += 1
+
+ end_frame = len(localscore) - 1
+ while end_frame >= 0 and localscore[end_frame] <= threshold:
+ beats[end_frame] = False
+ end_frame -= 1
+
+ return np.flatnonzero(beats).astype(np.int32)
+
+def compute_onset_envelope(mel_spec_db, n_fft=2048, hop_length=512):
+ """Compute onset strength envelope from a log-mel spectrogram (dB)."""
+ lag = 1
+ onset_diff = mel_spec_db[:, lag:] - mel_spec_db[:, :-lag]
+ onset_diff = np.maximum(0.0, onset_diff)
+ envelope_pre_pad = np.mean(onset_diff, axis=0)
+
+ pad_width = lag + n_fft // (2 * hop_length)
+ envelope = np.pad(envelope_pre_pad, (pad_width, 0), mode='constant')
+ envelope = envelope[:mel_spec_db.shape[1]]
+
+ return envelope
+
+def compute_mfcc(mel_spec_db, n_mfcc=20):
+ """Compute MFCC features from a log-mel spectrogram (dB)."""
+ mfcc = scipy.fft.dct(mel_spec_db, axis=0, type=2, norm='ortho')[:n_mfcc].T
+ return mfcc.astype(np.float32)
+
+
+def power_to_db(S, amin=1e-10, top_db=80.0, ref=1.0):
+ """Convert a power spectrogram (amplitude squared) to decibel (dB) units"""
+ S = np.asarray(S)
+ log_spec = 10.0 * np.log10(np.maximum(amin, S))
+ log_spec -= 10.0 * np.log10(np.maximum(amin, ref))
+ if top_db is not None:
+ log_spec = np.maximum(log_spec, log_spec.max() - top_db)
+ return log_spec
+
+
+class WanDancerEncodeAudio(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="WanDancerEncodeAudio",
+ category="conditioning/video_models",
+ inputs=[
+ io.Audio.Input("audio"),
+ io.Int.Input("video_frames", default=149, min=1, max=nodes.MAX_RESOLUTION, step=4),
+ io.Float.Input("audio_inject_scale", default=1.0, min=0.0, max=10.0, step=0.01, tooltip="The scale for the audio features when injected into the video model."),
+ ],
+ outputs=[
+ io.AudioEncoderOutput.Output(display_name="audio_encoder_output"),
+ io.String.Output(display_name="fps_string", tooltip="The calculated fps based on the audio length and the number of video frames. Used in the prompt."),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, video_frames, audio_inject_scale, audio) -> io.NodeOutput:
+ waveform = audio["waveform"][0]
+ sample_rate = audio["sample_rate"]
+ base_fps = 30
+ hop_length = 512
+ model_sr = 22050
+ n_fft = 2048
+
+ # start tempo from original audio (not the resampled one) to match the reference pipeline
+ if waveform.shape[0] > 1:
+ waveform = waveform.mean(dim=0, keepdim=False)
+
+ start_bpm = quick_tempo_estimate(waveform.squeeze().cpu().numpy(), sample_rate, hop_length=hop_length)
+
+ # resample to the sample rate used for feature extraction
+ resample_sr = base_fps * hop_length
+ waveform = torchaudio.functional.resample(waveform, sample_rate, resample_sr)
+
+ waveform_np = waveform.cpu().numpy().squeeze()
+ mel_spec = _compute_mel_spectrogram(waveform_np, model_sr, n_fft, hop_length, n_mels=128)
+ mel_spec_db = power_to_db(mel_spec, amin=1e-10, top_db=80.0, ref=1.0)
+ envelope = compute_onset_envelope(mel_spec_db, n_fft, hop_length)
+ mfcc = compute_mfcc(mel_spec_db, n_mfcc=20)
+ chroma = compute_chroma_cens(y=waveform_np, sr=model_sr, hop_length=hop_length).T
+ # detect peaks
+ peak_idxs = detect_onset_peaks(envelope, sr=model_sr, hop_length=hop_length)
+ peak_onehot = np.zeros_like(envelope, dtype=np.float32)
+ peak_onehot[peak_idxs] = 1.0
+ # detect beats
+ beat_tracking_tempo = estimate_tempo_from_onset(envelope, sr=model_sr, hop_length=hop_length, start_bpm=start_bpm)
+ beat_idxs = track_beats(envelope, beat_tracking_tempo, model_sr, hop_length, tightness=100, trim=True)
+ beat_onehot = np.zeros_like(envelope, dtype=np.float32)
+ beat_onehot[beat_idxs] = 1.0
+
+ audio_feature = np.concatenate(
+ [envelope[:, None], mfcc, chroma, peak_onehot[:, None], beat_onehot[:, None]],
+ axis=-1,
+ )
+ audio_feature = torch.from_numpy(audio_feature).unsqueeze(0).to(comfy.model_management.intermediate_device())
+
+ fps = float(base_fps / int(audio_feature.shape[1] / video_frames + 0.5))
+
+ audio_encoder_output = {
+ "audio_feature": audio_feature,
+ "fps": fps,
+ "audio_inject_scale": audio_inject_scale,
+ }
+
+ if int(fps + 0.5) != 30:
+ fps_string = " 帧率是{:.4f}".format(fps) # "frame rate is" in Chinese, as it was in the original pipeline
+ else:
+ fps_string = ", 帧率是30fps。" # to match the reference pipeline when the fps is 30
+
+ return io.NodeOutput(audio_encoder_output, fps_string)
+
+
+class WanDancerVideo(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="WanDancerVideo",
+ category="conditioning/video_models",
+ inputs=[
+ io.Conditioning.Input("positive"),
+ io.Conditioning.Input("negative"),
+ io.Vae.Input("vae"),
+ io.Int.Input("width", default=480, min=16, max=nodes.MAX_RESOLUTION, step=16),
+ io.Int.Input("height", default=832, min=16, max=nodes.MAX_RESOLUTION, step=16),
+ io.Int.Input("length", default=149, min=1, max=nodes.MAX_RESOLUTION, step=4, tooltip="The number of frames in the generated video. Should stay 149 for WanDancer."),
+ io.ClipVisionOutput.Input("clip_vision_output", optional=True, tooltip="The CLIP vision embeds for the first frame."),
+ io.ClipVisionOutput.Input("clip_vision_output_ref", optional=True, tooltip="The CLIP vision embeds for the reference image."),
+ io.Image.Input("start_image", optional=True, tooltip="The initial image(s) to be encoded, can be any number of frames."),
+ io.Mask.Input("mask", optional=True, tooltip="Image conditioning mask for the start image(s). White is kept, black is generated. Used for the local generations."),
+ io.AudioEncoderOutput.Input("audio_encoder_output", optional=True),
+ ],
+ outputs=[
+ io.Conditioning.Output(display_name="positive"),
+ io.Conditioning.Output(display_name="negative"),
+ io.Latent.Output(display_name="latent", tooltip="Empty latent."),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, positive, negative, vae, width, height, length, start_image=None, mask=None, clip_vision_output=None, clip_vision_output_ref=None, audio_encoder_output=None) -> io.NodeOutput:
+ latent = torch.zeros([1, 16, ((length - 1) // 4) + 1, height // 8, width // 8], device=comfy.model_management.intermediate_device())
+ if start_image is not None:
+ start_image = comfy.utils.common_upscale(start_image[:length].movedim(-1, 1), width, height, "bilinear", "center").movedim(1, -1)
+ image = torch.zeros((length, height, width, start_image.shape[-1]), device=start_image.device, dtype=start_image.dtype)
+ image[:start_image.shape[0]] = start_image
+
+ concat_latent_image = vae.encode(image[:, :, :, :3])
+ if mask is None:
+ concat_mask = torch.ones((1, 1, latent.shape[2], concat_latent_image.shape[-2], concat_latent_image.shape[-1]), device=start_image.device, dtype=start_image.dtype)
+ concat_mask[:, :, :((start_image.shape[0] - 1) // 4) + 1] = 0.0
+ else:
+ concat_mask = 1 - mask[:length].unsqueeze(0)
+ concat_mask = comfy.utils.common_upscale(concat_mask, concat_latent_image.shape[-2], concat_latent_image.shape[-1], "nearest-exact", "disabled")
+ concat_mask = torch.cat([torch.repeat_interleave(concat_mask[:, 0:1], repeats=4, dim=1), concat_mask[:, 1:]], dim=1)
+ concat_mask = concat_mask.view(1, concat_mask.shape[1] // 4, 4, concat_latent_image.shape[-2], concat_latent_image.shape[-1]).transpose(1, 2)
+
+ positive = node_helpers.conditioning_set_values(positive, {"concat_latent_image": concat_latent_image, "concat_mask": concat_mask})
+ negative = node_helpers.conditioning_set_values(negative, {"concat_latent_image": concat_latent_image, "concat_mask": concat_mask})
+
+ if clip_vision_output is not None:
+ positive = node_helpers.conditioning_set_values(positive, {"clip_vision_output": clip_vision_output, "clip_vision_output_ref": clip_vision_output_ref})
+ negative = node_helpers.conditioning_set_values(negative, {"clip_vision_output": clip_vision_output, "clip_vision_output_ref": clip_vision_output_ref})
+
+ if audio_encoder_output is not None:
+ positive = node_helpers.conditioning_set_values(positive, {"audio_embed": audio_encoder_output["audio_feature"], "fps": audio_encoder_output["fps"], "audio_inject_scale": audio_encoder_output.get("audio_inject_scale", 1.0)})
+ negative = node_helpers.conditioning_set_values(negative, {"audio_embed": audio_encoder_output["audio_feature"], "fps": audio_encoder_output["fps"], "audio_inject_scale": audio_encoder_output.get("audio_inject_scale", 1.0)})
+
+ out_latent = {}
+ out_latent["samples"] = latent
+ return io.NodeOutput(positive, negative, out_latent)
+
+
+class WanDancerPadKeyframes(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="WanDancerPadKeyframes",
+ category="image/video",
+ inputs=[
+ io.Image.Input("images",),
+ io.Int.Input("segment_length", default=149, min=1, max=10000, tooltip="Length of this segment (usually 149 frames)"),
+ io.Int.Input("segment_index", default=0, min=0, max=100, tooltip="Which segment this is (0 for first, 1 for second, etc.)"),
+ io.Audio.Input("audio", tooltip="Audio to calculate total output frames from and extract segment audio."),
+ ],
+ outputs=[
+ io.Image.Output(display_name="keyframes_sequence", tooltip="Padded keyframe sequence"),
+ io.Mask.Output(display_name="keyframes_mask", tooltip="Mask indicating valid frames"),
+ io.Audio.Output(display_name="audio_segment", tooltip="Audio segment for this video segment"),
+ ],
+ )
+
+ @classmethod
+ def do_execute(cls, images, segment_length, segment_index, audio):
+ B, H, W, C = images.shape
+ fps = 30
+
+ # calculate total frames
+ audio_duration = audio["waveform"].shape[-1] / audio["sample_rate"]
+ segment_duration = segment_length / fps
+ buffer = 0.2
+ num_segments = int((audio_duration - buffer) / segment_duration) + 1 if audio_duration > buffer else 0
+ total_frames = num_segments * segment_length
+
+ mask = torch.zeros((segment_length, H, W), device=images.device, dtype=images.dtype)
+ keyframes = torch.zeros((segment_length, H, W, C), dtype=images.dtype, device=images.device)
+
+ # guard: with no audio or no images, nothing to place — leave keyframes/mask zeroed
+ if total_frames > 0 and B > 0:
+ frame_interval = float(total_frames) / B
+ seg_num = int(math.ceil(total_frames / segment_length))
+ is_last_segment = (segment_index == seg_num - 1)
+
+ positions = []
+ images_before_this_segment = 0
+
+ # count images consumed by previous segments
+ for seg_idx in range(segment_index):
+ end_idx = (total_frames - segment_length * seg_idx - 1) if seg_idx == seg_num - 1 else (segment_length - 1)
+ cnt = 0
+ while cnt * frame_interval < end_idx - frame_interval:
+ cnt += 1
+ images_before_this_segment += cnt
+
+ # positions for current segment
+ end_index = (total_frames - segment_length * segment_index - 1) if is_last_segment else (segment_length - 1)
+ cnt = 0
+ while cnt * frame_interval < end_index - frame_interval:
+ pos = int(math.ceil(frame_interval * cnt))
+ positions.append((pos, images_before_this_segment + cnt))
+ cnt += 1
+ positions.append((end_index, images_before_this_segment + cnt))
+
+ valid_positions = [(pos, idx) for pos, idx in positions if idx < B and pos < segment_length]
+
+ if valid_positions:
+ seg_positions, img_indices = zip(*valid_positions)
+ seg_positions = torch.tensor(seg_positions, dtype=torch.long, device=images.device)
+ img_indices = torch.tensor(img_indices, dtype=torch.long, device=images.device)
+ mask[seg_positions] = 1
+ keyframes[seg_positions] = images[img_indices]
+
+ # extract audio segment
+ segment_duration = segment_length / fps
+ start_time = segment_index * segment_duration
+ end_time = min(start_time + segment_duration, audio_duration)
+
+ sample_rate = audio["sample_rate"]
+ start_sample = int(start_time * sample_rate)
+ end_sample = int(end_time * sample_rate)
+
+ audio_segment_waveform = audio["waveform"][:, :, start_sample:end_sample]
+ audio_segment = {
+ "waveform": audio_segment_waveform,
+ "sample_rate": sample_rate
+ }
+
+ return keyframes, mask, audio_segment
+
+ @classmethod
+ def execute(cls, images, segment_length, segment_index, audio=None) -> io.NodeOutput:
+ return io.NodeOutput(*cls.do_execute(images, segment_length, segment_index, audio))
+
+class WanDancerPadKeyframesList(io.ComfyNode):
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="WanDancerPadKeyframesList",
+ category="image/video",
+ inputs=[
+ io.Image.Input("images"),
+ io.Int.Input("segment_length", default=149, min=1, max=10000, tooltip="Length of each segment (usually 149 frames)"),
+ io.Int.Input("num_segments", default=1, min=1, max=100, tooltip="How many padded segments to emit as lists."),
+ io.Audio.Input("audio", tooltip="Audio to slice for each emitted segment."),
+ ],
+ outputs=[
+ io.Image.Output(display_name="keyframes_sequence", tooltip="Padded keyframe sequences", is_output_list=True),
+ io.Mask.Output(display_name="keyframes_mask", tooltip="Masks indicating valid frames", is_output_list=True),
+ io.Audio.Output(display_name="audio_segment", tooltip="Audio segment for each video segment", is_output_list=True),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, images, segment_length, num_segments, audio=None) -> io.NodeOutput:
+ outputs = [WanDancerPadKeyframes.do_execute(images, segment_length, i, audio) for i in range(num_segments)]
+ keyframes, masks, audio_segments = zip(*outputs)
+ return io.NodeOutput(list(keyframes), list(masks), list(audio_segments))
+
+class WanDancerExtension(ComfyExtension):
+ @override
+ async def get_node_list(self) -> list[type[io.ComfyNode]]:
+ return [
+ WanDancerVideo,
+ WanDancerEncodeAudio,
+ WanDancerPadKeyframes,
+ WanDancerPadKeyframesList,
+ ]
+
+async def comfy_entrypoint() -> WanDancerExtension:
+ return WanDancerExtension()
diff --git a/comfy_extras/void_noise_warp.py b/comfy_extras/void_noise_warp.py
new file mode 100644
index 000000000..fcc9a5f8b
--- /dev/null
+++ b/comfy_extras/void_noise_warp.py
@@ -0,0 +1,494 @@
+"""
+Optical-flow-warped noise for VOID Pass 2 refinement.
+
+Adapted from RyannDaGreat/CommonSource (MIT License, Ryan Burgert):
+ https://github.com/RyannDaGreat/CommonSource
+ - noise_warp.py (NoiseWarper / warp_xyωc / regaussianize / get_noise_from_video)
+ - raft.py (RaftOpticalFlow)
+
+Only the code paths that ``comfy_extras/nodes_void.py::VOIDWarpedNoise`` actually
+uses (torch THWC uint8 input, no background removal, no visualization, no disk
+I/O, default warp/noise params) have been inlined. External ``rp`` utilities
+have been replaced with equivalents from torch.nn.functional / einops. The
+RAFT optical-flow model itself is loaded offline via ``OpticalFlowLoader`` in
+``nodes_void.py`` and passed into ``get_noise_from_video`` by the caller; this
+module never downloads weights at runtime.
+"""
+
+import logging
+from typing import Optional
+
+import torch
+import torch.nn.functional as F
+from einops import rearrange
+
+import comfy.model_management
+
+
+# ---------------------------------------------------------------------------
+# Low-level torch image helpers (drop-in replacements for rp.torch_* primitives)
+# ---------------------------------------------------------------------------
+
+def _torch_resize_chw(image, size, interp, copy=True):
+ """Resize a CHW tensor.
+
+ ``size`` is either a scalar factor or a (h, w) tuple. ``interp`` is one
+ of ``"bilinear"``, ``"nearest"``, ``"area"``. When ``copy`` is False and
+ the requested size matches the input, returns the input tensor as is
+ (faster but callers must not mutate the result).
+ """
+ if image.ndim != 3:
+ raise ValueError(
+ f"_torch_resize_chw expects a 3D CHW tensor, got shape {tuple(image.shape)}"
+ )
+ _, in_h, in_w = image.shape
+ if isinstance(size, (int, float)) and not isinstance(size, bool):
+ new_h = max(1, int(in_h * size))
+ new_w = max(1, int(in_w * size))
+ else:
+ new_h, new_w = size
+
+ if (new_h, new_w) == (in_h, in_w):
+ return image.clone() if copy else image
+
+ kwargs = {}
+ if interp in ("bilinear", "bicubic"):
+ kwargs["align_corners"] = False
+ out = F.interpolate(image[None], size=(new_h, new_w), mode=interp, **kwargs)[0]
+ return out
+
+
+def _torch_remap_relative(image, dx, dy, interp="bilinear"):
+ """Relative remap of a CHW image via ``F.grid_sample``.
+
+ Equivalent to ``rp.torch_remap_image(image, dx, dy, relative=True, interp=interp)``
+ for ``interp`` in {"bilinear", "nearest"}. Out-of-bounds samples are 0.
+ """
+ if image.ndim != 3:
+ raise ValueError(
+ f"_torch_remap_relative expects a 3D CHW tensor, got shape {tuple(image.shape)}"
+ )
+ if dx.shape != dy.shape:
+ raise ValueError(
+ f"_torch_remap_relative: dx and dy must match, got {tuple(dx.shape)} vs {tuple(dy.shape)}"
+ )
+ _, h, w = image.shape
+
+ x_abs = dx + torch.arange(w, device=dx.device, dtype=dx.dtype)
+ y_abs = dy + torch.arange(h, device=dy.device, dtype=dy.dtype)[:, None]
+
+ x_norm = (x_abs / (w - 1)) * 2 - 1
+ y_norm = (y_abs / (h - 1)) * 2 - 1
+
+ grid = torch.stack([x_norm, y_norm], dim=-1)[None].to(image.dtype)
+ out = F.grid_sample(
+ image[None], grid, mode=interp, align_corners=True, padding_mode="zeros"
+ )[0]
+ return out
+
+
+def _torch_scatter_add_relative(image, dx, dy):
+ """Scatter-add a CHW image using relative floor-rounded (dx, dy) offsets.
+
+ Equivalent to ``rp.torch_scatter_add_image(image, dx, dy, relative=True,
+ interp='floor')``. Out-of-bounds targets are dropped.
+ """
+ if image.ndim != 3:
+ raise ValueError(
+ f"_torch_scatter_add_relative expects a 3D CHW tensor, got shape {tuple(image.shape)}"
+ )
+ in_c, in_h, in_w = image.shape
+ if dx.shape != (in_h, in_w) or dy.shape != (in_h, in_w):
+ raise ValueError(
+ f"_torch_scatter_add_relative: dx/dy must be ({in_h}, {in_w}), "
+ f"got dx={tuple(dx.shape)} dy={tuple(dy.shape)}"
+ )
+
+ x = dx.long() + torch.arange(in_w, device=dx.device, dtype=torch.long)
+ y = dy.long() + torch.arange(in_h, device=dy.device, dtype=torch.long)[:, None]
+
+ valid = ((y >= 0) & (y < in_h) & (x >= 0) & (x < in_w)).reshape(-1)
+ indices = (y * in_w + x).reshape(-1)[valid]
+
+ flat_image = rearrange(image, "c h w -> (h w) c")[valid]
+ out = torch.zeros((in_h * in_w, in_c), dtype=image.dtype, device=image.device)
+ out.index_add_(0, indices, flat_image)
+ return rearrange(out, "(h w) c -> c h w", h=in_h, w=in_w)
+
+
+# ---------------------------------------------------------------------------
+# Noise warping primitives (ported from noise_warp.py)
+# ---------------------------------------------------------------------------
+
+def unique_pixels(image):
+ """Find unique pixel values in a CHW tensor.
+
+ Returns ``(unique_colors [U, C], counts [U], index_matrix [H, W])`` where
+ ``index_matrix[i, j]`` is the index of the unique color at that pixel.
+ """
+ _, h, w = image.shape
+ flat = rearrange(image, "c h w -> (h w) c")
+ unique_colors, inverse_indices, counts = torch.unique(
+ flat, dim=0, return_inverse=True, return_counts=True, sorted=False,
+ )
+ index_matrix = rearrange(inverse_indices, "(h w) -> h w", h=h, w=w)
+ return unique_colors, counts, index_matrix
+
+
+def sum_indexed_values(image, index_matrix):
+ """For each unique index, sum the CHW image values at its pixels."""
+ _, h, w = image.shape
+ u = int(index_matrix.max().item()) + 1
+ flat = rearrange(image, "c h w -> (h w) c")
+ out = torch.zeros((u, flat.shape[1]), dtype=flat.dtype, device=flat.device)
+ out.index_add_(0, index_matrix.view(-1), flat)
+ return out
+
+
+def indexed_to_image(index_matrix, unique_colors):
+ """Build a CHW image from an index matrix and a (U, C) color table."""
+ h, w = index_matrix.shape
+ flat = unique_colors[index_matrix.view(-1)]
+ return rearrange(flat, "(h w) c -> c h w", h=h, w=w)
+
+
+def regaussianize(noise):
+ """Variance-preserving re-sampling of a CHW noise tensor.
+
+ Wherever the noise contains groups of identical pixel values (e.g. after
+ a nearest-neighbor warp that duplicated source pixels), adds zero-mean
+ foreign noise within each group and scales by ``1/sqrt(count)`` so the
+ output is unit-variance gaussian again.
+ """
+ _, hs, ws = noise.shape
+ _, counts, index_matrix = unique_pixels(noise[:1])
+
+ foreign_noise = torch.randn_like(noise)
+ summed = sum_indexed_values(foreign_noise, index_matrix)
+ meaned = indexed_to_image(index_matrix, summed / rearrange(counts, "u -> u 1"))
+ zeroed_foreign = foreign_noise - meaned
+
+ counts_image = indexed_to_image(index_matrix, rearrange(counts, "u -> u 1"))
+
+ output = noise / counts_image ** 0.5 + zeroed_foreign
+ return output, counts_image
+
+
+def xy_meshgrid_like_image(image):
+ """Return a (2, H, W) tensor of (x, y) pixel coordinates matching ``image``."""
+ _, h, w = image.shape
+ y, x = torch.meshgrid(
+ torch.arange(h, device=image.device, dtype=image.dtype),
+ torch.arange(w, device=image.device, dtype=image.dtype),
+ indexing="ij",
+ )
+ return torch.stack([x, y])
+
+
+def noise_to_state(noise):
+ """Pack a (C, H, W) noise tensor into a state tensor (3+C, H, W) = [dx, dy, ω, noise]."""
+ zeros = torch.zeros_like(noise[:1])
+ ones = torch.ones_like(noise[:1])
+ return torch.cat([zeros, zeros, ones, noise])
+
+
+def state_to_noise(state):
+ """Unpack the noise channels from a state tensor."""
+ return state[3:]
+
+
+def warp_state(state, flow):
+ """Warp a noise-warper state tensor along the given optical flow.
+
+ ``state`` has shape ``(3+c, h, w)`` (= dx, dy, ω, c noise channels).
+ ``flow`` has shape ``(2, h, w)`` (= dx, dy).
+ """
+ if flow.device != state.device:
+ raise ValueError(
+ f"warp_state: flow and state must be on the same device, "
+ f"got flow={flow.device} state={state.device}"
+ )
+ if state.ndim != 3:
+ raise ValueError(
+ f"warp_state: state must be 3D (3+C, H, W), got shape {tuple(state.shape)}"
+ )
+ xyoc, h, w = state.shape
+ if flow.shape != (2, h, w):
+ raise ValueError(
+ f"warp_state: flow must have shape (2, {h}, {w}), got {tuple(flow.shape)}"
+ )
+ device = state.device
+
+ x_ch, y_ch = 0, 1
+ xy = 2 # state[:xy] = [dx, dy]
+ xyw = 3 # state[:xyw] = [dx, dy, ω]
+ w_ch = 2 # state[w_ch] = ω
+ c = xyoc - xyw
+ oc = xyoc - xy
+ if c <= 0:
+ raise ValueError(
+ f"warp_state: state has no noise channels (expected 3+C with C>0, got {xyoc} channels)"
+ )
+ if not (state[w_ch] > 0).all():
+ raise ValueError("warp_state: all weights in state[2] must be > 0")
+
+ grid = xy_meshgrid_like_image(state)
+
+ init = torch.empty_like(state)
+ init[:xy] = 0
+ init[w_ch] = 1
+ init[-c:] = 0
+
+ # --- Expansion branch: nearest-neighbor remap with negated flow ---
+ pre_expand = torch.empty_like(state)
+ pre_expand[:xy] = _torch_remap_relative(state[:xy], -flow[0], -flow[1], "nearest")
+ pre_expand[-oc:] = _torch_remap_relative(state[-oc:], -flow[0], -flow[1], "nearest")
+ pre_expand[w_ch][pre_expand[w_ch] == 0] = 1
+
+ # --- Shrink branch: scatter-add state into new positions ---
+ pre_shrink = state.clone()
+ pre_shrink[:xy] += flow
+
+ pos = (grid + pre_shrink[:xy]).round()
+ in_bounds = (pos[x_ch] >= 0) & (pos[x_ch] < w) & (pos[y_ch] >= 0) & (pos[y_ch] < h)
+ pre_shrink = torch.where(~in_bounds[None], init, pre_shrink)
+
+ scat_xy = pre_shrink[:xy].round()
+ pre_shrink[:xy] -= scat_xy
+ pre_shrink[:xy] = 0 # xy_mode='none' in upstream
+
+ def scat(tensor):
+ return _torch_scatter_add_relative(tensor, scat_xy[0], scat_xy[1])
+
+ # rp.torch_scatter_add_image on a bool tensor errors on modern torch;
+ # scatter-sum a float ones tensor and threshold to get the mask instead.
+ shrink_mask = scat(torch.ones(1, h, w, dtype=state.dtype, device=device)) > 0
+
+ # Drop expansion samples at positions that will be filled by shrink.
+ pre_expand = torch.where(shrink_mask, init, pre_expand)
+
+ # Regaussianize both branches together so duplicated-source groups are
+ # counted globally, then split back apart.
+ concat = torch.cat([pre_shrink, pre_expand], dim=2) # along width
+ concat[-c:], counts_image = regaussianize(concat[-c:])
+ concat[w_ch] = concat[w_ch] / counts_image[0]
+ concat[w_ch] = concat[w_ch].nan_to_num()
+ pre_shrink, expand = torch.chunk(concat, chunks=2, dim=2)
+
+ shrink = torch.empty_like(pre_shrink)
+ shrink[w_ch] = scat(pre_shrink[w_ch][None])[0]
+ shrink[:xy] = scat(pre_shrink[:xy] * pre_shrink[w_ch][None]) / shrink[w_ch][None]
+ shrink[-c:] = scat(pre_shrink[-c:] * pre_shrink[w_ch][None]) / scat(
+ pre_shrink[w_ch][None] ** 2
+ ).sqrt()
+
+ output = torch.where(shrink_mask, shrink, expand)
+ output[w_ch] = output[w_ch] / output[w_ch].mean()
+ output[w_ch] += 1e-5
+ output[w_ch] **= 0.9999
+ return output
+
+
+class NoiseWarper:
+ """Maintain a warpable noise state and emit gaussian noise per frame.
+
+ Simplified from RyannDaGreat/CommonSource/noise_warp.py::NoiseWarper:
+ ``scale_factor``, ``post_noise_alpha``, ``progressive_noise_alpha``, and
+ ``warp_kwargs`` are all dropped since VOIDWarpedNoise always uses defaults.
+ """
+
+ def __init__(self, c, h, w, device, dtype=torch.float32):
+ if c <= 0 or h <= 0 or w <= 0:
+ raise ValueError(
+ f"NoiseWarper: c/h/w must all be positive, got c={c} h={h} w={w}"
+ )
+ self.c = c
+ self.h = h
+ self.w = w
+ self.device = device
+ self.dtype = dtype
+
+ noise = torch.randn(c, h, w, dtype=dtype, device=device)
+ self._state = noise_to_state(noise)
+
+ @property
+ def noise(self):
+ # With scale_factor=1 the "downsample to respect weights" step is a
+ # size-preserving no-op; the weight-variance correction math still
+ # runs to stay faithful to upstream.
+ n = state_to_noise(self._state)
+ weights = self._state[2:3]
+ return n * weights / (weights ** 2).sqrt()
+
+ def __call__(self, dx, dy):
+ if dx.shape != dy.shape:
+ raise ValueError(
+ f"NoiseWarper: dx and dy must match, got {tuple(dx.shape)} vs {tuple(dy.shape)}"
+ )
+ flow = torch.stack([dx, dy]).to(self.device, self.dtype)
+ _, oflowh, ofloww = flow.shape
+
+ flow = _torch_resize_chw(flow, (self.h, self.w), "bilinear", copy=True)
+ flowh, floww = flow.shape[-2:]
+
+ # Upstream scales flow[0] by flowh/oflowh and flow[1] by floww/ofloww
+ # (channel-order appears swapped but harmless when H and W are scaled
+ # by the same factor, which is always the case for our callers).
+ flow[0] *= flowh / oflowh
+ flow[1] *= floww / ofloww
+
+ self._state = warp_state(self._state, flow)
+ return self
+
+
+# ---------------------------------------------------------------------------
+# RAFT optical flow wrapper (ported from raft.py)
+# ---------------------------------------------------------------------------
+
+class RaftOpticalFlow:
+ """RAFT-large wrapper around a pre-loaded torchvision model.
+
+ ``model`` must be the ``torchvision.models.optical_flow.raft_large`` module
+ with its weights already populated; this class is load-agnostic so the
+ caller owns downloading/offload concerns (see ``OpticalFlowLoader`` in
+ ``nodes_void.py``). ``__call__`` returns a ``(2, H, W)`` flow.
+ """
+
+ def __init__(self, model, device=None):
+ if device is None:
+ device = comfy.model_management.get_torch_device()
+ device = torch.device(device) if not isinstance(device, torch.device) else device
+
+ model = model.to(device)
+ model.eval()
+ self.device = device
+ self.model = model
+
+ def _preprocess(self, image_chw):
+ image = image_chw.to(self.device, torch.float32)
+ _, h, w = image.shape
+ new_h = (h // 8) * 8
+ new_w = (w // 8) * 8
+ image = _torch_resize_chw(image, (new_h, new_w), "bilinear", copy=False)
+ image = image * 2 - 1
+ return image[None]
+
+ def __call__(self, from_image, to_image):
+ """``from_image``, ``to_image``: CHW float tensors in [0, 1]."""
+ if from_image.shape != to_image.shape:
+ raise ValueError(
+ f"RaftOpticalFlow: from_image and to_image must match, "
+ f"got {tuple(from_image.shape)} vs {tuple(to_image.shape)}"
+ )
+ _, h, w = from_image.shape
+ with torch.no_grad():
+ img1 = self._preprocess(from_image)
+ img2 = self._preprocess(to_image)
+ list_of_flows = self.model(img1, img2)
+ flow = list_of_flows[-1][0] # (2, new_h, new_w)
+ if flow.shape[-2:] != (h, w):
+ flow = _torch_resize_chw(flow, (h, w), "bilinear", copy=False)
+ return flow
+
+
+# ---------------------------------------------------------------------------
+# Narrow entry point used by VOIDWarpedNoise
+# ---------------------------------------------------------------------------
+
+def get_noise_from_video(
+ video_frames: torch.Tensor,
+ raft: RaftOpticalFlow,
+ *,
+ noise_channels: int = 16,
+ resize_frames: float = 0.5,
+ resize_flow: int = 8,
+ downscale_factor: int = 32,
+ device: Optional[torch.device] = None,
+) -> torch.Tensor:
+ """Produce optical-flow-warped gaussian noise from a video.
+
+ Args:
+ video_frames: ``(T, H, W, 3)`` uint8 torch tensor.
+ raft: Pre-loaded RAFT optical-flow wrapper (see ``RaftOpticalFlow``).
+ noise_channels: Channels in the output noise.
+ resize_frames: Pre-RAFT frame scale factor.
+ resize_flow: Post-flow up-scale factor applied to the optical flow;
+ the internal noise state is allocated at
+ ``(resize_flow * resize_frames * H, resize_flow * resize_frames * W)``.
+ downscale_factor: Area-pool factor applied to the noise before return;
+ should evenly divide the internal noise resolution.
+ device: Target device. Defaults to ``comfy.model_management.get_torch_device()``.
+
+ Returns:
+ ``(T, H', W', noise_channels)`` float32 noise tensor on ``device``.
+ """
+ if not isinstance(resize_flow, int) or resize_flow < 1:
+ raise ValueError(
+ f"get_noise_from_video: resize_flow must be a positive int, got {resize_flow!r}"
+ )
+ if video_frames.ndim != 4 or video_frames.shape[-1] != 3:
+ raise ValueError(
+ "get_noise_from_video: video_frames must have shape (T, H, W, 3), "
+ f"got {tuple(video_frames.shape)}"
+ )
+ if video_frames.dtype != torch.uint8:
+ raise TypeError(
+ "get_noise_from_video: video_frames must be uint8 in [0, 255], "
+ f"got dtype {video_frames.dtype}"
+ )
+
+ if device is None:
+ device = comfy.model_management.get_torch_device()
+ device = torch.device(device) if not isinstance(device, torch.device) else device
+
+ if device.type == "cpu":
+ logging.warning(
+ "VOIDWarpedNoise: running get_noise_from_video on CPU; this will be "
+ "slow (minutes for ~45 frames). Use CUDA for interactive use."
+ )
+
+ T = video_frames.shape[0]
+ frames = video_frames.to(device).permute(0, 3, 1, 2).to(torch.float32) / 255.0
+ if resize_frames != 1.0:
+ new_h = max(1, int(frames.shape[2] * resize_frames))
+ new_w = max(1, int(frames.shape[3] * resize_frames))
+ frames = F.interpolate(frames, size=(new_h, new_w), mode="area")
+
+ _, _, H, W = frames.shape
+ internal_h = resize_flow * H
+ internal_w = resize_flow * W
+ if internal_h % downscale_factor or internal_w % downscale_factor:
+ logging.warning(
+ "VOIDWarpedNoise: internal noise size %dx%d is not divisible by "
+ "downscale_factor %d; output noise may have artifacts.",
+ internal_h, internal_w, downscale_factor,
+ )
+
+ with torch.no_grad():
+ warper = NoiseWarper(
+ c=noise_channels, h=internal_h, w=internal_w, device=device,
+ )
+ down_h = warper.h // downscale_factor
+ down_w = warper.w // downscale_factor
+ output = torch.empty(
+ (T, down_h, down_w, noise_channels), dtype=torch.float32, device=device,
+ )
+
+ def downscale(noise_chw):
+ # Area-pool to 1/downscale_factor then multiply by downscale_factor
+ # to adjust std (sqrt of pool area == downscale_factor for a
+ # square pool).
+ down = _torch_resize_chw(noise_chw, 1.0 / downscale_factor, "area", copy=False)
+ return down * downscale_factor
+
+ output[0] = downscale(warper.noise).permute(1, 2, 0)
+
+ prev = frames[0]
+ for i in range(1, T):
+ curr = frames[i]
+ flow = raft(prev, curr).to(device)
+ warper(flow[0], flow[1])
+ output[i] = downscale(warper.noise).permute(1, 2, 0)
+ prev = curr
+
+ return output
diff --git a/comfyui_version.py b/comfyui_version.py
index 2a1eb9905..4c6f5eb2a 100644
--- a/comfyui_version.py
+++ b/comfyui_version.py
@@ -1,3 +1,3 @@
# This file is automatically generated by the build process when version is
# updated in pyproject.toml.
-__version__ = "0.19.3"
+__version__ = "0.21.1"
diff --git a/custom_nodes/websocket_image_save.py b/custom_nodes/websocket_image_save.py
index 15f87f9f5..6a8646d0e 100644
--- a/custom_nodes/websocket_image_save.py
+++ b/custom_nodes/websocket_image_save.py
@@ -22,7 +22,7 @@ class SaveImageWebsocket:
OUTPUT_NODE = True
- CATEGORY = "api/image"
+ CATEGORY = "image"
def save_images(self, images):
pbar = comfy.utils.ProgressBar(images.shape[0])
@@ -42,3 +42,7 @@ class SaveImageWebsocket:
NODE_CLASS_MAPPINGS = {
"SaveImageWebsocket": SaveImageWebsocket,
}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "SaveImageWebsocket": "Save Image (Websocket)",
+}
\ No newline at end of file
diff --git a/execution.py b/execution.py
index e15eb4bda..4c7de2e84 100644
--- a/execution.py
+++ b/execution.py
@@ -15,6 +15,7 @@ import torch
from comfy.cli_args import args
import comfy.memory_management
import comfy.model_management
+import comfy.model_prefetch
import comfy_aimdo.model_vbar
from latent_preview import set_preview_method
@@ -537,6 +538,7 @@ async def execute(server, dynprompt, caches, current_item, extra_data, executed,
if args.verbose == "DEBUG":
comfy_aimdo.control.analyze()
comfy.model_management.reset_cast_buffers()
+ comfy.model_prefetch.cleanup_prefetch_queues()
comfy_aimdo.model_vbar.vbars_reset_watermark_limits()
if has_pending_tasks:
@@ -624,7 +626,7 @@ async def execute(server, dynprompt, caches, current_item, extra_data, executed,
if comfy.model_management.is_oom(ex):
tips = "This error means you ran out of memory on your GPU.\n\nTIPS: If the workflow worked before you might have accidentally set the batch_size to a large number."
- logging.info("Memory summary: {}".format(comfy.model_management.debug_memory_summary()))
+ logging.info("Memory summary:\n{}".format(comfy.model_management.debug_memory_summary()))
logging.error("Got an OOM, unloading all loaded models.")
comfy.model_management.unload_all_models()
elif isinstance(ex, RuntimeError) and ("mat1 and mat2 shapes" in str(ex)) and "Sampler" in class_type:
@@ -779,7 +781,7 @@ class PromptExecutor:
if self.cache_type == CacheType.RAM_PRESSURE:
comfy.model_management.free_memory(0, None, pins_required=ram_headroom, ram_required=ram_headroom)
- comfy.memory_management.extra_ram_release(ram_headroom)
+ ram_release_callback(ram_headroom, free_active=True)
else:
# Only execute when the while-loop ends without break
# Send cached UI for intermediate output nodes that weren't executed
@@ -1017,7 +1019,12 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None):
combo_options = extra_info.get("options", [])
else:
combo_options = input_type
- if val not in combo_options:
+ is_multiselect = extra_info.get("multiselect", False)
+ if is_multiselect and isinstance(val, list):
+ invalid_vals = [v for v in val if v not in combo_options]
+ else:
+ invalid_vals = [val] if val not in combo_options else []
+ if invalid_vals:
input_config = info
list_info = ""
@@ -1032,7 +1039,7 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None):
error = {
"type": "value_not_in_list",
"message": "Value not in list",
- "details": f"{x}: '{val}' not in {list_info}",
+ "details": f"{x}: {', '.join(repr(v) for v in invalid_vals)} not in {list_info}",
"extra_info": {
"input_name": x,
"input_config": input_config,
diff --git a/extra_model_paths.yaml.example b/extra_model_paths.yaml.example
index 34df01681..9c395c0b2 100644
--- a/extra_model_paths.yaml.example
+++ b/extra_model_paths.yaml.example
@@ -28,7 +28,7 @@
#config for a1111 ui
#all you have to do is uncomment this (remove the #) and change the base_path to where yours is installed
-#a111:
+#a1111:
# base_path: path/to/stable-diffusion-webui/
# checkpoints: models/Stable-diffusion
# configs: models/Stable-diffusion
diff --git a/folder_paths.py b/folder_paths.py
index 80f4b291a..ad7f0f4fc 100644
--- a/folder_paths.py
+++ b/folder_paths.py
@@ -52,8 +52,14 @@ folder_names_and_paths["model_patches"] = ([os.path.join(models_dir, "model_patc
folder_names_and_paths["audio_encoders"] = ([os.path.join(models_dir, "audio_encoders")], supported_pt_extensions)
+folder_names_and_paths["background_removal"] = ([os.path.join(models_dir, "background_removal")], supported_pt_extensions)
+
folder_names_and_paths["frame_interpolation"] = ([os.path.join(models_dir, "frame_interpolation")], supported_pt_extensions)
+folder_names_and_paths["geometry_estimation"] = ([os.path.join(models_dir, "geometry_estimation")], supported_pt_extensions)
+
+folder_names_and_paths["optical_flow"] = ([os.path.join(models_dir, "optical_flow")], supported_pt_extensions)
+
output_directory = os.path.join(base_path, "output")
temp_directory = os.path.join(base_path, "temp")
input_directory = os.path.join(base_path, "input")
@@ -432,7 +438,9 @@ def get_save_image_path(filename_prefix: str, output_dir: str, image_width=0, im
prefix_len = len(os.path.basename(filename_prefix))
prefix = filename[:prefix_len + 1]
try:
- digits = int(filename[prefix_len + 1:].split('_')[0])
+ remainder = filename[prefix_len + 1:]
+ base_remainder = remainder.split('.')[0]
+ digits = int(base_remainder.split('_')[0])
except:
digits = 0
return digits, prefix
diff --git a/main.py b/main.py
index fd228b256..b2b25a195 100644
--- a/main.py
+++ b/main.py
@@ -1,13 +1,21 @@
import comfy.options
comfy.options.enable_args_parsing()
+from comfy.cli_args import args
+
+if args.list_feature_flags:
+ import json
+ from comfy_api.feature_flags import CLI_FEATURE_FLAG_REGISTRY
+ print(json.dumps(CLI_FEATURE_FLAG_REGISTRY, indent=2)) # noqa: T201
+ raise SystemExit(0)
+
import os
import importlib.util
import shutil
import importlib.metadata
import folder_paths
import time
-from comfy.cli_args import args, enables_dynamic_vram
+from comfy.cli_args import enables_dynamic_vram
from app.logger import setup_logger
setup_logger(log_level=args.verbose, use_stdout=args.log_stdout)
diff --git a/models/background_removal/put_background_removal_models_here b/models/background_removal/put_background_removal_models_here
new file mode 100644
index 000000000..e69de29bb
diff --git a/models/geometry_estimation/put_geometry_estimation_models_here b/models/geometry_estimation/put_geometry_estimation_models_here
new file mode 100644
index 000000000..e69de29bb
diff --git a/models/optical_flow/put_optical_flow_models_here b/models/optical_flow/put_optical_flow_models_here
new file mode 100644
index 000000000..e69de29bb
diff --git a/node_helpers.py b/node_helpers.py
index d3d834516..cac4e88dd 100644
--- a/node_helpers.py
+++ b/node_helpers.py
@@ -86,6 +86,6 @@ def image_alpha_fix(destination, source):
if destination.shape[-1] < source.shape[-1]:
source = source[...,:destination.shape[-1]]
elif destination.shape[-1] > source.shape[-1]:
- destination = torch.nn.functional.pad(destination, (0, 1))
- destination[..., -1] = 1.0
+ source = torch.nn.functional.pad(source, (0, 1))
+ source[..., -1] = 1.0
return destination, source
diff --git a/nodes.py b/nodes.py
index 82d7ef332..787d4d56c 100644
--- a/nodes.py
+++ b/nodes.py
@@ -32,7 +32,7 @@ import comfy.controlnet
from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict, FileLocator
from comfy_api.internal import register_versions, ComfyAPIWithVersion
from comfy_api.version_list import supported_versions
-from comfy_api.latest import io, ComfyExtension
+from comfy_api.latest import io, ComfyExtension, InputImpl
import comfy.clip_vision
@@ -330,7 +330,7 @@ class VAEDecodeTiled:
RETURN_TYPES = ("IMAGE",)
FUNCTION = "decode"
- CATEGORY = "_for_testing"
+ CATEGORY = "experimental"
def decode(self, vae, samples, tile_size, overlap=64, temporal_size=64, temporal_overlap=8):
if tile_size < overlap * 4:
@@ -377,7 +377,7 @@ class VAEEncodeTiled:
RETURN_TYPES = ("LATENT",)
FUNCTION = "encode"
- CATEGORY = "_for_testing"
+ CATEGORY = "experimental"
def encode(self, vae, pixels, tile_size, overlap, temporal_size=64, temporal_overlap=8):
t = vae.encode_tiled(pixels, tile_x=tile_size, tile_y=tile_size, overlap=overlap, tile_t=temporal_size, overlap_t=temporal_overlap)
@@ -493,7 +493,7 @@ class SaveLatent:
OUTPUT_NODE = True
- CATEGORY = "_for_testing"
+ CATEGORY = "experimental"
def save(self, samples, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
@@ -538,7 +538,7 @@ class LoadLatent:
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f)) and f.endswith(".latent")]
return {"required": {"latent": [sorted(files), ]}, }
- CATEGORY = "_for_testing"
+ CATEGORY = "experimental"
RETURN_TYPES = ("LATENT", )
FUNCTION = "load"
@@ -758,7 +758,7 @@ class LoraLoader:
FUNCTION = "load_lora"
CATEGORY = "loaders"
- DESCRIPTION = "LoRAs are used to modify diffusion and CLIP models, altering the way in which latents are denoised such as applying styles. Multiple LoRA nodes can be linked together."
+ DESCRIPTION = "This LoRA loader is used to modify both diffusion and CLIP models, altering the way in which latents are denoised such as applying styles. Multiple LoRA nodes can be linked together."
SEARCH_ALIASES = ["lora", "load lora", "apply lora", "lora loader", "lora model"]
def load_lora(self, model, clip, lora_name, strength_model, strength_clip):
@@ -767,17 +767,19 @@ class LoraLoader:
lora_path = folder_paths.get_full_path_or_raise("loras", lora_name)
lora = None
+ lora_metadata = None
if self.loaded_lora is not None:
if self.loaded_lora[0] == lora_path:
lora = self.loaded_lora[1]
+ lora_metadata = self.loaded_lora[2] if len(self.loaded_lora) > 2 else None
else:
self.loaded_lora = None
if lora is None:
- lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
- self.loaded_lora = (lora_path, lora)
+ lora, lora_metadata = comfy.utils.load_torch_file(lora_path, safe_load=True, return_metadata=True)
+ self.loaded_lora = (lora_path, lora, lora_metadata)
- model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip)
+ model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip, lora_metadata=lora_metadata)
return (model_lora, clip_lora)
class LoraLoaderModelOnly(LoraLoader):
@@ -788,6 +790,7 @@ class LoraLoaderModelOnly(LoraLoader):
"strength_model": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}),
}}
RETURN_TYPES = ("MODEL",)
+ DESCRIPTION = "This LoRAs loader is used to modify the diffusion model, altering the way in which latents are denoised such as applying styles. Multiple LoRA nodes can be linked together."
FUNCTION = "load_lora_model_only"
def load_lora_model_only(self, model, lora_name, strength_model):
@@ -795,50 +798,26 @@ class LoraLoaderModelOnly(LoraLoader):
class VAELoader:
video_taes = ["taehv", "lighttaew2_2", "lighttaew2_1", "lighttaehy1_5", "taeltx_2"]
- image_taes = ["taesd", "taesdxl", "taesd3", "taef1"]
+ image_taes = ["taesd", "taesdxl", "taesd3", "taef1", "taef2"]
+
@staticmethod
def vae_list(s):
vaes = folder_paths.get_filename_list("vae")
approx_vaes = folder_paths.get_filename_list("vae_approx")
- sdxl_taesd_enc = False
- sdxl_taesd_dec = False
- sd1_taesd_enc = False
- sd1_taesd_dec = False
- sd3_taesd_enc = False
- sd3_taesd_dec = False
- f1_taesd_enc = False
- f1_taesd_dec = False
-
+ have_img_encoder, have_img_decoder = set(), set()
for v in approx_vaes:
- if v.startswith("taesd_decoder."):
- sd1_taesd_dec = True
- elif v.startswith("taesd_encoder."):
- sd1_taesd_enc = True
- elif v.startswith("taesdxl_decoder."):
- sdxl_taesd_dec = True
- elif v.startswith("taesdxl_encoder."):
- sdxl_taesd_enc = True
- elif v.startswith("taesd3_decoder."):
- sd3_taesd_dec = True
- elif v.startswith("taesd3_encoder."):
- sd3_taesd_enc = True
- elif v.startswith("taef1_encoder."):
- f1_taesd_dec = True
- elif v.startswith("taef1_decoder."):
- f1_taesd_enc = True
- else:
+ parts = v.split("_", 1)
+ if len(parts) != 2 or parts[0] not in s.image_taes:
for tae in s.video_taes:
if v.startswith(tae):
vaes.append(v)
-
- if sd1_taesd_dec and sd1_taesd_enc:
- vaes.append("taesd")
- if sdxl_taesd_dec and sdxl_taesd_enc:
- vaes.append("taesdxl")
- if sd3_taesd_dec and sd3_taesd_enc:
- vaes.append("taesd3")
- if f1_taesd_dec and f1_taesd_enc:
- vaes.append("taef1")
+ break
+ continue
+ if parts[1].startswith("encoder."):
+ have_img_encoder.add(parts[0])
+ elif parts[1].startswith("decoder."):
+ have_img_decoder.add(parts[0])
+ vaes += [k for k in have_img_decoder if k in have_img_encoder]
vaes.append("pixel_space")
return vaes
@@ -901,6 +880,11 @@ class VAELoader:
else:
vae_path = folder_paths.get_full_path_or_raise("vae", vae_name)
sd, metadata = comfy.utils.load_torch_file(vae_path, return_metadata=True)
+ if vae_name == "taef2":
+ if metadata is None:
+ metadata = {"tae_latent_channels": 128}
+ else:
+ metadata["tae_latent_channels"] = 128
resolved = comfy.model_management.resolve_gpu_device_option(device)
vae = comfy.sd.VAE(sd=sd, metadata=metadata, device=resolved)
vae.throw_exception_if_invalid()
@@ -1066,7 +1050,7 @@ class CLIPLoader:
@classmethod
def INPUT_TYPES(s):
return {"required": { "clip_name": (folder_paths.get_filename_list("text_encoders"), ),
- "type": (["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "lumina2", "wan", "hidream", "chroma", "ace", "omnigen2", "qwen_image", "hunyuan_image", "flux2", "ovis", "longcat_image"], ),
+ "type": (["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "lumina2", "wan", "hidream", "chroma", "ace", "omnigen2", "qwen_image", "hunyuan_image", "flux2", "ovis", "longcat_image", "cogvideox"], ),
},
"optional": {
"device": (comfy.model_management.get_gpu_device_options(), {"advanced": True}),
@@ -1076,7 +1060,7 @@ class CLIPLoader:
CATEGORY = "advanced/loaders"
- DESCRIPTION = "[Recipes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 xxl/ clip-g / clip-l\nstable_audio: t5 base\nmochi: t5 xxl\ncosmos: old t5 xxl\nlumina2: gemma 2 2B\nwan: umt5 xxl\n hidream: llama-3.1 (Recommend) or t5\nomnigen2: qwen vl 2.5 3B"
+ DESCRIPTION = "[Recipes]\n\nstable_diffusion: clip-l\nstable_cascade: clip-g\nsd3: t5 xxl/ clip-g / clip-l\nstable_audio: t5 base\nmochi: t5 xxl\ncogvideox: t5 xxl (226-token padding)\ncosmos: old t5 xxl\nlumina2: gemma 2 2B\nwan: umt5 xxl\n hidream: llama-3.1 (Recommend) or t5\nomnigen2: qwen vl 2.5 3B"
@classmethod
def VALIDATE_INPUTS(cls, device="default"):
@@ -1345,7 +1329,7 @@ class LatentFromBatch:
@classmethod
def INPUT_TYPES(s):
return {"required": { "samples": ("LATENT",),
- "batch_index": ("INT", {"default": 0, "min": 0, "max": 63}),
+ "batch_index": ("INT", {"default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION}),
"length": ("INT", {"default": 1, "min": 1, "max": 64}),
}}
RETURN_TYPES = ("LATENT",)
@@ -1356,7 +1340,9 @@ class LatentFromBatch:
def frombatch(self, samples, batch_index, length):
s = samples.copy()
s_in = samples["samples"]
- batch_index = min(s_in.shape[0] - 1, batch_index)
+ if batch_index < 0:
+ batch_index += s_in.shape[0]
+ batch_index = max(0, min(s_in.shape[0] - 1, batch_index))
length = min(s_in.shape[0] - batch_index, length)
s["samples"] = s_in[batch_index:batch_index + length].clone()
if "noise_mask" in samples:
@@ -1567,7 +1553,7 @@ class LatentBlend:
RETURN_TYPES = ("LATENT",)
FUNCTION = "blend"
- CATEGORY = "_for_testing"
+ CATEGORY = "experimental"
def blend(self, samples1, samples2, blend_factor:float, blend_mode: str="normal"):
@@ -1644,7 +1630,7 @@ class SetLatentNoiseMask:
def common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0, disable_noise=False, start_step=None, last_step=None, force_full_denoise=False):
latent_image = latent["samples"]
- 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))
if disable_noise:
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
@@ -1663,6 +1649,7 @@ def common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive,
force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=seed)
out = latent.copy()
out.pop("downscale_ratio_spacial", None)
+ out.pop("downscale_ratio_temporal", None)
out["samples"] = samples
return (out, )
@@ -1818,22 +1805,27 @@ class LoadImage:
RETURN_TYPES = ("IMAGE", "MASK")
FUNCTION = "load_image"
+
def load_image(self, image):
image_path = folder_paths.get_annotated_filepath(image)
+ dtype = comfy.model_management.intermediate_dtype()
+ device = comfy.model_management.intermediate_device()
+
+ components = InputImpl.VideoFromFile(image_path).get_components()
+ if components.images.shape[0] > 0:
+ return (components.images.to(device=device, dtype=dtype), (1.0 - components.alpha[..., -1]).to(device=device, dtype=dtype) if components.alpha is not None else torch.zeros((components.images.shape[0], 64, 64), dtype=dtype, device=device))
+
+ # This code is left here to handle animated webp which pyav does not support loading
img = node_helpers.pillow(Image.open, image_path)
output_images = []
output_masks = []
w, h = None, None
- dtype = comfy.model_management.intermediate_dtype()
-
for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i)
- if i.mode == 'I':
- i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB")
if len(output_images) == 0:
@@ -1848,25 +1840,15 @@ class LoadImage:
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
- elif i.mode == 'P' and 'transparency' in i.info:
- mask = np.array(i.convert('RGBA').getchannel('A')).astype(np.float32) / 255.0
- mask = 1. - torch.from_numpy(mask)
else:
- mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
+ mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
output_images.append(image.to(dtype=dtype))
output_masks.append(mask.unsqueeze(0).to(dtype=dtype))
- if img.format == "MPO":
- break # ignore all frames except the first one for MPO format
+ output_image = torch.cat(output_images, dim=0)
+ output_mask = torch.cat(output_masks, dim=0)
- if len(output_images) > 1:
- output_image = torch.cat(output_images, dim=0)
- output_mask = torch.cat(output_masks, dim=0)
- else:
- output_image = output_images[0]
- output_mask = output_masks[0]
-
- return (output_image, output_mask)
+ return (output_image.to(device=device, dtype=dtype), output_mask.to(device=device, dtype=dtype))
@classmethod
def IS_CHANGED(s, image):
@@ -1883,57 +1865,49 @@ class LoadImage:
return True
-class LoadImageMask:
+
+class LoadImageMask(LoadImage):
ESSENTIALS_CATEGORY = "Image Tools"
SEARCH_ALIASES = ["import mask", "alpha mask", "channel mask"]
_color_channels = ["alpha", "red", "green", "blue"]
+
@classmethod
def INPUT_TYPES(s):
- input_dir = folder_paths.get_input_directory()
- files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
- return {"required":
- {"image": (sorted(files), {"image_upload": True}),
- "channel": (s._color_channels, ), }
- }
-
- CATEGORY = "mask"
+ types = super().INPUT_TYPES()
+ return {
+ "required": {
+ **types["required"],
+ "channel": (s._color_channels, )
+ }
+ }
+ CATEGORY = "image"
RETURN_TYPES = ("MASK",)
- FUNCTION = "load_image"
- def load_image(self, image, channel):
- image_path = folder_paths.get_annotated_filepath(image)
- i = node_helpers.pillow(Image.open, image_path)
- i = node_helpers.pillow(ImageOps.exif_transpose, i)
- if i.getbands() != ("R", "G", "B", "A"):
- if i.mode == 'I':
- i = i.point(lambda i: i * (1 / 255))
- i = i.convert("RGBA")
- mask = None
+ FUNCTION = "load_image_mask"
+
+ def load_image_mask(self, image, channel):
+ image_tensor, mask_tensor = super().load_image(image)
c = channel[0].upper()
- if c in i.getbands():
- mask = np.array(i.getchannel(c)).astype(np.float32) / 255.0
- mask = torch.from_numpy(mask)
- if c == 'A':
- mask = 1. - mask
+
+ if c == 'A':
+ return (mask_tensor,)
+
+ channel_idx = {'R': 0, 'G': 1, 'B': 2}.get(c, 0)
+
+ if channel_idx < image_tensor.shape[-1]:
+ return (image_tensor[..., channel_idx].clone(),)
else:
- mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
- return (mask.unsqueeze(0),)
+ empty_mask = torch.zeros(
+ image_tensor.shape[:-1],
+ dtype=image_tensor.dtype,
+ device=image_tensor.device
+ )
+ return (empty_mask,)
@classmethod
def IS_CHANGED(s, image, channel):
- image_path = folder_paths.get_annotated_filepath(image)
- m = hashlib.sha256()
- with open(image_path, 'rb') as f:
- m.update(f.read())
- return m.digest().hex()
-
- @classmethod
- def VALIDATE_INPUTS(s, image):
- if not folder_paths.exists_annotated_filepath(image):
- return "Invalid image file: {}".format(image)
-
- return True
+ return super().IS_CHANGED(image)
class LoadImageOutput(LoadImage):
@@ -2024,7 +1998,7 @@ class ImageInvert:
RETURN_TYPES = ("IMAGE",)
FUNCTION = "invert"
- CATEGORY = "image"
+ CATEGORY = "image/color"
def invert(self, image):
s = 1.0 - image
@@ -2040,7 +2014,7 @@ class ImageBatch:
RETURN_TYPES = ("IMAGE",)
FUNCTION = "batch"
- CATEGORY = "image"
+ CATEGORY = "image/batch"
DEPRECATED = True
def batch(self, image1, image2):
@@ -2097,7 +2071,7 @@ class ImagePadForOutpaint:
RETURN_TYPES = ("IMAGE", "MASK")
FUNCTION = "expand_image"
- CATEGORY = "image"
+ CATEGORY = "image/transform"
def expand_image(self, image, left, top, right, bottom, feathering):
d1, d2, d3, d4 = image.size()
@@ -2231,6 +2205,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"StyleModelLoader": "Load Style Model",
"CLIPVisionLoader": "Load CLIP Vision",
"UNETLoader": "Load Diffusion Model",
+ "unCLIPCheckpointLoader": "Load unCLIP Checkpoint",
+ "GLIGENLoader": "Load GLIGEN Model",
# Conditioning
"CLIPVisionEncode": "CLIP Vision Encode",
"StyleModelApply": "Apply Style Model",
@@ -2242,7 +2218,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"ConditioningSetArea": "Conditioning (Set Area)",
"ConditioningSetAreaPercentage": "Conditioning (Set Area with Percentage)",
"ConditioningSetMask": "Conditioning (Set Mask)",
- "ControlNetApply": "Apply ControlNet (OLD)",
+ "ControlNetApply": "Apply ControlNet (DEPRECATED)",
"ControlNetApplyAdvanced": "Apply ControlNet",
# Latent
"VAEEncodeForInpaint": "VAE Encode (for Inpainting)",
@@ -2260,6 +2236,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"LatentFromBatch" : "Latent From Batch",
"RepeatLatentBatch": "Repeat Latent Batch",
# Image
+ "EmptyImage": "Empty Image",
"SaveImage": "Save Image",
"PreviewImage": "Preview Image",
"LoadImage": "Load Image",
@@ -2267,18 +2244,18 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"LoadImageOutput": "Load Image (from Outputs)",
"ImageScale": "Upscale Image",
"ImageScaleBy": "Upscale Image By",
- "ImageInvert": "Invert Image",
+ "ImageInvert": "Invert Image Colors",
"ImagePadForOutpaint": "Pad Image for Outpainting",
- "ImageBatch": "Batch Images",
- "ImageCrop": "Image Crop",
- "ImageStitch": "Image Stitch",
- "ImageBlend": "Image Blend",
- "ImageBlur": "Image Blur",
- "ImageQuantize": "Image Quantize",
- "ImageSharpen": "Image Sharpen",
+ "ImageBatch": "Batch Images (DEPRECATED)",
+ "ImageCrop": "Crop Image",
+ "ImageStitch": "Stitch Images",
+ "ImageBlend": "Blend Images",
+ "ImageBlur": "Blur Image",
+ "ImageQuantize": "Quantize Image",
+ "ImageSharpen": "Sharpen Image",
"ImageScaleToTotalPixels": "Scale Image to Total Pixels",
"GetImageSize": "Get Image Size",
- # _for_testing
+ # experimental
"VAEDecodeTiled": "VAE Decode (Tiled)",
"VAEEncodeTiled": "VAE Encode (Tiled)",
}
@@ -2400,7 +2377,7 @@ async def load_custom_node(module_path: str, ignore=set(), module_parent="custom
logging.warning(f"Error while calling comfy_entrypoint in {module_path}: {e}")
return False
else:
- logging.warning(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS or NODES_LIST (need one).")
+ logging.warning(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS or comfy_entrypoint (need one).")
return False
except Exception as e:
logging.warning(traceback.format_exc())
@@ -2551,6 +2528,7 @@ async def init_builtin_extra_nodes():
"nodes_nop.py",
"nodes_kandinsky5.py",
"nodes_wanmove.py",
+ "nodes_ar_video.py",
"nodes_image_compare.py",
"nodes_zimage.py",
"nodes_glsl.py",
@@ -2565,9 +2543,15 @@ async def init_builtin_extra_nodes():
"nodes_number_convert.py",
"nodes_painter.py",
"nodes_curve.py",
+ "nodes_bg_removal.py",
"nodes_rtdetr.py",
"nodes_frame_interpolation.py",
- "nodes_sam3.py"
+ "nodes_sam3.py",
+ "nodes_void.py",
+ "nodes_wandancer.py",
+ "nodes_hidream_o1.py",
+ "nodes_save_3d.py",
+ "nodes_moge.py",
]
import_failed = []
diff --git a/openapi.yaml b/openapi.yaml
new file mode 100644
index 000000000..2658b9b86
--- /dev/null
+++ b/openapi.yaml
@@ -0,0 +1,8125 @@
+openapi: 3.1.0
+info:
+ title: ComfyUI API
+ description: |
+ API for ComfyUI - A powerful and modular stable diffusion GUI and backend.
+
+ This API allows you to interact with ComfyUI programmatically, including:
+ - Submitting and managing workflow executions
+ - Querying node/object information
+ - Uploading and viewing files
+ - Managing user settings and data
+ - Asset management (feature-gated)
+
+ ## Dual-path routing
+ Every route registered via `self.routes` in the ComfyUI server is available at
+ both its bare path (e.g. `/prompt`) and an `/api`-prefixed path (e.g. `/api/prompt`).
+ This spec uses the `/api`-prefixed versions as canonical.
+
+ ## Multi-user mode
+ When ComfyUI is started with `--multi-user`, the `Comfy-User` header identifies
+ the active user for settings, userdata, and history isolation. This is **not** a
+ security mechanism — it is an organisational convenience with no authentication
+ or authorisation behind it.
+ version: 1.0.0
+ license:
+ name: GNU General Public License v3.0
+ url: https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE
+
+servers:
+ - url: /
+ description: Default ComfyUI server (typically http://127.0.0.1:8188)
+
+tags:
+ - name: prompt
+ description: Workflow submission and prompt info
+ - name: queue
+ description: Queue inspection and management
+ - name: history
+ description: Execution history
+ - name: upload
+ description: File upload endpoints
+ - name: view
+ description: File viewing / download
+ - name: system
+ description: System stats and feature flags
+ - name: node
+ description: Node / object_info definitions
+ - name: model
+ description: Model folder and file listing
+ - name: user
+ description: User management (multi-user mode)
+ - name: userdata
+ description: Per-user file storage
+ - name: settings
+ description: Per-user settings
+ - name: extensions
+ description: Frontend extension JS files
+ - name: subgraph
+ description: Global subgraph blueprints
+ - name: internal
+ description: Internal / debug endpoints
+ - name: assets
+ description: Asset management (feature-gated behind enable-assets)
+
+ - name: auth
+ description: Authentication and session management (cloud-only)
+ - name: billing
+ description: Billing, subscriptions, and payment management (cloud-only)
+ - name: workspace
+ description: Workspace and team management (cloud-only)
+ - name: hub
+ description: "ComfyUI Hub: profiles, shared workflows, and labels (cloud-only)"
+ - name: workflows
+ description: Cloud workflow management and versioning (cloud-only)
+ - name: task
+ description: Background task management (cloud-only)
+ - name: runtime-only
+ description: Operations served exclusively by the cloud runtime with no local equivalent
+
+paths:
+ # ---------------------------------------------------------------------------
+ # WebSocket
+ # ---------------------------------------------------------------------------
+ /ws:
+ get:
+ operationId: connectWebSocket
+ tags: [system]
+ summary: WebSocket connection for real-time updates
+ description: |
+ Upgrades to a WebSocket connection that streams execution progress,
+ node status, and output messages. The server sends an initial `status`
+ message with the session ID (SID) on connect.
+
+ ## Message types (server → client)
+ The server sends JSON messages with a `type` field. See the
+ `x-websocket-messages` list below for the schema of each message type.
+ parameters:
+ - name: clientId
+ in: query
+ required: false
+ schema:
+ type: string
+ description: Client identifier. If omitted the server assigns one.
+ responses:
+ "101":
+ description: WebSocket upgrade successful
+ x-websocket-messages:
+ - type: status
+ schema:
+ $ref: "#/components/schemas/StatusWsMessage"
+ - type: progress
+ schema:
+ $ref: "#/components/schemas/ProgressWsMessage"
+ - type: progress_text
+ schema:
+ $ref: "#/components/schemas/ProgressTextWsMessage"
+ - type: progress_state
+ schema:
+ $ref: "#/components/schemas/ProgressStateWsMessage"
+ - type: executing
+ schema:
+ $ref: "#/components/schemas/ExecutingWsMessage"
+ - type: executed
+ schema:
+ $ref: "#/components/schemas/ExecutedWsMessage"
+ - type: execution_start
+ schema:
+ $ref: "#/components/schemas/ExecutionStartWsMessage"
+ - type: execution_success
+ schema:
+ $ref: "#/components/schemas/ExecutionSuccessWsMessage"
+ - type: execution_cached
+ schema:
+ $ref: "#/components/schemas/ExecutionCachedWsMessage"
+ - type: execution_interrupted
+ schema:
+ $ref: "#/components/schemas/ExecutionInterruptedWsMessage"
+ - type: execution_error
+ schema:
+ $ref: "#/components/schemas/ExecutionErrorWsMessage"
+ - type: logs
+ schema:
+ $ref: "#/components/schemas/LogsWsMessage"
+ - type: notification
+ schema:
+ $ref: "#/components/schemas/NotificationWsMessage"
+ - type: feature_flags
+ schema:
+ $ref: "#/components/schemas/FeatureFlagsWsMessage"
+ - type: asset_download
+ schema:
+ $ref: "#/components/schemas/AssetDownloadWsMessage"
+ - type: asset_export
+ schema:
+ $ref: "#/components/schemas/AssetExportWsMessage"
+
+ # ---------------------------------------------------------------------------
+ # Prompt
+ # ---------------------------------------------------------------------------
+ /api/prompt:
+ get:
+ operationId: getPromptInfo
+ tags: [prompt]
+ summary: Get queue status
+ description: Returns how many items remain in the execution queue.
+ responses:
+ "200":
+ description: Queue info
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PromptInfo"
+ post:
+ operationId: executePrompt
+ tags: [prompt]
+ summary: Submit a workflow for execution
+ description: Submits a workflow for execution. The server validates the graph, assigns a `prompt_id`, and enqueues it. Clients listen on `/ws` for execution progress and output messages.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PromptRequest"
+ responses:
+ "200":
+ description: Prompt accepted
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PromptResponse"
+ "400":
+ description: Validation or node errors
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PromptErrorResponse"
+
+ # ---------------------------------------------------------------------------
+ # Queue
+ # ---------------------------------------------------------------------------
+ /api/queue:
+ get:
+ operationId: getQueue
+ tags: [queue]
+ summary: Get running and pending queue items
+ description: Returns the server's current execution queue, split into the currently-running prompt and the list of pending prompts.
+ responses:
+ "200":
+ description: Queue contents
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/QueueInfo"
+ post:
+ operationId: manageQueue
+ tags: [queue]
+ summary: Clear or delete items from the queue
+ description: Mutates the execution queue. Supports clearing all queued prompts or deleting individual prompts by ID.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/QueueManageRequest"
+ responses:
+ "200":
+ description: Queue updated
+
+ /api/interrupt:
+ post:
+ operationId: interruptExecution
+ tags: [queue]
+ summary: Interrupt current execution
+ description: Interrupts the prompt that is currently executing. The next queued prompt (if any) will start immediately after.
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ prompt_id:
+ type: string
+ format: uuid
+ description: "If provided, only interrupts this specific running prompt. Otherwise interrupts all."
+ responses:
+ "200":
+ description: Interrupt signal sent
+
+ /api/free:
+ post:
+ operationId: freeMemory
+ tags: [queue]
+ summary: Free GPU memory and/or unload models
+ description: Frees GPU memory by unloading models and/or freeing the resident model cache, controlled by the request flags.
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ unload_models:
+ type: boolean
+ description: Unload all models from VRAM/RAM
+ free_memory:
+ type: boolean
+ description: Run garbage collection and free cached memory
+ responses:
+ "200":
+ description: Memory freed
+
+ # ---------------------------------------------------------------------------
+ # Jobs
+ # ---------------------------------------------------------------------------
+ /api/jobs:
+ get:
+ operationId: listJobs
+ tags: [queue]
+ summary: List jobs with filtering and pagination
+ description: Returns a paginated list of completed prompt executions, newest first.
+ parameters:
+ - name: status
+ in: query
+ schema:
+ type: string
+ description: Filter by job status
+ - name: workflow_id
+ in: query
+ schema:
+ type: string
+ description: Filter by workflow ID
+ - name: sort_by
+ in: query
+ schema:
+ type: string
+ description: Field to sort by
+ - name: sort_order
+ in: query
+ schema:
+ type: string
+ enum: [asc, desc]
+ description: Sort direction
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ description: Maximum number of results (default is unlimited/None)
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ default: 0
+ description: Pagination offset
+ responses:
+ "200":
+ description: Jobs list
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ jobs:
+ type: array
+ items:
+ $ref: "#/components/schemas/JobEntry"
+ pagination:
+ $ref: "#/components/schemas/PaginationInfo"
+
+ /api/jobs/{job_id}:
+ get:
+ operationId: getJob
+ tags: [queue]
+ summary: Get a single job by ID
+ description: Returns the full record for a single completed prompt execution, including its outputs, status, and metadata.
+ parameters:
+ - name: job_id
+ in: path
+ description: The job (prompt) ID to fetch.
+ required: true
+ schema:
+ type: string
+ format: uuid
+ responses:
+ "200":
+ description: Job detail
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/JobDetailResponse"
+ "404":
+ description: Job not found
+
+ # ---------------------------------------------------------------------------
+ # History
+ # ---------------------------------------------------------------------------
+ /api/history:
+ get:
+ operationId: getHistory
+ tags: [history]
+ summary: Get execution history
+ deprecated: true
+ description: |
+ **Deprecated.** Superseded by `GET /api/jobs`, which returns the same
+ execution records in a paginated, filterable format. Planned for removal
+ no earlier than a future major release; sunset timeline TBD.
+
+ Returns a dictionary keyed by prompt_id. Each value is a HistoryEntry
+ containing prompt metadata, outputs, status, and node meta.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ - name: max_items
+ in: query
+ schema:
+ type: integer
+ description: Maximum number of history entries to return
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ description: Pagination offset (number of entries to skip)
+ responses:
+ "200":
+ description: History dictionary keyed by prompt_id
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ $ref: "#/components/schemas/HistoryEntry"
+ post:
+ operationId: manageHistory
+ tags: [history]
+ summary: Clear or delete history entries
+ deprecated: true
+ description: |
+ **Deprecated.** Superseded by the forthcoming job-management endpoints
+ under `/api/jobs`. Planned for removal no earlier than a future major
+ release; sunset timeline TBD.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HistoryManageRequest"
+ responses:
+ "200":
+ description: History updated
+
+ /api/history/{prompt_id}:
+ get:
+ operationId: getHistoryByPromptId
+ tags: [history]
+ summary: Get history for a specific prompt
+ deprecated: true
+ description: |
+ **Deprecated.** Superseded by `GET /api/jobs/{job_id}`, which returns
+ the same execution record. Planned for removal no earlier than a future
+ major release; sunset timeline TBD.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ - name: prompt_id
+ in: path
+ description: The prompt ID to fetch history for.
+ required: true
+ schema:
+ type: string
+ format: uuid
+ responses:
+ "200":
+ description: Single-entry history dictionary. Returns an empty object `{}` if the prompt_id is not found.
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ $ref: "#/components/schemas/HistoryEntry"
+
+ # ---------------------------------------------------------------------------
+ # Upload
+ # ---------------------------------------------------------------------------
+ /api/upload/image:
+ post:
+ operationId: uploadImage
+ tags: [upload]
+ summary: Upload an image file
+ description: Uploads an image file into one of the input/output/temp directories so it can be referenced by workflow nodes.
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ required:
+ - image
+ properties:
+ image:
+ type: string
+ format: binary
+ description: Image file to upload
+ type:
+ type: string
+ enum: [input, temp, output]
+ default: input
+ description: Target directory type
+ overwrite:
+ type: string
+ description: 'Set to "true" to overwrite existing files'
+ subfolder:
+ type: string
+ description: Subfolder within the target directory
+ responses:
+ "200":
+ description: Upload result
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UploadResult"
+ "400":
+ description: No file provided or invalid request
+
+ /api/upload/mask:
+ post:
+ operationId: uploadMask
+ tags: [upload]
+ deprecated: true
+ summary: Upload a mask image (deprecated)
+ description: |
+ Deprecated. Clients should composite the mask onto the source image
+ client-side and upload the resulting image via POST /api/upload/image
+ instead. This endpoint will continue to function for older clients,
+ but will not receive new features.
+
+ Uploads a mask image associated with a previously-uploaded reference image.
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ required:
+ - image
+ - original_ref
+ properties:
+ image:
+ type: string
+ format: binary
+ description: Mask image (alpha channel is used)
+ original_ref:
+ type: object
+ description: Reference to the original image file
+ required:
+ - filename
+ properties:
+ filename:
+ type: string
+ description: Filename of the original image
+ additionalProperties: true
+ type:
+ type: string
+ enum: [input, temp, output]
+ default: input
+ description: Target directory type
+ overwrite:
+ type: string
+ description: 'Set to "true" to overwrite existing files'
+ subfolder:
+ type: string
+ description: Subfolder within the target directory
+ responses:
+ "200":
+ description: Upload result
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UploadResult"
+ "400":
+ description: No file provided or invalid request
+
+ # ---------------------------------------------------------------------------
+ # View
+ # ---------------------------------------------------------------------------
+ /api/view:
+ get:
+ operationId: viewFile
+ tags: [view]
+ summary: View or download a file
+ description: Serves a file (image, audio, or video) from the input/output/temp directory identified by the query parameters.
+ parameters:
+ - name: filename
+ in: query
+ required: true
+ schema:
+ type: string
+ description: Name of the file to view
+ - name: type
+ in: query
+ schema:
+ type: string
+ enum: [input, output, temp]
+ default: output
+ description: Directory type
+ - name: subfolder
+ in: query
+ schema:
+ type: string
+ description: Subfolder within the directory
+ - name: preview
+ in: query
+ schema:
+ type: string
+ description: Preview format hint (e.g. "webp;90")
+ - name: channel
+ in: query
+ schema:
+ type: string
+ enum: [rgba, rgb, a]
+ description: Channel extraction mode
+ responses:
+ "200":
+ description: File content
+ content:
+ image/*:
+ schema:
+ type: string
+ format: binary
+ video/*:
+ schema:
+ type: string
+ format: binary
+ audio/*:
+ schema:
+ type: string
+ format: binary
+ application/octet-stream:
+ schema:
+ type: string
+ format: binary
+ "404":
+ description: File not found
+
+ /api/view_metadata/{folder_name}:
+ get:
+ operationId: viewMetadata
+ tags: [view]
+ summary: Get metadata for a file (e.g. safetensors header)
+ description: Returns embedded metadata parsed from a file in the given folder — for example, the header of a safetensors model.
+ parameters:
+ - name: folder_name
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Folder type (output, input, temp, etc.)
+ - name: filename
+ in: query
+ required: true
+ schema:
+ type: string
+ description: Filename to read metadata from
+ responses:
+ "200":
+ description: File metadata
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties: true
+ "404":
+ description: File or metadata not found
+
+ # ---------------------------------------------------------------------------
+ # System
+ # ---------------------------------------------------------------------------
+ /api/system_stats:
+ get:
+ operationId: getSystemStats
+ tags: [system]
+ summary: Get system statistics
+ description: Returns hardware, Python, VRAM, and runtime statistics for the running ComfyUI process.
+ responses:
+ "200":
+ description: System stats
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SystemStatsResponse"
+
+ /api/features:
+ get:
+ operationId: getFeatures
+ tags: [system]
+ summary: Get enabled feature flags
+ description: Returns a dictionary of feature flag names to their enabled state. Cloud deployments may include additional typed fields alongside the boolean flags.
+ responses:
+ "200":
+ description: Feature flags
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ type: boolean
+ properties:
+ max_upload_size:
+ type: integer
+ format: int64
+ minimum: 0
+ description: "Maximum file upload size in bytes."
+ free_tier_credits:
+ type: integer
+ format: int32
+ minimum: 0
+ nullable: true
+ x-runtime: [cloud]
+ description: "[cloud-only] Credits available to free-tier users. Local ComfyUI returns null."
+ posthog_api_host:
+ type: string
+ format: uri
+ nullable: true
+ x-runtime: [cloud]
+ description: "[cloud-only] PostHog analytics proxy URL for frontend telemetry. Local ComfyUI returns null."
+ max_concurrent_jobs:
+ type: integer
+ format: int32
+ minimum: 0
+ nullable: true
+ x-runtime: [cloud]
+ description: "[cloud-only] Maximum concurrent jobs the authenticated user can run. Local ComfyUI returns null."
+ workflow_templates_version:
+ type: string
+ nullable: true
+ x-runtime: [cloud]
+ description: "[cloud-only] Version identifier for the workflow templates bundle. Local ComfyUI returns null."
+ workflow_templates_source:
+ type: string
+ nullable: true
+ enum: [dynamic_config_override, workflow_templates_version_json]
+ x-runtime: [cloud]
+ description: "[cloud-only] How the templates version was resolved. Local ComfyUI returns null."
+
+ # ---------------------------------------------------------------------------
+ # Node / Object Info
+ # ---------------------------------------------------------------------------
+ /api/object_info:
+ get:
+ operationId: getObjectInfo
+ tags: [node]
+ summary: Get all node definitions
+ description: |
+ Returns a dictionary of every registered node class, keyed by class name.
+ Each value is a NodeInfo object describing inputs, outputs, category, etc.
+ responses:
+ "200":
+ description: All node definitions
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ $ref: "#/components/schemas/NodeInfo"
+
+ /api/object_info/{node_class}:
+ get:
+ operationId: getObjectInfoByClass
+ tags: [node]
+ summary: Get a single node definition
+ description: Returns the `NodeInfo` definition for a single registered node class.
+ parameters:
+ - name: node_class
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Node class name (e.g. "KSampler")
+ responses:
+ "200":
+ description: Single node definition
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ $ref: "#/components/schemas/NodeInfo"
+ "404":
+ description: Node class not found
+
+ /api/embeddings:
+ get:
+ operationId: getEmbeddings
+ tags: [node]
+ summary: List available embedding names
+ description: Returns the list of text-encoder embeddings available on disk.
+ responses:
+ "200":
+ description: Embedding names
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: string
+
+ # ---------------------------------------------------------------------------
+ # Models
+ # ---------------------------------------------------------------------------
+ /api/models:
+ get:
+ operationId: getModelTypes
+ tags: [model]
+ summary: List model folder type names
+ description: Returns an array of model type names (e.g. checkpoints, loras, vae).
+ responses:
+ "200":
+ description: Model type names
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: string
+
+ /api/models/{folder}:
+ get:
+ operationId: getModelsByFolder
+ tags: [model]
+ summary: List model filenames in a folder
+ description: Returns the names of model files in the given folder. This endpoint predates `/api/experiment/models/{folder}` and returns names only — prefer the experiment endpoint for new integrations.
+ parameters:
+ - name: folder
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Model folder type name
+ responses:
+ "200":
+ description: Model filenames
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: string
+ "404":
+ description: Unknown folder type
+
+ /api/experiment/models:
+ get:
+ operationId: getExperimentModels
+ tags: [model]
+ summary: List model folders with paths
+ description: Returns an array of model folder objects with name and folder paths.
+ responses:
+ "200":
+ description: Model folders
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/ModelFolder"
+
+ /api/experiment/models/{folder}:
+ get:
+ operationId: getExperimentModelsByFolder
+ tags: [model]
+ summary: List model files with metadata
+ description: Returns the model files in the given folder with richer metadata (path index, mtime, size) than the legacy `/api/models/{folder}` endpoint.
+ parameters:
+ - name: folder
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Model folder type name
+ responses:
+ "200":
+ description: Model files with metadata
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/ModelFile"
+ "404":
+ description: Unknown folder type
+
+ /api/experiment/models/preview/{folder}/{path_index}/{filename}:
+ get:
+ operationId: getModelPreview
+ tags: [model]
+ summary: Get model preview image
+ description: Returns the preview image associated with a model file, if one exists alongside the model on disk.
+ parameters:
+ - name: folder
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Model folder type name
+ - name: path_index
+ in: path
+ required: true
+ schema:
+ type: integer
+ description: Path index within the folder
+ - name: filename
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Model filename
+ responses:
+ "200":
+ description: Preview image (WebP)
+ content:
+ image/webp:
+ schema:
+ type: string
+ format: binary
+ "404":
+ description: Preview not found
+
+ # ---------------------------------------------------------------------------
+ # Users
+ # ---------------------------------------------------------------------------
+ /api/users:
+ get:
+ operationId: getUsers
+ tags: [user]
+ summary: Get user storage info
+ description: |
+ Returns user storage configuration. In single-user mode returns
+ `{"storage": "server", "migrated": true/false}`. In multi-user mode
+ returns `{"storage": "server", "users": {"user_id": "user_dir", ...}}`.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ responses:
+ "200":
+ description: User info
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ storage:
+ type: string
+ description: Storage backend type (always "server")
+ migrated:
+ type: boolean
+ description: Whether migration from browser storage is complete (single-user)
+ users:
+ type: object
+ additionalProperties:
+ type: string
+ description: Map of user_id to directory name (multi-user)
+ post:
+ operationId: createUser
+ tags: [user]
+ summary: Create a new user (multi-user mode)
+ description: Creates a new user entry. Only meaningful when ComfyUI is running in multi-user mode.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - username
+ properties:
+ username:
+ type: string
+ description: Username for the new user
+ responses:
+ "200":
+ description: Created user ID
+ content:
+ application/json:
+ schema:
+ type: string
+ description: The generated user_id
+ "400":
+ description: Username already exists or invalid
+
+ # ---------------------------------------------------------------------------
+ # Userdata
+ # ---------------------------------------------------------------------------
+ /api/userdata:
+ get:
+ operationId: listUserdata
+ tags: [userdata]
+ summary: List files in a userdata directory
+ description: Lists files in the authenticated user's data directory. Returns either filename strings or full objects depending on the `full_info` query parameter.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ - name: dir
+ in: query
+ required: true
+ schema:
+ type: string
+ description: Directory path relative to the user's data folder
+ - name: recurse
+ in: query
+ schema:
+ type: boolean
+ description: Recurse into subdirectories
+ - name: full_info
+ in: query
+ schema:
+ type: boolean
+ description: Return full file info objects instead of just names
+ - name: split
+ in: query
+ schema:
+ type: boolean
+ description: Split paths into directory components
+ responses:
+ "200":
+ description: File listing
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ListUserdataResponse"
+ "404":
+ description: Directory not found
+
+ /api/v2/userdata:
+ get:
+ operationId: listUserdataV2
+ tags: [userdata]
+ summary: List files in userdata (v2 format)
+ description: Lists files in the authenticated user's data directory using the v2 response shape, which always returns full objects.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ - name: path
+ in: query
+ schema:
+ type: string
+ description: Directory path relative to user data root
+ responses:
+ "200":
+ description: File listing with metadata
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: object
+ properties:
+ name:
+ type: string
+ path:
+ type: string
+ type:
+ type: string
+ enum: [file, directory]
+ size:
+ type: integer
+ modified:
+ type: number
+ description: Unix timestamp
+
+ /api/userdata/{file}:
+ get:
+ operationId: getUserdataFile
+ tags: [userdata]
+ summary: Read a userdata file
+ description: Reads the contents of a file from the authenticated user's data directory.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ - name: file
+ in: path
+ required: true
+ schema:
+ type: string
+ description: File path relative to user data directory
+ responses:
+ "200":
+ description: File content
+ content:
+ application/octet-stream:
+ schema:
+ type: string
+ format: binary
+ "404":
+ description: File not found
+ post:
+ operationId: writeUserdataFile
+ tags: [userdata]
+ summary: Write or create a userdata file
+ description: Writes (creates or replaces) a file in the authenticated user's data directory.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ - name: file
+ in: path
+ required: true
+ schema:
+ type: string
+ description: File path relative to user data directory
+ - name: overwrite
+ in: query
+ schema:
+ type: boolean
+ description: Allow overwriting existing files
+ - name: full_info
+ in: query
+ schema:
+ type: boolean
+ description: Return full file info in response
+ requestBody:
+ required: true
+ content:
+ application/octet-stream:
+ schema:
+ type: string
+ format: binary
+ application/json:
+ schema: {}
+ responses:
+ "200":
+ description: File written
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UserDataResponse"
+ "409":
+ description: File exists and overwrite not set
+ delete:
+ operationId: deleteUserdataFile
+ tags: [userdata]
+ summary: Delete a userdata file
+ description: Deletes a file from the authenticated user's data directory.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ - name: file
+ in: path
+ required: true
+ schema:
+ type: string
+ description: File path relative to user data directory
+ responses:
+ "204":
+ description: File deleted
+ "404":
+ description: File not found
+
+ /api/userdata/{file}/move/{dest}:
+ post:
+ operationId: moveUserdataFile
+ tags: [userdata]
+ summary: Move or rename a userdata file
+ description: Renames or moves a file within the authenticated user's data directory.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ - name: file
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Source file path
+ - name: dest
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Destination file path
+ - name: overwrite
+ in: query
+ schema:
+ type: boolean
+ description: Allow overwriting at destination
+ - name: full_info
+ in: query
+ schema:
+ type: boolean
+ description: Return full file info in response
+ responses:
+ "200":
+ description: File moved
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UserDataResponse"
+ "404":
+ description: Source file not found
+ "409":
+ description: Destination exists and overwrite not set
+
+ # ---------------------------------------------------------------------------
+ # Settings
+ # ---------------------------------------------------------------------------
+ /api/settings:
+ get:
+ operationId: getSettings
+ tags: [settings]
+ summary: Get all user settings
+ description: Returns all settings for the authenticated user.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ responses:
+ "200":
+ description: Settings object
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties: true
+ post:
+ operationId: updateSettings
+ tags: [settings]
+ summary: Update user settings (partial merge)
+ description: Replaces the authenticated user's settings with the provided object.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties: true
+ description: Partial settings to merge
+ responses:
+ "200":
+ description: Settings updated
+
+ /api/settings/{id}:
+ get:
+ operationId: getSetting
+ tags: [settings]
+ summary: Get a single setting by key
+ description: Returns the value of a single setting, identified by key.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Setting key
+ responses:
+ "200":
+ description: Setting value (null if the setting does not exist)
+ content:
+ application/json:
+ schema:
+ nullable: true
+ description: The setting value (any JSON type), or null if not set
+ post:
+ operationId: updateSetting
+ tags: [settings]
+ summary: Set a single setting value
+ description: Sets the value of a single setting, identified by key.
+ parameters:
+ - $ref: "#/components/parameters/ComfyUserHeader"
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Setting key
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ description: The setting value (any JSON type)
+ responses:
+ "200":
+ description: Setting updated
+
+ # ---------------------------------------------------------------------------
+ # Extensions / Templates / i18n
+ # ---------------------------------------------------------------------------
+ /api/extensions:
+ get:
+ operationId: getExtensions
+ tags: [extensions]
+ summary: List frontend extension JS file paths
+ description: Returns the list of frontend extension JS URLs registered by custom nodes, to be loaded by the frontend on startup.
+ responses:
+ "200":
+ description: Array of JS file paths
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: string
+ description: Relative path to extension JS file
+
+ /api/workflow_templates:
+ get:
+ operationId: getWorkflowTemplates
+ tags: [extensions]
+ summary: Get workflow template mappings
+ description: Returns a map of custom node names to their provided workflow template names.
+ responses:
+ "200":
+ description: Template mappings
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ type: string
+ description: Map of node pack name to array of template names
+
+ /api/i18n:
+ get:
+ operationId: getI18n
+ tags: [extensions]
+ summary: Get internationalisation translation strings
+ description: Returns the URLs of translation files contributed by custom nodes, keyed by locale.
+ responses:
+ "200":
+ description: Translation map
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties: true
+ description: Nested map of locale to translation key-value pairs
+
+ # ---------------------------------------------------------------------------
+ # Subgraphs
+ # ---------------------------------------------------------------------------
+ /api/global_subgraphs:
+ get:
+ operationId: getGlobalSubgraphs
+ tags: [subgraph]
+ summary: List global subgraph blueprints
+ description: Returns a dictionary of subgraph IDs to their metadata.
+ responses:
+ "200":
+ description: Subgraph metadata dictionary
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ $ref: "#/components/schemas/GlobalSubgraphInfo"
+
+ /api/global_subgraphs/{id}:
+ get:
+ operationId: getGlobalSubgraph
+ tags: [subgraph]
+ summary: Get a global subgraph with full data
+ description: Returns the blueprint for a globally-registered subgraph, used by the frontend to materialize the subgraph node.
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Subgraph identifier
+ responses:
+ "200":
+ description: Full subgraph data
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GlobalSubgraphData"
+ "404":
+ description: Subgraph not found
+
+ # ---------------------------------------------------------------------------
+ # Node Replacements
+ # ---------------------------------------------------------------------------
+ /api/node_replacements:
+ get:
+ operationId: getNodeReplacements
+ tags: [node]
+ summary: Get node replacement mappings
+ description: |
+ Returns a dictionary mapping deprecated or replaced node class names
+ to their replacement node information.
+ responses:
+ "200":
+ description: Replacement mappings
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties: true
+
+ # ---------------------------------------------------------------------------
+ # Internal (x-internal: true)
+ # ---------------------------------------------------------------------------
+ /internal/logs:
+ get:
+ operationId: getInternalLogs
+ tags: [internal]
+ summary: Get server logs as text
+ description: Returns structured ComfyUI log entries from the in-memory log buffer.
+ x-internal: true
+ responses:
+ "200":
+ description: Log text
+ content:
+ text/plain:
+ schema:
+ type: string
+
+ /internal/logs/raw:
+ get:
+ operationId: getInternalLogsRaw
+ tags: [internal]
+ summary: Get raw structured log entries
+ description: Returns the raw ComfyUI log buffer as text, together with metadata about the current size limit.
+ x-internal: true
+ responses:
+ "200":
+ description: Structured log data
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ entries:
+ type: array
+ items:
+ type: object
+ properties:
+ t:
+ type: number
+ description: Timestamp
+ m:
+ type: string
+ description: Message
+ size:
+ type: object
+ properties:
+ cols:
+ type: integer
+ rows:
+ type: integer
+
+ /internal/logs/subscribe:
+ patch:
+ operationId: subscribeToLogs
+ tags: [internal]
+ summary: Subscribe or unsubscribe a WebSocket client to log streaming
+ description: Subscribes or unsubscribes the current client from live log streaming over the WebSocket.
+ x-internal: true
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - clientId
+ - enabled
+ properties:
+ clientId:
+ type: string
+ description: WebSocket client ID
+ enabled:
+ type: boolean
+ description: Enable or disable log streaming for this client
+ responses:
+ "200":
+ description: Subscription updated
+
+ /internal/folder_paths:
+ get:
+ operationId: getInternalFolderPaths
+ tags: [internal]
+ summary: Get configured folder paths
+ description: Returns the filesystem paths ComfyUI is configured to load models and other assets from, keyed by folder type.
+ x-internal: true
+ responses:
+ "200":
+ description: Dictionary of folder type to paths
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ type: array
+ items:
+ type: string
+ description: Map of folder type name to list of [path, ...] entries
+
+ /internal/files/{directory_type}:
+ get:
+ operationId: getInternalFiles
+ tags: [internal]
+ summary: List files in a directory type
+ description: Lists the files present in one of ComfyUI's known directories (input, output, or temp).
+ x-internal: true
+ parameters:
+ - name: directory_type
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Directory type (e.g. output, input, temp)
+ responses:
+ "200":
+ description: Array of filenames
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: string
+
+ # ---------------------------------------------------------------------------
+ # Assets (x-feature-gate: enable-assets)
+ # ---------------------------------------------------------------------------
+ /api/assets/hash/{hash}:
+ head:
+ operationId: checkAssetByHash
+ tags: [assets]
+ summary: Check if an asset with the given hash exists
+ description: Returns 204 if an asset with the given content hash already exists, 404 otherwise. Used by clients to deduplicate uploads before transferring bytes.
+ x-feature-gate: enable-assets
+ parameters:
+ - name: hash
+ in: path
+ required: true
+ schema:
+ type: string
+ description: "Blake3 hash of the asset (e.g. blake3:abc123...)"
+ responses:
+ "200":
+ description: Asset exists
+ "404":
+ description: No asset with this hash
+
+ /api/assets:
+ get:
+ operationId: listAssets
+ tags: [assets]
+ summary: List assets with filtering and pagination
+ description: Returns a paginated list of assets, optionally filtered by tags, name, or other query parameters.
+ x-feature-gate: enable-assets
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ default: 50
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ default: 0
+ - name: include_tags
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
+ style: form
+ explode: true
+ description: Tags that assets must have (AND logic)
+ - name: exclude_tags
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
+ style: form
+ explode: true
+ description: Tags that assets must not have
+ - name: name_contains
+ in: query
+ schema:
+ type: string
+ description: Filter assets whose name contains this substring
+ - name: metadata_filter
+ in: query
+ schema:
+ type: string
+ description: JSON-encoded metadata key/value filter
+ - name: sort
+ in: query
+ schema:
+ type: string
+ description: Field to sort by
+ - name: order
+ in: query
+ schema:
+ type: string
+ enum: [asc, desc]
+ description: Sort direction
+ - name: job_ids
+ in: query
+ schema:
+ type: string
+ x-runtime: [cloud]
+ description: "[cloud-only] Comma-separated UUIDs to filter assets by associated job."
+ - name: include_public
+ in: query
+ schema:
+ type: boolean
+ x-runtime: [cloud]
+ description: "[cloud-only] Include workspace-public assets in addition to the caller's own."
+ - name: asset_hash
+ in: query
+ schema:
+ type: string
+ x-runtime: [cloud]
+ description: "[cloud-only] Filter by exact content hash."
+ responses:
+ "200":
+ description: Asset list
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ListAssetsResponse"
+ post:
+ operationId: createAsset
+ tags: [assets]
+ summary: Upload a new asset
+ description: Uploads a new asset (binary content plus metadata) and registers it in the asset database.
+ x-feature-gate: enable-assets
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ required:
+ - file
+ properties:
+ file:
+ type: string
+ format: binary
+ description: Asset file to upload
+ name:
+ type: string
+ description: Display name for the asset
+ tags:
+ type: string
+ description: Comma-separated tags
+ user_metadata:
+ type: string
+ description: JSON-encoded user metadata
+ hash:
+ type: string
+ description: "Blake3 hash of the file content (e.g. blake3:abc123...)"
+ mime_type:
+ type: string
+ description: MIME type of the file (overrides auto-detected type)
+ preview_id:
+ type: string
+ format: uuid
+ description: ID of an existing asset to use as the preview image
+ id:
+ type: string
+ format: uuid
+ nullable: true
+ x-runtime: [cloud]
+ description: "[cloud-only] Client-supplied asset ID for idempotent creation. If an asset with this ID already exists, the existing asset is returned."
+ application/json:
+ schema:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] URL-based asset upload. Caller supplies a URL instead of a file body; the server fetches the content."
+ required:
+ - url
+ properties:
+ url:
+ type: string
+ format: uri
+ description: "[cloud-only] URL of the file to import as an asset"
+ name:
+ type: string
+ description: Display name for the asset
+ tags:
+ type: string
+ description: Comma-separated tags
+ user_metadata:
+ type: string
+ description: JSON-encoded user metadata
+ hash:
+ type: string
+ description: "Blake3 hash of the file content (e.g. blake3:abc123...)"
+ mime_type:
+ type: string
+ description: MIME type of the file (overrides auto-detected type)
+ preview_id:
+ type: string
+ format: uuid
+ description: ID of an existing asset to use as the preview image
+ id:
+ type: string
+ format: uuid
+ nullable: true
+ x-runtime: [cloud]
+ description: "[cloud-only] Client-supplied asset ID for idempotent creation. If an asset with this ID already exists, the existing asset is returned."
+ responses:
+ "201":
+ description: Asset created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AssetCreated"
+
+ /api/assets/from-hash:
+ post:
+ operationId: createAssetFromHash
+ tags: [assets]
+ summary: Create an asset reference from an existing hash
+ description: Registers a new asset that references existing content by hash, without re-uploading the bytes.
+ x-feature-gate: enable-assets
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - hash
+ - name
+ properties:
+ hash:
+ type: string
+ description: Blake3 hash of existing content
+ name:
+ type: string
+ description: Display name
+ tags:
+ type: array
+ items:
+ type: string
+ user_metadata:
+ type: object
+ additionalProperties: true
+ mime_type:
+ type: string
+ nullable: true
+ x-runtime: [cloud]
+ description: "[cloud-only] MIME type of the content, so the type is preserved without re-inspecting content. Ignored by local ComfyUI."
+ responses:
+ "201":
+ description: Asset created from hash
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AssetCreated"
+
+ /api/assets/{id}:
+ get:
+ operationId: getAsset
+ tags: [assets]
+ summary: Get asset metadata
+ description: Returns the metadata for a single asset.
+ x-feature-gate: enable-assets
+ parameters:
+ - name: id
+ in: path
+ description: The asset ID.
+ required: true
+ schema:
+ type: string
+ format: uuid
+ responses:
+ "200":
+ description: Asset metadata
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Asset"
+ "404":
+ description: Asset not found
+ put:
+ operationId: updateAsset
+ tags: [assets]
+ summary: Update asset metadata
+ description: Updates the mutable metadata of an asset (name, tags, etc.). Binary content is immutable.
+ x-feature-gate: enable-assets
+ parameters:
+ - name: id
+ in: path
+ description: The asset ID.
+ required: true
+ schema:
+ type: string
+ format: uuid
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ description: New display name for the asset
+ user_metadata:
+ type: object
+ additionalProperties: true
+ description: Custom user metadata to set
+ preview_id:
+ type: string
+ format: uuid
+ description: ID of the asset to use as the preview
+ mime_type:
+ type: string
+ nullable: true
+ x-runtime: [cloud]
+ description: "[cloud-only] MIME type override when auto-detection was wrong. Ignored by local ComfyUI."
+ responses:
+ "200":
+ description: Asset updated
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AssetUpdated"
+ delete:
+ operationId: deleteAsset
+ tags: [assets]
+ summary: Delete an asset
+ description: Removes an asset entry. Depending on the server configuration, the underlying content may also be deleted.
+ x-feature-gate: enable-assets
+ parameters:
+ - name: id
+ in: path
+ description: The asset ID.
+ required: true
+ schema:
+ type: string
+ format: uuid
+ - name: delete_content
+ in: query
+ schema:
+ type: boolean
+ description: Also delete the underlying content file
+ responses:
+ "204":
+ description: Asset deleted
+
+ /api/assets/{id}/content:
+ get:
+ operationId: getAssetContent
+ tags: [assets]
+ summary: Download asset file content
+ description: Returns the binary content of an asset. Supports range requests.
+ x-feature-gate: enable-assets
+ parameters:
+ - name: id
+ in: path
+ description: The asset ID.
+ required: true
+ schema:
+ type: string
+ format: uuid
+ responses:
+ "200":
+ description: Asset file content
+ content:
+ application/octet-stream:
+ schema:
+ type: string
+ format: binary
+ "404":
+ description: Asset not found
+
+ /api/assets/{id}/tags:
+ post:
+ operationId: addAssetTags
+ tags: [assets]
+ summary: Add tags to an asset
+ description: Adds one or more tags to an asset.
+ x-feature-gate: enable-assets
+ parameters:
+ - name: id
+ in: path
+ description: The asset ID.
+ required: true
+ schema:
+ type: string
+ format: uuid
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - tags
+ properties:
+ tags:
+ type: array
+ items:
+ type: string
+ responses:
+ "200":
+ description: Tags added
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TagsModificationResponse"
+ delete:
+ operationId: removeAssetTags
+ tags: [assets]
+ summary: Remove tags from an asset
+ description: Removes one or more tags from an asset.
+ x-feature-gate: enable-assets
+ parameters:
+ - name: id
+ in: path
+ description: The asset ID.
+ required: true
+ schema:
+ type: string
+ format: uuid
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - tags
+ properties:
+ tags:
+ type: array
+ items:
+ type: string
+ responses:
+ "200":
+ description: Tags removed
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TagsModificationResponse"
+
+ /api/tags:
+ get:
+ operationId: listTags
+ tags: [assets]
+ summary: List all known tags with counts
+ description: Returns the list of all tags known to the asset database, with counts.
+ x-feature-gate: enable-assets
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ - name: search
+ in: query
+ schema:
+ type: string
+ description: Search term for tag name
+ responses:
+ "200":
+ description: Tag list
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ListTagsResponse"
+
+ /api/assets/tags/refine:
+ get:
+ operationId: refineAssetTags
+ tags: [assets]
+ summary: Get tag counts for assets matching current filters
+ description: Returns suggested additional tags that would refine a filtered asset query, together with the count of assets each tag would select.
+ x-feature-gate: enable-assets
+ parameters:
+ - name: include_tags
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
+ style: form
+ explode: true
+ description: Tags that assets must have (AND logic)
+ - name: exclude_tags
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
+ style: form
+ explode: true
+ description: Tags that assets must not have
+ - name: name_contains
+ in: query
+ schema:
+ type: string
+ description: Filter assets whose name contains this substring
+ - name: metadata_filter
+ in: query
+ schema:
+ type: string
+ description: JSON-encoded metadata key/value filter
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ - name: sort
+ in: query
+ schema:
+ type: string
+ description: Field to sort by
+ - name: order
+ in: query
+ schema:
+ type: string
+ enum: [asc, desc]
+ description: Sort direction
+ responses:
+ "200":
+ description: Tag histogram
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AssetTagHistogramResponse"
+
+ /api/assets/seed:
+ post:
+ operationId: seedAssets
+ tags: [assets]
+ summary: Trigger asset scan/seed from filesystem
+ description: Starts a background job that scans the configured directories and registers any assets not yet present in the asset database.
+ x-feature-gate: enable-assets
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ roots:
+ type: array
+ items:
+ type: string
+ description: Root folder paths to scan (if omitted, scans all)
+ responses:
+ "200":
+ description: Seed started
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+
+ /api/assets/seed/status:
+ get:
+ operationId: getAssetSeedStatus
+ tags: [assets]
+ summary: Get asset scan progress
+ description: Returns the progress and status of the most recently-started asset seed job.
+ x-feature-gate: enable-assets
+ responses:
+ "200":
+ description: Scan progress
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties: true
+ description: Scan progress details (files scanned, total, status, etc.)
+
+ /api/assets/seed/cancel:
+ post:
+ operationId: cancelAssetSeed
+ tags: [assets]
+ summary: Cancel an in-progress asset scan
+ description: Requests cancellation of the currently-running asset seed job.
+ x-feature-gate: enable-assets
+ responses:
+ "200":
+ description: Scan cancelled
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+
+ /api/assets/prune:
+ post:
+ operationId: pruneAssets
+ tags: [assets]
+ summary: Mark assets whose backing files no longer exist on disk
+ description: Starts a background job that removes asset entries whose underlying content no longer exists on disk.
+ x-feature-gate: enable-assets
+ responses:
+ "200":
+ description: Prune result
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ marked:
+ type: integer
+ description: Number of assets marked as missing
+
+ # ===========================================================================
+ # Cloud-runtime FE-facing operations
+ #
+ # These operations are served by the cloud runtime. The local runtime returns
+ # 404 for all of these paths. Each operation is tagged x-runtime: [cloud].
+ # ===========================================================================
+
+ # ---------------------------------------------------------------------------
+ # Jobs / prompts (cloud)
+ # ---------------------------------------------------------------------------
+ /api/jobs/{job_id}/cancel:
+ post:
+ operationId: cancelJob
+ tags: [queue]
+ summary: Cancel a running or pending job
+ description: "[cloud-only] Requests cancellation of a job. If the job is currently executing, execution is interrupted. If it is pending in the queue, it is removed."
+ x-runtime: [cloud]
+ parameters:
+ - name: job_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The job ID to cancel.
+ responses:
+ "200":
+ description: Cancellation accepted
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudJobStatus"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/job/{job_id}/status:
+ get:
+ operationId: getCloudJobStatus
+ tags: [queue]
+ summary: Get status of a cloud job
+ deprecated: true
+ description: |
+ **Deprecated.** This endpoint is superseded by `GET /api/jobs/{job_id}`.
+ Clients should migrate; the endpoint is retained for backward
+ compatibility but will be removed in a future release.
+ x-runtime: [cloud]
+ parameters:
+ - name: job_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The job ID to check status for.
+ responses:
+ "200":
+ description: Job status
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudJobStatus"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/prompt/{prompt_id}:
+ get:
+ operationId: getCloudPrompt
+ tags: [prompt]
+ summary: Get a cloud prompt by ID
+ description: "[cloud-only] Returns the full prompt record for a cloud-executed prompt, including the submitted workflow graph and execution metadata."
+ x-runtime: [cloud]
+ parameters:
+ - name: prompt_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The prompt ID to fetch.
+ responses:
+ "200":
+ description: Cloud prompt detail
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudPrompt"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/history_v2:
+ get:
+ operationId: getHistoryV2
+ tags: [history]
+ summary: Get paginated execution history (v2)
+ deprecated: true
+ description: |
+ **Deprecated.** This endpoint is superseded by `GET /api/jobs`.
+ Clients should migrate; the endpoint is retained for backward
+ compatibility but will be removed in a future release.
+ x-runtime: [cloud]
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ default: 20
+ description: Maximum number of results
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ default: 0
+ description: Pagination offset
+ - name: status
+ in: query
+ schema:
+ type: string
+ description: Filter by execution status
+ responses:
+ "200":
+ description: History list
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HistoryV2Response"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/history_v2/{prompt_id}:
+ get:
+ operationId: getHistoryV2ByPromptId
+ tags: [history]
+ summary: Get v2 history for a specific prompt
+ deprecated: true
+ description: |
+ **Deprecated.** This endpoint is superseded by `GET /api/jobs/{prompt_id}`.
+ Clients should migrate; the endpoint is retained for backward
+ compatibility but will be removed in a future release.
+ x-runtime: [cloud]
+ parameters:
+ - name: prompt_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The prompt ID to fetch history for.
+ responses:
+ "200":
+ description: History entry
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HistoryV2Entry"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/logs:
+ get:
+ operationId: getCloudLogs
+ tags: [system]
+ summary: Get cloud execution logs
+ deprecated: true
+ description: |
+ **Deprecated.** This endpoint returns a static placeholder response and
+ provides no real log data. It is retained only to avoid breaking clients
+ that still call it. Clients should remove their dependency; the endpoint
+ will be removed in a future release.
+ x-runtime: [cloud]
+ parameters:
+ - name: job_id
+ in: query
+ schema:
+ type: string
+ description: Filter logs by job ID
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ default: 100
+ description: Maximum number of log entries
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ default: 0
+ description: Pagination offset
+ responses:
+ "200":
+ description: Log entries
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudLogsResponse"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ # ---------------------------------------------------------------------------
+ # Assets extensions (cloud)
+ # ---------------------------------------------------------------------------
+ /api/assets/download:
+ post:
+ operationId: downloadAssets
+ tags: [assets]
+ summary: Download assets to cloud runtime
+ description: "[cloud-only] Initiates a download of one or more assets to the cloud runtime environment. Returns a task ID for tracking download progress via WebSocket."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - assets
+ properties:
+ assets:
+ type: array
+ items:
+ $ref: "#/components/schemas/AssetDownloadRequest"
+ description: Assets to download
+ responses:
+ "200":
+ description: Download initiated
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ task_id:
+ type: string
+ description: Task ID for tracking progress via WebSocket
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/assets/export:
+ post:
+ operationId: exportAssets
+ tags: [assets]
+ summary: Export assets as a downloadable archive
+ description: "[cloud-only] Initiates a bulk export of assets. Returns a task ID for tracking progress via WebSocket. When complete, the export can be downloaded via the exports endpoint."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - asset_ids
+ properties:
+ asset_ids:
+ type: array
+ items:
+ type: string
+ format: uuid
+ description: IDs of assets to export
+ export_name:
+ type: string
+ description: Name for the export archive
+ responses:
+ "200":
+ description: Export initiated
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ task_id:
+ type: string
+ export_name:
+ type: string
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/assets/exports/{exportName}:
+ get:
+ operationId: getAssetExport
+ tags: [assets]
+ summary: Download a completed asset export
+ description: "[cloud-only] Returns the archive file for a completed asset export."
+ x-runtime: [cloud]
+ parameters:
+ - name: exportName
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Name of the export to download
+ responses:
+ "200":
+ description: Export archive file
+ content:
+ application/zip:
+ schema:
+ type: string
+ format: binary
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/assets/from-workflow:
+ post:
+ operationId: createAssetsFromWorkflow
+ tags: [assets]
+ summary: Create asset records from a workflow execution
+ description: "[cloud-only] Registers output files from a workflow execution as assets in the asset database."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - prompt_id
+ properties:
+ prompt_id:
+ type: string
+ format: uuid
+ description: Prompt ID whose outputs should be registered as assets
+ tags:
+ type: array
+ items:
+ type: string
+ description: Tags to apply to the created assets
+ responses:
+ "201":
+ description: Assets created
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ assets:
+ type: array
+ items:
+ $ref: "#/components/schemas/Asset"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/assets/import:
+ post:
+ operationId: importAssets
+ tags: [assets]
+ summary: Import assets from external URLs
+ description: "[cloud-only] Imports one or more assets from external URLs into the cloud asset store."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - imports
+ properties:
+ imports:
+ type: array
+ items:
+ $ref: "#/components/schemas/AssetImportRequest"
+ description: Assets to import
+ responses:
+ "200":
+ description: Import initiated
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ assets:
+ type: array
+ items:
+ $ref: "#/components/schemas/Asset"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/assets/remote-metadata:
+ get:
+ operationId: getAssetRemoteMetadata
+ tags: [assets]
+ summary: Fetch metadata for a remote asset URL
+ description: "[cloud-only] Fetches and returns metadata (content type, size, filename) for a remote URL without downloading the full content."
+ x-runtime: [cloud]
+ parameters:
+ - name: url
+ in: query
+ required: true
+ schema:
+ type: string
+ format: uri
+ description: URL to inspect
+ responses:
+ "200":
+ description: Remote metadata
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/RemoteAssetMetadata"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ # ---------------------------------------------------------------------------
+ # Custom nodes / hub (cloud)
+ # ---------------------------------------------------------------------------
+ /api/experiment/nodes:
+ get:
+ operationId: getNodeInfoSchema
+ tags: [runtime-only]
+ summary: Get pre-rendered node info schema
+ description: "[cloud-only] Returns the static ComfyUI object_info schema, identical for every caller, rendered once at startup with empty model/user-file context. Served by a raw HTTP handler that writes pre-rendered bytes with ETag + Cache-Control validators for RFC 7232 conditional GETs."
+ x-runtime: [cloud]
+ parameters:
+ - name: If-None-Match
+ in: header
+ required: false
+ schema:
+ type: string
+ description: Entity tag previously returned by this endpoint. When present and matching, the server returns 304 Not Modified.
+ responses:
+ "200":
+ description: Node info schema
+ headers:
+ ETag:
+ schema:
+ type: string
+ description: Entity tag for conditional request validation
+ Cache-Control:
+ schema:
+ type: string
+ description: Cache directives for the response
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ $ref: "#/components/schemas/NodeInfo"
+ "304":
+ description: Not Modified — returned when the client sends a matching If-None-Match header
+ post:
+ operationId: installCloudNode
+ tags: [node]
+ summary: Install a custom node package
+ description: "[cloud-only] Installs a custom node package in the cloud runtime by ID or repository URL."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: string
+ description: Node package ID or repository URL
+ version:
+ type: string
+ description: Specific version to install
+ responses:
+ "200":
+ description: Node installed
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudNode"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/experiment/nodes/{id}:
+ get:
+ operationId: getNodeByID
+ tags: [runtime-only]
+ summary: Get a single node definition by ID
+ description: "[cloud-only] Returns one node's definition from the pre-indexed object_info schema. Served by a raw HTTP handler that writes pre-rendered bytes with ETag + Cache-Control validators for RFC 7232 conditional GETs."
+ x-runtime: [cloud]
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Node class identifier
+ - name: If-None-Match
+ in: header
+ required: false
+ schema:
+ type: string
+ description: Entity tag previously returned by this endpoint. When present and matching, the server returns 304 Not Modified.
+ responses:
+ "200":
+ description: Single node definition
+ headers:
+ ETag:
+ schema:
+ type: string
+ description: Entity tag for conditional request validation
+ Cache-Control:
+ schema:
+ type: string
+ description: Cache directives for the response
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/NodeInfo"
+ "304":
+ description: Not Modified — returned when the client sends a matching If-None-Match header
+ "404":
+ description: Node not found
+ delete:
+ operationId: uninstallCloudNode
+ tags: [node]
+ summary: Uninstall a custom node package
+ description: "[cloud-only] Removes a custom node package from the cloud runtime."
+ x-runtime: [cloud]
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Custom node package ID
+ responses:
+ "204":
+ description: Node uninstalled
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/hub/assets/upload-url:
+ post:
+ operationId: getHubAssetUploadUrl
+ tags: [hub]
+ summary: Get a pre-signed upload URL for a hub asset
+ description: "[cloud-only] Returns a pre-signed URL that can be used to upload an asset file directly to storage."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - filename
+ - content_type
+ properties:
+ filename:
+ type: string
+ description: Name of the file to upload
+ content_type:
+ type: string
+ description: MIME type of the file
+ size:
+ type: integer
+ format: int64
+ description: File size in bytes
+ responses:
+ "200":
+ description: Upload URL
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ upload_url:
+ type: string
+ format: uri
+ description: Pre-signed upload URL
+ asset_url:
+ type: string
+ format: uri
+ description: Public URL after upload completes
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/hub/labels:
+ get:
+ operationId: listHubLabels
+ tags: [hub]
+ summary: List available hub labels
+ description: "[cloud-only] Returns the list of labels/categories available for tagging hub content."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Label list
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/HubLabel"
+
+ /api/hub/profiles:
+ get:
+ operationId: listHubProfiles
+ tags: [hub]
+ summary: List hub user profiles
+ description: "[cloud-only] Returns a paginated list of public hub user profiles."
+ x-runtime: [cloud]
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ description: Maximum number of results
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ description: Pagination offset
+ - name: search
+ in: query
+ schema:
+ type: string
+ description: Search by username or display name
+ responses:
+ "200":
+ description: Profile list
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ profiles:
+ type: array
+ items:
+ $ref: "#/components/schemas/HubProfile"
+ total:
+ type: integer
+ has_more:
+ type: boolean
+ post:
+ operationId: createHubProfile
+ tags: [hub]
+ summary: Create a Hub profile
+ description: "[cloud-only] Creates a hub profile for the specified workspace. Username is immutable after creation."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateHubProfileRequest"
+ responses:
+ "201":
+ description: Hub profile created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HubProfile"
+ "400":
+ description: Bad request (e.g. invalid username)
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "409":
+ description: Username already taken or profile already exists
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/hub/profiles/{username}:
+ get:
+ operationId: getHubProfile
+ tags: [hub]
+ summary: Get a hub profile by username
+ description: "[cloud-only] Returns the public hub profile for the given username."
+ x-runtime: [cloud]
+ parameters:
+ - name: username
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Hub username
+ responses:
+ "200":
+ description: Profile
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HubProfile"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/hub/profiles/check:
+ get:
+ operationId: checkHubProfileUsername
+ tags: [hub]
+ summary: Check if a hub username is available
+ description: "[cloud-only] Returns whether the given username is available for registration."
+ x-runtime: [cloud]
+ parameters:
+ - name: username
+ in: query
+ required: true
+ schema:
+ type: string
+ description: Username to check
+ responses:
+ "200":
+ description: Availability result
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ available:
+ type: boolean
+ username:
+ type: string
+
+ /api/hub/profiles/me:
+ get:
+ operationId: getMyHubProfile
+ tags: [hub]
+ summary: Get the authenticated user's hub profile
+ description: "[cloud-only] Returns the hub profile of the currently authenticated user."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Profile
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HubProfile"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ put:
+ operationId: updateMyHubProfile
+ tags: [hub]
+ summary: Update the authenticated user's hub profile
+ description: "[cloud-only] Updates the hub profile of the currently authenticated user."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ username:
+ type: string
+ display_name:
+ type: string
+ bio:
+ type: string
+ avatar_url:
+ type: string
+ format: uri
+ links:
+ type: array
+ items:
+ type: string
+ format: uri
+ responses:
+ "200":
+ description: Updated profile
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HubProfile"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "409":
+ description: Conflict
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/hub/workflows:
+ get:
+ operationId: listHubWorkflows
+ tags: [hub]
+ summary: List published hub workflows
+ description: "[cloud-only] Returns a paginated list of publicly shared workflows on the hub."
+ x-runtime: [cloud]
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ description: Maximum number of results
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ description: Pagination offset
+ - name: sort
+ in: query
+ schema:
+ type: string
+ description: Sort field (e.g. created_at, likes)
+ - name: order
+ in: query
+ schema:
+ type: string
+ enum: [asc, desc]
+ description: Sort direction
+ - name: search
+ in: query
+ schema:
+ type: string
+ description: Search by title or description
+ - name: labels
+ in: query
+ schema:
+ type: string
+ description: Filter by label IDs (comma-separated)
+ responses:
+ "200":
+ description: Hub workflow list
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HubWorkflowList"
+ post:
+ operationId: publishHubWorkflow
+ tags: [hub]
+ summary: Publish a workflow to the hub
+ description: "[cloud-only] Publishes a workflow to the hub with metadata, thumbnail, and sample images."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PublishHubWorkflowRequest"
+ responses:
+ "200":
+ description: Workflow published to hub
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HubWorkflowDetail"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Workflow or profile not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/hub/workflows/{share_id}:
+ get:
+ operationId: getHubWorkflow
+ tags: [hub]
+ summary: Get a published hub workflow by share ID
+ description: "[cloud-only] Returns the full details of a published workflow on the hub."
+ x-runtime: [cloud]
+ parameters:
+ - name: share_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Workflow share ID
+ responses:
+ "200":
+ description: Hub workflow
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HubWorkflow"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ delete:
+ operationId: deleteHubWorkflow
+ tags: [hub]
+ summary: Unpublish a workflow from the hub
+ description: "[cloud-only] Removes a workflow from the hub listing."
+ x-runtime: [cloud]
+ parameters:
+ - name: share_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Workflow share ID
+ responses:
+ "204":
+ description: Successfully unpublished
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Workflow not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/hub/workflows/index:
+ get:
+ operationId: getHubWorkflowIndex
+ tags: [hub]
+ summary: Get the hub workflow index
+ description: "[cloud-only] Returns the lightweight index of all hub workflows for client-side search and navigation."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Workflow index
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/HubWorkflowIndexEntry"
+
+ # ---------------------------------------------------------------------------
+ # Workflows (cloud)
+ # ---------------------------------------------------------------------------
+ /api/workflows:
+ get:
+ operationId: listCloudWorkflows
+ tags: [workflows]
+ summary: List cloud workflows
+ description: "[cloud-only] Returns a paginated list of the authenticated user's cloud workflows."
+ x-runtime: [cloud]
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ description: Maximum number of results
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ description: Pagination offset
+ - name: sort
+ in: query
+ schema:
+ type: string
+ description: Sort field
+ - name: order
+ in: query
+ schema:
+ type: string
+ enum: [asc, desc]
+ description: Sort direction
+ - name: search
+ in: query
+ schema:
+ type: string
+ description: Search by workflow name
+ responses:
+ "200":
+ description: Workflow list
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudWorkflowList"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ post:
+ operationId: createCloudWorkflow
+ tags: [workflows]
+ summary: Create a new cloud workflow
+ description: "[cloud-only] Creates a new cloud workflow with the provided name and optional initial content."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - name
+ properties:
+ name:
+ type: string
+ description: Workflow name
+ description:
+ type: string
+ description: Workflow description
+ content:
+ type: object
+ additionalProperties: true
+ description: Initial workflow graph JSON
+ responses:
+ "201":
+ description: Workflow created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudWorkflow"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workflows/{workflow_id}:
+ get:
+ operationId: getCloudWorkflow
+ tags: [workflows]
+ summary: Get a cloud workflow by ID
+ description: "[cloud-only] Returns the metadata for a cloud workflow."
+ x-runtime: [cloud]
+ parameters:
+ - name: workflow_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The workflow ID.
+ responses:
+ "200":
+ description: Workflow detail
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudWorkflow"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ patch:
+ operationId: updateCloudWorkflow
+ tags: [workflows]
+ summary: Update a cloud workflow
+ description: "[cloud-only] Updates the metadata (name, description) of an existing cloud workflow."
+ x-runtime: [cloud]
+ parameters:
+ - name: workflow_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The workflow ID.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ description:
+ type: string
+ responses:
+ "200":
+ description: Workflow updated
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudWorkflow"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ delete:
+ operationId: deleteCloudWorkflow
+ tags: [workflows]
+ summary: Delete a cloud workflow
+ description: "[cloud-only] Deletes a cloud workflow and all its versions."
+ x-runtime: [cloud]
+ parameters:
+ - name: workflow_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The workflow ID.
+ responses:
+ "204":
+ description: Workflow deleted
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workflows/{workflow_id}/content:
+ get:
+ operationId: getCloudWorkflowContent
+ tags: [workflows]
+ summary: Get the content of a cloud workflow
+ description: "[cloud-only] Returns the full workflow graph JSON for the latest version of a cloud workflow."
+ x-runtime: [cloud]
+ parameters:
+ - name: workflow_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The workflow ID.
+ - name: version_id
+ in: query
+ schema:
+ type: string
+ description: Specific version ID to fetch
+ responses:
+ "200":
+ description: Workflow content
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties: true
+ description: The full workflow graph JSON
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ put:
+ operationId: updateCloudWorkflowContent
+ tags: [workflows]
+ summary: Update the content of a cloud workflow
+ description: "[cloud-only] Saves new workflow graph JSON as a new version of the cloud workflow."
+ x-runtime: [cloud]
+ parameters:
+ - name: workflow_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The workflow ID.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties: true
+ description: The workflow graph JSON to save
+ responses:
+ "200":
+ description: Content updated
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudWorkflowVersion"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workflows/{workflow_id}/fork:
+ post:
+ operationId: forkCloudWorkflow
+ tags: [workflows]
+ summary: Fork a cloud workflow
+ description: "[cloud-only] Creates a copy of a cloud workflow under the authenticated user's account."
+ x-runtime: [cloud]
+ parameters:
+ - name: workflow_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The workflow ID to fork.
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name for the forked workflow (defaults to original name)
+ responses:
+ "201":
+ description: Forked workflow
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudWorkflow"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workflows/{workflow_id}/versions:
+ get:
+ operationId: listCloudWorkflowVersions
+ tags: [workflows]
+ summary: List versions of a cloud workflow
+ description: "[cloud-only] Returns the version history of a cloud workflow."
+ x-runtime: [cloud]
+ parameters:
+ - name: workflow_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The workflow ID.
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ description: Maximum number of results
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ description: Pagination offset
+ responses:
+ "200":
+ description: Version list
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ versions:
+ type: array
+ items:
+ $ref: "#/components/schemas/CloudWorkflowVersion"
+ total:
+ type: integer
+ has_more:
+ type: boolean
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ post:
+ operationId: createCloudWorkflowVersion
+ tags: [workflows]
+ summary: Create a new cloud workflow version
+ description: "[cloud-only] Creates a new workflow version with updated workflow JSON. Uses optimistic concurrency via base_version."
+ x-runtime: [cloud]
+ parameters:
+ - name: workflow_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The workflow ID.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateWorkflowVersionRequest"
+ responses:
+ "201":
+ description: Version created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/WorkflowVersionResponse"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden — not the workflow owner
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "409":
+ description: Version conflict — base_version does not match latest
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workflows/published/{share_id}:
+ get:
+ operationId: getPublishedWorkflow
+ tags: [workflows]
+ summary: Get a published workflow by share ID
+ description: "[cloud-only] Returns a publicly published cloud workflow by its share identifier."
+ x-runtime: [cloud]
+ parameters:
+ - name: share_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The workflow share ID.
+ responses:
+ "200":
+ description: Published workflow
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudWorkflow"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ # ---------------------------------------------------------------------------
+ # Auth / session (cloud)
+ # ---------------------------------------------------------------------------
+ /api/auth/session:
+ get:
+ operationId: getAuthSession
+ tags: [auth]
+ summary: Get the current authentication session
+ description: "[cloud-only] Returns the current session state for the authenticated user, including user identity and active workspace."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Session info
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AuthSession"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ post:
+ operationId: createAuthSession
+ tags: [auth]
+ summary: Create a session cookie
+ description: "[cloud-only] Creates a session cookie from the bearer token in the Authorization header. Returns a Set-Cookie header with a secure HttpOnly session cookie. Cookie authentication is not allowed for this endpoint."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Session created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateSessionResponse"
+ "400":
+ description: Bad request — invalid or expired ID token
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ delete:
+ operationId: deleteAuthSession
+ tags: [auth]
+ summary: Delete session cookie (logout)
+ description: "[cloud-only] Clears the session cookie and optionally revokes the session on the server."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Session deleted
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DeleteSessionResponse"
+
+ /api/auth/token:
+ post:
+ operationId: createAuthToken
+ tags: [auth]
+ summary: Exchange credentials for an access token
+ description: "[cloud-only] Exchanges authentication credentials (e.g. an authorization code) for an access token."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - grant_type
+ properties:
+ grant_type:
+ type: string
+ enum: [authorization_code, refresh_token]
+ description: OAuth2 grant type
+ code:
+ type: string
+ description: Authorization code (for authorization_code grant)
+ refresh_token:
+ type: string
+ description: Refresh token (for refresh_token grant)
+ redirect_uri:
+ type: string
+ format: uri
+ description: Redirect URI used in the authorization request
+ responses:
+ "200":
+ description: Token response
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AuthTokenResponse"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /.well-known/jwks.json:
+ get:
+ operationId: getJwks
+ tags: [auth]
+ summary: Get JSON Web Key Set
+ description: "[cloud-only] Returns the JSON Web Key Set (JWKS) used to verify JWTs issued by the cloud authentication service."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: JWKS
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/JwksResponse"
+
+ # ---------------------------------------------------------------------------
+ # Billing (cloud)
+ # ---------------------------------------------------------------------------
+ /api/billing/balance:
+ get:
+ operationId: getBillingBalance
+ tags: [billing]
+ summary: Get current credit balance
+ description: "[cloud-only] Returns the authenticated user's current credit balance and usage summary."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Balance info
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BillingBalance"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/billing/events:
+ get:
+ operationId: listBillingEvents
+ tags: [billing]
+ summary: List billing events
+ description: "[cloud-only] Returns a paginated list of billing events (charges, credits, refunds) for the authenticated user."
+ x-runtime: [cloud]
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ description: Maximum number of results
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ description: Pagination offset
+ - name: type
+ in: query
+ schema:
+ type: string
+ description: Filter by event type
+ responses:
+ "200":
+ description: Billing events
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BillingEventList"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/billing/ops/{id}:
+ get:
+ operationId: getBillingOp
+ tags: [billing]
+ summary: Get a billing operation by ID
+ description: "[cloud-only] Returns details of a specific billing operation."
+ x-runtime: [cloud]
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The billing operation ID.
+ responses:
+ "200":
+ description: Billing operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BillingOp"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/billing/payment-portal:
+ post:
+ operationId: createPaymentPortalSession
+ tags: [billing]
+ summary: Create a payment portal session
+ description: "[cloud-only] Creates a Stripe customer portal session for managing payment methods and invoices. Returns a URL to redirect the user to."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Portal session
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ url:
+ type: string
+ format: uri
+ description: Stripe portal URL
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/billing/plans:
+ get:
+ operationId: listBillingPlans
+ tags: [billing]
+ summary: List available billing plans
+ description: "[cloud-only] Returns the list of available subscription plans and their pricing."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Plan list
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/BillingPlan"
+
+ /api/billing/preview-subscribe:
+ post:
+ operationId: previewSubscription
+ tags: [billing]
+ summary: Preview a subscription change
+ description: "[cloud-only] Returns a preview of what a subscription change would cost, including prorations."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - plan_id
+ properties:
+ plan_id:
+ type: string
+ description: ID of the plan to preview
+ responses:
+ "200":
+ description: Subscription preview
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SubscriptionPreview"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/billing/status:
+ get:
+ operationId: getBillingStatus
+ tags: [billing]
+ summary: Get billing status
+ description: "[cloud-only] Returns the authenticated user's current billing and subscription status."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Billing status
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BillingStatus"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/billing/subscribe:
+ post:
+ operationId: createSubscription
+ tags: [billing]
+ summary: Subscribe to a billing plan
+ description: "[cloud-only] Creates a new subscription to the specified billing plan."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - plan_id
+ properties:
+ plan_id:
+ type: string
+ description: ID of the plan to subscribe to
+ payment_method_id:
+ type: string
+ description: Stripe payment method ID
+ responses:
+ "200":
+ description: Subscription created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BillingSubscription"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/billing/subscription/cancel:
+ post:
+ operationId: cancelSubscription
+ tags: [billing]
+ summary: Cancel the active subscription
+ description: "[cloud-only] Cancels the authenticated user's active subscription. The subscription remains active until the end of the current billing period."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Subscription cancelled
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BillingSubscription"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/billing/subscription/resubscribe:
+ post:
+ operationId: resubscribe
+ tags: [billing]
+ summary: Resubscribe after cancellation
+ description: "[cloud-only] Reactivates a subscription that was previously cancelled but has not yet expired."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Subscription reactivated
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BillingSubscription"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/billing/topup:
+ post:
+ operationId: topUpCredits
+ tags: [billing]
+ summary: Purchase additional credits
+ description: "[cloud-only] Purchases a one-time credit top-up using the user's payment method on file."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - amount
+ properties:
+ amount:
+ type: integer
+ description: Number of credits to purchase
+ responses:
+ "200":
+ description: Top-up successful
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BillingBalance"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ # ---------------------------------------------------------------------------
+ # Workspace (cloud)
+ # ---------------------------------------------------------------------------
+ /api/workspace/api-keys:
+ get:
+ operationId: listWorkspaceApiKeys
+ tags: [workspace]
+ summary: List workspace API keys
+ description: "[cloud-only] Returns the list of API keys for the current workspace."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: API key list
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/WorkspaceApiKey"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ post:
+ operationId: createWorkspaceApiKey
+ tags: [workspace]
+ summary: Create a workspace API key
+ description: "[cloud-only] Creates a new API key for the current workspace."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - name
+ properties:
+ name:
+ type: string
+ description: Display name for the API key
+ description:
+ type: string
+ description: User-provided description of the key's purpose
+ maxLength: 5000
+ responses:
+ "201":
+ description: API key created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/WorkspaceApiKeyCreated"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workspace/api-keys/{id}:
+ delete:
+ operationId: deleteWorkspaceApiKey
+ tags: [workspace]
+ summary: Delete a workspace API key
+ description: "[cloud-only] Revokes and deletes a workspace API key."
+ x-runtime: [cloud]
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The API key ID.
+ responses:
+ "204":
+ description: API key deleted
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workspace/invites:
+ get:
+ operationId: listWorkspaceInvites
+ tags: [workspace]
+ summary: List pending workspace invites
+ description: "[cloud-only] Returns the list of pending invitations for the current workspace."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Invite list
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/WorkspaceInvite"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ post:
+ operationId: createWorkspaceInvite
+ tags: [workspace]
+ summary: Invite a user to the workspace
+ description: "[cloud-only] Creates an invitation for a user to join the current workspace."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - email
+ properties:
+ email:
+ type: string
+ format: email
+ description: Email address to invite
+ role:
+ type: string
+ enum: [admin, member]
+ description: Role to assign
+ responses:
+ "201":
+ description: Invite created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/WorkspaceInvite"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "409":
+ description: Conflict
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workspace/invites/{inviteId}:
+ delete:
+ operationId: deleteWorkspaceInvite
+ tags: [workspace]
+ summary: Cancel a workspace invite
+ description: "[cloud-only] Cancels a pending workspace invitation."
+ x-runtime: [cloud]
+ parameters:
+ - name: inviteId
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The invite ID.
+ responses:
+ "204":
+ description: Invite cancelled
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workspace/leave:
+ post:
+ operationId: leaveWorkspace
+ tags: [workspace]
+ summary: Leave the current workspace
+ description: "[cloud-only] Removes the authenticated user from the current workspace."
+ x-runtime: [cloud]
+ responses:
+ "204":
+ description: Left workspace
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workspace/members:
+ get:
+ operationId: listWorkspaceMembers
+ tags: [workspace]
+ summary: List workspace members
+ description: "[cloud-only] Returns the list of members in the current workspace."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Member list
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/WorkspaceMember"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workspace/members/{user_id}/api-keys:
+ get:
+ operationId: listMemberApiKeys
+ tags: [workspace]
+ summary: List API keys for a workspace member
+ description: "[cloud-only] Returns the API keys belonging to a specific workspace member. Requires admin role."
+ x-runtime: [cloud]
+ parameters:
+ - name: user_id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The member's user ID.
+ responses:
+ "200":
+ description: API key list
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/WorkspaceApiKey"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ delete:
+ operationId: bulkRevokeMemberApiKeys
+ tags: [workspace]
+ summary: Bulk revoke a member's API keys
+ description: "[cloud-only] Revokes all active API keys for a specific workspace member. Only workspace owners can perform this action."
+ x-runtime: [cloud]
+ parameters:
+ - name: user_id
+ in: path
+ required: true
+ schema:
+ type: string
+ minLength: 1
+ description: The member's user ID.
+ responses:
+ "200":
+ description: Keys revoked
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/BulkRevokeAPIKeysResponse"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden — must be workspace owner
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workspace/members/{userId}:
+ patch:
+ operationId: updateWorkspaceMember
+ tags: [workspace]
+ summary: Update a workspace member's role
+ description: "[cloud-only] Updates the role of a workspace member. Requires admin role."
+ x-runtime: [cloud]
+ parameters:
+ - name: userId
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The member's user ID.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - role
+ properties:
+ role:
+ type: string
+ enum: [admin, member]
+ description: New role to assign
+ responses:
+ "200":
+ description: Member updated
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/WorkspaceMember"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ delete:
+ operationId: removeWorkspaceMember
+ tags: [workspace]
+ summary: Remove a member from the workspace
+ description: "[cloud-only] Removes a member from the current workspace. Requires admin role."
+ x-runtime: [cloud]
+ parameters:
+ - name: userId
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The member's user ID.
+ responses:
+ "204":
+ description: Member removed
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workspaces:
+ get:
+ operationId: listWorkspaces
+ tags: [workspace]
+ summary: List workspaces the user belongs to
+ description: "[cloud-only] Returns the list of workspaces the authenticated user is a member of."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Workspace list
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Workspace"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ post:
+ operationId: createWorkspace
+ tags: [workspace]
+ summary: Create a new workspace
+ description: "[cloud-only] Creates a new workspace. The authenticated user becomes the owner."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - name
+ properties:
+ name:
+ type: string
+ description: Workspace name
+ responses:
+ "201":
+ description: Workspace created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Workspace"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/workspaces/{id}:
+ get:
+ operationId: getWorkspace
+ tags: [workspace]
+ summary: Get a workspace by ID
+ description: "[cloud-only] Returns details of a workspace the user is a member of."
+ x-runtime: [cloud]
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The workspace ID.
+ responses:
+ "200":
+ description: Workspace detail
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Workspace"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ patch:
+ operationId: updateWorkspace
+ tags: [workspace]
+ summary: Update workspace settings
+ description: "[cloud-only] Updates the name or settings of a workspace. Requires admin role."
+ x-runtime: [cloud]
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The workspace ID.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ description: New workspace name
+ responses:
+ "200":
+ description: Workspace updated
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Workspace"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ delete:
+ operationId: deleteWorkspace
+ tags: [workspace]
+ summary: Delete a workspace
+ description: "[cloud-only] Soft-deletes a workspace. Requires owner role. Personal workspaces cannot be deleted."
+ x-runtime: [cloud]
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The workspace ID.
+ responses:
+ "204":
+ description: Workspace deleted
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "403":
+ description: Forbidden — must be workspace owner
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ # ---------------------------------------------------------------------------
+ # User / settings / misc (cloud)
+ # ---------------------------------------------------------------------------
+ /api/feedback:
+ post:
+ operationId: submitFeedback
+ tags: [user]
+ summary: Submit user feedback
+ description: "[cloud-only] Submits feedback from the user about their experience with the cloud runtime."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - message
+ properties:
+ message:
+ type: string
+ description: Feedback message
+ rating:
+ type: integer
+ minimum: 1
+ maximum: 5
+ description: Optional satisfaction rating
+ context:
+ type: object
+ additionalProperties: true
+ description: Additional context metadata
+ responses:
+ "200":
+ description: Feedback submitted
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: string
+ status:
+ type: string
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/files/mask-layers:
+ get:
+ operationId: getMaskLayers
+ tags: [assets]
+ summary: Get related mask layer filenames
+ description: "[cloud-only] Given a mask file (any of the 4 layers), returns all related mask layer filenames. Used by the mask editor to load the paint, mask, and painted layers when reopening a previously edited mask."
+ x-runtime: [cloud]
+ parameters:
+ - name: filename
+ in: query
+ required: true
+ schema:
+ type: string
+ description: Hash filename of any mask layer file
+ responses:
+ "200":
+ description: Related mask layers
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ mask:
+ type: string
+ description: Filename of the mask layer
+ nullable: true
+ paint:
+ type: string
+ description: Filename of the paint strokes layer
+ nullable: true
+ painted:
+ type: string
+ description: Filename of the painted image layer
+ nullable: true
+ painted_masked:
+ type: string
+ description: Filename of the final composite layer
+ nullable: true
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: File not found or not a mask file
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/internal/cloud_analytics:
+ post:
+ operationId: postCloudAnalytics
+ tags: [internal]
+ summary: Post client analytics events
+ description: "[cloud-only] Receives analytics events from the frontend for processing by the cloud analytics pipeline."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - events
+ properties:
+ events:
+ type: array
+ items:
+ type: object
+ required:
+ - event_name
+ properties:
+ event_name:
+ type: string
+ timestamp:
+ type: string
+ format: date-time
+ properties:
+ type: object
+ additionalProperties: true
+ responses:
+ "200":
+ description: Events accepted
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/invites/{token}/accept:
+ post:
+ operationId: acceptInvite
+ tags: [workspace]
+ summary: Accept a workspace invitation
+ description: "[cloud-only] Accepts a workspace invitation using the invite token. The authenticated user is added to the workspace."
+ x-runtime: [cloud]
+ parameters:
+ - name: token
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The invitation token.
+ responses:
+ "200":
+ description: Invite accepted
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Workspace"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/secrets:
+ get:
+ operationId: listSecrets
+ tags: [settings]
+ summary: List user secrets
+ description: "[cloud-only] Returns the list of secrets (API keys for third-party services) stored for the authenticated user. Secret values are redacted."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: Secret list
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/SecretMeta"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ post:
+ operationId: createSecret
+ tags: [settings]
+ summary: Create or update a secret
+ description: "[cloud-only] Stores a new secret or updates an existing one. Secrets are encrypted at rest."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - name
+ - value
+ properties:
+ name:
+ type: string
+ description: Secret name (unique per user)
+ value:
+ type: string
+ description: Secret value
+ responses:
+ "201":
+ description: Secret created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SecretMeta"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/secrets/{id}:
+ get:
+ operationId: getSecret
+ tags: [settings]
+ summary: Get secret metadata
+ description: "[cloud-only] Returns metadata for a specific secret. Does not return the plaintext secret value."
+ x-runtime: [cloud]
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The secret ID.
+ responses:
+ "200":
+ description: Secret metadata
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SecretMeta"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ patch:
+ operationId: updateSecret
+ tags: [settings]
+ summary: Update a secret
+ description: "[cloud-only] Updates an existing secret's name and/or value. Both fields are optional; only provided fields are updated."
+ x-runtime: [cloud]
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: The secret ID.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UpdateSecretRequest"
+ responses:
+ "200":
+ description: Secret updated
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SecretMeta"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "409":
+ description: Conflict — a secret with this name already exists
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ delete:
+ operationId: deleteSecret
+ tags: [settings]
+ summary: Delete a secret
+ description: "[cloud-only] Permanently deletes a stored secret."
+ x-runtime: [cloud]
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: The secret ID.
+ responses:
+ "204":
+ description: Secret deleted
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/user:
+ get:
+ operationId: getCloudUser
+ tags: [user]
+ summary: Get the authenticated cloud user
+ description: "[cloud-only] Returns the profile and account information for the currently authenticated user."
+ x-runtime: [cloud]
+ responses:
+ "200":
+ description: User profile
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudUser"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ put:
+ operationId: updateCloudUser
+ tags: [user]
+ summary: Update the authenticated cloud user profile
+ description: "[cloud-only] Updates the profile information for the currently authenticated user."
+ x-runtime: [cloud]
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ display_name:
+ type: string
+ avatar_url:
+ type: string
+ format: uri
+ responses:
+ "200":
+ description: Updated profile
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudUser"
+ "400":
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/userdata/{file}/publish:
+ get:
+ operationId: getUserdataFilePublish
+ tags: [userdata]
+ summary: Get publish info for a userdata file
+ description: "[cloud-only] Returns the publish status and share info for a userdata workflow file."
+ x-runtime: [cloud]
+ parameters:
+ - name: file
+ in: path
+ required: true
+ schema:
+ type: string
+ description: File path relative to user data directory
+ responses:
+ "200":
+ description: Publish info (publish_time is null if never published)
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/WorkflowPublishInfo"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Workflow not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ post:
+ operationId: publishUserdataFile
+ tags: [userdata]
+ summary: Publish a userdata file to the cloud
+ description: "[cloud-only] Makes a userdata file available via a public URL for sharing or embedding."
+ x-runtime: [cloud]
+ parameters:
+ - name: file
+ in: path
+ required: true
+ schema:
+ type: string
+ description: File path relative to user data directory
+ responses:
+ "200":
+ description: Published file URL
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ url:
+ type: string
+ format: uri
+ description: Public URL of the published file
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/vhs/queryvideo:
+ get:
+ operationId: queryVhsVideo
+ tags: [view]
+ summary: Query VHS video metadata
+ description: "[cloud-only] Returns metadata about a video file processed by the VHS (Video Helper Suite) integration."
+ x-runtime: [cloud]
+ parameters:
+ - name: filename
+ in: query
+ required: true
+ schema:
+ type: string
+ description: Video filename
+ - name: type
+ in: query
+ schema:
+ type: string
+ enum: [input, output, temp]
+ description: Directory type
+ - name: subfolder
+ in: query
+ schema:
+ type: string
+ description: Subfolder within the directory
+ responses:
+ "200":
+ description: Video metadata
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties: true
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/vhs/viewaudio:
+ get:
+ operationId: viewVhsAudio
+ tags: [view]
+ summary: View or download VHS audio
+ description: "[cloud-only] Returns audio content from a VHS-processed file."
+ x-runtime: [cloud]
+ parameters:
+ - name: filename
+ in: query
+ required: true
+ schema:
+ type: string
+ description: Audio filename
+ - name: type
+ in: query
+ schema:
+ type: string
+ enum: [input, output, temp]
+ description: Directory type
+ - name: subfolder
+ in: query
+ schema:
+ type: string
+ description: Subfolder within the directory
+ responses:
+ "200":
+ description: Audio content
+ content:
+ audio/*:
+ schema:
+ type: string
+ format: binary
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/vhs/viewvideo:
+ get:
+ operationId: viewVhsVideo
+ tags: [view]
+ summary: View or download VHS video
+ description: "[cloud-only] Returns video content from a VHS-processed file."
+ x-runtime: [cloud]
+ parameters:
+ - name: filename
+ in: query
+ required: true
+ schema:
+ type: string
+ description: Video filename
+ - name: type
+ in: query
+ schema:
+ type: string
+ enum: [input, output, temp]
+ description: Directory type
+ - name: subfolder
+ in: query
+ schema:
+ type: string
+ description: Subfolder within the directory
+ responses:
+ "200":
+ description: Video content
+ content:
+ video/*:
+ schema:
+ type: string
+ format: binary
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/viewvideo:
+ get:
+ operationId: viewVideo
+ tags: [view]
+ summary: View or download a video file
+ deprecated: true
+ description: |
+ **Deprecated.** This endpoint is an alias of `GET /api/view` added for
+ legacy history-queue video playback. Callers should use `/api/view`
+ directly; the endpoint is retained for backward compatibility but will
+ be removed in a future release.
+ x-runtime: [cloud]
+ parameters:
+ - name: filename
+ in: query
+ required: true
+ schema:
+ type: string
+ description: Video filename
+ - name: type
+ in: query
+ schema:
+ type: string
+ enum: [input, output, temp]
+ description: Directory type
+ - name: subfolder
+ in: query
+ schema:
+ type: string
+ description: Subfolder within the directory
+ responses:
+ "200":
+ description: Video content
+ content:
+ video/*:
+ schema:
+ type: string
+ format: binary
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/tasks:
+ get:
+ operationId: listTasks
+ tags: [task]
+ summary: List background tasks
+ description: "[cloud-only] Retrieve a paginated list of background tasks for the authenticated user. Supports filtering by task type, status, and creation time."
+ x-runtime: [cloud]
+ parameters:
+ - name: task_name
+ in: query
+ schema:
+ type: string
+ description: Filter by task type name (exact match).
+ - name: idempotency_key
+ in: query
+ schema:
+ type: string
+ description: Filter by idempotency key (exact match).
+ - name: status
+ in: query
+ schema:
+ type: string
+ description: Filter by one or more statuses (comma-separated).
+ - name: created_after
+ in: query
+ schema:
+ type: string
+ format: date-time
+ description: Filter tasks created after this timestamp.
+ - name: created_before
+ in: query
+ schema:
+ type: string
+ format: date-time
+ description: Filter tasks created before this timestamp.
+ - name: sort_order
+ in: query
+ schema:
+ type: string
+ enum: [asc, desc]
+ default: desc
+ description: Sort direction by create_time.
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ minimum: 0
+ default: 0
+ description: Pagination offset (0-based).
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ description: Maximum items per page (1-100).
+ responses:
+ "200":
+ description: Tasks retrieved
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TasksListResponse"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "422":
+ description: Validation error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+ /api/tasks/{task_id}:
+ get:
+ operationId: getTask
+ tags: [task]
+ summary: Get task details
+ description: "[cloud-only] Retrieve full details for a specific background task."
+ x-runtime: [cloud]
+ parameters:
+ - name: task_id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: Task identifier (UUID).
+ responses:
+ "200":
+ description: Task details
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TaskResponse"
+ "401":
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+ "404":
+ description: Task not found
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CloudError"
+
+components:
+ parameters:
+ ComfyUserHeader:
+ name: Comfy-User
+ in: header
+ required: false
+ schema:
+ type: string
+ description: |
+ Identifies the active user in multi-user mode. Used for settings,
+ userdata, and history isolation. This is not a security mechanism —
+ it is an organisational convenience with no authentication behind it.
+
+ schemas:
+ # -------------------------------------------------------------------
+ # Prompt
+ # -------------------------------------------------------------------
+ PromptRequest:
+ type: object
+ description: A workflow submission. Wraps the prompt graph plus optional client identifier and extra per-request data.
+ required:
+ - prompt
+ properties:
+ prompt:
+ type: object
+ description: |
+ The workflow graph to execute. Keys are node IDs (strings);
+ values are objects with class_type and inputs.
+ additionalProperties: true
+ number:
+ type: number
+ description: Priority number for the queue (lower numbers have higher priority)
+ front:
+ type: boolean
+ description: If true, adds the prompt to the front of the queue
+ extra_data:
+ type: object
+ description: Extra data associated with the prompt (e.g. extra_pnginfo)
+ additionalProperties: true
+ client_id:
+ type: string
+ description: WebSocket client ID to receive progress updates
+ prompt_id:
+ type: string
+ format: uuid
+ description: "Client-supplied prompt ID. Server generates a UUID if omitted."
+ partial_execution_targets:
+ type: array
+ items:
+ type: string
+ description: List of node IDs to execute (partial graph execution)
+ workflow_id:
+ type: string
+ format: uuid
+ nullable: true
+ x-runtime: [cloud]
+ description: "[cloud-only] Cloud workflow entity ID for tracking and gallery association. Ignored by local ComfyUI."
+ workflow_version_id:
+ type: string
+ format: uuid
+ nullable: true
+ x-runtime: [cloud]
+ description: "[cloud-only] Cloud workflow version ID for pinning execution to a specific version. Ignored by local ComfyUI."
+
+ PromptResponse:
+ type: object
+ description: Server acknowledgement of a workflow submission. Includes the assigned `prompt_id` and current queue position.
+ properties:
+ prompt_id:
+ type: string
+ format: uuid
+ description: Unique identifier for the prompt execution
+ number:
+ type: number
+ description: Priority number in the queue
+ node_errors:
+ type: object
+ description: Validation errors keyed by node ID
+ additionalProperties:
+ $ref: "#/components/schemas/NodeError"
+ error:
+ description: Top-level prompt error (string message or structured error)
+ oneOf:
+ - type: string
+ - $ref: "#/components/schemas/PromptError"
+
+ PromptErrorResponse:
+ type: object
+ description: Error response when prompt validation fails
+ additionalProperties: true
+
+ PromptError:
+ type: object
+ description: Structured prompt validation error
+ properties:
+ type:
+ type: string
+ message:
+ type: string
+ details:
+ type: string
+
+ Error:
+ type: object
+ description: Detailed node-level error
+ properties:
+ type:
+ type: string
+ message:
+ type: string
+ details:
+ type: string
+ extra_info:
+ type: object
+ properties:
+ input_name:
+ type: string
+ additionalProperties: true
+
+ NodeError:
+ type: object
+ description: Error details for a single node
+ properties:
+ errors:
+ type: array
+ items:
+ $ref: "#/components/schemas/Error"
+ class_type:
+ type: string
+ description: The node's class type
+ dependent_outputs:
+ type: array
+ items: {}
+
+ PromptInfo:
+ type: object
+ description: Summary of a queued or recently-executed prompt, as returned by the queue and history endpoints.
+ properties:
+ exec_info:
+ type: object
+ properties:
+ queue_remaining:
+ type: integer
+ description: Number of items remaining in the queue
+
+ # -------------------------------------------------------------------
+ # Queue
+ # -------------------------------------------------------------------
+ QueueInfo:
+ type: object
+ description: Queue information with pending and running items
+ properties:
+ queue_running:
+ type: array
+ description: Currently running queue items
+ items:
+ type: array
+ description: |
+ Queue item tuple: [number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive]
+ items: {}
+ prefixItems:
+ - type: number
+ description: Priority number
+ - type: string
+ format: uuid
+ description: prompt_id
+ - type: object
+ description: prompt graph
+ additionalProperties: true
+ - type: object
+ description: extra_data
+ additionalProperties: true
+ - type: array
+ description: outputs_to_execute (list of output node IDs)
+ items:
+ type: string
+ - type: object
+ description: sensitive data (may be omitted)
+ additionalProperties: true
+ queue_pending:
+ type: array
+ description: Pending queue items (oldest first)
+ items:
+ type: array
+ description: |
+ Queue item tuple: [number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive]
+ items: {}
+ prefixItems:
+ - type: number
+ description: Priority number
+ - type: string
+ format: uuid
+ description: prompt_id
+ - type: object
+ description: prompt graph
+ additionalProperties: true
+ - type: object
+ description: extra_data
+ additionalProperties: true
+ - type: array
+ description: outputs_to_execute (list of output node IDs)
+ items:
+ type: string
+ - type: object
+ description: sensitive data (may be omitted)
+ additionalProperties: true
+
+ QueueManageRequest:
+ type: object
+ description: Request to clear or delete from queue
+ properties:
+ clear:
+ type: boolean
+ description: If true, clear all pending items
+ delete:
+ type: array
+ items:
+ type: string
+ description: Array of prompt IDs to delete from queue
+
+ # -------------------------------------------------------------------
+ # History
+ # -------------------------------------------------------------------
+ HistoryEntry:
+ type: object
+ description: A single execution history entry
+ properties:
+ prompt:
+ type: array
+ description: |
+ Prompt tuple: [number, prompt_id, prompt_graph, extra_data, output_node_ids]
+ items: {}
+ outputs:
+ type: object
+ description: Output data from execution keyed by node ID
+ additionalProperties: true
+ status:
+ type: object
+ description: Execution status (status_str, completed, messages, etc.)
+ additionalProperties: true
+ meta:
+ type: object
+ description: Metadata about the execution and nodes
+ additionalProperties: true
+
+ HistoryManageRequest:
+ type: object
+ description: Request to clear or delete history entries
+ properties:
+ clear:
+ type: boolean
+ description: If true, clear all history
+ delete:
+ type: array
+ items:
+ type: string
+ description: Array of prompt IDs to delete from history
+
+ # -------------------------------------------------------------------
+ # Jobs
+ # -------------------------------------------------------------------
+ JobEntry:
+ type: object
+ description: Lightweight job data for list views
+ required:
+ - id
+ - status
+ properties:
+ id:
+ type: string
+ format: uuid
+ description: Unique job identifier (same as prompt_id)
+ status:
+ type: string
+ description: Current job status
+ create_time:
+ type: number
+ description: Job creation timestamp
+ execution_start_time:
+ type: number
+ description: Workflow execution start timestamp
+ execution_end_time:
+ type: number
+ description: Workflow execution end timestamp
+ preview_output:
+ type: object
+ additionalProperties: true
+ description: Primary preview output
+ outputs_count:
+ type: integer
+ description: Total number of output files
+
+ JobDetailResponse:
+ type: object
+ description: Full job details including workflow and outputs
+ required:
+ - id
+ - status
+ properties:
+ id:
+ type: string
+ format: uuid
+ status:
+ type: string
+ workflow:
+ type: object
+ additionalProperties: true
+ description: Full ComfyUI workflow
+ outputs:
+ type: object
+ additionalProperties: true
+ description: Full outputs object from execution
+ execution_error:
+ $ref: "#/components/schemas/ExecutionError"
+ create_time:
+ type: number
+ update_time:
+ type: number
+ execution_start_time:
+ type: number
+ execution_end_time:
+ type: number
+ preview_output:
+ type: object
+ additionalProperties: true
+ outputs_count:
+ type: integer
+ execution_status:
+ type: object
+ additionalProperties: true
+ execution_meta:
+ type: object
+ additionalProperties: true
+
+ ExecutionError:
+ type: object
+ description: Detailed execution error from ComfyUI
+ properties:
+ node_id:
+ type: string
+ description: ID of the node that failed
+ node_type:
+ type: string
+ description: Type name of the node
+ exception_message:
+ type: string
+ description: Human-readable error message
+ exception_type:
+ type: string
+ description: Python exception type
+ traceback:
+ type: array
+ items:
+ type: string
+ description: Traceback lines
+ current_inputs:
+ type: object
+ additionalProperties: true
+ current_outputs:
+ type: object
+ additionalProperties: true
+
+ PaginationInfo:
+ type: object
+ description: Pagination metadata returned alongside list responses.
+ properties:
+ offset:
+ type: integer
+ limit:
+ type: integer
+ total:
+ type: integer
+ has_more:
+ type: boolean
+
+ # -------------------------------------------------------------------
+ # Upload / View
+ # -------------------------------------------------------------------
+ UploadResult:
+ type: object
+ description: Response body returned by the image/mask upload endpoints, describing where the uploaded file now lives.
+ properties:
+ name:
+ type: string
+ description: Saved filename (may be renamed to avoid collisions)
+ subfolder:
+ type: string
+ description: Subfolder the file was saved to
+ type:
+ type: string
+ description: Directory type (input, temp)
+
+ # -------------------------------------------------------------------
+ # System
+ # -------------------------------------------------------------------
+ DeviceStats:
+ type: object
+ description: GPU/compute device statistics
+ required:
+ - name
+ - type
+ - index
+ properties:
+ name:
+ type: string
+ description: Device name
+ type:
+ type: string
+ description: Device type (cuda, mps, cpu, etc.)
+ index:
+ type: number
+ nullable: true
+ description: |
+ Device index within its type (e.g. CUDA ordinal for `cuda:0`,
+ `cuda:1`). `null` for devices with no index, including the CPU
+ device returned in `--cpu` mode (PyTorch's `torch.device('cpu').index`
+ is `None`).
+ vram_total:
+ type: number
+ description: Total VRAM in bytes
+ vram_free:
+ type: number
+ description: Free VRAM in bytes
+ torch_vram_total:
+ type: number
+ description: Total PyTorch-managed VRAM in bytes
+ torch_vram_free:
+ type: number
+ description: Free PyTorch-managed VRAM in bytes
+
+ SystemStatsResponse:
+ type: object
+ description: Hardware, VRAM, Python, and ComfyUI version information for the running process.
+ required:
+ - system
+ - devices
+ properties:
+ system:
+ type: object
+ required:
+ - os
+ - python_version
+ - embedded_python
+ - comfyui_version
+ - pytorch_version
+ - argv
+ - ram_total
+ - ram_free
+ properties:
+ os:
+ type: string
+ description: Operating system
+ python_version:
+ type: string
+ description: Python version
+ embedded_python:
+ type: boolean
+ description: Whether using embedded Python
+ comfyui_version:
+ type: string
+ description: ComfyUI version string
+ pytorch_version:
+ type: string
+ description: PyTorch version
+ required_frontend_version:
+ type: string
+ description: Required frontend version
+ argv:
+ type: array
+ items:
+ type: string
+ description: Command line arguments
+ ram_total:
+ type: number
+ description: Total RAM in bytes
+ ram_free:
+ type: number
+ description: Free RAM in bytes
+ installed_templates_version:
+ type: string
+ nullable: true
+ description: Version of the currently installed workflow templates
+ required_templates_version:
+ type: string
+ nullable: true
+ description: Minimum required workflow templates version for this ComfyUI build
+ comfy_package_versions:
+ type: array
+ description: Installed and required versions for every comfy* package pinned in requirements.txt
+ items:
+ type: object
+ required:
+ - name
+ - installed
+ - required
+ properties:
+ name:
+ type: string
+ installed:
+ type: string
+ nullable: true
+ required:
+ type: string
+ nullable: true
+ devices:
+ type: array
+ items:
+ $ref: "#/components/schemas/DeviceStats"
+
+ # -------------------------------------------------------------------
+ # Node / Object Info
+ # -------------------------------------------------------------------
+ NodeInfo:
+ type: object
+ description: 'Definition of a registered node class: its inputs, outputs, category, and display metadata.'
+ properties:
+ input:
+ type: object
+ description: Input specifications (required and optional groups)
+ additionalProperties: true
+ input_order:
+ type: object
+ description: Ordered input names per group
+ additionalProperties:
+ type: array
+ items:
+ type: string
+ output:
+ type: array
+ items:
+ type: string
+ description: Output type names
+ output_is_list:
+ type: array
+ items:
+ type: boolean
+ description: Whether each output is a list
+ output_name:
+ type: array
+ items:
+ type: string
+ description: Display names of outputs
+ name:
+ type: string
+ description: Internal class name
+ display_name:
+ type: string
+ description: Human-readable display name
+ description:
+ type: string
+ description: Node description
+ python_module:
+ type: string
+ description: Python module implementing the node
+ category:
+ type: string
+ description: Node category path
+ output_node:
+ type: boolean
+ description: Whether this is an output node
+ output_tooltips:
+ type: array
+ items:
+ type: string
+ description: Tooltips for each output
+ deprecated:
+ type: boolean
+ description: Whether the node is deprecated
+ experimental:
+ type: boolean
+ description: Whether the node is experimental
+ api_node:
+ type: boolean
+ description: Whether this is an API node
+ is_input_list:
+ type: boolean
+ description: Whether the node accepts list inputs
+ dev_only:
+ type: boolean
+ description: Whether the node is developer-only (hidden in production UI)
+ has_intermediate_output:
+ type: boolean
+ description: Whether the node emits intermediate output during execution
+ search_aliases:
+ type: array
+ items:
+ type: string
+ description: Alternative search terms for finding this node
+ essentials_category:
+ type: string
+ nullable: true
+ description: |
+ Category override used by the essentials pack. The
+ `essentials_category` key may be present with a string value,
+ present and `null`, or absent entirely:
+
+ - V1 nodes: `essentials_category` is **omitted** when the node
+ class doesn't define an `ESSENTIALS_CATEGORY` attribute, and
+ **`null`** if the attribute is explicitly set to `None`.
+ - V3 nodes (`comfy_api.latest.io`): `essentials_category` is
+ **always present**, and **`null`** for nodes whose `Schema`
+ doesn't populate it.
+
+ # -------------------------------------------------------------------
+ # Models
+ # -------------------------------------------------------------------
+ ModelFolder:
+ type: object
+ description: A configured model folder and the list of disk paths it resolves to.
+ required:
+ - name
+ - folders
+ properties:
+ name:
+ type: string
+ description: Model folder type name (e.g. "checkpoints")
+ folders:
+ type: array
+ items:
+ type: string
+ description: Filesystem paths for this model type
+
+ ModelFile:
+ type: object
+ description: A single model file in a folder, with filesystem metadata.
+ required:
+ - name
+ - pathIndex
+ properties:
+ name:
+ type: string
+ description: Model filename
+ pathIndex:
+ type: integer
+ description: Index into the folder's paths array
+ modified:
+ type: number
+ description: File modification timestamp
+ created:
+ type: number
+ description: File creation timestamp
+ size:
+ type: integer
+ format: int64
+ description: File size in bytes
+
+ # -------------------------------------------------------------------
+ # Subgraphs
+ # -------------------------------------------------------------------
+ GlobalSubgraphInfo:
+ type: object
+ description: Metadata for a global subgraph blueprint (without full data)
+ required:
+ - source
+ - name
+ - info
+ properties:
+ source:
+ type: string
+ description: Source type ("templates" or "custom_node")
+ name:
+ type: string
+ description: Display name of the subgraph blueprint
+ info:
+ type: object
+ description: Additional information about the subgraph
+ required:
+ - node_pack
+ properties:
+ node_pack:
+ type: string
+ description: The node pack/module providing this subgraph
+ data:
+ type: string
+ description: The full subgraph JSON data (may be empty in list view)
+
+ GlobalSubgraphData:
+ type: object
+ description: Full data for a global subgraph blueprint
+ required:
+ - source
+ - name
+ - info
+ - data
+ properties:
+ source:
+ type: string
+ description: Source type ("templates" or "custom_node")
+ name:
+ type: string
+ description: Display name of the subgraph blueprint
+ info:
+ type: object
+ description: Additional information about the subgraph
+ required:
+ - node_pack
+ properties:
+ node_pack:
+ type: string
+ description: The node pack/module providing this subgraph
+ data:
+ type: string
+ description: The full subgraph JSON data as a string
+
+ # -------------------------------------------------------------------
+ # Userdata
+ # -------------------------------------------------------------------
+ UserDataResponse:
+ description: |
+ Response body for the POST endpoints `/api/userdata/{file}` and
+ `/api/userdata/{file}/move/{dest}`. Returns a single item whose
+ shape depends on the `full_info` query parameter.
+ x-variant-selector:
+ full_info=true: file-info object (`GetUserDataResponseFullFile`)
+ default: relative path string
+ oneOf:
+ - $ref: "#/components/schemas/GetUserDataResponseFullFile"
+ - type: string
+ description: Relative path of the written or moved file. Returned when `full_info` is absent or false.
+
+ ListUserdataResponse:
+ description: |
+ Response body for `GET /api/userdata`. The array item shape is
+ determined by the `full_info` and `split` query parameters.
+ x-variant-selector:
+ full_info=true: array of file-info objects (`GetUserDataResponseFullFile`)
+ split=true: array of `[relative_path, ...path_components]` arrays
+ default: array of relative path strings
+ oneOf:
+ - type: array
+ items:
+ $ref: "#/components/schemas/GetUserDataResponseFullFile"
+ description: Returned when `full_info=true`.
+ - type: array
+ items:
+ type: array
+ items:
+ type: string
+ minItems: 2
+ description: |
+ Returned when `split=true` and `full_info=false`. Each inner
+ array is `[relative_path, ...path_components]`.
+ - type: array
+ items:
+ type: string
+ description: Default shape — array of file paths relative to the user data root.
+
+ GetUserDataResponseFullFile:
+ type: object
+ description: A single entry in a full-info user data listing.
+ properties:
+ path:
+ type: string
+ description: File name or path relative to the user directory
+ created:
+ type: number
+ description: Unix timestamp of file creation
+ size:
+ type: integer
+ description: File size in bytes
+ modified:
+ type: integer
+ format: int64
+ description: Unix timestamp of last modification in milliseconds
+
+ # -------------------------------------------------------------------
+ # Assets
+ # -------------------------------------------------------------------
+ Asset:
+ type: object
+ description: A registered asset — an input/output file tracked in the asset database with content hash and metadata.
+ required:
+ - id
+ - name
+ - size
+ - created_at
+ - updated_at
+ properties:
+ id:
+ type: string
+ format: uuid
+ description: Unique identifier for the asset
+ name:
+ type: string
+ description: Name of the asset file
+ hash:
+ type: string
+ nullable: true
+ description: Blake3 content hash of the asset (preferred over asset_hash)
+ pattern: "^blake3:[a-f0-9]{64}$"
+ asset_hash:
+ type: string
+ nullable: true
+ deprecated: true
+ description: "Deprecated: use `hash` instead. Blake3 hash of the asset content."
+ pattern: "^blake3:[a-f0-9]{64}$"
+ size:
+ type: integer
+ format: int64
+ description: Size of the asset in bytes
+ mime_type:
+ type: string
+ description: MIME type of the asset
+ tags:
+ type: array
+ items:
+ type: string
+ description: Tags associated with the asset
+ user_metadata:
+ type: object
+ description: Custom user metadata
+ additionalProperties: true
+ metadata:
+ type: object
+ description: System-managed metadata (read-only)
+ additionalProperties: true
+ readOnly: true
+ preview_url:
+ type: string
+ format: uri
+ description: URL for asset preview/thumbnail
+ preview_id:
+ type: string
+ format: uuid
+ description: ID of the preview asset if available
+ prompt_id:
+ type: string
+ format: uuid
+ nullable: true
+ deprecated: true
+ description: "Deprecated: use job_id instead. ID of the prompt that created this asset."
+ job_id:
+ type: string
+ format: uuid
+ nullable: true
+ description: ID of the job that created this asset
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+ last_access_time:
+ type: string
+ format: date-time
+ is_immutable:
+ type: boolean
+ description: Whether this asset is immutable
+
+ AssetCreated:
+ description: Response body returned after successfully registering a new asset.
+ allOf:
+ - $ref: "#/components/schemas/Asset"
+ - type: object
+ required:
+ - created_new
+ properties:
+ created_new:
+ type: boolean
+ description: Whether this was a new creation (true) or returned existing (false)
+
+ AssetUpdated:
+ type: object
+ description: Response body returned after updating an asset's metadata.
+ required:
+ - id
+ - updated_at
+ properties:
+ id:
+ type: string
+ format: uuid
+ name:
+ type: string
+ hash:
+ type: string
+ nullable: true
+ description: Blake3 content hash of the asset (preferred over asset_hash)
+ pattern: "^blake3:[a-f0-9]{64}$"
+ asset_hash:
+ type: string
+ nullable: true
+ deprecated: true
+ description: "Deprecated: use `hash` instead. Blake3 hash of the asset content."
+ pattern: "^blake3:[a-f0-9]{64}$"
+ tags:
+ type: array
+ items:
+ type: string
+ mime_type:
+ type: string
+ user_metadata:
+ type: object
+ additionalProperties: true
+ prompt_id:
+ type: string
+ format: uuid
+ nullable: true
+ deprecated: true
+ description: "Deprecated: use job_id instead. ID of the prompt that created this asset."
+ job_id:
+ type: string
+ format: uuid
+ nullable: true
+ description: ID of the job that created this asset
+ updated_at:
+ type: string
+ format: date-time
+
+ ListAssetsResponse:
+ type: object
+ description: Paginated list of assets.
+ required:
+ - assets
+ - total
+ - has_more
+ properties:
+ assets:
+ type: array
+ items:
+ $ref: "#/components/schemas/Asset"
+ total:
+ type: integer
+ has_more:
+ type: boolean
+
+ TagInfo:
+ type: object
+ description: A tag known to the asset database, with the number of assets bearing it.
+ required:
+ - name
+ - count
+ properties:
+ name:
+ type: string
+ count:
+ type: integer
+
+ ListTagsResponse:
+ type: object
+ description: Flat list of all tags, with counts.
+ required:
+ - tags
+ - total
+ - has_more
+ properties:
+ tags:
+ type: array
+ items:
+ $ref: "#/components/schemas/TagInfo"
+ total:
+ type: integer
+ has_more:
+ type: boolean
+
+ AssetTagHistogramResponse:
+ type: object
+ description: Tags that would refine a filtered asset query, with the count of assets each tag would additionally select.
+ required:
+ - tag_counts
+ properties:
+ tag_counts:
+ type: object
+ additionalProperties:
+ type: integer
+ description: Map of tag names to occurrence counts
+
+ TagsModificationResponse:
+ type: object
+ description: Response body returned after adding or removing tags on an asset.
+ required:
+ - total_tags
+ properties:
+ added:
+ type: array
+ items:
+ type: string
+ description: Tags successfully added
+ removed:
+ type: array
+ items:
+ type: string
+ description: Tags successfully removed
+ already_present:
+ type: array
+ items:
+ type: string
+ description: Tags already present (for add)
+ not_present:
+ type: array
+ items:
+ type: string
+ description: Tags not present (for remove)
+ total_tags:
+ type: array
+ items:
+ type: string
+ description: All tags on the asset after the operation
+
+ # -------------------------------------------------------------------
+ # Result / Output types
+ # -------------------------------------------------------------------
+ ResultItem:
+ type: object
+ description: A single output file reference
+ properties:
+ filename:
+ type: string
+ subfolder:
+ type: string
+ type:
+ type: string
+ enum: [input, output, temp]
+ display_name:
+ type: string
+
+ NodeOutputs:
+ type: object
+ description: |
+ Outputs from a single node execution. Known keys are listed below,
+ but custom nodes may add arbitrary keys (additionalProperties).
+ properties:
+ images:
+ type: array
+ items:
+ $ref: "#/components/schemas/ResultItem"
+ audio:
+ type: array
+ items:
+ $ref: "#/components/schemas/ResultItem"
+ video:
+ type: array
+ items:
+ $ref: "#/components/schemas/ResultItem"
+ animated:
+ type: array
+ items:
+ type: boolean
+ text:
+ oneOf:
+ - type: string
+ - type: array
+ items:
+ type: string
+ additionalProperties: true
+
+ TerminalSize:
+ type: object
+ description: Terminal dimensions
+ properties:
+ cols:
+ type: number
+ row:
+ type: number
+
+ LogEntry:
+ type: object
+ description: A single log entry
+ properties:
+ t:
+ type: string
+ description: Timestamp
+ m:
+ type: string
+ description: Log message
+
+ StatusWsMessageStatus:
+ type: object
+ description: Inner payload of a `status` WebSocket message, describing the execution queue state.
+ properties:
+ exec_info:
+ type: object
+ required:
+ - queue_remaining
+ properties:
+ queue_remaining:
+ type: integer
+
+ StatusWsMessage:
+ type: object
+ description: Initial status message sent on connect + queue status updates
+ properties:
+ status:
+ $ref: "#/components/schemas/StatusWsMessageStatus"
+ sid:
+ type: string
+ description: Session ID assigned by the server
+
+ ProgressWsMessage:
+ type: object
+ description: Node execution progress (step N of M)
+ required:
+ - value
+ - max
+ - prompt_id
+ - node
+ properties:
+ value:
+ type: integer
+ description: Current step
+ max:
+ type: integer
+ description: Total steps
+ prompt_id:
+ type: string
+ node:
+ type: string
+ description: Node ID currently executing
+
+ ProgressTextWsMessage:
+ type: object
+ description: Text-based progress update from a node
+ properties:
+ nodeId:
+ type: string
+ text:
+ type: string
+ prompt_id:
+ type: string
+
+ NodeProgressState:
+ type: object
+ description: Progress state for a single node
+ properties:
+ value:
+ type: number
+ max:
+ type: number
+ state:
+ type: string
+ enum: [pending, running, finished, error]
+ node_id:
+ type: string
+ prompt_id:
+ type: string
+ display_node_id:
+ type: string
+ parent_node_id:
+ type: string
+ real_node_id:
+ type: string
+
+ ProgressStateWsMessage:
+ type: object
+ description: Bulk progress state for all nodes in a prompt
+ required:
+ - prompt_id
+ - nodes
+ properties:
+ prompt_id:
+ type: string
+ nodes:
+ type: object
+ description: Map of node ID to progress state
+ additionalProperties:
+ $ref: "#/components/schemas/NodeProgressState"
+
+ ExecutingWsMessage:
+ type: object
+ description: Fired when a node begins execution
+ required:
+ - node
+ - display_node
+ - prompt_id
+ properties:
+ node:
+ type: string
+ description: Node ID
+ display_node:
+ type: string
+ description: Display node ID (may differ for subgraphs)
+ prompt_id:
+ type: string
+
+ ExecutedWsMessage:
+ type: object
+ description: Fired when a node completes execution with output
+ required:
+ - node
+ - display_node
+ - prompt_id
+ - output
+ properties:
+ node:
+ type: string
+ display_node:
+ type: string
+ prompt_id:
+ type: string
+ output:
+ $ref: "#/components/schemas/NodeOutputs"
+ merge:
+ type: boolean
+ description: Whether to merge with existing output
+
+ ExecutionWsMessageBase:
+ type: object
+ description: Base fields for execution lifecycle messages
+ required:
+ - prompt_id
+ - timestamp
+ properties:
+ prompt_id:
+ type: string
+ timestamp:
+ type: integer
+ description: Unix timestamp in milliseconds
+
+ ExecutionStartWsMessage:
+ allOf:
+ - $ref: "#/components/schemas/ExecutionWsMessageBase"
+ description: Fired when prompt execution begins
+
+ ExecutionSuccessWsMessage:
+ allOf:
+ - $ref: "#/components/schemas/ExecutionWsMessageBase"
+ description: Fired when prompt execution completes successfully
+
+ ExecutionCachedWsMessage:
+ allOf:
+ - $ref: "#/components/schemas/ExecutionWsMessageBase"
+ - type: object
+ properties:
+ nodes:
+ type: array
+ items:
+ type: string
+ description: List of node IDs that were cached
+ description: Fired when nodes are served from cache
+
+ ExecutionInterruptedWsMessage:
+ allOf:
+ - $ref: "#/components/schemas/ExecutionWsMessageBase"
+ - type: object
+ properties:
+ node_id:
+ type: string
+ node_type:
+ type: string
+ executed:
+ type: array
+ items:
+ type: string
+ description: Node IDs that completed before interruption
+ description: Fired when execution is interrupted by user
+
+ ExecutionErrorWsMessage:
+ allOf:
+ - $ref: "#/components/schemas/ExecutionWsMessageBase"
+ - type: object
+ properties:
+ node_id:
+ type: string
+ node_type:
+ type: string
+ executed:
+ type: array
+ items:
+ type: string
+ exception_message:
+ type: string
+ exception_type:
+ type: string
+ traceback:
+ type: array
+ items:
+ type: string
+ current_inputs: {}
+ current_outputs: {}
+ description: Fired when a node throws an exception during execution
+
+ LogsWsMessage:
+ type: object
+ description: Streaming log entries from the server
+ properties:
+ size:
+ $ref: "#/components/schemas/TerminalSize"
+ entries:
+ type: array
+ items:
+ $ref: "#/components/schemas/LogEntry"
+
+ NotificationWsMessage:
+ type: object
+ description: Server notification (e.g. model download complete)
+ properties:
+ value:
+ type: string
+ id:
+ type: string
+
+ FeatureFlagsWsMessage:
+ type: object
+ description: Feature flags sent on connect
+ additionalProperties: true
+
+ AssetDownloadWsMessage:
+ type: object
+ description: Asset download progress
+ required:
+ - task_id
+ - asset_name
+ - bytes_total
+ - bytes_downloaded
+ - progress
+ - status
+ properties:
+ task_id:
+ type: string
+ asset_name:
+ type: string
+ bytes_total:
+ type: number
+ bytes_downloaded:
+ type: number
+ progress:
+ type: number
+ description: 0.0 to 1.0
+ status:
+ type: string
+ enum: [created, running, completed, failed]
+ asset_id:
+ type: string
+ error:
+ type: string
+
+ AssetExportWsMessage:
+ type: object
+ description: Bulk asset export progress
+ required:
+ - task_id
+ - assets_total
+ - assets_attempted
+ - assets_failed
+ - bytes_total
+ - bytes_processed
+ - progress
+ - status
+ properties:
+ task_id:
+ type: string
+ export_name:
+ type: string
+ assets_total:
+ type: number
+ assets_attempted:
+ type: number
+ assets_failed:
+ type: number
+ bytes_total:
+ type: number
+ bytes_processed:
+ type: number
+ progress:
+ type: number
+ description: 0.0 to 1.0
+ status:
+ type: string
+ enum: [created, running, completed, failed]
+ error:
+ type: string
+
+ # -------------------------------------------------------------------
+ # Cloud-runtime schemas
+ #
+ # These schemas are exclusively referenced by cloud-runtime operations.
+ # Tagged x-runtime: [cloud].
+ # -------------------------------------------------------------------
+ CloudError:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Standard error response from cloud endpoints."
+ required:
+ - error
+ properties:
+ error:
+ type: string
+ description: Error message
+ code:
+ type: string
+ description: Machine-readable error code
+ details:
+ type: object
+ additionalProperties: true
+ description: Additional error context
+
+ CloudJobStatus:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Status of a cloud job."
+ required:
+ - id
+ - status
+ properties:
+ id:
+ type: string
+ format: uuid
+ status:
+ type: string
+ enum: [pending, running, completed, failed, cancelled]
+ progress:
+ type: number
+ minimum: 0
+ maximum: 1
+ description: "Execution progress (0.0 to 1.0)"
+ started_at:
+ type: string
+ format: date-time
+ nullable: true
+ completed_at:
+ type: string
+ format: date-time
+ nullable: true
+
+ CloudPrompt:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A cloud-executed prompt record."
+ required:
+ - id
+ - status
+ properties:
+ id:
+ type: string
+ format: uuid
+ status:
+ type: string
+ workflow:
+ type: object
+ additionalProperties: true
+ outputs:
+ type: object
+ additionalProperties: true
+ created_at:
+ type: string
+ format: date-time
+ completed_at:
+ type: string
+ format: date-time
+ nullable: true
+
+ HistoryV2Response:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Paginated execution history in v2 format."
+ required:
+ - items
+ - total
+ - has_more
+ properties:
+ items:
+ type: array
+ items:
+ $ref: "#/components/schemas/HistoryV2Entry"
+ total:
+ type: integer
+ has_more:
+ type: boolean
+
+ HistoryV2Entry:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A single execution history entry in v2 format."
+ required:
+ - id
+ - status
+ properties:
+ id:
+ type: string
+ format: uuid
+ status:
+ type: string
+ workflow:
+ type: object
+ additionalProperties: true
+ outputs:
+ type: object
+ additionalProperties: true
+ created_at:
+ type: string
+ format: date-time
+ started_at:
+ type: string
+ format: date-time
+ nullable: true
+ completed_at:
+ type: string
+ format: date-time
+ nullable: true
+ preview_output:
+ type: object
+ additionalProperties: true
+
+ CloudLogsResponse:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Paginated cloud execution logs."
+ required:
+ - entries
+ properties:
+ entries:
+ type: array
+ items:
+ type: object
+ properties:
+ timestamp:
+ type: string
+ format: date-time
+ level:
+ type: string
+ enum: [debug, info, warn, error]
+ message:
+ type: string
+ job_id:
+ type: string
+ format: uuid
+ total:
+ type: integer
+ has_more:
+ type: boolean
+
+ AssetDownloadRequest:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A single asset to download to the cloud runtime."
+ required:
+ - asset_id
+ properties:
+ asset_id:
+ type: string
+ format: uuid
+ description: ID of the asset to download
+ target_path:
+ type: string
+ description: Target path on the runtime filesystem
+
+ AssetImportRequest:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A single asset to import from an external URL."
+ required:
+ - url
+ properties:
+ url:
+ type: string
+ format: uri
+ description: URL of the asset to import
+ name:
+ type: string
+ description: Display name for the imported asset
+ tags:
+ type: array
+ items:
+ type: string
+
+ RemoteAssetMetadata:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Metadata fetched from a remote asset URL."
+ properties:
+ content_type:
+ type: string
+ description: MIME type of the remote file
+ content_length:
+ type: integer
+ format: int64
+ description: Size in bytes
+ filename:
+ type: string
+ description: Suggested filename from Content-Disposition or URL
+
+ CloudNode:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] An installed custom node package in the cloud runtime."
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ version:
+ type: string
+ description:
+ type: string
+ author:
+ type: string
+ repository:
+ type: string
+ format: uri
+ installed_at:
+ type: string
+ format: date-time
+ enabled:
+ type: boolean
+
+ HubLabel:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A label/category used for tagging hub content."
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ color:
+ type: string
+ description: Hex color code for the label
+
+ HubProfile:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A public user profile on the ComfyUI Hub."
+ required:
+ - username
+ properties:
+ username:
+ type: string
+ display_name:
+ type: string
+ bio:
+ type: string
+ avatar_url:
+ type: string
+ format: uri
+ links:
+ type: array
+ items:
+ type: string
+ format: uri
+ workflow_count:
+ type: integer
+ created_at:
+ type: string
+ format: date-time
+
+ HubWorkflow:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A published workflow on the ComfyUI Hub."
+ required:
+ - share_id
+ - name
+ properties:
+ share_id:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ author:
+ $ref: "#/components/schemas/HubProfile"
+ labels:
+ type: array
+ items:
+ $ref: "#/components/schemas/HubLabel"
+ thumbnail_url:
+ type: string
+ format: uri
+ content:
+ type: object
+ additionalProperties: true
+ description: Workflow graph JSON
+ likes:
+ type: integer
+ views:
+ type: integer
+ forks:
+ type: integer
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+
+ HubWorkflowList:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Paginated list of hub workflows."
+ required:
+ - workflows
+ - total
+ - has_more
+ properties:
+ workflows:
+ type: array
+ items:
+ $ref: "#/components/schemas/HubWorkflow"
+ total:
+ type: integer
+ has_more:
+ type: boolean
+
+ HubWorkflowIndexEntry:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Lightweight entry in the hub workflow index for client-side search."
+ required:
+ - share_id
+ - name
+ properties:
+ share_id:
+ type: string
+ name:
+ type: string
+ author_username:
+ type: string
+ labels:
+ type: array
+ items:
+ type: string
+ likes:
+ type: integer
+ updated_at:
+ type: string
+ format: date-time
+
+ CloudWorkflow:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A cloud-managed workflow with version history."
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: string
+ format: uuid
+ name:
+ type: string
+ description:
+ type: string
+ share_id:
+ type: string
+ nullable: true
+ description: Public share identifier if published
+ latest_version_id:
+ type: string
+ format: uuid
+ nullable: true
+ thumbnail_url:
+ type: string
+ format: uri
+ nullable: true
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+
+ CloudWorkflowList:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Paginated list of cloud workflows."
+ required:
+ - workflows
+ - total
+ - has_more
+ properties:
+ workflows:
+ type: array
+ items:
+ $ref: "#/components/schemas/CloudWorkflow"
+ total:
+ type: integer
+ has_more:
+ type: boolean
+
+ CloudWorkflowVersion:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A version of a cloud workflow."
+ required:
+ - id
+ - workflow_id
+ properties:
+ id:
+ type: string
+ format: uuid
+ workflow_id:
+ type: string
+ format: uuid
+ version_number:
+ type: integer
+ created_at:
+ type: string
+ format: date-time
+
+ AuthSession:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Current authentication session state."
+ required:
+ - user
+ properties:
+ user:
+ $ref: "#/components/schemas/CloudUser"
+ workspace:
+ $ref: "#/components/schemas/Workspace"
+ expires_at:
+ type: string
+ format: date-time
+
+ AuthTokenResponse:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] OAuth2 token response."
+ required:
+ - access_token
+ - token_type
+ properties:
+ access_token:
+ type: string
+ token_type:
+ type: string
+ description: Always "Bearer"
+ expires_in:
+ type: integer
+ description: Token lifetime in seconds
+ refresh_token:
+ type: string
+ nullable: true
+ scope:
+ type: string
+
+ JwksResponse:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] JSON Web Key Set for JWT verification."
+ required:
+ - keys
+ properties:
+ keys:
+ type: array
+ items:
+ type: object
+ required:
+ - kty
+ - kid
+ - use
+ properties:
+ kty:
+ type: string
+ description: Key type (e.g. RSA)
+ kid:
+ type: string
+ description: Key ID
+ use:
+ type: string
+ description: Key use (e.g. sig)
+ alg:
+ type: string
+ description: Algorithm (e.g. RS256)
+ n:
+ type: string
+ description: RSA modulus (base64url)
+ e:
+ type: string
+ description: RSA exponent (base64url)
+ additionalProperties: true
+
+ BillingBalance:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Current credit balance and usage summary."
+ required:
+ - credits_remaining
+ properties:
+ credits_remaining:
+ type: integer
+ description: Available credits
+ credits_used:
+ type: integer
+ description: Credits used in current billing period
+ credits_total:
+ type: integer
+ description: Total credits allocated in current period
+
+ BillingEvent:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A billing event (charge, credit, refund)."
+ required:
+ - id
+ - type
+ - amount
+ - created_at
+ properties:
+ id:
+ type: string
+ type:
+ type: string
+ enum: [charge, credit, refund, topup, subscription]
+ amount:
+ type: integer
+ description: Amount in credits
+ description:
+ type: string
+ job_id:
+ type: string
+ format: uuid
+ nullable: true
+ created_at:
+ type: string
+ format: date-time
+
+ BillingEventList:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Paginated list of billing events."
+ required:
+ - events
+ - total
+ - has_more
+ properties:
+ events:
+ type: array
+ items:
+ $ref: "#/components/schemas/BillingEvent"
+ total:
+ type: integer
+ has_more:
+ type: boolean
+
+ BillingOp:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A billing operation record."
+ required:
+ - id
+ - status
+ properties:
+ id:
+ type: string
+ status:
+ type: string
+ enum: [pending, completed, failed]
+ type:
+ type: string
+ amount:
+ type: integer
+ created_at:
+ type: string
+ format: date-time
+ completed_at:
+ type: string
+ format: date-time
+ nullable: true
+
+ BillingPlan:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A subscription plan with pricing details."
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ credits_per_month:
+ type: integer
+ price_cents:
+ type: integer
+ description: Monthly price in cents (USD)
+ currency:
+ type: string
+ default: usd
+ features:
+ type: array
+ items:
+ type: string
+ description: List of plan features
+
+ BillingStatus:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Overall billing and subscription status."
+ properties:
+ subscription:
+ $ref: "#/components/schemas/BillingSubscription"
+ balance:
+ $ref: "#/components/schemas/BillingBalance"
+ has_payment_method:
+ type: boolean
+
+ BillingSubscription:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Active subscription details."
+ required:
+ - id
+ - status
+ - plan_id
+ properties:
+ id:
+ type: string
+ status:
+ type: string
+ enum: [active, cancelled, past_due, trialing]
+ plan_id:
+ type: string
+ plan_name:
+ type: string
+ current_period_start:
+ type: string
+ format: date-time
+ current_period_end:
+ type: string
+ format: date-time
+ cancel_at_period_end:
+ type: boolean
+
+ SubscriptionPreview:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Preview of a subscription change including prorations."
+ properties:
+ plan_id:
+ type: string
+ plan_name:
+ type: string
+ amount_due:
+ type: integer
+ description: Amount due in cents
+ proration_amount:
+ type: integer
+ description: Proration adjustment in cents
+ currency:
+ type: string
+ next_billing_date:
+ type: string
+ format: date-time
+
+ Workspace:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A cloud workspace for team collaboration."
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ owner_id:
+ type: string
+ member_count:
+ type: integer
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+
+ WorkspaceMember:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A member of a cloud workspace."
+ required:
+ - user_id
+ - role
+ properties:
+ user_id:
+ type: string
+ email:
+ type: string
+ format: email
+ display_name:
+ type: string
+ avatar_url:
+ type: string
+ format: uri
+ role:
+ type: string
+ enum: [owner, admin, member]
+ joined_at:
+ type: string
+ format: date-time
+
+ WorkspaceInvite:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A pending workspace invitation."
+ required:
+ - id
+ - email
+ - role
+ properties:
+ id:
+ type: string
+ email:
+ type: string
+ format: email
+ role:
+ type: string
+ enum: [admin, member]
+ invited_by:
+ type: string
+ created_at:
+ type: string
+ format: date-time
+ expires_at:
+ type: string
+ format: date-time
+
+ WorkspaceApiKey:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A workspace API key (secret value redacted)."
+ required:
+ - id
+ - name
+ - description
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ maxLength: 5000
+ description: User-provided description of the key's purpose. Always present in responses; empty string when no description was supplied on create.
+ prefix:
+ type: string
+ description: First few characters of the key for identification
+ created_at:
+ type: string
+ format: date-time
+ last_used_at:
+ type: string
+ format: date-time
+ nullable: true
+ created_by:
+ type: string
+
+ WorkspaceApiKeyCreated:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A newly created workspace API key, including the full secret value (shown only once)."
+ required:
+ - id
+ - name
+ - description
+ - key
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ maxLength: 5000
+ description: User-provided description of the key's purpose. Always present in responses; empty string when no description was supplied on create.
+ key:
+ type: string
+ description: Full API key value (only returned on creation)
+ prefix:
+ type: string
+ created_at:
+ type: string
+ format: date-time
+
+ CloudUser:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] A cloud-authenticated user profile."
+ required:
+ - id
+ - email
+ properties:
+ id:
+ type: string
+ email:
+ type: string
+ format: email
+ display_name:
+ type: string
+ avatar_url:
+ type: string
+ format: uri
+ created_at:
+ type: string
+ format: date-time
+
+ SecretMeta:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Metadata for a stored secret (value is never returned)."
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ provider:
+ type: string
+ description: "[cloud-only] Provider identifier (e.g., huggingface, civitai)."
+ x-runtime: [cloud]
+ last_used_at:
+ type: string
+ format: date-time
+ description: "[cloud-only] When the secret was last used for decryption."
+ x-runtime: [cloud]
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+
+ UpdateSecretRequest:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Request body for updating an existing user secret."
+ properties:
+ name:
+ type: string
+ description: New name for the secret
+ secret_value:
+ type: string
+ description: New secret value (API key, token, etc.)
+
+ CreateSessionResponse:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Response after creating a session cookie."
+ required:
+ - success
+ properties:
+ success:
+ type: boolean
+ expiresIn:
+ type: integer
+ description: Session expiration time in seconds.
+
+ DeleteSessionResponse:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Response after deleting a session cookie."
+ required:
+ - success
+ properties:
+ success:
+ type: boolean
+
+ CreateHubProfileRequest:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Request body for creating a new Hub profile."
+ required:
+ - workspace_id
+ - username
+ properties:
+ workspace_id:
+ type: string
+ username:
+ type: string
+ description: Unique URL-safe slug. Immutable after creation.
+ display_name:
+ type: string
+ description:
+ type: string
+ avatar_token:
+ type: string
+ website_urls:
+ type: array
+ items:
+ type: string
+
+ PublishHubWorkflowRequest:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Request body for publishing or updating a workflow on the Hub."
+ required:
+ - username
+ - name
+ - workflow_filename
+ - asset_ids
+ properties:
+ username:
+ type: string
+ name:
+ type: string
+ workflow_filename:
+ type: string
+ asset_ids:
+ type: array
+ items:
+ type: string
+ description:
+ type: string
+ tags:
+ type: array
+ items:
+ type: string
+ models:
+ type: array
+ items:
+ type: string
+ custom_nodes:
+ type: array
+ items:
+ type: string
+ tutorial_url:
+ type: string
+ metadata:
+ type: object
+ additionalProperties: true
+ thumbnail_type:
+ type: string
+ enum: [image, video, image_comparison]
+ thumbnail_token_or_url:
+ type: string
+ thumbnail_comparison_token_or_url:
+ type: string
+ sample_image_tokens_or_urls:
+ type: array
+ items:
+ type: string
+
+ HubWorkflowDetail:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Full Hub workflow detail including versions, assets, and statistics."
+ required:
+ - share_id
+ - workflow_id
+ - name
+ - workflow_json
+ - assets
+ - profile
+ - status
+ properties:
+ share_id:
+ type: string
+ workflow_id:
+ type: string
+ name:
+ type: string
+ status:
+ type: string
+ enum: [pending, approved, rejected, deprecated]
+ description:
+ type: string
+ thumbnail_type:
+ type: string
+ enum: [image, video, image_comparison]
+ thumbnail_url:
+ type: string
+ thumbnail_comparison_url:
+ type: string
+ tutorial_url:
+ type: string
+ metadata:
+ type: object
+ additionalProperties: true
+ sample_image_urls:
+ type: array
+ items:
+ type: string
+ publish_time:
+ type: string
+ format: date-time
+ nullable: true
+ workflow_json:
+ type: object
+ additionalProperties: true
+ assets:
+ type: array
+ items:
+ $ref: "#/components/schemas/AssetInfo"
+ profile:
+ $ref: "#/components/schemas/HubProfile"
+
+ AssetInfo:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Lightweight asset reference used in workflow publishing payloads."
+ required:
+ - id
+ - filename
+ properties:
+ id:
+ type: string
+ filename:
+ type: string
+ mime_type:
+ type: string
+ size_bytes:
+ type: integer
+ format: int64
+
+ BulkRevokeAPIKeysResponse:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Response after bulk-revoking API keys for a workspace member."
+ required:
+ - revoked_count
+ properties:
+ revoked_count:
+ type: integer
+ minimum: 0
+
+ CreateWorkflowVersionRequest:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Request body for creating a new version of a saved workflow."
+ required:
+ - base_version
+ - workflow_json
+ properties:
+ base_version:
+ type: integer
+ description: Version number this change is based on (for optimistic concurrency).
+ workflow_json:
+ type: object
+ additionalProperties: true
+
+ WorkflowVersionResponse:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Metadata for a single workflow version."
+ required:
+ - id
+ - version
+ - latest_version
+ - created_by
+ - created_at
+ properties:
+ id:
+ type: string
+ version:
+ type: integer
+ latest_version:
+ type: integer
+ created_by:
+ type: string
+ created_at:
+ type: string
+ format: date-time
+
+ WorkflowPublishInfo:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Publishing metadata for a workflow shared to the Hub."
+ required:
+ - workflow_id
+ - share_id
+ - listed
+ - assets
+ properties:
+ workflow_id:
+ type: string
+ share_id:
+ type: string
+ publish_time:
+ type: string
+ format: date-time
+ nullable: true
+ listed:
+ type: boolean
+ assets:
+ type: array
+ items:
+ $ref: "#/components/schemas/AssetInfo"
+
+ TaskEntry:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Task data for list views."
+ required:
+ - id
+ - task_name
+ - status
+ - create_time
+ properties:
+ id:
+ type: string
+ format: uuid
+ task_name:
+ type: string
+ status:
+ type: string
+ enum: [created, running, completed, failed]
+ create_time:
+ type: string
+ format: date-time
+ started_at:
+ type: string
+ format: date-time
+ completed_at:
+ type: string
+ format: date-time
+
+ TaskResponse:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Full task details including payload and result."
+ required:
+ - id
+ - idempotency_key
+ - task_name
+ - payload
+ - status
+ - create_time
+ - update_time
+ properties:
+ id:
+ type: string
+ format: uuid
+ idempotency_key:
+ type: string
+ task_name:
+ type: string
+ payload:
+ type: object
+ additionalProperties: true
+ status:
+ type: string
+ enum: [created, running, completed, failed]
+ result:
+ type: object
+ additionalProperties: true
+ create_time:
+ type: string
+ format: date-time
+ update_time:
+ type: string
+ format: date-time
+ started_at:
+ type: string
+ format: date-time
+ completed_at:
+ type: string
+ format: date-time
+ error:
+ type: string
+
+ TasksListResponse:
+ type: object
+ x-runtime: [cloud]
+ description: "[cloud-only] Paginated list of background tasks for the authenticated user."
+ required:
+ - tasks
+ - pagination
+ properties:
+ tasks:
+ type: array
+ items:
+ $ref: "#/components/schemas/TaskEntry"
+ pagination:
+ $ref: "#/components/schemas/PaginationInfo"
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 8fa92ecbe..0a1554428 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "ComfyUI"
-version = "0.19.3"
+version = "0.21.1"
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.10"
diff --git a/requirements.txt b/requirements.txt
index d08980f81..f499a10ae 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
-comfyui-frontend-package==1.42.11
-comfyui-workflow-templates==0.9.57
-comfyui-embedded-docs==0.4.3
+comfyui-frontend-package==1.43.18
+comfyui-workflow-templates==0.9.77
+comfyui-embedded-docs==0.5.0
torch
torchsde
torchvision
@@ -19,11 +19,11 @@ scipy
tqdm
psutil
alembic
-SQLAlchemy
+SQLAlchemy>=2.0.0
filelock
av>=14.2.0
comfy-kitchen>=0.2.8
-comfy-aimdo==0.0.214
+comfy-aimdo==0.3.0
requests
simpleeval>=1.0.0
blake3
diff --git a/server.py b/server.py
index 881da8e66..44470b904 100644
--- a/server.py
+++ b/server.py
@@ -1,3 +1,4 @@
+import errno
import os
import sys
import asyncio
@@ -655,6 +656,7 @@ class PromptServer():
required_frontend_version = FrontendManager.get_required_frontend_version()
installed_templates_version = FrontendManager.get_installed_templates_version()
required_templates_version = FrontendManager.get_required_templates_version()
+ comfy_package_versions = FrontendManager.get_comfy_package_versions()
system_stats = {
"system": {
@@ -665,6 +667,7 @@ class PromptServer():
"required_frontend_version": required_frontend_version,
"installed_templates_version": installed_templates_version,
"required_templates_version": required_templates_version,
+ "comfy_package_versions": comfy_package_versions,
"python_version": sys.version,
"pytorch_version": comfy.model_management.torch_version,
"embedded_python": os.path.split(os.path.split(sys.executable)[0])[1] == "python_embeded",
@@ -1245,7 +1248,13 @@ class PromptServer():
address = addr[0]
port = addr[1]
site = web.TCPSite(runner, address, port, ssl_context=ssl_ctx)
- await site.start()
+ try:
+ await site.start()
+ except OSError as e:
+ if e.errno == errno.EADDRINUSE:
+ logging.error(f"Port {port} is already in use on address {address}. Please close the other application or use a different port with --port.")
+ raise SystemExit(1)
+ raise
if not hasattr(self, 'address'):
self.address = address #TODO: remove this
diff --git a/tests-unit/app_test/frontend_manager_test.py b/tests-unit/app_test/frontend_manager_test.py
index 1d5a84b47..8c8a2eb48 100644
--- a/tests-unit/app_test/frontend_manager_test.py
+++ b/tests-unit/app_test/frontend_manager_test.py
@@ -52,7 +52,10 @@ def mock_provider(mock_releases):
@pytest.fixture(autouse=True)
def clear_cache():
import utils.install_util
+ import app.frontend_management
+
utils.install_util.PACKAGE_VERSIONS = {}
+ app.frontend_management.COMFY_PACKAGE_VERSIONS = []
def test_get_release(mock_provider, mock_releases):
@@ -147,7 +150,7 @@ def test_init_frontend_default_with_mocks():
# Act
with (
- patch("app.frontend_management.check_frontend_version") as mock_check,
+ patch("app.frontend_management.check_comfy_packages_versions") as mock_check,
patch.object(
FrontendManager, "default_frontend_path", return_value="/mocked/path"
),
@@ -168,7 +171,7 @@ def test_init_frontend_fallback_on_error():
patch.object(
FrontendManager, "init_frontend_unsafe", side_effect=Exception("Test error")
),
- patch("app.frontend_management.check_frontend_version") as mock_check,
+ patch("app.frontend_management.check_comfy_packages_versions") as mock_check,
patch.object(
FrontendManager, "default_frontend_path", return_value="/default/path"
),
@@ -277,7 +280,9 @@ def test_get_installed_templates_version():
def test_get_installed_templates_version_not_installed():
# Act
- with patch("app.frontend_management.version", side_effect=Exception("Package not found")):
+ with patch(
+ "app.frontend_management.version", side_effect=Exception("Package not found")
+ ):
version = FrontendManager.get_installed_templates_version()
# Assert
diff --git a/tests-unit/app_test/node_replace_manager_test.py b/tests-unit/app_test/node_replace_manager_test.py
new file mode 100644
index 000000000..8a3fd18bb
--- /dev/null
+++ b/tests-unit/app_test/node_replace_manager_test.py
@@ -0,0 +1,90 @@
+"""Tests for NodeReplaceManager registration behavior."""
+import importlib
+import sys
+import types
+
+import pytest
+
+
+@pytest.fixture
+def NodeReplaceManager(monkeypatch):
+ """Provide NodeReplaceManager with `nodes` stubbed.
+
+ `app.node_replace_manager` does `import nodes` at module level, which pulls in
+ torch + the full ComfyUI graph. register() doesn't actually need it, so we
+ stub `nodes` per-test (via monkeypatch so it's torn down) and reload the
+ module so it picks up the stub instead of any cached real import.
+ """
+ fake_nodes = types.ModuleType("nodes")
+ fake_nodes.NODE_CLASS_MAPPINGS = {}
+ monkeypatch.setitem(sys.modules, "nodes", fake_nodes)
+ monkeypatch.delitem(sys.modules, "app.node_replace_manager", raising=False)
+ module = importlib.import_module("app.node_replace_manager")
+ yield module.NodeReplaceManager
+ # Drop the freshly-imported module so the next test (or a later real import
+ # of `nodes`) starts from a clean slate.
+ sys.modules.pop("app.node_replace_manager", None)
+
+
+class FakeNodeReplace:
+ """Lightweight stand-in for comfy_api.latest._io.NodeReplace."""
+ def __init__(self, new_node_id, old_node_id, old_widget_ids=None,
+ input_mapping=None, output_mapping=None):
+ self.new_node_id = new_node_id
+ self.old_node_id = old_node_id
+ self.old_widget_ids = old_widget_ids
+ self.input_mapping = input_mapping
+ self.output_mapping = output_mapping
+
+
+def test_register_adds_replacement(NodeReplaceManager):
+ manager = NodeReplaceManager()
+ manager.register(FakeNodeReplace(new_node_id="NewNode", old_node_id="OldNode"))
+ assert manager.has_replacement("OldNode")
+ assert len(manager.get_replacement("OldNode")) == 1
+
+
+def test_register_allows_multiple_alternatives_for_same_old_node(NodeReplaceManager):
+ """Different new_node_ids for the same old_node_id should all be kept."""
+ manager = NodeReplaceManager()
+ manager.register(FakeNodeReplace(new_node_id="AltA", old_node_id="OldNode"))
+ manager.register(FakeNodeReplace(new_node_id="AltB", old_node_id="OldNode"))
+ replacements = manager.get_replacement("OldNode")
+ assert len(replacements) == 2
+ assert {r.new_node_id for r in replacements} == {"AltA", "AltB"}
+
+
+def test_register_is_idempotent_for_duplicate_pair(NodeReplaceManager):
+ """Re-registering the same (old_node_id, new_node_id) should be a no-op."""
+ manager = NodeReplaceManager()
+ manager.register(FakeNodeReplace(new_node_id="NewNode", old_node_id="OldNode"))
+ manager.register(FakeNodeReplace(new_node_id="NewNode", old_node_id="OldNode"))
+ manager.register(FakeNodeReplace(new_node_id="NewNode", old_node_id="OldNode"))
+ assert len(manager.get_replacement("OldNode")) == 1
+
+
+def test_register_idempotent_preserves_first_registration(NodeReplaceManager):
+ """First registration wins; later duplicates with different mappings are ignored."""
+ manager = NodeReplaceManager()
+ first = FakeNodeReplace(
+ new_node_id="NewNode", old_node_id="OldNode",
+ input_mapping=[{"new_id": "a", "old_id": "x"}],
+ )
+ second = FakeNodeReplace(
+ new_node_id="NewNode", old_node_id="OldNode",
+ input_mapping=[{"new_id": "b", "old_id": "y"}],
+ )
+ manager.register(first)
+ manager.register(second)
+ replacements = manager.get_replacement("OldNode")
+ assert len(replacements) == 1
+ assert replacements[0] is first
+
+
+def test_register_dedupe_does_not_affect_other_old_nodes(NodeReplaceManager):
+ manager = NodeReplaceManager()
+ manager.register(FakeNodeReplace(new_node_id="NewA", old_node_id="OldA"))
+ manager.register(FakeNodeReplace(new_node_id="NewA", old_node_id="OldA"))
+ manager.register(FakeNodeReplace(new_node_id="NewB", old_node_id="OldB"))
+ assert len(manager.get_replacement("OldA")) == 1
+ assert len(manager.get_replacement("OldB")) == 1
diff --git a/tests-unit/comfy_api_test/multicombo_serialization_test.py b/tests-unit/comfy_api_test/multicombo_serialization_test.py
new file mode 100644
index 000000000..421c65a0d
--- /dev/null
+++ b/tests-unit/comfy_api_test/multicombo_serialization_test.py
@@ -0,0 +1,78 @@
+from comfy_api.latest._io import Combo, MultiCombo
+
+
+def test_multicombo_serializes_multi_select_as_object():
+ multi_combo = MultiCombo.Input(
+ id="providers",
+ options=["a", "b", "c"],
+ default=["a"],
+ )
+
+ serialized = multi_combo.as_dict()
+
+ assert serialized["multiselect"] is True
+ assert "multi_select" in serialized
+ assert serialized["multi_select"] == {}
+
+
+def test_multicombo_serializes_multi_select_with_placeholder_and_chip():
+ multi_combo = MultiCombo.Input(
+ id="providers",
+ options=["a", "b", "c"],
+ default=["a"],
+ placeholder="Select providers",
+ chip=True,
+ )
+
+ serialized = multi_combo.as_dict()
+
+ assert serialized["multiselect"] is True
+ assert serialized["multi_select"] == {
+ "placeholder": "Select providers",
+ "chip": True,
+ }
+
+
+def test_combo_does_not_serialize_multiselect():
+ """Regular Combo should not have multiselect in its serialized output."""
+ combo = Combo.Input(
+ id="choice",
+ options=["a", "b", "c"],
+ )
+
+ serialized = combo.as_dict()
+
+ # Combo sets multiselect=False, but prune_dict keeps False (not None),
+ # so it should be present but False
+ assert serialized.get("multiselect") is False
+ assert "multi_select" not in serialized
+
+
+def _validate_combo_values(val, combo_options, is_multiselect):
+ """Reproduce the validation logic from execution.py for testing."""
+ if is_multiselect and isinstance(val, list):
+ return [v for v in val if v not in combo_options]
+ else:
+ return [val] if val not in combo_options else []
+
+
+def test_multicombo_validation_accepts_valid_list():
+ options = ["a", "b", "c"]
+ assert _validate_combo_values(["a", "b"], options, True) == []
+
+
+def test_multicombo_validation_rejects_invalid_values():
+ options = ["a", "b", "c"]
+ assert _validate_combo_values(["a", "x"], options, True) == ["x"]
+
+
+def test_multicombo_validation_accepts_empty_list():
+ options = ["a", "b", "c"]
+ assert _validate_combo_values([], options, True) == []
+
+
+def test_combo_validation_rejects_list_even_with_valid_items():
+ """A regular Combo should not accept a list value."""
+ options = ["a", "b", "c"]
+ invalid = _validate_combo_values(["a", "b"], options, False)
+ assert len(invalid) > 0
diff --git a/tests-unit/comfy_extras_test/nodes_math_test.py b/tests-unit/comfy_extras_test/nodes_math_test.py
index fa4cdcac3..714e37c32 100644
--- a/tests-unit/comfy_extras_test/nodes_math_test.py
+++ b/tests-unit/comfy_extras_test/nodes_math_test.py
@@ -124,9 +124,11 @@ class TestMathExpressionExecute:
with pytest.raises(Exception, match="not defined"):
self._exec("str(a)", a=42)
- def test_boolean_result_raises(self):
- with pytest.raises(ValueError, match="got bool"):
- self._exec("a > b", a=5, b=3)
+ def test_boolean_result(self):
+ result = self._exec("a > b", a=5, b=3)
+ assert result[2] is True
+ result = self._exec("a > b", a=3, b=5)
+ assert result[2] is False
def test_empty_expression_raises(self):
with pytest.raises(ValueError, match="Expression cannot be empty"):
diff --git a/tests-unit/comfy_test/model_detection_test.py b/tests-unit/comfy_test/model_detection_test.py
index 2551a417b..4e9350602 100644
--- a/tests-unit/comfy_test/model_detection_test.py
+++ b/tests-unit/comfy_test/model_detection_test.py
@@ -1,9 +1,23 @@
+from collections import defaultdict
+
import torch
from comfy.model_detection import detect_unet_config, model_config_from_unet_config
import comfy.supported_models
+def _freeze(value):
+ """Recursively convert a value to a hashable form so configs can be
+ compared/used as dict keys or set members."""
+ if isinstance(value, dict):
+ return frozenset((k, _freeze(v)) for k, v in value.items())
+ if isinstance(value, (list, tuple)):
+ return tuple(_freeze(v) for v in value)
+ if isinstance(value, set):
+ return frozenset(_freeze(v) for v in value)
+ return value
+
+
def _make_longcat_comfyui_sd():
"""Minimal ComfyUI-format state dict for pre-converted LongCat-Image weights."""
sd = {}
@@ -110,3 +124,21 @@ class TestModelDetection:
model_config = model_config_from_unet_config(unet_config, sd)
assert model_config is not None
assert type(model_config).__name__ == "FluxSchnell"
+
+ def test_unet_config_and_required_keys_combination_is_unique(self):
+ """Each model in the registry must have a unique combination of
+ ``unet_config`` and ``required_keys``. If two models share the same
+ combination, ``BASE.matches`` cannot disambiguate between them and the
+ first one in the list will always win."""
+ models = comfy.supported_models.models
+ groups = defaultdict(list)
+ for model in models:
+ key = (_freeze(model.unet_config), _freeze(model.required_keys))
+ groups[key].append(model.__name__)
+
+ duplicates = {k: names for k, names in groups.items() if len(names) > 1}
+ assert not duplicates, (
+ "Found models sharing the same (unet_config, required_keys) "
+ "combination, which makes detection ambiguous: "
+ + "; ".join(", ".join(names) for names in duplicates.values())
+ )
diff --git a/tests-unit/deploy_environment_test.py b/tests-unit/deploy_environment_test.py
new file mode 100644
index 000000000..c3497fbb0
--- /dev/null
+++ b/tests-unit/deploy_environment_test.py
@@ -0,0 +1,109 @@
+"""Tests for comfy.deploy_environment."""
+
+import os
+
+import pytest
+
+from comfy import deploy_environment
+from comfy.deploy_environment import get_deploy_environment
+
+
+@pytest.fixture(autouse=True)
+def _reset_cache_and_install_dir(tmp_path, monkeypatch):
+ """Reset the functools cache and point the ComfyUI install dir at a tmp dir for each test."""
+ get_deploy_environment.cache_clear()
+ monkeypatch.setattr(deploy_environment, "_COMFY_INSTALL_DIR", str(tmp_path))
+ yield
+ get_deploy_environment.cache_clear()
+
+
+def _write_env_file(tmp_path, content: str) -> str:
+ """Write the env file with exact content (no newline translation).
+
+ `newline=""` disables Python's text-mode newline translation so the bytes
+ on disk match the literal string passed in, regardless of host OS.
+ Newline-style tests (CRLF, lone CR) rely on this.
+ """
+ path = os.path.join(str(tmp_path), ".comfy_environment")
+ with open(path, "w", encoding="utf-8", newline="") as f:
+ f.write(content)
+ return path
+
+
+class TestGetDeployEnvironment:
+ def test_returns_local_git_when_file_missing(self):
+ assert get_deploy_environment() == "local-git"
+
+ def test_reads_value_from_file(self, tmp_path):
+ _write_env_file(tmp_path, "local-desktop2-standalone\n")
+ assert get_deploy_environment() == "local-desktop2-standalone"
+
+ def test_strips_trailing_whitespace_and_newline(self, tmp_path):
+ _write_env_file(tmp_path, " local-desktop2-standalone \n")
+ assert get_deploy_environment() == "local-desktop2-standalone"
+
+ def test_only_first_line_is_used(self, tmp_path):
+ _write_env_file(tmp_path, "first-line\nsecond-line\n")
+ assert get_deploy_environment() == "first-line"
+
+ def test_crlf_line_ending(self, tmp_path):
+ # Windows editors often save text files with CRLF line endings.
+ # The CR must not end up in the returned value.
+ _write_env_file(tmp_path, "local-desktop2-standalone\r\n")
+ assert get_deploy_environment() == "local-desktop2-standalone"
+
+ def test_crlf_multiline_only_first_line_used(self, tmp_path):
+ _write_env_file(tmp_path, "first-line\r\nsecond-line\r\n")
+ assert get_deploy_environment() == "first-line"
+
+ def test_crlf_with_surrounding_whitespace(self, tmp_path):
+ _write_env_file(tmp_path, " local-desktop2-standalone \r\n")
+ assert get_deploy_environment() == "local-desktop2-standalone"
+
+ def test_lone_cr_line_ending(self, tmp_path):
+ # Classic-Mac / some legacy editors use a bare CR.
+ # Universal-newlines decoding treats it as a line terminator too.
+ _write_env_file(tmp_path, "local-desktop2-standalone\r")
+ assert get_deploy_environment() == "local-desktop2-standalone"
+
+ def test_empty_file_falls_back_to_default(self, tmp_path):
+ _write_env_file(tmp_path, "")
+ assert get_deploy_environment() == "local-git"
+
+ def test_empty_after_whitespace_strip_falls_back_to_default(self, tmp_path):
+ _write_env_file(tmp_path, " \n")
+ assert get_deploy_environment() == "local-git"
+
+ def test_strips_control_chars_within_first_line(self, tmp_path):
+ # Embedded NUL/control chars in the value should be stripped
+ # (header-injection / smuggling protection).
+ _write_env_file(tmp_path, "abc\x00\x07xyz\n")
+ assert get_deploy_environment() == "abcxyz"
+
+ def test_strips_non_ascii_characters(self, tmp_path):
+ _write_env_file(tmp_path, "café-é\n")
+ assert get_deploy_environment() == "caf-"
+
+ def test_caps_read_at_128_bytes(self, tmp_path):
+ # A single huge line with no newline must not be fully read into memory.
+ huge = "x" * 10_000
+ _write_env_file(tmp_path, huge)
+ result = get_deploy_environment()
+ assert result == "x" * 128
+
+ def test_result_is_cached_across_calls(self, tmp_path):
+ path = _write_env_file(tmp_path, "first_value\n")
+ assert get_deploy_environment() == "first_value"
+ # Overwrite the file — cached value should still be returned.
+ with open(path, "w", encoding="utf-8") as f:
+ f.write("second_value\n")
+ assert get_deploy_environment() == "first_value"
+
+ def test_unreadable_file_falls_back_to_default(self, tmp_path, monkeypatch):
+ _write_env_file(tmp_path, "should_not_be_used\n")
+
+ def _boom(*args, **kwargs):
+ raise OSError("simulated read failure")
+
+ monkeypatch.setattr("builtins.open", _boom)
+ assert get_deploy_environment() == "local-git"
diff --git a/tests-unit/feature_flags_test.py b/tests-unit/feature_flags_test.py
index f2702cfc8..8ec52a124 100644
--- a/tests-unit/feature_flags_test.py
+++ b/tests-unit/feature_flags_test.py
@@ -1,10 +1,15 @@
"""Tests for feature flags functionality."""
+import pytest
+
from comfy_api.feature_flags import (
get_connection_feature,
supports_feature,
get_server_features,
+ CLI_FEATURE_FLAG_REGISTRY,
SERVER_FEATURE_FLAGS,
+ _coerce_flag_value,
+ _parse_cli_feature_flags,
)
@@ -96,3 +101,83 @@ class TestFeatureFlags:
result = get_connection_feature(sockets_metadata, "sid1", "any_feature")
assert result is False
assert supports_feature(sockets_metadata, "sid1", "any_feature") is False
+
+
+class TestCoerceFlagValue:
+ """Test suite for _coerce_flag_value."""
+
+ def test_registered_bool_true(self):
+ assert _coerce_flag_value("show_signin_button", "true") is True
+ assert _coerce_flag_value("show_signin_button", "True") is True
+
+ def test_registered_bool_false(self):
+ assert _coerce_flag_value("show_signin_button", "false") is False
+ assert _coerce_flag_value("show_signin_button", "FALSE") is False
+
+ def test_unregistered_key_stays_string(self):
+ assert _coerce_flag_value("unknown_flag", "true") == "true"
+ assert _coerce_flag_value("unknown_flag", "42") == "42"
+
+ def test_bool_typo_raises(self):
+ """Strict bool: typos like 'ture' or 'yes' must raise so the flag can be dropped."""
+ with pytest.raises(ValueError):
+ _coerce_flag_value("show_signin_button", "ture")
+ with pytest.raises(ValueError):
+ _coerce_flag_value("show_signin_button", "yes")
+ with pytest.raises(ValueError):
+ _coerce_flag_value("show_signin_button", "1")
+ with pytest.raises(ValueError):
+ _coerce_flag_value("show_signin_button", "")
+
+ def test_failed_int_coercion_raises(self, monkeypatch):
+ """Malformed values for typed flags must raise; caller decides what to do."""
+ monkeypatch.setitem(
+ CLI_FEATURE_FLAG_REGISTRY,
+ "test_int_flag",
+ {"type": "int", "default": 0, "description": "test"},
+ )
+ with pytest.raises(ValueError):
+ _coerce_flag_value("test_int_flag", "not_a_number")
+
+
+class TestParseCliFeatureFlags:
+ """Test suite for _parse_cli_feature_flags."""
+
+ def test_single_flag(self, monkeypatch):
+ monkeypatch.setattr("comfy_api.feature_flags.args", type("Args", (), {"feature_flag": ["show_signin_button=true"]})())
+ result = _parse_cli_feature_flags()
+ assert result == {"show_signin_button": True}
+
+ def test_missing_equals_defaults_to_true(self, monkeypatch):
+ """Bare flag without '=' is treated as the string 'true' (and coerced if registered)."""
+ monkeypatch.setattr("comfy_api.feature_flags.args", type("Args", (), {"feature_flag": ["show_signin_button", "valid=1"]})())
+ result = _parse_cli_feature_flags()
+ assert result == {"show_signin_button": True, "valid": "1"}
+
+ def test_empty_key_skipped(self, monkeypatch):
+ monkeypatch.setattr("comfy_api.feature_flags.args", type("Args", (), {"feature_flag": ["=value", "valid=1"]})())
+ result = _parse_cli_feature_flags()
+ assert result == {"valid": "1"}
+
+ def test_invalid_bool_value_dropped(self, monkeypatch, caplog):
+ """A typo'd bool value must be dropped entirely, not silently set to False
+ and not stored as a raw string. A warning must be logged."""
+ monkeypatch.setattr(
+ "comfy_api.feature_flags.args",
+ type("Args", (), {"feature_flag": ["show_signin_button=ture", "valid=1"]})(),
+ )
+ with caplog.at_level("WARNING"):
+ result = _parse_cli_feature_flags()
+ assert result == {"valid": "1"}
+ assert "show_signin_button" not in result
+ assert any("show_signin_button" in r.message and "drop" in r.message.lower() for r in caplog.records)
+
+
+class TestCliFeatureFlagRegistry:
+ """Test suite for the CLI feature flag registry."""
+
+ def test_registry_entries_have_required_fields(self):
+ for key, info in CLI_FEATURE_FLAG_REGISTRY.items():
+ assert "type" in info, f"{key} missing 'type'"
+ assert "default" in info, f"{key} missing 'default'"
+ assert "description" in info, f"{key} missing 'description'"
diff --git a/tests-unit/prompt_server_test/user_manager_test.py b/tests-unit/prompt_server_test/user_manager_test.py
index b939d8e68..27118400f 100644
--- a/tests-unit/prompt_server_test/user_manager_test.py
+++ b/tests-unit/prompt_server_test/user_manager_test.py
@@ -69,7 +69,11 @@ async def test_listuserdata_full_info(aiohttp_client, app, tmp_path):
assert len(result) == 1
assert result[0]["path"] == "file1.txt"
assert "size" in result[0]
- assert "modified" in result[0]
+ assert isinstance(result[0]["modified"], int)
+ assert isinstance(result[0]["created"], int)
+ # Verify millisecond magnitude (timestamps after year 2000 in ms are > 946684800000)
+ assert result[0]["modified"] > 946684800000
+ assert result[0]["created"] > 946684800000
async def test_listuserdata_split_path(aiohttp_client, app, tmp_path):
diff --git a/tests/execution/testing_nodes/testing-pack/api_test_nodes.py b/tests/execution/testing_nodes/testing-pack/api_test_nodes.py
index b2eaae05e..70c2a9e95 100644
--- a/tests/execution/testing_nodes/testing-pack/api_test_nodes.py
+++ b/tests/execution/testing_nodes/testing-pack/api_test_nodes.py
@@ -21,7 +21,7 @@ class TestAsyncProgressUpdate(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
FUNCTION = "execute"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
async def execute(self, value, sleep_seconds):
start = time.time()
@@ -51,7 +51,7 @@ class TestSyncProgressUpdate(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
FUNCTION = "execute"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
def execute(self, value, sleep_seconds):
start = time.time()
diff --git a/tests/execution/testing_nodes/testing-pack/async_test_nodes.py b/tests/execution/testing_nodes/testing-pack/async_test_nodes.py
index 547eea6f4..589dabf17 100644
--- a/tests/execution/testing_nodes/testing-pack/async_test_nodes.py
+++ b/tests/execution/testing_nodes/testing-pack/async_test_nodes.py
@@ -21,7 +21,7 @@ class TestAsyncValidation(ComfyNodeABC):
RETURN_TYPES = ("IMAGE",)
FUNCTION = "process"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
@classmethod
async def VALIDATE_INPUTS(cls, value, threshold):
@@ -53,7 +53,7 @@ class TestAsyncError(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
FUNCTION = "error_execution"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
async def error_execution(self, value, error_after):
await asyncio.sleep(error_after)
@@ -74,7 +74,7 @@ class TestAsyncValidationError(ComfyNodeABC):
RETURN_TYPES = ("IMAGE",)
FUNCTION = "process"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
@classmethod
async def VALIDATE_INPUTS(cls, value, max_value):
@@ -105,7 +105,7 @@ class TestAsyncTimeout(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
FUNCTION = "timeout_execution"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
async def timeout_execution(self, value, timeout, operation_time):
try:
@@ -129,7 +129,7 @@ class TestSyncError(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
FUNCTION = "sync_error"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
def sync_error(self, value):
raise RuntimeError("Intentional sync execution error for testing")
@@ -150,7 +150,7 @@ class TestAsyncLazyCheck(ComfyNodeABC):
RETURN_TYPES = ("IMAGE",)
FUNCTION = "process"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
async def check_lazy_status(self, condition, input1, input2):
# Simulate async checking (e.g., querying remote service)
@@ -184,7 +184,7 @@ class TestDynamicAsyncGeneration(ComfyNodeABC):
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_async_workflow"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
def generate_async_workflow(self, image1, image2, num_async_nodes, sleep_duration):
g = GraphBuilder()
@@ -229,7 +229,7 @@ class TestAsyncResourceUser(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
FUNCTION = "use_resource"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
async def use_resource(self, value, resource_id, duration):
# Check if resource is already in use
@@ -265,7 +265,7 @@ class TestAsyncBatchProcessing(ComfyNodeABC):
RETURN_TYPES = ("IMAGE",)
FUNCTION = "process_batch"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
async def process_batch(self, images, process_time_per_item, unique_id):
batch_size = images.shape[0]
@@ -305,7 +305,7 @@ class TestAsyncConcurrentLimit(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
FUNCTION = "limited_execution"
- CATEGORY = "_for_testing/async"
+ CATEGORY = "experimental/async"
async def limited_execution(self, value, duration, node_id):
async with self._semaphore:
diff --git a/tests/execution/testing_nodes/testing-pack/specific_tests.py b/tests/execution/testing_nodes/testing-pack/specific_tests.py
index 4f8f01ae4..2eb5d520e 100644
--- a/tests/execution/testing_nodes/testing-pack/specific_tests.py
+++ b/tests/execution/testing_nodes/testing-pack/specific_tests.py
@@ -409,7 +409,7 @@ class TestSleep(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
FUNCTION = "sleep"
- CATEGORY = "_for_testing"
+ CATEGORY = "experimental"
async def sleep(self, value, seconds, unique_id):
pbar = ProgressBar(seconds, node_id=unique_id)
@@ -440,7 +440,7 @@ class TestParallelSleep(ComfyNodeABC):
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "parallel_sleep"
- CATEGORY = "_for_testing"
+ CATEGORY = "experimental"
OUTPUT_NODE = True
def parallel_sleep(self, image1, image2, image3, sleep1, sleep2, sleep3, unique_id):
@@ -474,7 +474,7 @@ class TestOutputNodeWithSocketOutput:
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "process"
- CATEGORY = "_for_testing"
+ CATEGORY = "experimental"
OUTPUT_NODE = True
def process(self, image, value):