mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-12-20 03:23:00 +08:00
Merge branch 'master' into dr-support-pip-cm
This commit is contained in:
commit
20ac0052f8
40
.github/workflows/stable-release.yml
vendored
40
.github/workflows/stable-release.yml
vendored
@ -8,11 +8,11 @@ on:
|
|||||||
description: 'Git tag'
|
description: 'Git tag'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
cu:
|
cache_tag:
|
||||||
description: 'CUDA version'
|
description: 'Cached dependencies tag'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
default: "129"
|
default: "cu129"
|
||||||
python_minor:
|
python_minor:
|
||||||
description: 'Python minor version'
|
description: 'Python minor version'
|
||||||
required: true
|
required: true
|
||||||
@ -23,7 +23,11 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
default: "6"
|
default: "6"
|
||||||
|
rel_name:
|
||||||
|
description: 'Release name'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: "nvidia"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
package_comfy_windows:
|
package_comfy_windows:
|
||||||
@ -42,15 +46,15 @@ jobs:
|
|||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
cu${{ inputs.cu }}_python_deps.tar
|
${{ inputs.cache_tag }}_python_deps.tar
|
||||||
update_comfyui_and_python_dependencies.bat
|
update_comfyui_and_python_dependencies.bat
|
||||||
key: ${{ runner.os }}-build-cu${{ inputs.cu }}-${{ inputs.python_minor }}
|
key: ${{ runner.os }}-build-${{ inputs.cache_tag }}-${{ inputs.python_minor }}
|
||||||
- shell: bash
|
- shell: bash
|
||||||
run: |
|
run: |
|
||||||
mv cu${{ inputs.cu }}_python_deps.tar ../
|
mv ${{ inputs.cache_tag }}_python_deps.tar ../
|
||||||
mv update_comfyui_and_python_dependencies.bat ../
|
mv update_comfyui_and_python_dependencies.bat ../
|
||||||
cd ..
|
cd ..
|
||||||
tar xf cu${{ inputs.cu }}_python_deps.tar
|
tar xf ${{ inputs.cache_tag }}_python_deps.tar
|
||||||
pwd
|
pwd
|
||||||
ls
|
ls
|
||||||
|
|
||||||
@ -65,12 +69,19 @@ jobs:
|
|||||||
echo 'import site' >> ./python3${{ inputs.python_minor }}._pth
|
echo 'import site' >> ./python3${{ inputs.python_minor }}._pth
|
||||||
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
||||||
./python.exe get-pip.py
|
./python.exe get-pip.py
|
||||||
./python.exe -s -m pip install ../cu${{ inputs.cu }}_python_deps/*
|
./python.exe -s -m pip install ../${{ inputs.cache_tag }}_python_deps/*
|
||||||
|
|
||||||
|
grep comfyui ../ComfyUI/requirements.txt > ./requirements_comfyui.txt
|
||||||
|
./python.exe -s -m pip install -r requirements_comfyui.txt
|
||||||
|
rm requirements_comfyui.txt
|
||||||
|
|
||||||
sed -i '1i../ComfyUI' ./python3${{ inputs.python_minor }}._pth
|
sed -i '1i../ComfyUI' ./python3${{ inputs.python_minor }}._pth
|
||||||
|
|
||||||
|
if test -f ./Lib/site-packages/torch/lib/dnnl.lib; then
|
||||||
rm ./Lib/site-packages/torch/lib/dnnl.lib #I don't think this is actually used and I need the space
|
rm ./Lib/site-packages/torch/lib/dnnl.lib #I don't think this is actually used and I need the space
|
||||||
rm ./Lib/site-packages/torch/lib/libprotoc.lib
|
rm ./Lib/site-packages/torch/lib/libprotoc.lib
|
||||||
rm ./Lib/site-packages/torch/lib/libprotobuf.lib
|
rm ./Lib/site-packages/torch/lib/libprotobuf.lib
|
||||||
|
fi
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
@ -91,7 +102,7 @@ jobs:
|
|||||||
cd ..
|
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
|
"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
|
||||||
mv ComfyUI_windows_portable.7z ComfyUI/ComfyUI_windows_portable_nvidia.7z
|
mv ComfyUI_windows_portable.7z ComfyUI/ComfyUI_windows_portable_${{ inputs.rel_name }}.7z
|
||||||
|
|
||||||
cd ComfyUI_windows_portable
|
cd ComfyUI_windows_portable
|
||||||
python_embeded/python.exe -s ComfyUI/main.py --quick-test-for-ci --cpu
|
python_embeded/python.exe -s ComfyUI/main.py --quick-test-for-ci --cpu
|
||||||
@ -101,10 +112,9 @@ jobs:
|
|||||||
ls
|
ls
|
||||||
|
|
||||||
- name: Upload binaries to release
|
- name: Upload binaries to release
|
||||||
uses: svenstaro/upload-release-action@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
files: ComfyUI_windows_portable_${{ inputs.rel_name }}.7z
|
||||||
file: ComfyUI_windows_portable_nvidia.7z
|
tag_name: ${{ inputs.git_tag }}
|
||||||
tag: ${{ inputs.git_tag }}
|
|
||||||
overwrite: true
|
|
||||||
draft: true
|
draft: true
|
||||||
|
overwrite_files: true
|
||||||
|
|||||||
2
.github/workflows/test-unit.yml
vendored
2
.github/workflows/test-unit.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-2022, macos-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@ -56,7 +56,8 @@ jobs:
|
|||||||
..\python_embeded\python.exe -s -m pip install --upgrade torch torchvision torchaudio ${{ inputs.xformers }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r ../ComfyUI/requirements.txt pygit2
|
..\python_embeded\python.exe -s -m pip install --upgrade torch torchvision torchaudio ${{ inputs.xformers }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r ../ComfyUI/requirements.txt pygit2
|
||||||
pause" > update_comfyui_and_python_dependencies.bat
|
pause" > update_comfyui_and_python_dependencies.bat
|
||||||
|
|
||||||
python -m pip wheel --no-cache-dir torch torchvision torchaudio ${{ inputs.xformers }} ${{ inputs.extra_dependencies }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r requirements.txt pygit2 -w ./temp_wheel_dir
|
grep -v comfyui requirements.txt > requirements_nocomfyui.txt
|
||||||
|
python -m pip wheel --no-cache-dir torch torchvision torchaudio ${{ inputs.xformers }} ${{ inputs.extra_dependencies }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r requirements_nocomfyui.txt pygit2 -w ./temp_wheel_dir
|
||||||
python -m pip install --no-cache-dir ./temp_wheel_dir/*
|
python -m pip install --no-cache-dir ./temp_wheel_dir/*
|
||||||
echo installed basic
|
echo installed basic
|
||||||
ls -lah temp_wheel_dir
|
ls -lah temp_wheel_dir
|
||||||
|
|||||||
24
CODEOWNERS
24
CODEOWNERS
@ -1,25 +1,3 @@
|
|||||||
# Admins
|
# Admins
|
||||||
* @comfyanonymous
|
* @comfyanonymous
|
||||||
|
* @kosinkadink
|
||||||
# Note: Github teams syntax cannot be used here as the repo is not owned by Comfy-Org.
|
|
||||||
# Inlined the team members for now.
|
|
||||||
|
|
||||||
# Maintainers
|
|
||||||
*.md @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne @guill
|
|
||||||
/tests/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne @guill
|
|
||||||
/tests-unit/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne @guill
|
|
||||||
/notebooks/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne @guill
|
|
||||||
/script_examples/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne @guill
|
|
||||||
/.github/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne @guill
|
|
||||||
/requirements.txt @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne @guill
|
|
||||||
/pyproject.toml @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @Kosinkadink @christian-byrne @guill
|
|
||||||
|
|
||||||
# Python web server
|
|
||||||
/api_server/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @christian-byrne @guill
|
|
||||||
/app/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @christian-byrne @guill
|
|
||||||
/utils/ @yoland68 @robinjhuang @webfiltered @pythongosssss @ltdrdata @christian-byrne @guill
|
|
||||||
|
|
||||||
# Node developers
|
|
||||||
/comfy_extras/ @yoland68 @robinjhuang @pythongosssss @ltdrdata @Kosinkadink @webfiltered @christian-byrne @guill
|
|
||||||
/comfy/comfy_types/ @yoland68 @robinjhuang @pythongosssss @ltdrdata @Kosinkadink @webfiltered @christian-byrne @guill
|
|
||||||
/comfy_api_nodes/ @yoland68 @robinjhuang @pythongosssss @ltdrdata @Kosinkadink @webfiltered @christian-byrne @guill
|
|
||||||
|
|||||||
@ -233,7 +233,7 @@ Nvidia users should install stable pytorch using this command:
|
|||||||
|
|
||||||
This is the command to install pytorch nightly instead which might have performance improvements.
|
This is the command to install pytorch nightly instead which might have performance improvements.
|
||||||
|
|
||||||
```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu129```
|
```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu130```
|
||||||
|
|
||||||
#### Troubleshooting
|
#### Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,7 @@ def get_installed_frontend_version():
|
|||||||
frontend_version_str = version("comfyui-frontend-package")
|
frontend_version_str = version("comfyui-frontend-package")
|
||||||
return frontend_version_str
|
return frontend_version_str
|
||||||
|
|
||||||
|
|
||||||
def get_required_frontend_version():
|
def get_required_frontend_version():
|
||||||
"""Get the required frontend version from requirements.txt."""
|
"""Get the required frontend version from requirements.txt."""
|
||||||
try:
|
try:
|
||||||
@ -63,6 +64,7 @@ def get_required_frontend_version():
|
|||||||
logging.error(f"Error reading requirements.txt: {e}")
|
logging.error(f"Error reading requirements.txt: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def check_frontend_version():
|
def check_frontend_version():
|
||||||
"""Check if the frontend version is up to date."""
|
"""Check if the frontend version is up to date."""
|
||||||
|
|
||||||
@ -203,6 +205,37 @@ class FrontendManager:
|
|||||||
"""Get the required frontend package version."""
|
"""Get the required frontend package version."""
|
||||||
return get_required_frontend_version()
|
return get_required_frontend_version()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_installed_templates_version(cls) -> str:
|
||||||
|
"""Get the currently installed workflow templates package version."""
|
||||||
|
try:
|
||||||
|
templates_version_str = version("comfyui-workflow-templates")
|
||||||
|
return templates_version_str
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_required_templates_version(cls) -> str:
|
||||||
|
"""Get the required workflow templates version from requirements.txt."""
|
||||||
|
try:
|
||||||
|
with open(requirements_path, "r", encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("comfyui-workflow-templates=="):
|
||||||
|
version_str = line.split("==")[-1]
|
||||||
|
if not is_valid_version(version_str):
|
||||||
|
logging.error(f"Invalid templates version format in requirements.txt: {version_str}")
|
||||||
|
return None
|
||||||
|
return version_str
|
||||||
|
logging.error("comfyui-workflow-templates not found in requirements.txt")
|
||||||
|
return None
|
||||||
|
except FileNotFoundError:
|
||||||
|
logging.error("requirements.txt not found. Cannot determine required templates version.")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error reading requirements.txt: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_frontend_path(cls) -> str:
|
def default_frontend_path(cls) -> str:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -37,7 +37,10 @@ def rope(pos: Tensor, dim: int, theta: int) -> Tensor:
|
|||||||
|
|
||||||
def apply_rope1(x: Tensor, freqs_cis: Tensor):
|
def apply_rope1(x: Tensor, freqs_cis: Tensor):
|
||||||
x_ = x.to(dtype=freqs_cis.dtype).reshape(*x.shape[:-1], -1, 1, 2)
|
x_ = x.to(dtype=freqs_cis.dtype).reshape(*x.shape[:-1], -1, 1, 2)
|
||||||
x_out = freqs_cis[..., 0] * x_[..., 0] + freqs_cis[..., 1] * x_[..., 1]
|
|
||||||
|
x_out = freqs_cis[..., 0] * x_[..., 0]
|
||||||
|
x_out.addcmul_(freqs_cis[..., 1], x_[..., 1])
|
||||||
|
|
||||||
return x_out.reshape(*x.shape).type_as(x)
|
return x_out.reshape(*x.shape).type_as(x)
|
||||||
|
|
||||||
def apply_rope(xq: Tensor, xk: Tensor, freqs_cis: Tensor):
|
def apply_rope(xq: Tensor, xk: Tensor, freqs_cis: Tensor):
|
||||||
|
|||||||
@ -237,6 +237,7 @@ class WanAttentionBlock(nn.Module):
|
|||||||
freqs, transformer_options=transformer_options)
|
freqs, transformer_options=transformer_options)
|
||||||
|
|
||||||
x = torch.addcmul(x, y, repeat_e(e[2], x))
|
x = torch.addcmul(x, y, repeat_e(e[2], x))
|
||||||
|
del y
|
||||||
|
|
||||||
# cross-attention & ffn
|
# cross-attention & ffn
|
||||||
x = x + self.cross_attn(self.norm3(x), context, context_img_len=context_img_len, transformer_options=transformer_options)
|
x = x + self.cross_attn(self.norm3(x), context, context_img_len=context_img_len, transformer_options=transformer_options)
|
||||||
|
|||||||
@ -360,7 +360,7 @@ def calc_cond_uncond_batch(model, cond, uncond, x_in, timestep, model_options):
|
|||||||
def cfg_function(model, cond_pred, uncond_pred, cond_scale, x, timestep, model_options={}, cond=None, uncond=None):
|
def cfg_function(model, cond_pred, uncond_pred, cond_scale, x, timestep, model_options={}, cond=None, uncond=None):
|
||||||
if "sampler_cfg_function" in model_options:
|
if "sampler_cfg_function" in model_options:
|
||||||
args = {"cond": x - cond_pred, "uncond": x - uncond_pred, "cond_scale": cond_scale, "timestep": timestep, "input": x, "sigma": timestep,
|
args = {"cond": x - cond_pred, "uncond": x - uncond_pred, "cond_scale": cond_scale, "timestep": timestep, "input": x, "sigma": timestep,
|
||||||
"cond_denoised": cond_pred, "uncond_denoised": uncond_pred, "model": model, "model_options": model_options}
|
"cond_denoised": cond_pred, "uncond_denoised": uncond_pred, "model": model, "model_options": model_options, "input_cond": cond, "input_uncond": uncond}
|
||||||
cfg_result = x - model_options["sampler_cfg_function"](args)
|
cfg_result = x - model_options["sampler_cfg_function"](args)
|
||||||
else:
|
else:
|
||||||
cfg_result = uncond_pred + (cond_pred - uncond_pred) * cond_scale
|
cfg_result = uncond_pred + (cond_pred - uncond_pred) * cond_scale
|
||||||
|
|||||||
@ -63,7 +63,13 @@ class HunyuanImageTEModel(QwenImageTEModel):
|
|||||||
self.byt5_small = None
|
self.byt5_small = None
|
||||||
|
|
||||||
def encode_token_weights(self, token_weight_pairs):
|
def encode_token_weights(self, token_weight_pairs):
|
||||||
cond, p, extra = super().encode_token_weights(token_weight_pairs)
|
tok_pairs = token_weight_pairs["qwen25_7b"][0]
|
||||||
|
template_end = -1
|
||||||
|
if tok_pairs[0][0] == 27:
|
||||||
|
if len(tok_pairs) > 36: # refiner prompt uses a fixed 36 template_end
|
||||||
|
template_end = 36
|
||||||
|
|
||||||
|
cond, p, extra = super().encode_token_weights(token_weight_pairs, template_end=template_end)
|
||||||
if self.byt5_small is not None and "byt5" in token_weight_pairs:
|
if self.byt5_small is not None and "byt5" in token_weight_pairs:
|
||||||
out = self.byt5_small.encode_token_weights(token_weight_pairs["byt5"])
|
out = self.byt5_small.encode_token_weights(token_weight_pairs["byt5"])
|
||||||
extra["conditioning_byt5small"] = out[0]
|
extra["conditioning_byt5small"] = out[0]
|
||||||
|
|||||||
@ -18,6 +18,15 @@ class QwenImageTokenizer(sd1_clip.SD1Tokenizer):
|
|||||||
self.llama_template_images = "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n<|vision_start|><|image_pad|><|vision_end|>{}<|im_end|>\n<|im_start|>assistant\n"
|
self.llama_template_images = "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n<|vision_start|><|image_pad|><|vision_end|>{}<|im_end|>\n<|im_start|>assistant\n"
|
||||||
|
|
||||||
def tokenize_with_weights(self, text, return_word_ids=False, llama_template=None, images=[], **kwargs):
|
def tokenize_with_weights(self, text, return_word_ids=False, llama_template=None, images=[], **kwargs):
|
||||||
|
skip_template = False
|
||||||
|
if text.startswith('<|im_start|>'):
|
||||||
|
skip_template = True
|
||||||
|
if text.startswith('<|start_header_id|>'):
|
||||||
|
skip_template = True
|
||||||
|
|
||||||
|
if skip_template:
|
||||||
|
llama_text = text
|
||||||
|
else:
|
||||||
if llama_template is None:
|
if llama_template is None:
|
||||||
if len(images) > 0:
|
if len(images) > 0:
|
||||||
llama_text = self.llama_template_images.format(text)
|
llama_text = self.llama_template_images.format(text)
|
||||||
@ -47,10 +56,11 @@ class QwenImageTEModel(sd1_clip.SD1ClipModel):
|
|||||||
def __init__(self, device="cpu", dtype=None, model_options={}):
|
def __init__(self, device="cpu", dtype=None, model_options={}):
|
||||||
super().__init__(device=device, dtype=dtype, name="qwen25_7b", clip_model=Qwen25_7BVLIModel, model_options=model_options)
|
super().__init__(device=device, dtype=dtype, name="qwen25_7b", clip_model=Qwen25_7BVLIModel, model_options=model_options)
|
||||||
|
|
||||||
def encode_token_weights(self, token_weight_pairs):
|
def encode_token_weights(self, token_weight_pairs, template_end=-1):
|
||||||
out, pooled, extra = super().encode_token_weights(token_weight_pairs)
|
out, pooled, extra = super().encode_token_weights(token_weight_pairs)
|
||||||
tok_pairs = token_weight_pairs["qwen25_7b"][0]
|
tok_pairs = token_weight_pairs["qwen25_7b"][0]
|
||||||
count_im_start = 0
|
count_im_start = 0
|
||||||
|
if template_end == -1:
|
||||||
for i, v in enumerate(tok_pairs):
|
for i, v in enumerate(tok_pairs):
|
||||||
elem = v[0]
|
elem = v[0]
|
||||||
if not torch.is_tensor(elem):
|
if not torch.is_tensor(elem):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from inspect import cleandoc
|
from inspect import cleandoc
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
|
from typing_extensions import override
|
||||||
|
from comfy_api.latest import ComfyExtension, io as comfy_io
|
||||||
from comfy_api.input_impl.video_types import VideoFromFile
|
from comfy_api.input_impl.video_types import VideoFromFile
|
||||||
from comfy_api_nodes.apis.luma_api import (
|
from comfy_api_nodes.apis.luma_api import (
|
||||||
LumaImageModel,
|
LumaImageModel,
|
||||||
@ -51,174 +52,186 @@ def image_result_url_extractor(response: LumaGeneration):
|
|||||||
def video_result_url_extractor(response: LumaGeneration):
|
def video_result_url_extractor(response: LumaGeneration):
|
||||||
return response.assets.video if hasattr(response, "assets") and hasattr(response.assets, "video") else None
|
return response.assets.video if hasattr(response, "assets") and hasattr(response.assets, "video") else None
|
||||||
|
|
||||||
class LumaReferenceNode(ComfyNodeABC):
|
class LumaReferenceNode(comfy_io.ComfyNode):
|
||||||
"""
|
"""
|
||||||
Holds an image and weight for use with Luma Generate Image node.
|
Holds an image and weight for use with Luma Generate Image node.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN_TYPES = (LumaIO.LUMA_REF,)
|
@classmethod
|
||||||
RETURN_NAMES = ("luma_ref",)
|
def define_schema(cls) -> comfy_io.Schema:
|
||||||
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value
|
return comfy_io.Schema(
|
||||||
FUNCTION = "create_luma_reference"
|
node_id="LumaReferenceNode",
|
||||||
CATEGORY = "api node/image/Luma"
|
display_name="Luma Reference",
|
||||||
|
category="api node/image/Luma",
|
||||||
|
description=cleandoc(cls.__doc__ or ""),
|
||||||
|
inputs=[
|
||||||
|
comfy_io.Image.Input(
|
||||||
|
"image",
|
||||||
|
tooltip="Image to use as reference.",
|
||||||
|
),
|
||||||
|
comfy_io.Float.Input(
|
||||||
|
"weight",
|
||||||
|
default=1.0,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
step=0.01,
|
||||||
|
tooltip="Weight of image reference.",
|
||||||
|
),
|
||||||
|
comfy_io.Custom(LumaIO.LUMA_REF).Input(
|
||||||
|
"luma_ref",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[comfy_io.Custom(LumaIO.LUMA_REF).Output(display_name="luma_ref")],
|
||||||
|
hidden=[
|
||||||
|
comfy_io.Hidden.auth_token_comfy_org,
|
||||||
|
comfy_io.Hidden.api_key_comfy_org,
|
||||||
|
comfy_io.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def execute(
|
||||||
return {
|
cls, image: torch.Tensor, weight: float, luma_ref: LumaReferenceChain = None
|
||||||
"required": {
|
) -> comfy_io.NodeOutput:
|
||||||
"image": (
|
|
||||||
IO.IMAGE,
|
|
||||||
{
|
|
||||||
"tooltip": "Image to use as reference.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"weight": (
|
|
||||||
IO.FLOAT,
|
|
||||||
{
|
|
||||||
"default": 1.0,
|
|
||||||
"min": 0.0,
|
|
||||||
"max": 1.0,
|
|
||||||
"step": 0.01,
|
|
||||||
"tooltip": "Weight of image reference.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"optional": {"luma_ref": (LumaIO.LUMA_REF,)},
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_luma_reference(
|
|
||||||
self, image: torch.Tensor, weight: float, luma_ref: LumaReferenceChain = None
|
|
||||||
):
|
|
||||||
if luma_ref is not None:
|
if luma_ref is not None:
|
||||||
luma_ref = luma_ref.clone()
|
luma_ref = luma_ref.clone()
|
||||||
else:
|
else:
|
||||||
luma_ref = LumaReferenceChain()
|
luma_ref = LumaReferenceChain()
|
||||||
luma_ref.add(LumaReference(image=image, weight=round(weight, 2)))
|
luma_ref.add(LumaReference(image=image, weight=round(weight, 2)))
|
||||||
return (luma_ref,)
|
return comfy_io.NodeOutput(luma_ref)
|
||||||
|
|
||||||
|
|
||||||
class LumaConceptsNode(ComfyNodeABC):
|
class LumaConceptsNode(comfy_io.ComfyNode):
|
||||||
"""
|
"""
|
||||||
Holds one or more Camera Concepts for use with Luma Text to Video and Luma Image to Video nodes.
|
Holds one or more Camera Concepts for use with Luma Text to Video and Luma Image to Video nodes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN_TYPES = (LumaIO.LUMA_CONCEPTS,)
|
@classmethod
|
||||||
RETURN_NAMES = ("luma_concepts",)
|
def define_schema(cls) -> comfy_io.Schema:
|
||||||
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value
|
return comfy_io.Schema(
|
||||||
FUNCTION = "create_concepts"
|
node_id="LumaConceptsNode",
|
||||||
CATEGORY = "api node/video/Luma"
|
display_name="Luma Concepts",
|
||||||
|
category="api node/video/Luma",
|
||||||
|
description=cleandoc(cls.__doc__ or ""),
|
||||||
|
inputs=[
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"concept1",
|
||||||
|
options=get_luma_concepts(include_none=True),
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"concept2",
|
||||||
|
options=get_luma_concepts(include_none=True),
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"concept3",
|
||||||
|
options=get_luma_concepts(include_none=True),
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"concept4",
|
||||||
|
options=get_luma_concepts(include_none=True),
|
||||||
|
),
|
||||||
|
comfy_io.Custom(LumaIO.LUMA_CONCEPTS).Input(
|
||||||
|
"luma_concepts",
|
||||||
|
tooltip="Optional Camera Concepts to add to the ones chosen here.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[comfy_io.Custom(LumaIO.LUMA_CONCEPTS).Output(display_name="luma_concepts")],
|
||||||
|
hidden=[
|
||||||
|
comfy_io.Hidden.auth_token_comfy_org,
|
||||||
|
comfy_io.Hidden.api_key_comfy_org,
|
||||||
|
comfy_io.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def execute(
|
||||||
return {
|
cls,
|
||||||
"required": {
|
|
||||||
"concept1": (get_luma_concepts(include_none=True),),
|
|
||||||
"concept2": (get_luma_concepts(include_none=True),),
|
|
||||||
"concept3": (get_luma_concepts(include_none=True),),
|
|
||||||
"concept4": (get_luma_concepts(include_none=True),),
|
|
||||||
},
|
|
||||||
"optional": {
|
|
||||||
"luma_concepts": (
|
|
||||||
LumaIO.LUMA_CONCEPTS,
|
|
||||||
{
|
|
||||||
"tooltip": "Optional Camera Concepts to add to the ones chosen here."
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_concepts(
|
|
||||||
self,
|
|
||||||
concept1: str,
|
concept1: str,
|
||||||
concept2: str,
|
concept2: str,
|
||||||
concept3: str,
|
concept3: str,
|
||||||
concept4: str,
|
concept4: str,
|
||||||
luma_concepts: LumaConceptChain = None,
|
luma_concepts: LumaConceptChain = None,
|
||||||
):
|
) -> comfy_io.NodeOutput:
|
||||||
chain = LumaConceptChain(str_list=[concept1, concept2, concept3, concept4])
|
chain = LumaConceptChain(str_list=[concept1, concept2, concept3, concept4])
|
||||||
if luma_concepts is not None:
|
if luma_concepts is not None:
|
||||||
chain = luma_concepts.clone_and_merge(chain)
|
chain = luma_concepts.clone_and_merge(chain)
|
||||||
return (chain,)
|
return comfy_io.NodeOutput(chain)
|
||||||
|
|
||||||
|
|
||||||
class LumaImageGenerationNode(ComfyNodeABC):
|
class LumaImageGenerationNode(comfy_io.ComfyNode):
|
||||||
"""
|
"""
|
||||||
Generates images synchronously based on prompt and aspect ratio.
|
Generates images synchronously based on prompt and aspect ratio.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN_TYPES = (IO.IMAGE,)
|
@classmethod
|
||||||
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value
|
def define_schema(cls) -> comfy_io.Schema:
|
||||||
FUNCTION = "api_call"
|
return comfy_io.Schema(
|
||||||
API_NODE = True
|
node_id="LumaImageNode",
|
||||||
CATEGORY = "api node/image/Luma"
|
display_name="Luma Text to Image",
|
||||||
|
category="api node/image/Luma",
|
||||||
|
description=cleandoc(cls.__doc__ or ""),
|
||||||
|
inputs=[
|
||||||
|
comfy_io.String.Input(
|
||||||
|
"prompt",
|
||||||
|
multiline=True,
|
||||||
|
default="",
|
||||||
|
tooltip="Prompt for the image generation",
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"model",
|
||||||
|
options=[model.value for model in LumaImageModel],
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"aspect_ratio",
|
||||||
|
options=[ratio.value for ratio in LumaAspectRatio],
|
||||||
|
default=LumaAspectRatio.ratio_16_9,
|
||||||
|
),
|
||||||
|
comfy_io.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=0xFFFFFFFFFFFFFFFF,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed to determine if node should re-run; actual results are nondeterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
comfy_io.Float.Input(
|
||||||
|
"style_image_weight",
|
||||||
|
default=1.0,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
step=0.01,
|
||||||
|
tooltip="Weight of style image. Ignored if no style_image provided.",
|
||||||
|
),
|
||||||
|
comfy_io.Custom(LumaIO.LUMA_REF).Input(
|
||||||
|
"image_luma_ref",
|
||||||
|
tooltip="Luma Reference node connection to influence generation with input images; up to 4 images can be considered.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
comfy_io.Image.Input(
|
||||||
|
"style_image",
|
||||||
|
tooltip="Style reference image; only 1 image will be used.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
comfy_io.Image.Input(
|
||||||
|
"character_image",
|
||||||
|
tooltip="Character reference images; can be a batch of multiple, up to 4 images can be considered.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[comfy_io.Image.Output()],
|
||||||
|
hidden=[
|
||||||
|
comfy_io.Hidden.auth_token_comfy_org,
|
||||||
|
comfy_io.Hidden.api_key_comfy_org,
|
||||||
|
comfy_io.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
async def execute(
|
||||||
return {
|
cls,
|
||||||
"required": {
|
|
||||||
"prompt": (
|
|
||||||
IO.STRING,
|
|
||||||
{
|
|
||||||
"multiline": True,
|
|
||||||
"default": "",
|
|
||||||
"tooltip": "Prompt for the image generation",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"model": ([model.value for model in LumaImageModel],),
|
|
||||||
"aspect_ratio": (
|
|
||||||
[ratio.value for ratio in LumaAspectRatio],
|
|
||||||
{
|
|
||||||
"default": LumaAspectRatio.ratio_16_9,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"seed": (
|
|
||||||
IO.INT,
|
|
||||||
{
|
|
||||||
"default": 0,
|
|
||||||
"min": 0,
|
|
||||||
"max": 0xFFFFFFFFFFFFFFFF,
|
|
||||||
"control_after_generate": True,
|
|
||||||
"tooltip": "Seed to determine if node should re-run; actual results are nondeterministic regardless of seed.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"style_image_weight": (
|
|
||||||
IO.FLOAT,
|
|
||||||
{
|
|
||||||
"default": 1.0,
|
|
||||||
"min": 0.0,
|
|
||||||
"max": 1.0,
|
|
||||||
"step": 0.01,
|
|
||||||
"tooltip": "Weight of style image. Ignored if no style_image provided.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"optional": {
|
|
||||||
"image_luma_ref": (
|
|
||||||
LumaIO.LUMA_REF,
|
|
||||||
{
|
|
||||||
"tooltip": "Luma Reference node connection to influence generation with input images; up to 4 images can be considered."
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"style_image": (
|
|
||||||
IO.IMAGE,
|
|
||||||
{"tooltip": "Style reference image; only 1 image will be used."},
|
|
||||||
),
|
|
||||||
"character_image": (
|
|
||||||
IO.IMAGE,
|
|
||||||
{
|
|
||||||
"tooltip": "Character reference images; can be a batch of multiple, up to 4 images can be considered."
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"hidden": {
|
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
|
||||||
"unique_id": "UNIQUE_ID",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
async def api_call(
|
|
||||||
self,
|
|
||||||
prompt: str,
|
prompt: str,
|
||||||
model: str,
|
model: str,
|
||||||
aspect_ratio: str,
|
aspect_ratio: str,
|
||||||
@ -227,27 +240,29 @@ class LumaImageGenerationNode(ComfyNodeABC):
|
|||||||
image_luma_ref: LumaReferenceChain = None,
|
image_luma_ref: LumaReferenceChain = None,
|
||||||
style_image: torch.Tensor = None,
|
style_image: torch.Tensor = None,
|
||||||
character_image: torch.Tensor = None,
|
character_image: torch.Tensor = None,
|
||||||
unique_id: str = None,
|
) -> comfy_io.NodeOutput:
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
validate_string(prompt, strip_whitespace=True, min_length=3)
|
validate_string(prompt, strip_whitespace=True, min_length=3)
|
||||||
|
auth_kwargs = {
|
||||||
|
"auth_token": cls.hidden.auth_token_comfy_org,
|
||||||
|
"comfy_api_key": cls.hidden.api_key_comfy_org,
|
||||||
|
}
|
||||||
# handle image_luma_ref
|
# handle image_luma_ref
|
||||||
api_image_ref = None
|
api_image_ref = None
|
||||||
if image_luma_ref is not None:
|
if image_luma_ref is not None:
|
||||||
api_image_ref = await self._convert_luma_refs(
|
api_image_ref = await cls._convert_luma_refs(
|
||||||
image_luma_ref, max_refs=4, auth_kwargs=kwargs,
|
image_luma_ref, max_refs=4, auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
# handle style_luma_ref
|
# handle style_luma_ref
|
||||||
api_style_ref = None
|
api_style_ref = None
|
||||||
if style_image is not None:
|
if style_image is not None:
|
||||||
api_style_ref = await self._convert_style_image(
|
api_style_ref = await cls._convert_style_image(
|
||||||
style_image, weight=style_image_weight, auth_kwargs=kwargs,
|
style_image, weight=style_image_weight, auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
# handle character_ref images
|
# handle character_ref images
|
||||||
character_ref = None
|
character_ref = None
|
||||||
if character_image is not None:
|
if character_image is not None:
|
||||||
download_urls = await upload_images_to_comfyapi(
|
download_urls = await upload_images_to_comfyapi(
|
||||||
character_image, max_images=4, auth_kwargs=kwargs,
|
character_image, max_images=4, auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
character_ref = LumaCharacterRef(
|
character_ref = LumaCharacterRef(
|
||||||
identity0=LumaImageIdentity(images=download_urls)
|
identity0=LumaImageIdentity(images=download_urls)
|
||||||
@ -268,7 +283,7 @@ class LumaImageGenerationNode(ComfyNodeABC):
|
|||||||
style_ref=api_style_ref,
|
style_ref=api_style_ref,
|
||||||
character_ref=character_ref,
|
character_ref=character_ref,
|
||||||
),
|
),
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
response_api: LumaGeneration = await operation.execute()
|
response_api: LumaGeneration = await operation.execute()
|
||||||
|
|
||||||
@ -283,18 +298,19 @@ class LumaImageGenerationNode(ComfyNodeABC):
|
|||||||
failed_statuses=[LumaState.failed],
|
failed_statuses=[LumaState.failed],
|
||||||
status_extractor=lambda x: x.state,
|
status_extractor=lambda x: x.state,
|
||||||
result_url_extractor=image_result_url_extractor,
|
result_url_extractor=image_result_url_extractor,
|
||||||
node_id=unique_id,
|
node_id=cls.hidden.unique_id,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
response_poll = await operation.execute()
|
response_poll = await operation.execute()
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(response_poll.assets.image) as img_response:
|
async with session.get(response_poll.assets.image) as img_response:
|
||||||
img = process_image_response(await img_response.content.read())
|
img = process_image_response(await img_response.content.read())
|
||||||
return (img,)
|
return comfy_io.NodeOutput(img)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
async def _convert_luma_refs(
|
async def _convert_luma_refs(
|
||||||
self, luma_ref: LumaReferenceChain, max_refs: int, auth_kwargs: Optional[dict[str,str]] = None
|
cls, luma_ref: LumaReferenceChain, max_refs: int, auth_kwargs: Optional[dict[str,str]] = None
|
||||||
):
|
):
|
||||||
luma_urls = []
|
luma_urls = []
|
||||||
ref_count = 0
|
ref_count = 0
|
||||||
@ -308,82 +324,84 @@ class LumaImageGenerationNode(ComfyNodeABC):
|
|||||||
break
|
break
|
||||||
return luma_ref.create_api_model(download_urls=luma_urls, max_refs=max_refs)
|
return luma_ref.create_api_model(download_urls=luma_urls, max_refs=max_refs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
async def _convert_style_image(
|
async def _convert_style_image(
|
||||||
self, style_image: torch.Tensor, weight: float, auth_kwargs: Optional[dict[str,str]] = None
|
cls, style_image: torch.Tensor, weight: float, auth_kwargs: Optional[dict[str,str]] = None
|
||||||
):
|
):
|
||||||
chain = LumaReferenceChain(
|
chain = LumaReferenceChain(
|
||||||
first_ref=LumaReference(image=style_image, weight=weight)
|
first_ref=LumaReference(image=style_image, weight=weight)
|
||||||
)
|
)
|
||||||
return await self._convert_luma_refs(chain, max_refs=1, auth_kwargs=auth_kwargs)
|
return await cls._convert_luma_refs(chain, max_refs=1, auth_kwargs=auth_kwargs)
|
||||||
|
|
||||||
|
|
||||||
class LumaImageModifyNode(ComfyNodeABC):
|
class LumaImageModifyNode(comfy_io.ComfyNode):
|
||||||
"""
|
"""
|
||||||
Modifies images synchronously based on prompt and aspect ratio.
|
Modifies images synchronously based on prompt and aspect ratio.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN_TYPES = (IO.IMAGE,)
|
@classmethod
|
||||||
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value
|
def define_schema(cls) -> comfy_io.Schema:
|
||||||
FUNCTION = "api_call"
|
return comfy_io.Schema(
|
||||||
API_NODE = True
|
node_id="LumaImageModifyNode",
|
||||||
CATEGORY = "api node/image/Luma"
|
display_name="Luma Image to Image",
|
||||||
|
category="api node/image/Luma",
|
||||||
|
description=cleandoc(cls.__doc__ or ""),
|
||||||
|
inputs=[
|
||||||
|
comfy_io.Image.Input(
|
||||||
|
"image",
|
||||||
|
),
|
||||||
|
comfy_io.String.Input(
|
||||||
|
"prompt",
|
||||||
|
multiline=True,
|
||||||
|
default="",
|
||||||
|
tooltip="Prompt for the image generation",
|
||||||
|
),
|
||||||
|
comfy_io.Float.Input(
|
||||||
|
"image_weight",
|
||||||
|
default=0.1,
|
||||||
|
min=0.0,
|
||||||
|
max=0.98,
|
||||||
|
step=0.01,
|
||||||
|
tooltip="Weight of the image; the closer to 1.0, the less the image will be modified.",
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"model",
|
||||||
|
options=[model.value for model in LumaImageModel],
|
||||||
|
),
|
||||||
|
comfy_io.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=0xFFFFFFFFFFFFFFFF,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed to determine if node should re-run; actual results are nondeterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[comfy_io.Image.Output()],
|
||||||
|
hidden=[
|
||||||
|
comfy_io.Hidden.auth_token_comfy_org,
|
||||||
|
comfy_io.Hidden.api_key_comfy_org,
|
||||||
|
comfy_io.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
async def execute(
|
||||||
return {
|
cls,
|
||||||
"required": {
|
|
||||||
"image": (IO.IMAGE,),
|
|
||||||
"prompt": (
|
|
||||||
IO.STRING,
|
|
||||||
{
|
|
||||||
"multiline": True,
|
|
||||||
"default": "",
|
|
||||||
"tooltip": "Prompt for the image generation",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"image_weight": (
|
|
||||||
IO.FLOAT,
|
|
||||||
{
|
|
||||||
"default": 0.1,
|
|
||||||
"min": 0.0,
|
|
||||||
"max": 0.98,
|
|
||||||
"step": 0.01,
|
|
||||||
"tooltip": "Weight of the image; the closer to 1.0, the less the image will be modified.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"model": ([model.value for model in LumaImageModel],),
|
|
||||||
"seed": (
|
|
||||||
IO.INT,
|
|
||||||
{
|
|
||||||
"default": 0,
|
|
||||||
"min": 0,
|
|
||||||
"max": 0xFFFFFFFFFFFFFFFF,
|
|
||||||
"control_after_generate": True,
|
|
||||||
"tooltip": "Seed to determine if node should re-run; actual results are nondeterministic regardless of seed.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"optional": {},
|
|
||||||
"hidden": {
|
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
|
||||||
"unique_id": "UNIQUE_ID",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
async def api_call(
|
|
||||||
self,
|
|
||||||
prompt: str,
|
prompt: str,
|
||||||
model: str,
|
model: str,
|
||||||
image: torch.Tensor,
|
image: torch.Tensor,
|
||||||
image_weight: float,
|
image_weight: float,
|
||||||
seed,
|
seed,
|
||||||
unique_id: str = None,
|
) -> comfy_io.NodeOutput:
|
||||||
**kwargs,
|
auth_kwargs = {
|
||||||
):
|
"auth_token": cls.hidden.auth_token_comfy_org,
|
||||||
|
"comfy_api_key": cls.hidden.api_key_comfy_org,
|
||||||
|
}
|
||||||
# first, upload image
|
# first, upload image
|
||||||
download_urls = await upload_images_to_comfyapi(
|
download_urls = await upload_images_to_comfyapi(
|
||||||
image, max_images=1, auth_kwargs=kwargs,
|
image, max_images=1, auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
image_url = download_urls[0]
|
image_url = download_urls[0]
|
||||||
# next, make Luma call with download url provided
|
# next, make Luma call with download url provided
|
||||||
@ -401,7 +419,7 @@ class LumaImageModifyNode(ComfyNodeABC):
|
|||||||
url=image_url, weight=round(max(min(1.0-image_weight, 0.98), 0.0), 2)
|
url=image_url, weight=round(max(min(1.0-image_weight, 0.98), 0.0), 2)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
response_api: LumaGeneration = await operation.execute()
|
response_api: LumaGeneration = await operation.execute()
|
||||||
|
|
||||||
@ -416,88 +434,84 @@ class LumaImageModifyNode(ComfyNodeABC):
|
|||||||
failed_statuses=[LumaState.failed],
|
failed_statuses=[LumaState.failed],
|
||||||
status_extractor=lambda x: x.state,
|
status_extractor=lambda x: x.state,
|
||||||
result_url_extractor=image_result_url_extractor,
|
result_url_extractor=image_result_url_extractor,
|
||||||
node_id=unique_id,
|
node_id=cls.hidden.unique_id,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
response_poll = await operation.execute()
|
response_poll = await operation.execute()
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(response_poll.assets.image) as img_response:
|
async with session.get(response_poll.assets.image) as img_response:
|
||||||
img = process_image_response(await img_response.content.read())
|
img = process_image_response(await img_response.content.read())
|
||||||
return (img,)
|
return comfy_io.NodeOutput(img)
|
||||||
|
|
||||||
|
|
||||||
class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
class LumaTextToVideoGenerationNode(comfy_io.ComfyNode):
|
||||||
"""
|
"""
|
||||||
Generates videos synchronously based on prompt and output_size.
|
Generates videos synchronously based on prompt and output_size.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN_TYPES = (IO.VIDEO,)
|
@classmethod
|
||||||
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value
|
def define_schema(cls) -> comfy_io.Schema:
|
||||||
FUNCTION = "api_call"
|
return comfy_io.Schema(
|
||||||
API_NODE = True
|
node_id="LumaVideoNode",
|
||||||
CATEGORY = "api node/video/Luma"
|
display_name="Luma Text to Video",
|
||||||
|
category="api node/video/Luma",
|
||||||
|
description=cleandoc(cls.__doc__ or ""),
|
||||||
|
inputs=[
|
||||||
|
comfy_io.String.Input(
|
||||||
|
"prompt",
|
||||||
|
multiline=True,
|
||||||
|
default="",
|
||||||
|
tooltip="Prompt for the video generation",
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"model",
|
||||||
|
options=[model.value for model in LumaVideoModel],
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"aspect_ratio",
|
||||||
|
options=[ratio.value for ratio in LumaAspectRatio],
|
||||||
|
default=LumaAspectRatio.ratio_16_9,
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"resolution",
|
||||||
|
options=[resolution.value for resolution in LumaVideoOutputResolution],
|
||||||
|
default=LumaVideoOutputResolution.res_540p,
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"duration",
|
||||||
|
options=[dur.value for dur in LumaVideoModelOutputDuration],
|
||||||
|
),
|
||||||
|
comfy_io.Boolean.Input(
|
||||||
|
"loop",
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
|
comfy_io.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=0xFFFFFFFFFFFFFFFF,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed to determine if node should re-run; actual results are nondeterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
comfy_io.Custom(LumaIO.LUMA_CONCEPTS).Input(
|
||||||
|
"luma_concepts",
|
||||||
|
tooltip="Optional Camera Concepts to dictate camera motion via the Luma Concepts node.",
|
||||||
|
optional=True,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
outputs=[comfy_io.Video.Output()],
|
||||||
|
hidden=[
|
||||||
|
comfy_io.Hidden.auth_token_comfy_org,
|
||||||
|
comfy_io.Hidden.api_key_comfy_org,
|
||||||
|
comfy_io.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
async def execute(
|
||||||
return {
|
cls,
|
||||||
"required": {
|
|
||||||
"prompt": (
|
|
||||||
IO.STRING,
|
|
||||||
{
|
|
||||||
"multiline": True,
|
|
||||||
"default": "",
|
|
||||||
"tooltip": "Prompt for the video generation",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"model": ([model.value for model in LumaVideoModel],),
|
|
||||||
"aspect_ratio": (
|
|
||||||
[ratio.value for ratio in LumaAspectRatio],
|
|
||||||
{
|
|
||||||
"default": LumaAspectRatio.ratio_16_9,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"resolution": (
|
|
||||||
[resolution.value for resolution in LumaVideoOutputResolution],
|
|
||||||
{
|
|
||||||
"default": LumaVideoOutputResolution.res_540p,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"duration": ([dur.value for dur in LumaVideoModelOutputDuration],),
|
|
||||||
"loop": (
|
|
||||||
IO.BOOLEAN,
|
|
||||||
{
|
|
||||||
"default": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"seed": (
|
|
||||||
IO.INT,
|
|
||||||
{
|
|
||||||
"default": 0,
|
|
||||||
"min": 0,
|
|
||||||
"max": 0xFFFFFFFFFFFFFFFF,
|
|
||||||
"control_after_generate": True,
|
|
||||||
"tooltip": "Seed to determine if node should re-run; actual results are nondeterministic regardless of seed.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"optional": {
|
|
||||||
"luma_concepts": (
|
|
||||||
LumaIO.LUMA_CONCEPTS,
|
|
||||||
{
|
|
||||||
"tooltip": "Optional Camera Concepts to dictate camera motion via the Luma Concepts node."
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"hidden": {
|
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
|
||||||
"unique_id": "UNIQUE_ID",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
async def api_call(
|
|
||||||
self,
|
|
||||||
prompt: str,
|
prompt: str,
|
||||||
model: str,
|
model: str,
|
||||||
aspect_ratio: str,
|
aspect_ratio: str,
|
||||||
@ -506,13 +520,15 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
|||||||
loop: bool,
|
loop: bool,
|
||||||
seed,
|
seed,
|
||||||
luma_concepts: LumaConceptChain = None,
|
luma_concepts: LumaConceptChain = None,
|
||||||
unique_id: str = None,
|
) -> comfy_io.NodeOutput:
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
validate_string(prompt, strip_whitespace=False, min_length=3)
|
validate_string(prompt, strip_whitespace=False, min_length=3)
|
||||||
duration = duration if model != LumaVideoModel.ray_1_6 else None
|
duration = duration if model != LumaVideoModel.ray_1_6 else None
|
||||||
resolution = resolution if model != LumaVideoModel.ray_1_6 else None
|
resolution = resolution if model != LumaVideoModel.ray_1_6 else None
|
||||||
|
|
||||||
|
auth_kwargs = {
|
||||||
|
"auth_token": cls.hidden.auth_token_comfy_org,
|
||||||
|
"comfy_api_key": cls.hidden.api_key_comfy_org,
|
||||||
|
}
|
||||||
operation = SynchronousOperation(
|
operation = SynchronousOperation(
|
||||||
endpoint=ApiEndpoint(
|
endpoint=ApiEndpoint(
|
||||||
path="/proxy/luma/generations",
|
path="/proxy/luma/generations",
|
||||||
@ -529,12 +545,12 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
|||||||
loop=loop,
|
loop=loop,
|
||||||
concepts=luma_concepts.create_api_model() if luma_concepts else None,
|
concepts=luma_concepts.create_api_model() if luma_concepts else None,
|
||||||
),
|
),
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
response_api: LumaGeneration = await operation.execute()
|
response_api: LumaGeneration = await operation.execute()
|
||||||
|
|
||||||
if unique_id:
|
if cls.hidden.unique_id:
|
||||||
PromptServer.instance.send_progress_text(f"Luma video generation started: {response_api.id}", unique_id)
|
PromptServer.instance.send_progress_text(f"Luma video generation started: {response_api.id}", cls.hidden.unique_id)
|
||||||
|
|
||||||
operation = PollingOperation(
|
operation = PollingOperation(
|
||||||
poll_endpoint=ApiEndpoint(
|
poll_endpoint=ApiEndpoint(
|
||||||
@ -547,90 +563,94 @@ class LumaTextToVideoGenerationNode(ComfyNodeABC):
|
|||||||
failed_statuses=[LumaState.failed],
|
failed_statuses=[LumaState.failed],
|
||||||
status_extractor=lambda x: x.state,
|
status_extractor=lambda x: x.state,
|
||||||
result_url_extractor=video_result_url_extractor,
|
result_url_extractor=video_result_url_extractor,
|
||||||
node_id=unique_id,
|
node_id=cls.hidden.unique_id,
|
||||||
estimated_duration=LUMA_T2V_AVERAGE_DURATION,
|
estimated_duration=LUMA_T2V_AVERAGE_DURATION,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
response_poll = await operation.execute()
|
response_poll = await operation.execute()
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(response_poll.assets.video) as vid_response:
|
async with session.get(response_poll.assets.video) as vid_response:
|
||||||
return (VideoFromFile(BytesIO(await vid_response.content.read())),)
|
return comfy_io.NodeOutput(VideoFromFile(BytesIO(await vid_response.content.read())))
|
||||||
|
|
||||||
|
|
||||||
class LumaImageToVideoGenerationNode(ComfyNodeABC):
|
class LumaImageToVideoGenerationNode(comfy_io.ComfyNode):
|
||||||
"""
|
"""
|
||||||
Generates videos synchronously based on prompt, input images, and output_size.
|
Generates videos synchronously based on prompt, input images, and output_size.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN_TYPES = (IO.VIDEO,)
|
@classmethod
|
||||||
DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value
|
def define_schema(cls) -> comfy_io.Schema:
|
||||||
FUNCTION = "api_call"
|
return comfy_io.Schema(
|
||||||
API_NODE = True
|
node_id="LumaImageToVideoNode",
|
||||||
CATEGORY = "api node/video/Luma"
|
display_name="Luma Image to Video",
|
||||||
|
category="api node/video/Luma",
|
||||||
|
description=cleandoc(cls.__doc__ or ""),
|
||||||
|
inputs=[
|
||||||
|
comfy_io.String.Input(
|
||||||
|
"prompt",
|
||||||
|
multiline=True,
|
||||||
|
default="",
|
||||||
|
tooltip="Prompt for the video generation",
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"model",
|
||||||
|
options=[model.value for model in LumaVideoModel],
|
||||||
|
),
|
||||||
|
# comfy_io.Combo.Input(
|
||||||
|
# "aspect_ratio",
|
||||||
|
# options=[ratio.value for ratio in LumaAspectRatio],
|
||||||
|
# default=LumaAspectRatio.ratio_16_9,
|
||||||
|
# ),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"resolution",
|
||||||
|
options=[resolution.value for resolution in LumaVideoOutputResolution],
|
||||||
|
default=LumaVideoOutputResolution.res_540p,
|
||||||
|
),
|
||||||
|
comfy_io.Combo.Input(
|
||||||
|
"duration",
|
||||||
|
options=[dur.value for dur in LumaVideoModelOutputDuration],
|
||||||
|
),
|
||||||
|
comfy_io.Boolean.Input(
|
||||||
|
"loop",
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
|
comfy_io.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=0xFFFFFFFFFFFFFFFF,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="Seed to determine if node should re-run; actual results are nondeterministic regardless of seed.",
|
||||||
|
),
|
||||||
|
comfy_io.Image.Input(
|
||||||
|
"first_image",
|
||||||
|
tooltip="First frame of generated video.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
comfy_io.Image.Input(
|
||||||
|
"last_image",
|
||||||
|
tooltip="Last frame of generated video.",
|
||||||
|
optional=True,
|
||||||
|
),
|
||||||
|
comfy_io.Custom(LumaIO.LUMA_CONCEPTS).Input(
|
||||||
|
"luma_concepts",
|
||||||
|
tooltip="Optional Camera Concepts to dictate camera motion via the Luma Concepts node.",
|
||||||
|
optional=True,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
outputs=[comfy_io.Video.Output()],
|
||||||
|
hidden=[
|
||||||
|
comfy_io.Hidden.auth_token_comfy_org,
|
||||||
|
comfy_io.Hidden.api_key_comfy_org,
|
||||||
|
comfy_io.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
async def execute(
|
||||||
return {
|
cls,
|
||||||
"required": {
|
|
||||||
"prompt": (
|
|
||||||
IO.STRING,
|
|
||||||
{
|
|
||||||
"multiline": True,
|
|
||||||
"default": "",
|
|
||||||
"tooltip": "Prompt for the video generation",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"model": ([model.value for model in LumaVideoModel],),
|
|
||||||
# "aspect_ratio": ([ratio.value for ratio in LumaAspectRatio], {
|
|
||||||
# "default": LumaAspectRatio.ratio_16_9,
|
|
||||||
# }),
|
|
||||||
"resolution": (
|
|
||||||
[resolution.value for resolution in LumaVideoOutputResolution],
|
|
||||||
{
|
|
||||||
"default": LumaVideoOutputResolution.res_540p,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"duration": ([dur.value for dur in LumaVideoModelOutputDuration],),
|
|
||||||
"loop": (
|
|
||||||
IO.BOOLEAN,
|
|
||||||
{
|
|
||||||
"default": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"seed": (
|
|
||||||
IO.INT,
|
|
||||||
{
|
|
||||||
"default": 0,
|
|
||||||
"min": 0,
|
|
||||||
"max": 0xFFFFFFFFFFFFFFFF,
|
|
||||||
"control_after_generate": True,
|
|
||||||
"tooltip": "Seed to determine if node should re-run; actual results are nondeterministic regardless of seed.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"optional": {
|
|
||||||
"first_image": (
|
|
||||||
IO.IMAGE,
|
|
||||||
{"tooltip": "First frame of generated video."},
|
|
||||||
),
|
|
||||||
"last_image": (IO.IMAGE, {"tooltip": "Last frame of generated video."}),
|
|
||||||
"luma_concepts": (
|
|
||||||
LumaIO.LUMA_CONCEPTS,
|
|
||||||
{
|
|
||||||
"tooltip": "Optional Camera Concepts to dictate camera motion via the Luma Concepts node."
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"hidden": {
|
|
||||||
"auth_token": "AUTH_TOKEN_COMFY_ORG",
|
|
||||||
"comfy_api_key": "API_KEY_COMFY_ORG",
|
|
||||||
"unique_id": "UNIQUE_ID",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
async def api_call(
|
|
||||||
self,
|
|
||||||
prompt: str,
|
prompt: str,
|
||||||
model: str,
|
model: str,
|
||||||
resolution: str,
|
resolution: str,
|
||||||
@ -640,14 +660,16 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
|
|||||||
first_image: torch.Tensor = None,
|
first_image: torch.Tensor = None,
|
||||||
last_image: torch.Tensor = None,
|
last_image: torch.Tensor = None,
|
||||||
luma_concepts: LumaConceptChain = None,
|
luma_concepts: LumaConceptChain = None,
|
||||||
unique_id: str = None,
|
) -> comfy_io.NodeOutput:
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if first_image is None and last_image is None:
|
if first_image is None and last_image is None:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"At least one of first_image and last_image requires an input."
|
"At least one of first_image and last_image requires an input."
|
||||||
)
|
)
|
||||||
keyframes = await self._convert_to_keyframes(first_image, last_image, auth_kwargs=kwargs)
|
auth_kwargs = {
|
||||||
|
"auth_token": cls.hidden.auth_token_comfy_org,
|
||||||
|
"comfy_api_key": cls.hidden.api_key_comfy_org,
|
||||||
|
}
|
||||||
|
keyframes = await cls._convert_to_keyframes(first_image, last_image, auth_kwargs=auth_kwargs)
|
||||||
duration = duration if model != LumaVideoModel.ray_1_6 else None
|
duration = duration if model != LumaVideoModel.ray_1_6 else None
|
||||||
resolution = resolution if model != LumaVideoModel.ray_1_6 else None
|
resolution = resolution if model != LumaVideoModel.ray_1_6 else None
|
||||||
|
|
||||||
@ -668,12 +690,12 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
|
|||||||
keyframes=keyframes,
|
keyframes=keyframes,
|
||||||
concepts=luma_concepts.create_api_model() if luma_concepts else None,
|
concepts=luma_concepts.create_api_model() if luma_concepts else None,
|
||||||
),
|
),
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
response_api: LumaGeneration = await operation.execute()
|
response_api: LumaGeneration = await operation.execute()
|
||||||
|
|
||||||
if unique_id:
|
if cls.hidden.unique_id:
|
||||||
PromptServer.instance.send_progress_text(f"Luma video generation started: {response_api.id}", unique_id)
|
PromptServer.instance.send_progress_text(f"Luma video generation started: {response_api.id}", cls.hidden.unique_id)
|
||||||
|
|
||||||
operation = PollingOperation(
|
operation = PollingOperation(
|
||||||
poll_endpoint=ApiEndpoint(
|
poll_endpoint=ApiEndpoint(
|
||||||
@ -686,18 +708,19 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
|
|||||||
failed_statuses=[LumaState.failed],
|
failed_statuses=[LumaState.failed],
|
||||||
status_extractor=lambda x: x.state,
|
status_extractor=lambda x: x.state,
|
||||||
result_url_extractor=video_result_url_extractor,
|
result_url_extractor=video_result_url_extractor,
|
||||||
node_id=unique_id,
|
node_id=cls.hidden.unique_id,
|
||||||
estimated_duration=LUMA_I2V_AVERAGE_DURATION,
|
estimated_duration=LUMA_I2V_AVERAGE_DURATION,
|
||||||
auth_kwargs=kwargs,
|
auth_kwargs=auth_kwargs,
|
||||||
)
|
)
|
||||||
response_poll = await operation.execute()
|
response_poll = await operation.execute()
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(response_poll.assets.video) as vid_response:
|
async with session.get(response_poll.assets.video) as vid_response:
|
||||||
return (VideoFromFile(BytesIO(await vid_response.content.read())),)
|
return comfy_io.NodeOutput(VideoFromFile(BytesIO(await vid_response.content.read())))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
async def _convert_to_keyframes(
|
async def _convert_to_keyframes(
|
||||||
self,
|
cls,
|
||||||
first_image: torch.Tensor = None,
|
first_image: torch.Tensor = None,
|
||||||
last_image: torch.Tensor = None,
|
last_image: torch.Tensor = None,
|
||||||
auth_kwargs: Optional[dict[str,str]] = None,
|
auth_kwargs: Optional[dict[str,str]] = None,
|
||||||
@ -719,23 +742,18 @@ class LumaImageToVideoGenerationNode(ComfyNodeABC):
|
|||||||
return LumaKeyframes(frame0=frame0, frame1=frame1)
|
return LumaKeyframes(frame0=frame0, frame1=frame1)
|
||||||
|
|
||||||
|
|
||||||
# A dictionary that contains all nodes you want to export with their names
|
class LumaExtension(ComfyExtension):
|
||||||
# NOTE: names should be globally unique
|
@override
|
||||||
NODE_CLASS_MAPPINGS = {
|
async def get_node_list(self) -> list[type[comfy_io.ComfyNode]]:
|
||||||
"LumaImageNode": LumaImageGenerationNode,
|
return [
|
||||||
"LumaImageModifyNode": LumaImageModifyNode,
|
LumaImageGenerationNode,
|
||||||
"LumaVideoNode": LumaTextToVideoGenerationNode,
|
LumaImageModifyNode,
|
||||||
"LumaImageToVideoNode": LumaImageToVideoGenerationNode,
|
LumaTextToVideoGenerationNode,
|
||||||
"LumaReferenceNode": LumaReferenceNode,
|
LumaImageToVideoGenerationNode,
|
||||||
"LumaConceptsNode": LumaConceptsNode,
|
LumaReferenceNode,
|
||||||
}
|
LumaConceptsNode,
|
||||||
|
]
|
||||||
|
|
||||||
# A dictionary that contains the friendly/humanly readable titles for the nodes
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
async def comfy_entrypoint() -> LumaExtension:
|
||||||
"LumaImageNode": "Luma Text to Image",
|
return LumaExtension()
|
||||||
"LumaImageModifyNode": "Luma Image to Image",
|
|
||||||
"LumaVideoNode": "Luma Text to Video",
|
|
||||||
"LumaImageToVideoNode": "Luma Image to Video",
|
|
||||||
"LumaReferenceNode": "Luma Reference",
|
|
||||||
"LumaConceptsNode": "Luma Concepts",
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,43 +1,52 @@
|
|||||||
from nodes import MAX_RESOLUTION
|
from typing_extensions import override
|
||||||
|
|
||||||
class CLIPTextEncodeSDXLRefiner:
|
import nodes
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
|
|
||||||
|
class CLIPTextEncodeSDXLRefiner(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {
|
return io.Schema(
|
||||||
"ascore": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01}),
|
node_id="CLIPTextEncodeSDXLRefiner",
|
||||||
"width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
category="advanced/conditioning",
|
||||||
"height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
inputs=[
|
||||||
"text": ("STRING", {"multiline": True, "dynamicPrompts": True}), "clip": ("CLIP", ),
|
io.Float.Input("ascore", default=6.0, min=0.0, max=1000.0, step=0.01),
|
||||||
}}
|
io.Int.Input("width", default=1024, min=0, max=nodes.MAX_RESOLUTION),
|
||||||
RETURN_TYPES = ("CONDITIONING",)
|
io.Int.Input("height", default=1024, min=0, max=nodes.MAX_RESOLUTION),
|
||||||
FUNCTION = "encode"
|
io.String.Input("text", multiline=True, dynamic_prompts=True),
|
||||||
|
io.Clip.Input("clip"),
|
||||||
|
],
|
||||||
|
outputs=[io.Conditioning.Output()],
|
||||||
|
)
|
||||||
|
|
||||||
CATEGORY = "advanced/conditioning"
|
@classmethod
|
||||||
|
def execute(cls, clip, ascore, width, height, text) -> io.NodeOutput:
|
||||||
def encode(self, clip, ascore, width, height, text):
|
|
||||||
tokens = clip.tokenize(text)
|
tokens = clip.tokenize(text)
|
||||||
return (clip.encode_from_tokens_scheduled(tokens, add_dict={"aesthetic_score": ascore, "width": width, "height": height}), )
|
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens, add_dict={"aesthetic_score": ascore, "width": width, "height": height}))
|
||||||
|
|
||||||
class CLIPTextEncodeSDXL:
|
class CLIPTextEncodeSDXL(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {
|
return io.Schema(
|
||||||
"clip": ("CLIP", ),
|
node_id="CLIPTextEncodeSDXL",
|
||||||
"width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
category="advanced/conditioning",
|
||||||
"height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
inputs=[
|
||||||
"crop_w": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION}),
|
io.Clip.Input("clip"),
|
||||||
"crop_h": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION}),
|
io.Int.Input("width", default=1024, min=0, max=nodes.MAX_RESOLUTION),
|
||||||
"target_width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
io.Int.Input("height", default=1024, min=0, max=nodes.MAX_RESOLUTION),
|
||||||
"target_height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
io.Int.Input("crop_w", default=0, min=0, max=nodes.MAX_RESOLUTION),
|
||||||
"text_g": ("STRING", {"multiline": True, "dynamicPrompts": True}),
|
io.Int.Input("crop_h", default=0, min=0, max=nodes.MAX_RESOLUTION),
|
||||||
"text_l": ("STRING", {"multiline": True, "dynamicPrompts": True}),
|
io.Int.Input("target_width", default=1024, min=0, max=nodes.MAX_RESOLUTION),
|
||||||
}}
|
io.Int.Input("target_height", default=1024, min=0, max=nodes.MAX_RESOLUTION),
|
||||||
RETURN_TYPES = ("CONDITIONING",)
|
io.String.Input("text_g", multiline=True, dynamic_prompts=True),
|
||||||
FUNCTION = "encode"
|
io.String.Input("text_l", multiline=True, dynamic_prompts=True),
|
||||||
|
],
|
||||||
|
outputs=[io.Conditioning.Output()],
|
||||||
|
)
|
||||||
|
|
||||||
CATEGORY = "advanced/conditioning"
|
@classmethod
|
||||||
|
def execute(cls, clip, width, height, crop_w, crop_h, target_width, target_height, text_g, text_l) -> io.NodeOutput:
|
||||||
def encode(self, clip, width, height, crop_w, crop_h, target_width, target_height, text_g, text_l):
|
|
||||||
tokens = clip.tokenize(text_g)
|
tokens = clip.tokenize(text_g)
|
||||||
tokens["l"] = clip.tokenize(text_l)["l"]
|
tokens["l"] = clip.tokenize(text_l)["l"]
|
||||||
if len(tokens["l"]) != len(tokens["g"]):
|
if len(tokens["l"]) != len(tokens["g"]):
|
||||||
@ -46,9 +55,17 @@ class CLIPTextEncodeSDXL:
|
|||||||
tokens["l"] += empty["l"]
|
tokens["l"] += empty["l"]
|
||||||
while len(tokens["l"]) > len(tokens["g"]):
|
while len(tokens["l"]) > len(tokens["g"]):
|
||||||
tokens["g"] += empty["g"]
|
tokens["g"] += empty["g"]
|
||||||
return (clip.encode_from_tokens_scheduled(tokens, add_dict={"width": width, "height": height, "crop_w": crop_w, "crop_h": crop_h, "target_width": target_width, "target_height": target_height}), )
|
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens, add_dict={"width": width, "height": height, "crop_w": crop_w, "crop_h": crop_h, "target_width": target_width, "target_height": target_height}))
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"CLIPTextEncodeSDXLRefiner": CLIPTextEncodeSDXLRefiner,
|
class ClipSdxlExtension(ComfyExtension):
|
||||||
"CLIPTextEncodeSDXL": CLIPTextEncodeSDXL,
|
@override
|
||||||
}
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
CLIPTextEncodeSDXLRefiner,
|
||||||
|
CLIPTextEncodeSDXL,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> ClipSdxlExtension:
|
||||||
|
return ClipSdxlExtension()
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
# Code based on https://github.com/WikiChao/FreSca (MIT License)
|
# Code based on https://github.com/WikiChao/FreSca (MIT License)
|
||||||
import torch
|
import torch
|
||||||
import torch.fft as fft
|
import torch.fft as fft
|
||||||
|
from typing_extensions import override
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
|
|
||||||
def Fourier_filter(x, scale_low=1.0, scale_high=1.5, freq_cutoff=20):
|
def Fourier_filter(x, scale_low=1.0, scale_high=1.5, freq_cutoff=20):
|
||||||
@ -51,25 +53,31 @@ def Fourier_filter(x, scale_low=1.0, scale_high=1.5, freq_cutoff=20):
|
|||||||
return x_filtered
|
return x_filtered
|
||||||
|
|
||||||
|
|
||||||
class FreSca:
|
class FreSca(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {
|
return io.Schema(
|
||||||
"required": {
|
node_id="FreSca",
|
||||||
"model": ("MODEL",),
|
display_name="FreSca",
|
||||||
"scale_low": ("FLOAT", {"default": 1.0, "min": 0, "max": 10, "step": 0.01,
|
category="_for_testing",
|
||||||
"tooltip": "Scaling factor for low-frequency components"}),
|
description="Applies frequency-dependent scaling to the guidance",
|
||||||
"scale_high": ("FLOAT", {"default": 1.25, "min": 0, "max": 10, "step": 0.01,
|
inputs=[
|
||||||
"tooltip": "Scaling factor for high-frequency components"}),
|
io.Model.Input("model"),
|
||||||
"freq_cutoff": ("INT", {"default": 20, "min": 1, "max": 10000, "step": 1,
|
io.Float.Input("scale_low", default=1.0, min=0, max=10, step=0.01,
|
||||||
"tooltip": "Number of frequency indices around center to consider as low-frequency"}),
|
tooltip="Scaling factor for low-frequency components"),
|
||||||
}
|
io.Float.Input("scale_high", default=1.25, min=0, max=10, step=0.01,
|
||||||
}
|
tooltip="Scaling factor for high-frequency components"),
|
||||||
RETURN_TYPES = ("MODEL",)
|
io.Int.Input("freq_cutoff", default=20, min=1, max=10000, step=1,
|
||||||
FUNCTION = "patch"
|
tooltip="Number of frequency indices around center to consider as low-frequency"),
|
||||||
CATEGORY = "_for_testing"
|
],
|
||||||
DESCRIPTION = "Applies frequency-dependent scaling to the guidance"
|
outputs=[
|
||||||
def patch(self, model, scale_low, scale_high, freq_cutoff):
|
io.Model.Output(),
|
||||||
|
],
|
||||||
|
is_experimental=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, model, scale_low, scale_high, freq_cutoff):
|
||||||
def custom_cfg_function(args):
|
def custom_cfg_function(args):
|
||||||
conds_out = args["conds_out"]
|
conds_out = args["conds_out"]
|
||||||
if len(conds_out) <= 1 or None in args["conds"][:2]:
|
if len(conds_out) <= 1 or None in args["conds"][:2]:
|
||||||
@ -91,13 +99,16 @@ class FreSca:
|
|||||||
m = model.clone()
|
m = model.clone()
|
||||||
m.set_model_sampler_pre_cfg_function(custom_cfg_function)
|
m.set_model_sampler_pre_cfg_function(custom_cfg_function)
|
||||||
|
|
||||||
return (m,)
|
return io.NodeOutput(m)
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
class FreScaExtension(ComfyExtension):
|
||||||
"FreSca": FreSca,
|
@override
|
||||||
}
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
FreSca,
|
||||||
|
]
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
||||||
"FreSca": "FreSca",
|
async def comfy_entrypoint() -> FreScaExtension:
|
||||||
}
|
return FreScaExtension()
|
||||||
|
|||||||
@ -1,55 +1,73 @@
|
|||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
import folder_paths
|
import folder_paths
|
||||||
import comfy.sd
|
import comfy.sd
|
||||||
import comfy.model_management
|
import comfy.model_management
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
|
|
||||||
class QuadrupleCLIPLoader:
|
class QuadrupleCLIPLoader(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": { "clip_name1": (folder_paths.get_filename_list("text_encoders"), ),
|
return io.Schema(
|
||||||
"clip_name2": (folder_paths.get_filename_list("text_encoders"), ),
|
node_id="QuadrupleCLIPLoader",
|
||||||
"clip_name3": (folder_paths.get_filename_list("text_encoders"), ),
|
category="advanced/loaders",
|
||||||
"clip_name4": (folder_paths.get_filename_list("text_encoders"), )
|
description="[Recipes]\n\nhidream: long clip-l, long clip-g, t5xxl, llama_8b_3.1_instruct",
|
||||||
}}
|
inputs=[
|
||||||
RETURN_TYPES = ("CLIP",)
|
io.Combo.Input("clip_name1", options=folder_paths.get_filename_list("text_encoders")),
|
||||||
FUNCTION = "load_clip"
|
io.Combo.Input("clip_name2", options=folder_paths.get_filename_list("text_encoders")),
|
||||||
|
io.Combo.Input("clip_name3", options=folder_paths.get_filename_list("text_encoders")),
|
||||||
|
io.Combo.Input("clip_name4", options=folder_paths.get_filename_list("text_encoders")),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Clip.Output(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
CATEGORY = "advanced/loaders"
|
@classmethod
|
||||||
|
def execute(cls, clip_name1, clip_name2, clip_name3, clip_name4):
|
||||||
DESCRIPTION = "[Recipes]\n\nhidream: long clip-l, long clip-g, t5xxl, llama_8b_3.1_instruct"
|
|
||||||
|
|
||||||
def load_clip(self, clip_name1, clip_name2, clip_name3, clip_name4):
|
|
||||||
clip_path1 = folder_paths.get_full_path_or_raise("text_encoders", clip_name1)
|
clip_path1 = folder_paths.get_full_path_or_raise("text_encoders", clip_name1)
|
||||||
clip_path2 = folder_paths.get_full_path_or_raise("text_encoders", clip_name2)
|
clip_path2 = folder_paths.get_full_path_or_raise("text_encoders", clip_name2)
|
||||||
clip_path3 = folder_paths.get_full_path_or_raise("text_encoders", clip_name3)
|
clip_path3 = folder_paths.get_full_path_or_raise("text_encoders", clip_name3)
|
||||||
clip_path4 = folder_paths.get_full_path_or_raise("text_encoders", clip_name4)
|
clip_path4 = folder_paths.get_full_path_or_raise("text_encoders", clip_name4)
|
||||||
clip = comfy.sd.load_clip(ckpt_paths=[clip_path1, clip_path2, clip_path3, clip_path4], embedding_directory=folder_paths.get_folder_paths("embeddings"))
|
clip = comfy.sd.load_clip(ckpt_paths=[clip_path1, clip_path2, clip_path3, clip_path4], embedding_directory=folder_paths.get_folder_paths("embeddings"))
|
||||||
return (clip,)
|
return io.NodeOutput(clip)
|
||||||
|
|
||||||
class CLIPTextEncodeHiDream:
|
class CLIPTextEncodeHiDream(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {
|
return io.Schema(
|
||||||
"clip": ("CLIP", ),
|
node_id="CLIPTextEncodeHiDream",
|
||||||
"clip_l": ("STRING", {"multiline": True, "dynamicPrompts": True}),
|
category="advanced/conditioning",
|
||||||
"clip_g": ("STRING", {"multiline": True, "dynamicPrompts": True}),
|
inputs=[
|
||||||
"t5xxl": ("STRING", {"multiline": True, "dynamicPrompts": True}),
|
io.Clip.Input("clip"),
|
||||||
"llama": ("STRING", {"multiline": True, "dynamicPrompts": True})
|
io.String.Input("clip_l", multiline=True, dynamic_prompts=True),
|
||||||
}}
|
io.String.Input("clip_g", multiline=True, dynamic_prompts=True),
|
||||||
RETURN_TYPES = ("CONDITIONING",)
|
io.String.Input("t5xxl", multiline=True, dynamic_prompts=True),
|
||||||
FUNCTION = "encode"
|
io.String.Input("llama", multiline=True, dynamic_prompts=True),
|
||||||
|
],
|
||||||
CATEGORY = "advanced/conditioning"
|
outputs=[
|
||||||
|
io.Conditioning.Output(),
|
||||||
def encode(self, clip, clip_l, clip_g, t5xxl, llama):
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, clip, clip_l, clip_g, t5xxl, llama):
|
||||||
tokens = clip.tokenize(clip_g)
|
tokens = clip.tokenize(clip_g)
|
||||||
tokens["l"] = clip.tokenize(clip_l)["l"]
|
tokens["l"] = clip.tokenize(clip_l)["l"]
|
||||||
tokens["t5xxl"] = clip.tokenize(t5xxl)["t5xxl"]
|
tokens["t5xxl"] = clip.tokenize(t5xxl)["t5xxl"]
|
||||||
tokens["llama"] = clip.tokenize(llama)["llama"]
|
tokens["llama"] = clip.tokenize(llama)["llama"]
|
||||||
return (clip.encode_from_tokens_scheduled(tokens), )
|
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens))
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"QuadrupleCLIPLoader": QuadrupleCLIPLoader,
|
class HiDreamExtension(ComfyExtension):
|
||||||
"CLIPTextEncodeHiDream": CLIPTextEncodeHiDream,
|
@override
|
||||||
}
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
QuadrupleCLIPLoader,
|
||||||
|
CLIPTextEncodeHiDream,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> HiDreamExtension:
|
||||||
|
return HiDreamExtension()
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
#Taken from: https://github.com/tfernd/HyperTile/
|
#Taken from: https://github.com/tfernd/HyperTile/
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
from typing_extensions import override
|
||||||
from einops import rearrange
|
from einops import rearrange
|
||||||
# Use torch rng for consistency across generations
|
# Use torch rng for consistency across generations
|
||||||
from torch import randint
|
from torch import randint
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
def random_divisor(value: int, min_value: int, /, max_options: int = 1) -> int:
|
def random_divisor(value: int, min_value: int, /, max_options: int = 1) -> int:
|
||||||
min_value = min(min_value, value)
|
min_value = min(min_value, value)
|
||||||
@ -20,25 +22,31 @@ def random_divisor(value: int, min_value: int, /, max_options: int = 1) -> int:
|
|||||||
|
|
||||||
return ns[idx]
|
return ns[idx]
|
||||||
|
|
||||||
class HyperTile:
|
class HyperTile(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": { "model": ("MODEL",),
|
return io.Schema(
|
||||||
"tile_size": ("INT", {"default": 256, "min": 1, "max": 2048}),
|
node_id="HyperTile",
|
||||||
"swap_size": ("INT", {"default": 2, "min": 1, "max": 128}),
|
category="model_patches/unet",
|
||||||
"max_depth": ("INT", {"default": 0, "min": 0, "max": 10}),
|
inputs=[
|
||||||
"scale_depth": ("BOOLEAN", {"default": False}),
|
io.Model.Input("model"),
|
||||||
}}
|
io.Int.Input("tile_size", default=256, min=1, max=2048),
|
||||||
RETURN_TYPES = ("MODEL",)
|
io.Int.Input("swap_size", default=2, min=1, max=128),
|
||||||
FUNCTION = "patch"
|
io.Int.Input("max_depth", default=0, min=0, max=10),
|
||||||
|
io.Boolean.Input("scale_depth", default=False),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Model.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
CATEGORY = "model_patches/unet"
|
@classmethod
|
||||||
|
def execute(cls, model, tile_size, swap_size, max_depth, scale_depth) -> io.NodeOutput:
|
||||||
def patch(self, model, tile_size, swap_size, max_depth, scale_depth):
|
|
||||||
latent_tile_size = max(32, tile_size) // 8
|
latent_tile_size = max(32, tile_size) // 8
|
||||||
self.temp = None
|
temp = None
|
||||||
|
|
||||||
def hypertile_in(q, k, v, extra_options):
|
def hypertile_in(q, k, v, extra_options):
|
||||||
|
nonlocal temp
|
||||||
model_chans = q.shape[-2]
|
model_chans = q.shape[-2]
|
||||||
orig_shape = extra_options['original_shape']
|
orig_shape = extra_options['original_shape']
|
||||||
apply_to = []
|
apply_to = []
|
||||||
@ -58,14 +66,15 @@ class HyperTile:
|
|||||||
|
|
||||||
if nh * nw > 1:
|
if nh * nw > 1:
|
||||||
q = rearrange(q, "b (nh h nw w) c -> (b nh nw) (h w) c", h=h // nh, w=w // nw, nh=nh, nw=nw)
|
q = rearrange(q, "b (nh h nw w) c -> (b nh nw) (h w) c", h=h // nh, w=w // nw, nh=nh, nw=nw)
|
||||||
self.temp = (nh, nw, h, w)
|
temp = (nh, nw, h, w)
|
||||||
return q, k, v
|
return q, k, v
|
||||||
|
|
||||||
return q, k, v
|
return q, k, v
|
||||||
def hypertile_out(out, extra_options):
|
def hypertile_out(out, extra_options):
|
||||||
if self.temp is not None:
|
nonlocal temp
|
||||||
nh, nw, h, w = self.temp
|
if temp is not None:
|
||||||
self.temp = None
|
nh, nw, h, w = temp
|
||||||
|
temp = None
|
||||||
out = rearrange(out, "(b nh nw) hw c -> b nh nw hw c", nh=nh, nw=nw)
|
out = rearrange(out, "(b nh nw) hw c -> b nh nw hw c", nh=nh, nw=nw)
|
||||||
out = rearrange(out, "b nh nw (h w) c -> b (nh h nw w) c", h=h // nh, w=w // nw)
|
out = rearrange(out, "b nh nw (h w) c -> b (nh h nw w) c", h=h // nh, w=w // nw)
|
||||||
return out
|
return out
|
||||||
@ -76,6 +85,14 @@ class HyperTile:
|
|||||||
m.set_model_attn1_output_patch(hypertile_out)
|
m.set_model_attn1_output_patch(hypertile_out)
|
||||||
return (m, )
|
return (m, )
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"HyperTile": HyperTile,
|
class HyperTileExtension(ComfyExtension):
|
||||||
}
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
HyperTile,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> HyperTileExtension:
|
||||||
|
return HyperTileExtension()
|
||||||
|
|||||||
@ -1,20 +1,22 @@
|
|||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import comfy.model_management as mm
|
import comfy.model_management as mm
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
class LotusConditioning:
|
|
||||||
|
class LotusConditioning(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {
|
return io.Schema(
|
||||||
"required": {
|
node_id="LotusConditioning",
|
||||||
},
|
category="conditioning/lotus",
|
||||||
}
|
inputs=[],
|
||||||
|
outputs=[io.Conditioning.Output(display_name="conditioning")],
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("CONDITIONING",)
|
@classmethod
|
||||||
RETURN_NAMES = ("conditioning",)
|
def execute(cls) -> io.NodeOutput:
|
||||||
FUNCTION = "conditioning"
|
|
||||||
CATEGORY = "conditioning/lotus"
|
|
||||||
|
|
||||||
def conditioning(self):
|
|
||||||
device = mm.get_torch_device()
|
device = mm.get_torch_device()
|
||||||
#lotus uses a frozen encoder and null conditioning, i'm just inlining the results of that operation since it doesn't change
|
#lotus uses a frozen encoder and null conditioning, i'm just inlining the results of that operation since it doesn't change
|
||||||
#and getting parity with the reference implementation would otherwise require inference and 800mb of tensors
|
#and getting parity with the reference implementation would otherwise require inference and 800mb of tensors
|
||||||
@ -22,8 +24,16 @@ class LotusConditioning:
|
|||||||
|
|
||||||
cond = [[prompt_embeds, {}]]
|
cond = [[prompt_embeds, {}]]
|
||||||
|
|
||||||
return (cond,)
|
return io.NodeOutput(cond)
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"LotusConditioning" : LotusConditioning,
|
class LotusExtension(ComfyExtension):
|
||||||
}
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
LotusConditioning,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> LotusExtension:
|
||||||
|
return LotusExtension()
|
||||||
|
|||||||
@ -1,20 +1,27 @@
|
|||||||
from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict
|
from typing_extensions import override
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
class RenormCFG:
|
|
||||||
|
class RenormCFG(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": { "model": ("MODEL",),
|
return io.Schema(
|
||||||
"cfg_trunc": ("FLOAT", {"default": 100, "min": 0.0, "max": 100.0, "step": 0.01}),
|
node_id="RenormCFG",
|
||||||
"renorm_cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.01}),
|
category="advanced/model",
|
||||||
}}
|
inputs=[
|
||||||
RETURN_TYPES = ("MODEL",)
|
io.Model.Input("model"),
|
||||||
FUNCTION = "patch"
|
io.Float.Input("cfg_trunc", default=100, min=0.0, max=100.0, step=0.01),
|
||||||
|
io.Float.Input("renorm_cfg", default=1.0, min=0.0, max=100.0, step=0.01),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Model.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
CATEGORY = "advanced/model"
|
@classmethod
|
||||||
|
def execute(cls, model, cfg_trunc, renorm_cfg) -> io.NodeOutput:
|
||||||
def patch(self, model, cfg_trunc, renorm_cfg):
|
|
||||||
def renorm_cfg_func(args):
|
def renorm_cfg_func(args):
|
||||||
cond_denoised = args["cond_denoised"]
|
cond_denoised = args["cond_denoised"]
|
||||||
uncond_denoised = args["uncond_denoised"]
|
uncond_denoised = args["uncond_denoised"]
|
||||||
@ -53,10 +60,10 @@ class RenormCFG:
|
|||||||
|
|
||||||
m = model.clone()
|
m = model.clone()
|
||||||
m.set_model_sampler_cfg_function(renorm_cfg_func)
|
m.set_model_sampler_cfg_function(renorm_cfg_func)
|
||||||
return (m, )
|
return io.NodeOutput(m)
|
||||||
|
|
||||||
|
|
||||||
class CLIPTextEncodeLumina2(ComfyNodeABC):
|
class CLIPTextEncodeLumina2(io.ComfyNode):
|
||||||
SYSTEM_PROMPT = {
|
SYSTEM_PROMPT = {
|
||||||
"superior": "You are an assistant designed to generate superior images with the superior "\
|
"superior": "You are an assistant designed to generate superior images with the superior "\
|
||||||
"degree of image-text alignment based on textual prompts or user prompts.",
|
"degree of image-text alignment based on textual prompts or user prompts.",
|
||||||
@ -69,36 +76,52 @@ class CLIPTextEncodeLumina2(ComfyNodeABC):
|
|||||||
"Alignment: You are an assistant designed to generate high-quality images with the highest "\
|
"Alignment: You are an assistant designed to generate high-quality images with the highest "\
|
||||||
"degree of image-text alignment based on textual prompts."
|
"degree of image-text alignment based on textual prompts."
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s) -> InputTypeDict:
|
def define_schema(cls):
|
||||||
return {
|
return io.Schema(
|
||||||
"required": {
|
node_id="CLIPTextEncodeLumina2",
|
||||||
"system_prompt": (list(CLIPTextEncodeLumina2.SYSTEM_PROMPT.keys()), {"tooltip": CLIPTextEncodeLumina2.SYSTEM_PROMPT_TIP}),
|
display_name="CLIP Text Encode for Lumina2",
|
||||||
"user_prompt": (IO.STRING, {"multiline": True, "dynamicPrompts": True, "tooltip": "The text to be encoded."}),
|
category="conditioning",
|
||||||
"clip": (IO.CLIP, {"tooltip": "The CLIP model used for encoding the text."})
|
description="Encodes a system prompt and a user prompt using a CLIP model into an embedding "
|
||||||
}
|
"that can be used to guide the diffusion model towards generating specific images.",
|
||||||
}
|
inputs=[
|
||||||
RETURN_TYPES = (IO.CONDITIONING,)
|
io.Combo.Input(
|
||||||
OUTPUT_TOOLTIPS = ("A conditioning containing the embedded text used to guide the diffusion model.",)
|
"system_prompt",
|
||||||
FUNCTION = "encode"
|
options=list(cls.SYSTEM_PROMPT.keys()),
|
||||||
|
tooltip=cls.SYSTEM_PROMPT_TIP,
|
||||||
|
),
|
||||||
|
io.String.Input(
|
||||||
|
"user_prompt",
|
||||||
|
multiline=True,
|
||||||
|
dynamic_prompts=True,
|
||||||
|
tooltip="The text to be encoded.",
|
||||||
|
),
|
||||||
|
io.Clip.Input("clip", tooltip="The CLIP model used for encoding the text."),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Conditioning.Output(
|
||||||
|
tooltip="A conditioning containing the embedded text used to guide the diffusion model.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
CATEGORY = "conditioning"
|
@classmethod
|
||||||
DESCRIPTION = "Encodes a system prompt and a user prompt using a CLIP model into an embedding that can be used to guide the diffusion model towards generating specific images."
|
def execute(cls, clip, user_prompt, system_prompt) -> io.NodeOutput:
|
||||||
|
|
||||||
def encode(self, clip, user_prompt, system_prompt):
|
|
||||||
if clip is None:
|
if clip is None:
|
||||||
raise RuntimeError("ERROR: clip input is invalid: None\n\nIf the clip is from a checkpoint loader node your checkpoint does not contain a valid clip or text encoder model.")
|
raise RuntimeError("ERROR: clip input is invalid: None\n\nIf the clip is from a checkpoint loader node your checkpoint does not contain a valid clip or text encoder model.")
|
||||||
system_prompt = CLIPTextEncodeLumina2.SYSTEM_PROMPT[system_prompt]
|
system_prompt = cls.SYSTEM_PROMPT[system_prompt]
|
||||||
prompt = f'{system_prompt} <Prompt Start> {user_prompt}'
|
prompt = f'{system_prompt} <Prompt Start> {user_prompt}'
|
||||||
tokens = clip.tokenize(prompt)
|
tokens = clip.tokenize(prompt)
|
||||||
return (clip.encode_from_tokens_scheduled(tokens), )
|
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens))
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
class Lumina2Extension(ComfyExtension):
|
||||||
"CLIPTextEncodeLumina2": CLIPTextEncodeLumina2,
|
@override
|
||||||
"RenormCFG": RenormCFG
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
}
|
return [
|
||||||
|
CLIPTextEncodeLumina2,
|
||||||
|
RenormCFG,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
async def comfy_entrypoint() -> Lumina2Extension:
|
||||||
"CLIPTextEncodeLumina2": "CLIP Text Encode for Lumina2",
|
return Lumina2Extension()
|
||||||
}
|
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import folder_paths
|
|||||||
import comfy.clip_model
|
import comfy.clip_model
|
||||||
import comfy.clip_vision
|
import comfy.clip_vision
|
||||||
import comfy.ops
|
import comfy.ops
|
||||||
|
from typing_extensions import override
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
# code for model from: https://github.com/TencentARC/PhotoMaker/blob/main/photomaker/model.py under Apache License Version 2.0
|
# code for model from: https://github.com/TencentARC/PhotoMaker/blob/main/photomaker/model.py under Apache License Version 2.0
|
||||||
VISION_CONFIG_DICT = {
|
VISION_CONFIG_DICT = {
|
||||||
@ -116,41 +118,52 @@ class PhotoMakerIDEncoder(comfy.clip_model.CLIPVisionModelProjection):
|
|||||||
return updated_prompt_embeds
|
return updated_prompt_embeds
|
||||||
|
|
||||||
|
|
||||||
class PhotoMakerLoader:
|
class PhotoMakerLoader(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": { "photomaker_model_name": (folder_paths.get_filename_list("photomaker"), )}}
|
return io.Schema(
|
||||||
|
node_id="PhotoMakerLoader",
|
||||||
|
category="_for_testing/photomaker",
|
||||||
|
inputs=[
|
||||||
|
io.Combo.Input("photomaker_model_name", options=folder_paths.get_filename_list("photomaker")),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Photomaker.Output(),
|
||||||
|
],
|
||||||
|
is_experimental=True,
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("PHOTOMAKER",)
|
@classmethod
|
||||||
FUNCTION = "load_photomaker_model"
|
def execute(cls, photomaker_model_name):
|
||||||
|
|
||||||
CATEGORY = "_for_testing/photomaker"
|
|
||||||
|
|
||||||
def load_photomaker_model(self, photomaker_model_name):
|
|
||||||
photomaker_model_path = folder_paths.get_full_path_or_raise("photomaker", photomaker_model_name)
|
photomaker_model_path = folder_paths.get_full_path_or_raise("photomaker", photomaker_model_name)
|
||||||
photomaker_model = PhotoMakerIDEncoder()
|
photomaker_model = PhotoMakerIDEncoder()
|
||||||
data = comfy.utils.load_torch_file(photomaker_model_path, safe_load=True)
|
data = comfy.utils.load_torch_file(photomaker_model_path, safe_load=True)
|
||||||
if "id_encoder" in data:
|
if "id_encoder" in data:
|
||||||
data = data["id_encoder"]
|
data = data["id_encoder"]
|
||||||
photomaker_model.load_state_dict(data)
|
photomaker_model.load_state_dict(data)
|
||||||
return (photomaker_model,)
|
return io.NodeOutput(photomaker_model)
|
||||||
|
|
||||||
|
|
||||||
class PhotoMakerEncode:
|
class PhotoMakerEncode(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": { "photomaker": ("PHOTOMAKER",),
|
return io.Schema(
|
||||||
"image": ("IMAGE",),
|
node_id="PhotoMakerEncode",
|
||||||
"clip": ("CLIP", ),
|
category="_for_testing/photomaker",
|
||||||
"text": ("STRING", {"multiline": True, "dynamicPrompts": True, "default": "photograph of photomaker"}),
|
inputs=[
|
||||||
}}
|
io.Photomaker.Input("photomaker"),
|
||||||
|
io.Image.Input("image"),
|
||||||
|
io.Clip.Input("clip"),
|
||||||
|
io.String.Input("text", multiline=True, dynamic_prompts=True, default="photograph of photomaker"),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Conditioning.Output(),
|
||||||
|
],
|
||||||
|
is_experimental=True,
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("CONDITIONING",)
|
@classmethod
|
||||||
FUNCTION = "apply_photomaker"
|
def execute(cls, photomaker, image, clip, text):
|
||||||
|
|
||||||
CATEGORY = "_for_testing/photomaker"
|
|
||||||
|
|
||||||
def apply_photomaker(self, photomaker, image, clip, text):
|
|
||||||
special_token = "photomaker"
|
special_token = "photomaker"
|
||||||
pixel_values = comfy.clip_vision.clip_preprocess(image.to(photomaker.load_device)).float()
|
pixel_values = comfy.clip_vision.clip_preprocess(image.to(photomaker.load_device)).float()
|
||||||
try:
|
try:
|
||||||
@ -178,11 +191,16 @@ class PhotoMakerEncode:
|
|||||||
else:
|
else:
|
||||||
out = cond
|
out = cond
|
||||||
|
|
||||||
return ([[out, {"pooled_output": pooled}]], )
|
return io.NodeOutput([[out, {"pooled_output": pooled}]])
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
class PhotomakerExtension(ComfyExtension):
|
||||||
"PhotoMakerLoader": PhotoMakerLoader,
|
@override
|
||||||
"PhotoMakerEncode": PhotoMakerEncode,
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
}
|
return [
|
||||||
|
PhotoMakerLoader,
|
||||||
|
PhotoMakerEncode,
|
||||||
|
]
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> PhotomakerExtension:
|
||||||
|
return PhotomakerExtension()
|
||||||
|
|||||||
@ -1,24 +1,38 @@
|
|||||||
from nodes import MAX_RESOLUTION
|
from typing_extensions import override
|
||||||
|
import nodes
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
class CLIPTextEncodePixArtAlpha:
|
class CLIPTextEncodePixArtAlpha(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {
|
return io.Schema(
|
||||||
"width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
node_id="CLIPTextEncodePixArtAlpha",
|
||||||
"height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
category="advanced/conditioning",
|
||||||
|
description="Encodes text and sets the resolution conditioning for PixArt Alpha. Does not apply to PixArt Sigma.",
|
||||||
|
inputs=[
|
||||||
|
io.Int.Input("width", default=1024, min=0, max=nodes.MAX_RESOLUTION),
|
||||||
|
io.Int.Input("height", default=1024, min=0, max=nodes.MAX_RESOLUTION),
|
||||||
# "aspect_ratio": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
# "aspect_ratio": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
||||||
"text": ("STRING", {"multiline": True, "dynamicPrompts": True}), "clip": ("CLIP", ),
|
io.String.Input("text", multiline=True, dynamic_prompts=True),
|
||||||
}}
|
io.Clip.Input("clip"),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Conditioning.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("CONDITIONING",)
|
@classmethod
|
||||||
FUNCTION = "encode"
|
def execute(cls, clip, width, height, text):
|
||||||
CATEGORY = "advanced/conditioning"
|
|
||||||
DESCRIPTION = "Encodes text and sets the resolution conditioning for PixArt Alpha. Does not apply to PixArt Sigma."
|
|
||||||
|
|
||||||
def encode(self, clip, width, height, text):
|
|
||||||
tokens = clip.tokenize(text)
|
tokens = clip.tokenize(text)
|
||||||
return (clip.encode_from_tokens_scheduled(tokens, add_dict={"width": width, "height": height}),)
|
return io.NodeOutput(clip.encode_from_tokens_scheduled(tokens, add_dict={"width": width, "height": height}))
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"CLIPTextEncodePixArtAlpha": CLIPTextEncodePixArtAlpha,
|
class PixArtExtension(ComfyExtension):
|
||||||
}
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
CLIPTextEncodePixArtAlpha,
|
||||||
|
]
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> PixArtExtension:
|
||||||
|
return PixArtExtension()
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
from typing_extensions import override
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
@ -7,33 +8,27 @@ import math
|
|||||||
import comfy.utils
|
import comfy.utils
|
||||||
import comfy.model_management
|
import comfy.model_management
|
||||||
import node_helpers
|
import node_helpers
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
class Blend:
|
class Blend(io.ComfyNode):
|
||||||
def __init__(self):
|
@classmethod
|
||||||
pass
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="ImageBlend",
|
||||||
|
category="image/postprocessing",
|
||||||
|
inputs=[
|
||||||
|
io.Image.Input("image1"),
|
||||||
|
io.Image.Input("image2"),
|
||||||
|
io.Float.Input("blend_factor", default=0.5, min=0.0, max=1.0, step=0.01),
|
||||||
|
io.Combo.Input("blend_mode", options=["normal", "multiply", "screen", "overlay", "soft_light", "difference"]),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Image.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def execute(cls, image1: torch.Tensor, image2: torch.Tensor, blend_factor: float, blend_mode: str) -> io.NodeOutput:
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"image1": ("IMAGE",),
|
|
||||||
"image2": ("IMAGE",),
|
|
||||||
"blend_factor": ("FLOAT", {
|
|
||||||
"default": 0.5,
|
|
||||||
"min": 0.0,
|
|
||||||
"max": 1.0,
|
|
||||||
"step": 0.01
|
|
||||||
}),
|
|
||||||
"blend_mode": (["normal", "multiply", "screen", "overlay", "soft_light", "difference"],),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
FUNCTION = "blend_images"
|
|
||||||
|
|
||||||
CATEGORY = "image/postprocessing"
|
|
||||||
|
|
||||||
def blend_images(self, image1: torch.Tensor, image2: torch.Tensor, blend_factor: float, blend_mode: str):
|
|
||||||
image1, image2 = node_helpers.image_alpha_fix(image1, image2)
|
image1, image2 = node_helpers.image_alpha_fix(image1, image2)
|
||||||
image2 = image2.to(image1.device)
|
image2 = image2.to(image1.device)
|
||||||
if image1.shape != image2.shape:
|
if image1.shape != image2.shape:
|
||||||
@ -41,12 +36,13 @@ class Blend:
|
|||||||
image2 = comfy.utils.common_upscale(image2, image1.shape[2], image1.shape[1], upscale_method='bicubic', crop='center')
|
image2 = comfy.utils.common_upscale(image2, image1.shape[2], image1.shape[1], upscale_method='bicubic', crop='center')
|
||||||
image2 = image2.permute(0, 2, 3, 1)
|
image2 = image2.permute(0, 2, 3, 1)
|
||||||
|
|
||||||
blended_image = self.blend_mode(image1, image2, blend_mode)
|
blended_image = cls.blend_mode(image1, image2, blend_mode)
|
||||||
blended_image = image1 * (1 - blend_factor) + blended_image * blend_factor
|
blended_image = image1 * (1 - blend_factor) + blended_image * blend_factor
|
||||||
blended_image = torch.clamp(blended_image, 0, 1)
|
blended_image = torch.clamp(blended_image, 0, 1)
|
||||||
return (blended_image,)
|
return io.NodeOutput(blended_image)
|
||||||
|
|
||||||
def blend_mode(self, img1, img2, mode):
|
@classmethod
|
||||||
|
def blend_mode(cls, img1, img2, mode):
|
||||||
if mode == "normal":
|
if mode == "normal":
|
||||||
return img2
|
return img2
|
||||||
elif mode == "multiply":
|
elif mode == "multiply":
|
||||||
@ -56,13 +52,13 @@ class Blend:
|
|||||||
elif mode == "overlay":
|
elif mode == "overlay":
|
||||||
return torch.where(img1 <= 0.5, 2 * img1 * img2, 1 - 2 * (1 - img1) * (1 - img2))
|
return torch.where(img1 <= 0.5, 2 * img1 * img2, 1 - 2 * (1 - img1) * (1 - img2))
|
||||||
elif mode == "soft_light":
|
elif mode == "soft_light":
|
||||||
return torch.where(img2 <= 0.5, img1 - (1 - 2 * img2) * img1 * (1 - img1), img1 + (2 * img2 - 1) * (self.g(img1) - img1))
|
return torch.where(img2 <= 0.5, img1 - (1 - 2 * img2) * img1 * (1 - img1), img1 + (2 * img2 - 1) * (cls.g(img1) - img1))
|
||||||
elif mode == "difference":
|
elif mode == "difference":
|
||||||
return img1 - img2
|
return img1 - img2
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported blend mode: {mode}")
|
raise ValueError(f"Unsupported blend mode: {mode}")
|
||||||
|
|
||||||
def g(self, x):
|
@classmethod
|
||||||
|
def g(cls, x):
|
||||||
return torch.where(x <= 0.25, ((16 * x - 12) * x + 4) * x, torch.sqrt(x))
|
return torch.where(x <= 0.25, ((16 * x - 12) * x + 4) * x, torch.sqrt(x))
|
||||||
|
|
||||||
def gaussian_kernel(kernel_size: int, sigma: float, device=None):
|
def gaussian_kernel(kernel_size: int, sigma: float, device=None):
|
||||||
@ -71,38 +67,26 @@ def gaussian_kernel(kernel_size: int, sigma: float, device=None):
|
|||||||
g = torch.exp(-(d * d) / (2.0 * sigma * sigma))
|
g = torch.exp(-(d * d) / (2.0 * sigma * sigma))
|
||||||
return g / g.sum()
|
return g / g.sum()
|
||||||
|
|
||||||
class Blur:
|
class Blur(io.ComfyNode):
|
||||||
def __init__(self):
|
@classmethod
|
||||||
pass
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="ImageBlur",
|
||||||
|
category="image/postprocessing",
|
||||||
|
inputs=[
|
||||||
|
io.Image.Input("image"),
|
||||||
|
io.Int.Input("blur_radius", default=1, min=1, max=31, step=1),
|
||||||
|
io.Float.Input("sigma", default=1.0, min=0.1, max=10.0, step=0.1),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Image.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def execute(cls, image: torch.Tensor, blur_radius: int, sigma: float) -> io.NodeOutput:
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"image": ("IMAGE",),
|
|
||||||
"blur_radius": ("INT", {
|
|
||||||
"default": 1,
|
|
||||||
"min": 1,
|
|
||||||
"max": 31,
|
|
||||||
"step": 1
|
|
||||||
}),
|
|
||||||
"sigma": ("FLOAT", {
|
|
||||||
"default": 1.0,
|
|
||||||
"min": 0.1,
|
|
||||||
"max": 10.0,
|
|
||||||
"step": 0.1
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
FUNCTION = "blur"
|
|
||||||
|
|
||||||
CATEGORY = "image/postprocessing"
|
|
||||||
|
|
||||||
def blur(self, image: torch.Tensor, blur_radius: int, sigma: float):
|
|
||||||
if blur_radius == 0:
|
if blur_radius == 0:
|
||||||
return (image,)
|
return io.NodeOutput(image)
|
||||||
|
|
||||||
image = image.to(comfy.model_management.get_torch_device())
|
image = image.to(comfy.model_management.get_torch_device())
|
||||||
batch_size, height, width, channels = image.shape
|
batch_size, height, width, channels = image.shape
|
||||||
@ -115,31 +99,24 @@ class Blur:
|
|||||||
blurred = F.conv2d(padded_image, kernel, padding=kernel_size // 2, groups=channels)[:,:,blur_radius:-blur_radius, blur_radius:-blur_radius]
|
blurred = F.conv2d(padded_image, kernel, padding=kernel_size // 2, groups=channels)[:,:,blur_radius:-blur_radius, blur_radius:-blur_radius]
|
||||||
blurred = blurred.permute(0, 2, 3, 1)
|
blurred = blurred.permute(0, 2, 3, 1)
|
||||||
|
|
||||||
return (blurred.to(comfy.model_management.intermediate_device()),)
|
return io.NodeOutput(blurred.to(comfy.model_management.intermediate_device()))
|
||||||
|
|
||||||
class Quantize:
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
class Quantize(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {
|
return io.Schema(
|
||||||
"required": {
|
node_id="ImageQuantize",
|
||||||
"image": ("IMAGE",),
|
category="image/postprocessing",
|
||||||
"colors": ("INT", {
|
inputs=[
|
||||||
"default": 256,
|
io.Image.Input("image"),
|
||||||
"min": 1,
|
io.Int.Input("colors", default=256, min=1, max=256, step=1),
|
||||||
"max": 256,
|
io.Combo.Input("dither", options=["none", "floyd-steinberg", "bayer-2", "bayer-4", "bayer-8", "bayer-16"]),
|
||||||
"step": 1
|
],
|
||||||
}),
|
outputs=[
|
||||||
"dither": (["none", "floyd-steinberg", "bayer-2", "bayer-4", "bayer-8", "bayer-16"],),
|
io.Image.Output(),
|
||||||
},
|
],
|
||||||
}
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
FUNCTION = "quantize"
|
|
||||||
|
|
||||||
CATEGORY = "image/postprocessing"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def bayer(im, pal_im, order):
|
def bayer(im, pal_im, order):
|
||||||
@ -167,7 +144,8 @@ class Quantize:
|
|||||||
im = im.quantize(palette=pal_im, dither=Image.Dither.NONE)
|
im = im.quantize(palette=pal_im, dither=Image.Dither.NONE)
|
||||||
return im
|
return im
|
||||||
|
|
||||||
def quantize(self, image: torch.Tensor, colors: int, dither: str):
|
@classmethod
|
||||||
|
def execute(cls, image: torch.Tensor, colors: int, dither: str) -> io.NodeOutput:
|
||||||
batch_size, height, width, _ = image.shape
|
batch_size, height, width, _ = image.shape
|
||||||
result = torch.zeros_like(image)
|
result = torch.zeros_like(image)
|
||||||
|
|
||||||
@ -187,46 +165,29 @@ class Quantize:
|
|||||||
quantized_array = torch.tensor(np.array(quantized_image.convert("RGB"))).float() / 255
|
quantized_array = torch.tensor(np.array(quantized_image.convert("RGB"))).float() / 255
|
||||||
result[b] = quantized_array
|
result[b] = quantized_array
|
||||||
|
|
||||||
return (result,)
|
return io.NodeOutput(result)
|
||||||
|
|
||||||
class Sharpen:
|
class Sharpen(io.ComfyNode):
|
||||||
def __init__(self):
|
@classmethod
|
||||||
pass
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="ImageSharpen",
|
||||||
|
category="image/postprocessing",
|
||||||
|
inputs=[
|
||||||
|
io.Image.Input("image"),
|
||||||
|
io.Int.Input("sharpen_radius", default=1, min=1, max=31, step=1),
|
||||||
|
io.Float.Input("sigma", default=1.0, min=0.1, max=10.0, step=0.01),
|
||||||
|
io.Float.Input("alpha", default=1.0, min=0.0, max=5.0, step=0.01),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Image.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def execute(cls, image: torch.Tensor, sharpen_radius: int, sigma:float, alpha: float) -> io.NodeOutput:
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"image": ("IMAGE",),
|
|
||||||
"sharpen_radius": ("INT", {
|
|
||||||
"default": 1,
|
|
||||||
"min": 1,
|
|
||||||
"max": 31,
|
|
||||||
"step": 1
|
|
||||||
}),
|
|
||||||
"sigma": ("FLOAT", {
|
|
||||||
"default": 1.0,
|
|
||||||
"min": 0.1,
|
|
||||||
"max": 10.0,
|
|
||||||
"step": 0.01
|
|
||||||
}),
|
|
||||||
"alpha": ("FLOAT", {
|
|
||||||
"default": 1.0,
|
|
||||||
"min": 0.0,
|
|
||||||
"max": 5.0,
|
|
||||||
"step": 0.01
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
|
||||||
FUNCTION = "sharpen"
|
|
||||||
|
|
||||||
CATEGORY = "image/postprocessing"
|
|
||||||
|
|
||||||
def sharpen(self, image: torch.Tensor, sharpen_radius: int, sigma:float, alpha: float):
|
|
||||||
if sharpen_radius == 0:
|
if sharpen_radius == 0:
|
||||||
return (image,)
|
return io.NodeOutput(image)
|
||||||
|
|
||||||
batch_size, height, width, channels = image.shape
|
batch_size, height, width, channels = image.shape
|
||||||
image = image.to(comfy.model_management.get_torch_device())
|
image = image.to(comfy.model_management.get_torch_device())
|
||||||
@ -245,23 +206,29 @@ class Sharpen:
|
|||||||
|
|
||||||
result = torch.clamp(sharpened, 0, 1)
|
result = torch.clamp(sharpened, 0, 1)
|
||||||
|
|
||||||
return (result.to(comfy.model_management.intermediate_device()),)
|
return io.NodeOutput(result.to(comfy.model_management.intermediate_device()))
|
||||||
|
|
||||||
class ImageScaleToTotalPixels:
|
class ImageScaleToTotalPixels(io.ComfyNode):
|
||||||
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
|
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
|
||||||
crop_methods = ["disabled", "center"]
|
crop_methods = ["disabled", "center"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": { "image": ("IMAGE",), "upscale_method": (s.upscale_methods,),
|
return io.Schema(
|
||||||
"megapixels": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 16.0, "step": 0.01}),
|
node_id="ImageScaleToTotalPixels",
|
||||||
}}
|
category="image/upscaling",
|
||||||
RETURN_TYPES = ("IMAGE",)
|
inputs=[
|
||||||
FUNCTION = "upscale"
|
io.Image.Input("image"),
|
||||||
|
io.Combo.Input("upscale_method", options=cls.upscale_methods),
|
||||||
|
io.Float.Input("megapixels", default=1.0, min=0.01, max=16.0, step=0.01),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Image.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
CATEGORY = "image/upscaling"
|
@classmethod
|
||||||
|
def execute(cls, image, upscale_method, megapixels) -> io.NodeOutput:
|
||||||
def upscale(self, image, upscale_method, megapixels):
|
|
||||||
samples = image.movedim(-1,1)
|
samples = image.movedim(-1,1)
|
||||||
total = int(megapixels * 1024 * 1024)
|
total = int(megapixels * 1024 * 1024)
|
||||||
|
|
||||||
@ -271,12 +238,18 @@ class ImageScaleToTotalPixels:
|
|||||||
|
|
||||||
s = comfy.utils.common_upscale(samples, width, height, upscale_method, "disabled")
|
s = comfy.utils.common_upscale(samples, width, height, upscale_method, "disabled")
|
||||||
s = s.movedim(1,-1)
|
s = s.movedim(1,-1)
|
||||||
return (s,)
|
return io.NodeOutput(s)
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
class PostProcessingExtension(ComfyExtension):
|
||||||
"ImageBlend": Blend,
|
@override
|
||||||
"ImageBlur": Blur,
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
"ImageQuantize": Quantize,
|
return [
|
||||||
"ImageSharpen": Sharpen,
|
Blend,
|
||||||
"ImageScaleToTotalPixels": ImageScaleToTotalPixels,
|
Blur,
|
||||||
}
|
Quantize,
|
||||||
|
Sharpen,
|
||||||
|
ImageScaleToTotalPixels,
|
||||||
|
]
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> PostProcessingExtension:
|
||||||
|
return PostProcessingExtension()
|
||||||
|
|||||||
@ -1,24 +1,29 @@
|
|||||||
import node_helpers
|
import node_helpers
|
||||||
import comfy.utils
|
import comfy.utils
|
||||||
import math
|
import math
|
||||||
|
from typing_extensions import override
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
|
|
||||||
class TextEncodeQwenImageEdit:
|
class TextEncodeQwenImageEdit(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {
|
return io.Schema(
|
||||||
"clip": ("CLIP", ),
|
node_id="TextEncodeQwenImageEdit",
|
||||||
"prompt": ("STRING", {"multiline": True, "dynamicPrompts": True}),
|
category="advanced/conditioning",
|
||||||
},
|
inputs=[
|
||||||
"optional": {"vae": ("VAE", ),
|
io.Clip.Input("clip"),
|
||||||
"image": ("IMAGE", ),}}
|
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
|
||||||
|
io.Vae.Input("vae", optional=True),
|
||||||
|
io.Image.Input("image", optional=True),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Conditioning.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("CONDITIONING",)
|
@classmethod
|
||||||
FUNCTION = "encode"
|
def execute(cls, clip, prompt, vae=None, image=None) -> io.NodeOutput:
|
||||||
|
|
||||||
CATEGORY = "advanced/conditioning"
|
|
||||||
|
|
||||||
def encode(self, clip, prompt, vae=None, image=None):
|
|
||||||
ref_latent = None
|
ref_latent = None
|
||||||
if image is None:
|
if image is None:
|
||||||
images = []
|
images = []
|
||||||
@ -40,28 +45,30 @@ class TextEncodeQwenImageEdit:
|
|||||||
conditioning = clip.encode_from_tokens_scheduled(tokens)
|
conditioning = clip.encode_from_tokens_scheduled(tokens)
|
||||||
if ref_latent is not None:
|
if ref_latent is not None:
|
||||||
conditioning = node_helpers.conditioning_set_values(conditioning, {"reference_latents": [ref_latent]}, append=True)
|
conditioning = node_helpers.conditioning_set_values(conditioning, {"reference_latents": [ref_latent]}, append=True)
|
||||||
return (conditioning, )
|
return io.NodeOutput(conditioning)
|
||||||
|
|
||||||
|
|
||||||
class TextEncodeQwenImageEditPlus:
|
class TextEncodeQwenImageEditPlus(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {
|
return io.Schema(
|
||||||
"clip": ("CLIP", ),
|
node_id="TextEncodeQwenImageEditPlus",
|
||||||
"prompt": ("STRING", {"multiline": True, "dynamicPrompts": True}),
|
category="advanced/conditioning",
|
||||||
},
|
inputs=[
|
||||||
"optional": {"vae": ("VAE", ),
|
io.Clip.Input("clip"),
|
||||||
"image1": ("IMAGE", ),
|
io.String.Input("prompt", multiline=True, dynamic_prompts=True),
|
||||||
"image2": ("IMAGE", ),
|
io.Vae.Input("vae", optional=True),
|
||||||
"image3": ("IMAGE", ),
|
io.Image.Input("image1", optional=True),
|
||||||
}}
|
io.Image.Input("image2", optional=True),
|
||||||
|
io.Image.Input("image3", optional=True),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Conditioning.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("CONDITIONING",)
|
@classmethod
|
||||||
FUNCTION = "encode"
|
def execute(cls, clip, prompt, vae=None, image1=None, image2=None, image3=None) -> io.NodeOutput:
|
||||||
|
|
||||||
CATEGORY = "advanced/conditioning"
|
|
||||||
|
|
||||||
def encode(self, clip, prompt, vae=None, image1=None, image2=None, image3=None):
|
|
||||||
ref_latents = []
|
ref_latents = []
|
||||||
images = [image1, image2, image3]
|
images = [image1, image2, image3]
|
||||||
images_vl = []
|
images_vl = []
|
||||||
@ -94,10 +101,17 @@ class TextEncodeQwenImageEditPlus:
|
|||||||
conditioning = clip.encode_from_tokens_scheduled(tokens)
|
conditioning = clip.encode_from_tokens_scheduled(tokens)
|
||||||
if len(ref_latents) > 0:
|
if len(ref_latents) > 0:
|
||||||
conditioning = node_helpers.conditioning_set_values(conditioning, {"reference_latents": ref_latents}, append=True)
|
conditioning = node_helpers.conditioning_set_values(conditioning, {"reference_latents": ref_latents}, append=True)
|
||||||
return (conditioning, )
|
return io.NodeOutput(conditioning)
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
class QwenExtension(ComfyExtension):
|
||||||
"TextEncodeQwenImageEdit": TextEncodeQwenImageEdit,
|
@override
|
||||||
"TextEncodeQwenImageEditPlus": TextEncodeQwenImageEditPlus,
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
}
|
return [
|
||||||
|
TextEncodeQwenImageEdit,
|
||||||
|
TextEncodeQwenImageEditPlus,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> QwenExtension:
|
||||||
|
return QwenExtension()
|
||||||
|
|||||||
@ -1,18 +1,25 @@
|
|||||||
|
from typing_extensions import override
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
class LatentRebatch:
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
|
|
||||||
|
class LatentRebatch(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": { "latents": ("LATENT",),
|
return io.Schema(
|
||||||
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
|
node_id="RebatchLatents",
|
||||||
}}
|
display_name="Rebatch Latents",
|
||||||
RETURN_TYPES = ("LATENT",)
|
category="latent/batch",
|
||||||
INPUT_IS_LIST = True
|
is_input_list=True,
|
||||||
OUTPUT_IS_LIST = (True, )
|
inputs=[
|
||||||
|
io.Latent.Input("latents"),
|
||||||
FUNCTION = "rebatch"
|
io.Int.Input("batch_size", default=1, min=1, max=4096),
|
||||||
|
],
|
||||||
CATEGORY = "latent/batch"
|
outputs=[
|
||||||
|
io.Latent.Output(is_output_list=True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_batch(latents, list_ind, offset):
|
def get_batch(latents, list_ind, offset):
|
||||||
@ -53,7 +60,8 @@ class LatentRebatch:
|
|||||||
result = [torch.cat((b1, b2)) if torch.is_tensor(b1) else b1 + b2 for b1, b2 in zip(batch1, batch2)]
|
result = [torch.cat((b1, b2)) if torch.is_tensor(b1) else b1 + b2 for b1, b2 in zip(batch1, batch2)]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def rebatch(self, latents, batch_size):
|
@classmethod
|
||||||
|
def execute(cls, latents, batch_size):
|
||||||
batch_size = batch_size[0]
|
batch_size = batch_size[0]
|
||||||
|
|
||||||
output_list = []
|
output_list = []
|
||||||
@ -63,24 +71,24 @@ class LatentRebatch:
|
|||||||
for i in range(len(latents)):
|
for i in range(len(latents)):
|
||||||
# fetch new entry of list
|
# fetch new entry of list
|
||||||
#samples, masks, indices = self.get_batch(latents, i)
|
#samples, masks, indices = self.get_batch(latents, i)
|
||||||
next_batch = self.get_batch(latents, i, processed)
|
next_batch = cls.get_batch(latents, i, processed)
|
||||||
processed += len(next_batch[2])
|
processed += len(next_batch[2])
|
||||||
# set to current if current is None
|
# set to current if current is None
|
||||||
if current_batch[0] is None:
|
if current_batch[0] is None:
|
||||||
current_batch = next_batch
|
current_batch = next_batch
|
||||||
# add previous to list if dimensions do not match
|
# add previous to list if dimensions do not match
|
||||||
elif next_batch[0].shape[-1] != current_batch[0].shape[-1] or next_batch[0].shape[-2] != current_batch[0].shape[-2]:
|
elif next_batch[0].shape[-1] != current_batch[0].shape[-1] or next_batch[0].shape[-2] != current_batch[0].shape[-2]:
|
||||||
sliced, _ = self.slice_batch(current_batch, 1, batch_size)
|
sliced, _ = cls.slice_batch(current_batch, 1, batch_size)
|
||||||
output_list.append({'samples': sliced[0][0], 'noise_mask': sliced[1][0], 'batch_index': sliced[2][0]})
|
output_list.append({'samples': sliced[0][0], 'noise_mask': sliced[1][0], 'batch_index': sliced[2][0]})
|
||||||
current_batch = next_batch
|
current_batch = next_batch
|
||||||
# cat if everything checks out
|
# cat if everything checks out
|
||||||
else:
|
else:
|
||||||
current_batch = self.cat_batch(current_batch, next_batch)
|
current_batch = cls.cat_batch(current_batch, next_batch)
|
||||||
|
|
||||||
# add to list if dimensions gone above target batch size
|
# add to list if dimensions gone above target batch size
|
||||||
if current_batch[0].shape[0] > batch_size:
|
if current_batch[0].shape[0] > batch_size:
|
||||||
num = current_batch[0].shape[0] // batch_size
|
num = current_batch[0].shape[0] // batch_size
|
||||||
sliced, remainder = self.slice_batch(current_batch, num, batch_size)
|
sliced, remainder = cls.slice_batch(current_batch, num, batch_size)
|
||||||
|
|
||||||
for i in range(num):
|
for i in range(num):
|
||||||
output_list.append({'samples': sliced[0][i], 'noise_mask': sliced[1][i], 'batch_index': sliced[2][i]})
|
output_list.append({'samples': sliced[0][i], 'noise_mask': sliced[1][i], 'batch_index': sliced[2][i]})
|
||||||
@ -89,7 +97,7 @@ class LatentRebatch:
|
|||||||
|
|
||||||
#add remainder
|
#add remainder
|
||||||
if current_batch[0] is not None:
|
if current_batch[0] is not None:
|
||||||
sliced, _ = self.slice_batch(current_batch, 1, batch_size)
|
sliced, _ = cls.slice_batch(current_batch, 1, batch_size)
|
||||||
output_list.append({'samples': sliced[0][0], 'noise_mask': sliced[1][0], 'batch_index': sliced[2][0]})
|
output_list.append({'samples': sliced[0][0], 'noise_mask': sliced[1][0], 'batch_index': sliced[2][0]})
|
||||||
|
|
||||||
#get rid of empty masks
|
#get rid of empty masks
|
||||||
@ -97,23 +105,27 @@ class LatentRebatch:
|
|||||||
if s['noise_mask'].mean() == 1.0:
|
if s['noise_mask'].mean() == 1.0:
|
||||||
del s['noise_mask']
|
del s['noise_mask']
|
||||||
|
|
||||||
return (output_list,)
|
return io.NodeOutput(output_list)
|
||||||
|
|
||||||
class ImageRebatch:
|
class ImageRebatch(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": { "images": ("IMAGE",),
|
return io.Schema(
|
||||||
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
|
node_id="RebatchImages",
|
||||||
}}
|
display_name="Rebatch Images",
|
||||||
RETURN_TYPES = ("IMAGE",)
|
category="image/batch",
|
||||||
INPUT_IS_LIST = True
|
is_input_list=True,
|
||||||
OUTPUT_IS_LIST = (True, )
|
inputs=[
|
||||||
|
io.Image.Input("images"),
|
||||||
|
io.Int.Input("batch_size", default=1, min=1, max=4096),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Image.Output(is_output_list=True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
FUNCTION = "rebatch"
|
@classmethod
|
||||||
|
def execute(cls, images, batch_size):
|
||||||
CATEGORY = "image/batch"
|
|
||||||
|
|
||||||
def rebatch(self, images, batch_size):
|
|
||||||
batch_size = batch_size[0]
|
batch_size = batch_size[0]
|
||||||
|
|
||||||
output_list = []
|
output_list = []
|
||||||
@ -125,14 +137,17 @@ class ImageRebatch:
|
|||||||
for i in range(0, len(all_images), batch_size):
|
for i in range(0, len(all_images), batch_size):
|
||||||
output_list.append(torch.cat(all_images[i:i+batch_size], dim=0))
|
output_list.append(torch.cat(all_images[i:i+batch_size], dim=0))
|
||||||
|
|
||||||
return (output_list,)
|
return io.NodeOutput(output_list)
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"RebatchLatents": LatentRebatch,
|
|
||||||
"RebatchImages": ImageRebatch,
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
class RebatchExtension(ComfyExtension):
|
||||||
"RebatchLatents": "Rebatch Latents",
|
@override
|
||||||
"RebatchImages": "Rebatch Images",
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
}
|
return [
|
||||||
|
LatentRebatch,
|
||||||
|
ImageRebatch,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> RebatchExtension:
|
||||||
|
return RebatchExtension()
|
||||||
|
|||||||
@ -2,10 +2,13 @@ import torch
|
|||||||
from torch import einsum
|
from torch import einsum
|
||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
import math
|
import math
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
from einops import rearrange, repeat
|
from einops import rearrange, repeat
|
||||||
from comfy.ldm.modules.attention import optimized_attention
|
from comfy.ldm.modules.attention import optimized_attention
|
||||||
import comfy.samplers
|
import comfy.samplers
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
|
|
||||||
# from comfy/ldm/modules/attention.py
|
# from comfy/ldm/modules/attention.py
|
||||||
# but modified to return attention scores as well as output
|
# but modified to return attention scores as well as output
|
||||||
@ -104,19 +107,26 @@ def gaussian_blur_2d(img, kernel_size, sigma):
|
|||||||
img = F.conv2d(img, kernel2d, groups=img.shape[-3])
|
img = F.conv2d(img, kernel2d, groups=img.shape[-3])
|
||||||
return img
|
return img
|
||||||
|
|
||||||
class SelfAttentionGuidance:
|
class SelfAttentionGuidance(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": { "model": ("MODEL",),
|
return io.Schema(
|
||||||
"scale": ("FLOAT", {"default": 0.5, "min": -2.0, "max": 5.0, "step": 0.01}),
|
node_id="SelfAttentionGuidance",
|
||||||
"blur_sigma": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 10.0, "step": 0.1}),
|
display_name="Self-Attention Guidance",
|
||||||
}}
|
category="_for_testing",
|
||||||
RETURN_TYPES = ("MODEL",)
|
inputs=[
|
||||||
FUNCTION = "patch"
|
io.Model.Input("model"),
|
||||||
|
io.Float.Input("scale", default=0.5, min=-2.0, max=5.0, step=0.01),
|
||||||
|
io.Float.Input("blur_sigma", default=2.0, min=0.0, max=10.0, step=0.1),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Model.Output(),
|
||||||
|
],
|
||||||
|
is_experimental=True,
|
||||||
|
)
|
||||||
|
|
||||||
CATEGORY = "_for_testing"
|
@classmethod
|
||||||
|
def execute(cls, model, scale, blur_sigma):
|
||||||
def patch(self, model, scale, blur_sigma):
|
|
||||||
m = model.clone()
|
m = model.clone()
|
||||||
|
|
||||||
attn_scores = None
|
attn_scores = None
|
||||||
@ -170,12 +180,16 @@ class SelfAttentionGuidance:
|
|||||||
# unet.mid_block.attentions[0].transformer_blocks[0].attn1.patch
|
# unet.mid_block.attentions[0].transformer_blocks[0].attn1.patch
|
||||||
m.set_model_attn1_replace(attn_and_record, "middle", 0, 0)
|
m.set_model_attn1_replace(attn_and_record, "middle", 0, 0)
|
||||||
|
|
||||||
return (m, )
|
return io.NodeOutput(m)
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"SelfAttentionGuidance": SelfAttentionGuidance,
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
class SagExtension(ComfyExtension):
|
||||||
"SelfAttentionGuidance": "Self-Attention Guidance",
|
@override
|
||||||
}
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
SelfAttentionGuidance,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> SagExtension:
|
||||||
|
return SagExtension()
|
||||||
|
|||||||
@ -1,23 +1,31 @@
|
|||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import comfy.utils
|
import comfy.utils
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
class SD_4XUpscale_Conditioning:
|
class SD_4XUpscale_Conditioning(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": { "images": ("IMAGE",),
|
return io.Schema(
|
||||||
"positive": ("CONDITIONING",),
|
node_id="SD_4XUpscale_Conditioning",
|
||||||
"negative": ("CONDITIONING",),
|
category="conditioning/upscale_diffusion",
|
||||||
"scale_ratio": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
inputs=[
|
||||||
"noise_augmentation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
|
io.Image.Input("images"),
|
||||||
}}
|
io.Conditioning.Input("positive"),
|
||||||
RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "LATENT")
|
io.Conditioning.Input("negative"),
|
||||||
RETURN_NAMES = ("positive", "negative", "latent")
|
io.Float.Input("scale_ratio", default=4.0, min=0.0, max=10.0, step=0.01),
|
||||||
|
io.Float.Input("noise_augmentation", default=0.0, min=0.0, max=1.0, step=0.001),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Conditioning.Output(display_name="positive"),
|
||||||
|
io.Conditioning.Output(display_name="negative"),
|
||||||
|
io.Latent.Output(display_name="latent"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
FUNCTION = "encode"
|
@classmethod
|
||||||
|
def execute(cls, images, positive, negative, scale_ratio, noise_augmentation):
|
||||||
CATEGORY = "conditioning/upscale_diffusion"
|
|
||||||
|
|
||||||
def encode(self, images, positive, negative, scale_ratio, noise_augmentation):
|
|
||||||
width = max(1, round(images.shape[-2] * scale_ratio))
|
width = max(1, round(images.shape[-2] * scale_ratio))
|
||||||
height = max(1, round(images.shape[-3] * scale_ratio))
|
height = max(1, round(images.shape[-3] * scale_ratio))
|
||||||
|
|
||||||
@ -39,8 +47,16 @@ class SD_4XUpscale_Conditioning:
|
|||||||
out_cn.append(n)
|
out_cn.append(n)
|
||||||
|
|
||||||
latent = torch.zeros([images.shape[0], 4, height // 4, width // 4])
|
latent = torch.zeros([images.shape[0], 4, height // 4, width // 4])
|
||||||
return (out_cp, out_cn, {"samples":latent})
|
return io.NodeOutput(out_cp, out_cn, {"samples":latent})
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"SD_4XUpscale_Conditioning": SD_4XUpscale_Conditioning,
|
class SdUpscaleExtension(ComfyExtension):
|
||||||
}
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
SD_4XUpscale_Conditioning,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> SdUpscaleExtension:
|
||||||
|
return SdUpscaleExtension()
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
# TCFG: Tangential Damping Classifier-free Guidance - (arXiv: https://arxiv.org/abs/2503.18137)
|
# TCFG: Tangential Damping Classifier-free Guidance - (arXiv: https://arxiv.org/abs/2503.18137)
|
||||||
|
|
||||||
|
from typing_extensions import override
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
|
|
||||||
def score_tangential_damping(cond_score: torch.Tensor, uncond_score: torch.Tensor) -> torch.Tensor:
|
def score_tangential_damping(cond_score: torch.Tensor, uncond_score: torch.Tensor) -> torch.Tensor:
|
||||||
@ -26,23 +27,24 @@ def score_tangential_damping(cond_score: torch.Tensor, uncond_score: torch.Tenso
|
|||||||
return uncond_score_td.reshape_as(uncond_score).to(uncond_score.dtype)
|
return uncond_score_td.reshape_as(uncond_score).to(uncond_score.dtype)
|
||||||
|
|
||||||
|
|
||||||
class TCFG(ComfyNodeABC):
|
class TCFG(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls) -> InputTypeDict:
|
def define_schema(cls):
|
||||||
return {
|
return io.Schema(
|
||||||
"required": {
|
node_id="TCFG",
|
||||||
"model": (IO.MODEL, {}),
|
display_name="Tangential Damping CFG",
|
||||||
}
|
category="advanced/guidance",
|
||||||
}
|
description="TCFG – Tangential Damping CFG (2503.18137)\n\nRefine the uncond (negative) to align with the cond (positive) for improving quality.",
|
||||||
|
inputs=[
|
||||||
|
io.Model.Input("model"),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Model.Output(display_name="patched_model"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = (IO.MODEL,)
|
@classmethod
|
||||||
RETURN_NAMES = ("patched_model",)
|
def execute(cls, model):
|
||||||
FUNCTION = "patch"
|
|
||||||
|
|
||||||
CATEGORY = "advanced/guidance"
|
|
||||||
DESCRIPTION = "TCFG – Tangential Damping CFG (2503.18137)\n\nRefine the uncond (negative) to align with the cond (positive) for improving quality."
|
|
||||||
|
|
||||||
def patch(self, model):
|
|
||||||
m = model.clone()
|
m = model.clone()
|
||||||
|
|
||||||
def tangential_damping_cfg(args):
|
def tangential_damping_cfg(args):
|
||||||
@ -59,13 +61,16 @@ class TCFG(ComfyNodeABC):
|
|||||||
return [cond_pred, uncond_pred_td] + conds_out[2:]
|
return [cond_pred, uncond_pred_td] + conds_out[2:]
|
||||||
|
|
||||||
m.set_model_sampler_pre_cfg_function(tangential_damping_cfg)
|
m.set_model_sampler_pre_cfg_function(tangential_damping_cfg)
|
||||||
return (m,)
|
return io.NodeOutput(m)
|
||||||
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
class TcfgExtension(ComfyExtension):
|
||||||
"TCFG": TCFG,
|
@override
|
||||||
}
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
TCFG,
|
||||||
|
]
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
||||||
"TCFG": "Tangential Damping CFG",
|
async def comfy_entrypoint() -> TcfgExtension:
|
||||||
}
|
return TcfgExtension()
|
||||||
|
|||||||
1
main.py
1
main.py
@ -153,6 +153,7 @@ if __name__ == "__main__":
|
|||||||
if args.cuda_device is not None:
|
if args.cuda_device is not None:
|
||||||
os.environ['CUDA_VISIBLE_DEVICES'] = str(args.cuda_device)
|
os.environ['CUDA_VISIBLE_DEVICES'] = str(args.cuda_device)
|
||||||
os.environ['HIP_VISIBLE_DEVICES'] = str(args.cuda_device)
|
os.environ['HIP_VISIBLE_DEVICES'] = str(args.cuda_device)
|
||||||
|
os.environ["ASCEND_RT_VISIBLE_DEVICES"] = str(args.cuda_device)
|
||||||
logging.info("Set cuda device to: {}".format(args.cuda_device))
|
logging.info("Set cuda device to: {}".format(args.cuda_device))
|
||||||
|
|
||||||
if args.oneapi_device_selector is not None:
|
if args.oneapi_device_selector is not None:
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
comfyui-frontend-package==1.26.13
|
comfyui-frontend-package==1.26.13
|
||||||
comfyui-workflow-templates==0.1.86
|
comfyui-workflow-templates==0.1.88
|
||||||
comfyui-embedded-docs==0.2.6
|
comfyui-embedded-docs==0.2.6
|
||||||
comfyui_manager==4.0.2
|
comfyui_manager==4.0.2
|
||||||
torch
|
torch
|
||||||
|
|||||||
@ -556,6 +556,8 @@ class PromptServer():
|
|||||||
vram_total, torch_vram_total = comfy.model_management.get_total_memory(device, torch_total_too=True)
|
vram_total, torch_vram_total = comfy.model_management.get_total_memory(device, torch_total_too=True)
|
||||||
vram_free, torch_vram_free = comfy.model_management.get_free_memory(device, torch_free_too=True)
|
vram_free, torch_vram_free = comfy.model_management.get_free_memory(device, torch_free_too=True)
|
||||||
required_frontend_version = FrontendManager.get_required_frontend_version()
|
required_frontend_version = FrontendManager.get_required_frontend_version()
|
||||||
|
installed_templates_version = FrontendManager.get_installed_templates_version()
|
||||||
|
required_templates_version = FrontendManager.get_required_templates_version()
|
||||||
|
|
||||||
system_stats = {
|
system_stats = {
|
||||||
"system": {
|
"system": {
|
||||||
@ -564,6 +566,8 @@ class PromptServer():
|
|||||||
"ram_free": ram_free,
|
"ram_free": ram_free,
|
||||||
"comfyui_version": __version__,
|
"comfyui_version": __version__,
|
||||||
"required_frontend_version": required_frontend_version,
|
"required_frontend_version": required_frontend_version,
|
||||||
|
"installed_templates_version": installed_templates_version,
|
||||||
|
"required_templates_version": required_templates_version,
|
||||||
"python_version": sys.version,
|
"python_version": sys.version,
|
||||||
"pytorch_version": comfy.model_management.torch_version,
|
"pytorch_version": comfy.model_management.torch_version,
|
||||||
"embedded_python": os.path.split(os.path.split(sys.executable)[0])[1] == "python_embeded",
|
"embedded_python": os.path.split(os.path.split(sys.executable)[0])[1] == "python_embeded",
|
||||||
|
|||||||
@ -205,3 +205,74 @@ numpy"""
|
|||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert version is None
|
assert version is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_templates_version():
|
||||||
|
# Arrange
|
||||||
|
expected_version = "0.1.41"
|
||||||
|
mock_requirements_content = """torch
|
||||||
|
torchsde
|
||||||
|
comfyui-frontend-package==1.25.0
|
||||||
|
comfyui-workflow-templates==0.1.41
|
||||||
|
other-package==1.0.0
|
||||||
|
numpy"""
|
||||||
|
|
||||||
|
# Act
|
||||||
|
with patch("builtins.open", mock_open(read_data=mock_requirements_content)):
|
||||||
|
version = FrontendManager.get_required_templates_version()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert version == expected_version
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_templates_version_not_found():
|
||||||
|
# Arrange
|
||||||
|
mock_requirements_content = """torch
|
||||||
|
torchsde
|
||||||
|
comfyui-frontend-package==1.25.0
|
||||||
|
other-package==1.0.0
|
||||||
|
numpy"""
|
||||||
|
|
||||||
|
# Act
|
||||||
|
with patch("builtins.open", mock_open(read_data=mock_requirements_content)):
|
||||||
|
version = FrontendManager.get_required_templates_version()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert version is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_templates_version_invalid_semver():
|
||||||
|
# Arrange
|
||||||
|
mock_requirements_content = """torch
|
||||||
|
torchsde
|
||||||
|
comfyui-workflow-templates==1.0.0.beta
|
||||||
|
other-package==1.0.0
|
||||||
|
numpy"""
|
||||||
|
|
||||||
|
# Act
|
||||||
|
with patch("builtins.open", mock_open(read_data=mock_requirements_content)):
|
||||||
|
version = FrontendManager.get_required_templates_version()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert version is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_installed_templates_version():
|
||||||
|
# Arrange
|
||||||
|
expected_version = "0.1.40"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
with patch("app.frontend_management.version", return_value=expected_version):
|
||||||
|
version = FrontendManager.get_installed_templates_version()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert version == expected_version
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_installed_templates_version_not_installed():
|
||||||
|
# Act
|
||||||
|
with patch("app.frontend_management.version", side_effect=Exception("Package not found")):
|
||||||
|
version = FrontendManager.get_installed_templates_version()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert version is None
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user