Merge upstream

This commit is contained in:
doctorpangloss 2024-07-04 11:58:55 -07:00
commit a13088ccec
76 changed files with 5981 additions and 449 deletions

45
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Bug Report
description: "Something is broken inside of ComfyUI. (Do not use this if you're just having issues and need help, or if the issue relates to a custom node)"
labels: [ "Potential Bug" ]
body:
- type: markdown
attributes:
value: |
Before submitting a **Bug Report**, please ensure the following:
**1:** You are running the latest version of ComfyUI.
**2:** You have looked at the existing bug reports and made sure this isn't already reported.
**3:** This is an actual bug in ComfyUI, not just a support question and not caused by an custom node. A bug is when you can specify exact steps to replicate what went wrong and others will be able to repeat your steps and see the same issue happen.
If unsure, ask on the [ComfyUI Matrix Space](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) or the [Comfy Org Discord](https://discord.gg/comfyorg) first.
- type: textarea
attributes:
label: Expected Behavior
description: "What you expected to happen."
validations:
required: true
- type: textarea
attributes:
label: Actual Behavior
description: "What actually happened. Please include a screenshot of the issue if possible."
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: "Describe how to reproduce the issue. Please be sure to attach a workflow JSON or PNG, ideally one that doesn't require custom nodes to test. If the bug open happens when certain custom nodes are used, most likely that custom node is what has the bug rather than ComfyUI, in which case it should be reported to the node's author."
validations:
required: true
- type: textarea
attributes:
label: Debug Logs
description: "Please copy the output from your terminal logs here."
render: powershell
validations:
required: true
- type: textarea
attributes:
label: Other
description: "Any other additional information you think might be helpful."
validations:
required: false

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: ComfyUI Matrix Space
url: https://app.element.io/#/room/%23comfyui_space%3Amatrix.org
about: The ComfyUI Matrix Space is available for support and general discussion related to ComfyUI (Matrix is like Discord but open source).
- name: Comfy Org Discord
url: https://discord.gg/comfyorg
about: The Comfy Org Discord is available for support and general discussion related to ComfyUI.

View File

@ -0,0 +1,32 @@
name: Feature Request
description: "You have an idea for something new you would like to see added to ComfyUI's core."
labels: [ "Feature" ]
body:
- type: markdown
attributes:
value: |
Before submitting a **Feature Request**, please ensure the following:
**1:** You are running the latest version of ComfyUI.
**2:** You have looked to make sure there is not already a feature that does what you need, and there is not already a Feature Request listed for the same idea.
**3:** This is something that makes sense to add to ComfyUI Core, and wouldn't make more sense as a custom node.
If unsure, ask on the [ComfyUI Matrix Space](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) or the [Comfy Org Discord](https://discord.gg/comfyorg) first.
- type: textarea
attributes:
label: Feature Idea
description: "Describe the feature you want to see."
validations:
required: true
- type: textarea
attributes:
label: Existing Solutions
description: "Please search through available custom nodes / extensions to see if there are existing custom solutions for this. If so, please link the options you found here as a reference."
validations:
required: false
- type: textarea
attributes:
label: Other
description: "Any other additional information you think might be helpful."
validations:
required: false

32
.github/ISSUE_TEMPLATE/user-support.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: User Support
description: "Use this if you need help with something, or you're experiencing an issue."
labels: [ "User Support" ]
body:
- type: markdown
attributes:
value: |
Before submitting a **User Report** issue, please ensure the following:
**1:** You are running the latest version of ComfyUI.
**2:** You have made an effort to find public answers to your question before asking here. In other words, you googled it first, and scrolled through recent help topics.
If unsure, ask on the [ComfyUI Matrix Space](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) or the [Comfy Org Discord](https://discord.gg/comfyorg) first.
- type: textarea
attributes:
label: Your question
description: "Post your question here. Please be as detailed as possible."
validations:
required: true
- type: textarea
attributes:
label: Logs
description: "If your question relates to an issue you're experiencing, please go to `Server` -> `Logs` -> potentially set `View Type` to `Debug` as well, then copypaste all the text into here."
render: powershell
validations:
required: false
- type: textarea
attributes:
label: Other
description: "Any other additional information you think might be helpful."
validations:
required: false

63
.github/workflows/test-browser.yml vendored Normal file
View File

@ -0,0 +1,63 @@
# This is a temporary action during frontend TS migration.
# This file should be removed after TS migration is completed.
# The browser test is here to ensure TS repo is working the same way as the
# current JS code.
# If you are adding UI feature, please sync your changes to the TS repo:
# huchenlei/ComfyUI_frontend and update test expectation files accordingly.
name: Playwright Browser Tests CI
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v4
with:
repository: "comfyanonymous/ComfyUI"
path: "ComfyUI"
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v4
with:
repository: "huchenlei/ComfyUI_frontend"
path: "ComfyUI_frontend"
ref: "fcc54d803e5b6a9b08a462a1d94899318c96dcbb"
- uses: actions/setup-node@v3
with:
node-version: lts/*
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
working-directory: ComfyUI
- name: Start ComfyUI server
run: |
python main.py --cpu &
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
- name: Install ComfyUI_frontend dependencies
run: |
npm ci
working-directory: ComfyUI_frontend
- name: Install Playwright Browsers
run: npx playwright install --with-deps
working-directory: ComfyUI_frontend
- name: Run Playwright tests
run: npx playwright test
working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: ComfyUI_frontend/playwright-report/
retention-days: 30

2
.gitignore vendored
View File

@ -12,6 +12,7 @@
**/put*here
/extra_model_paths.yaml
/.vs
.vscode/
.idea/
venv/
/web/extensions/*
@ -174,3 +175,4 @@ cython_debug/
/tests-ui/data/object_info.json
/user/
*.log

41
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,41 @@
# Contributing to ComfyUI
Welcome, and thank you for your interest in contributing to ComfyUI!
There are several ways in which you can contribute, beyond writing code. The goal of this document is to provide a high-level overview of how you can get involved.
## Asking Questions
Have a question? Instead of opening an issue, please ask on [Discord](https://comfy.org/discord) or [Matrix](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) channels. Our team and the community will help you.
## Providing Feedback
Your comments and feedback are welcome, and the development team is available via a handful of different channels.
See the `#bug-report`, `#feature-request` and `#feedback` channels on Discord.
## Reporting Issues
Have you identified a reproducible problem in ComfyUI? Do you have a feature request? We want to hear about it! Here's how you can report your issue as effectively as possible.
### Look For an Existing Issue
Before you create a new issue, please do a search in [open issues](https://github.com/comfyanonymous/ComfyUI/issues) to see if the issue or feature request has already been filed.
If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment:
* 👍 - upvote
* 👎 - downvote
If you cannot find an existing issue that describes your bug or feature, create a new issue. We have an issue template in place to organize new issues.
### Creating Pull Requests
* Please refer to the article on [creating pull requests](https://github.com/comfyanonymous/ComfyUI/wiki/How-to-Contribute-Code) and contributing to this project.
## Thank You
Your contributions to open source, large or small, make great projects like this possible. Thank you for taking the time to contribute.

View File

@ -21,16 +21,16 @@ A vanilla, up-to-date fork of [ComfyUI](https://github.com/comfyanonymous/comfyu
### Upstream Features
- Nodes/graph/flowchart interface to experiment and create complex Stable Diffusion workflows without needing to code anything.
- Fully supports SD1.x, SD2.x, [SDXL](https://comfyanonymous.github.io/ComfyUI_examples/sdxl/), [Stable Video Diffusion](https://comfyanonymous.github.io/ComfyUI_examples/video/) and [Stable Cascade](https://comfyanonymous.github.io/ComfyUI_examples/stable_cascade/)
- Fully supports SD1.x, SD2.x, [SDXL](https://comfyanonymous.github.io/ComfyUI_examples/sdxl/), [Stable Video Diffusion](https://comfyanonymous.github.io/ComfyUI_examples/video/), [Stable Cascade](https://comfyanonymous.github.io/ComfyUI_examples/stable_cascade/), [SD3](https://comfyanonymous.github.io/ComfyUI_examples/sd3/) and [Stable Audio](https://comfyanonymous.github.io/ComfyUI_examples/audio/)
- Asynchronous Queue system
- Many optimizations: Only re-executes the parts of the workflow that changes between executions.
- Command line option: ```--lowvram``` to make it work on GPUs with less than 3GB vram (enabled automatically on GPUs with low vram)
- Smart memory management: can automatically run models on GPUs with as low as 1GB vram.
- Works even if you don't have a GPU with: ```--cpu``` (slow)
- Can load ckpt, safetensors and diffusers models/checkpoints. Standalone VAEs and CLIP models.
- Embeddings/Textual inversion
- [Loras (regular, locon and loha)](https://comfyanonymous.github.io/ComfyUI_examples/lora/)
- [Hypernetworks](https://comfyanonymous.github.io/ComfyUI_examples/hypernetworks/)
- Loading full workflows (with seeds) from generated PNG files.
- Loading full workflows (with seeds) from generated PNG, WebP and FLAC files.
- Saving/Loading workflows as Json files.
- Nodes interface can be used to create complex workflows like one for [Hires fix](https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/) or much more advanced ones.
- [Area Composition](https://comfyanonymous.github.io/ComfyUI_examples/area_composition/)

View File

@ -2,7 +2,8 @@ import json
import os
import re
import uuid
import glob
import shutil
from aiohttp import web
from ..cli_args import args
from ..cmd.folder_paths import user_directory
@ -53,16 +54,16 @@ class UserManager():
if os.path.commonpath((root_dir, user_root)) != root_dir:
raise PermissionError()
parent = user_root
if file is not None:
# prevent leaving /{type}/{user}
path = os.path.abspath(os.path.join(user_root, file))
if os.path.commonpath((user_root, path)) != user_root:
raise PermissionError()
parent = os.path.split(path)[0]
if create_dir and not os.path.exists(parent):
os.mkdir(parent)
os.makedirs(parent, exist_ok=True)
return path
@ -104,24 +105,32 @@ class UserManager():
user_id = self.add_user(username)
return web.json_response(user_id)
@routes.get("/userdata/{file}")
async def getuserdata(request):
file = request.match_info.get("file", None)
if not file:
@routes.get("/userdata")
async def listuserdata(request):
directory = request.rel_url.query.get('dir', '')
if not directory:
return web.Response(status=400)
path = self.get_request_user_filepath(request, file)
path = self.get_request_user_filepath(request, directory)
if not path:
return web.Response(status=403)
if not os.path.exists(path):
return web.Response(status=404)
return web.FileResponse(path)
recurse = request.rel_url.query.get('recurse', '').lower() == "true"
results = glob.glob(os.path.join(
glob.escape(path), '**/*'), recursive=recurse)
results = [os.path.relpath(x, path) for x in results if os.path.isfile(x)]
@routes.post("/userdata/{file}")
async def post_userdata(request):
file = request.match_info.get("file", None)
split_path = request.rel_url.query.get('split', '').lower() == "true"
if split_path:
results = [[x] + x.split(os.sep) for x in results]
return web.json_response(results)
def get_user_data_path(request, check_exists = False, param = "file"):
file = request.match_info.get(param, None)
if not file:
return web.Response(status=400)
@ -129,8 +138,62 @@ class UserManager():
if not path:
return web.Response(status=403)
if check_exists and not os.path.exists(path):
return web.Response(status=404)
return path
@routes.get("/userdata/{file}")
async def getuserdata(request):
path = get_user_data_path(request, check_exists=True)
if not isinstance(path, str):
return path
return web.FileResponse(path)
@routes.post("/userdata/{file}")
async def post_userdata(request):
path = get_user_data_path(request)
if not isinstance(path, str):
return path
overwrite = request.query["overwrite"] != "false"
if not overwrite and os.path.exists(path):
return web.Response(status=409)
body = await request.read()
with open(path, "wb") as f:
f.write(body)
resp = os.path.relpath(path, self.get_request_user_filepath(request, None))
return web.json_response(resp)
return web.Response(status=200)
@routes.delete("/userdata/{file}")
async def delete_userdata(request):
path = get_user_data_path(request, check_exists=True)
if not isinstance(path, str):
return path
os.remove(path)
return web.Response(status=204)
@routes.post("/userdata/{file}/move/{dest}")
async def move_userdata(request):
source = get_user_data_path(request, check_exists=True)
if not isinstance(source, str):
return source
dest = get_user_data_path(request, check_exists=False, param="dest")
if not isinstance(source, str):
return dest
overwrite = request.query["overwrite"] != "false"
if not overwrite and os.path.exists(dest):
return web.Response(status=409)
print(f"moving '{source}' -> '{dest}'")
shutil.move(source, dest)
resp = os.path.relpath(dest, self.get_request_user_filepath(request, None))
return web.json_response(resp)

View File

@ -289,7 +289,8 @@ class ControlNet(nn.Module):
guided_hint = self.input_hint_block(hint, emb, context)
outs = []
out_output = []
out_middle = []
hs = []
if self.num_classes is not None:
@ -304,10 +305,10 @@ class ControlNet(nn.Module):
guided_hint = None
else:
h = module(h, emb, context)
outs.append(zero_conv(h, emb, context))
out_output.append(zero_conv(h, emb, context))
h = self.middle_block(h, emb, context)
outs.append(self.middle_block_out(h, emb, context))
out_middle.append(self.middle_block_out(h, emb, context))
return outs
return {"middle": out_middle, "output": out_output}

96
comfy/cldm/mmdit.py Normal file
View File

@ -0,0 +1,96 @@
import torch
from typing import Dict, Optional, List
from einops import einops
from torch import Tensor
from ..ldm.modules.diffusionmodules.mmdit import MMDiT, PatchEmbed
def default(x, y):
if x is not None:
return x
return y
class ControlNet(MMDiT):
def __init__(
self,
num_blocks = None,
dtype = None,
device = None,
operations = None,
**kwargs,
):
super().__init__(dtype=dtype, device=device, operations=operations, final_layer=False, num_blocks=num_blocks, **kwargs)
# controlnet_blocks
self.controlnet_blocks = torch.nn.ModuleList([])
for _ in range(len(self.joint_blocks)):
self.controlnet_blocks.append(operations.Linear(self.hidden_size, self.hidden_size, device=device, dtype=dtype))
self.pos_embed_input = PatchEmbed(
None,
self.patch_size,
self.in_channels,
self.hidden_size,
bias=True,
strict_img_size=False,
dtype=dtype,
device=device,
operations=operations
)
def forward(
self,
x: torch.Tensor,
timesteps: torch.Tensor,
y: Optional[torch.Tensor] = None,
context: Optional[torch.Tensor] = None,
hint = None,
) -> Dict[str, List[Tensor]]:
#weird sd3 controlnet specific stuff
y = torch.zeros_like(y)
if self.context_processor is not None:
context = self.context_processor(context)
hw = x.shape[-2:]
x = self.x_embedder(x) + self.cropped_pos_embed(hw, device=x.device).to(dtype=x.dtype, device=x.device)
x += self.pos_embed_input(hint)
c = self.t_embedder(timesteps, dtype=x.dtype)
if y is not None and self.y_embedder is not None:
y = self.y_embedder(y)
c = c + y
if context is not None:
context = self.context_embedder(context)
if self.register_length > 0:
context = torch.cat(
(
einops.repeat(self.register, "1 ... -> b ...", b=x.shape[0]),
default(context, torch.Tensor([]).type_as(x)),
),
1,
)
output = []
blocks = len(self.joint_blocks)
for i in range(blocks):
context, x = self.joint_blocks[i](
context,
x,
c=c,
use_checkpoint=self.use_checkpoint,
)
out = self.controlnet_blocks[i](x)
count = self.depth // blocks
if i == blocks - 1:
count -= 1
for j in range(count):
output.append(out)
return {"output": output}

View File

@ -149,6 +149,7 @@ def create_parser() -> argparse.ArgumentParser:
help="Windows standalone build: Enable convenient things that most people using the standalone windows build will probably enjoy (like auto opening the page on startup).")
parser.add_argument("--disable-metadata", action="store_true", help="Disable saving prompt metadata in files.")
parser.add_argument("--disable-all-custom-nodes", action="store_true", help="Disable loading all custom nodes.")
parser.add_argument("--multi-user", action="store_true", help="Enables per-user storage.")
parser.add_argument("--create-directories", action="store_true",

View File

@ -72,6 +72,7 @@ class Configuration(dict):
quick_test_for_ci (bool): Enable quick testing mode for CI.
windows_standalone_build (bool): Enable features for standalone Windows build.
disable_metadata (bool): Disable saving metadata with outputs.
disable_all_custom_nodes (bool): Disable loading all custom nodes.
multi_user (bool): Enable multi-user mode.
plausible_analytics_base_url (Optional[str]): Base URL for server-side analytics.
plausible_analytics_domain (Optional[str]): Domain for analytics events.
@ -144,6 +145,7 @@ class Configuration(dict):
self.quick_test_for_ci: bool = False
self.windows_standalone_build: bool = False
self.disable_metadata: bool = False
self.disable_all_custom_nodes: bool = False
self.multi_user: bool = False
self.plausible_analytics_base_url: Optional[str] = None
self.plausible_analytics_domain: Optional[str] = None

View File

@ -9,6 +9,7 @@ import os
import struct
import traceback
import uuid
import hashlib
from asyncio import Future, AbstractEventLoop
from enum import Enum
from io import BytesIO
@ -145,7 +146,6 @@ class PromptServer(ExecutorToClientProgress):
# On reconnect if we are the currently executing client send the current node
if self.client_id == sid and self.last_node_id is not None:
await self.send("executing", {"node": self.last_node_id}, sid)
async for msg in ws:
if msg.type == aiohttp.WSMsgType.ERROR:
logging.warning('ws connection closed with exception %s' % ws.exception())
@ -189,9 +189,23 @@ class PromptServer(ExecutorToClientProgress):
return type_dir, dir_type
def compare_image_hash(filepath, image):
# function to compare hashes of two images to see if it already exists, fix to #3465
if os.path.exists(filepath):
a = hashlib.sha256()
b = hashlib.sha256()
with open(filepath, "rb") as f:
a.update(f.read())
b.update(image.file.read())
image.file.seek(0)
f.close()
return a.hexdigest() == b.hexdigest()
return False
async def image_upload(post, image_save_function=None):
image = post.get("image")
overwrite = post.get("overwrite")
image_is_duplicate = False
image_upload_type = post.get("type")
upload_dir, image_upload_type = get_dir_by_type(image_upload_type)
@ -218,15 +232,19 @@ class PromptServer(ExecutorToClientProgress):
else:
i = 1
while os.path.exists(filepath):
if compare_image_hash(filepath, image): #compare hash to prevent saving of duplicates with same name, fix for #3465
image_is_duplicate = True
break
filename = f"{split[0]} ({i}){split[1]}"
filepath = os.path.join(full_output_folder, filename)
i += 1
if image_save_function is not None:
image_save_function(image, post, filepath)
else:
async with aiofiles.open(filepath, mode='wb') as file:
await file.write(image.file.read())
if not image_is_duplicate:
if image_save_function is not None:
image_save_function(image, post, filepath)
else:
async with aiofiles.open(filepath, mode='wb') as file:
await file.write(image.file.read())
return web.json_response({"name": filename, "subfolder": subfolder, "type": image_upload_type})
else:
@ -702,9 +720,21 @@ class PromptServer(ExecutorToClientProgress):
@external_address.setter
def external_address(self, value):
self._external_address = value
def add_routes(self):
self.user_manager.add_routes(self.routes)
# Prefix every route with /api for easier matching for delegation.
# This is very useful for frontend dev server, which need to forward
# everything except serving of static files.
# Currently both the old endpoints without prefix and new endpoints with
# prefix are supported.
api_routes = web.RouteTableDef()
for route in self.routes:
# Custom nodes might add extra static routes. Only process non-static
# routes to add /api prefix.
if isinstance(route, web.RouteDef):
api_routes.route(route.method, "/api" + route.path)(route.handler, **route.kwargs)
self.app.add_routes(api_routes)
self.app.add_routes(self.routes)
for name, dir in self.nodes.EXTENSION_WEB_DIRS.items():

View File

@ -8,8 +8,9 @@ from . import model_management
from . import model_detection
from . import model_patcher
from . import ops
from . import latent_formats
from .cldm import cldm
from .cldm import cldm, mmdit
from .t2i_adapter import adapter
from .ldm.cascade import controlnet
@ -38,6 +39,8 @@ class ControlBase:
self.cond_hint = None
self.strength = 1.0
self.timestep_percent_range = (0.0, 1.0)
self.latent_format = None
self.vae = None
self.global_average_pooling = False
self.timestep_range = None
self.compression_ratio = 8
@ -48,10 +51,12 @@ class ControlBase:
self.device = device
self.previous_controlnet = None
def set_cond_hint(self, cond_hint, strength=1.0, timestep_percent_range=(0.0, 1.0)):
def set_cond_hint(self, cond_hint, strength=1.0, timestep_percent_range=(0.0, 1.0), vae=None):
self.cond_hint_original = cond_hint
self.strength = strength
self.timestep_percent_range = timestep_percent_range
if self.latent_format is not None:
self.vae = vae
return self
def pre_run(self, model, percent_to_timestep_function):
@ -84,43 +89,35 @@ class ControlBase:
c.global_average_pooling = self.global_average_pooling
c.compression_ratio = self.compression_ratio
c.upscale_algorithm = self.upscale_algorithm
c.latent_format = self.latent_format
c.vae = self.vae
def inference_memory_requirements(self, dtype):
if self.previous_controlnet is not None:
return self.previous_controlnet.inference_memory_requirements(dtype)
return 0
def control_merge(self, control_input, control_output, control_prev, output_dtype):
def control_merge(self, control, control_prev, output_dtype):
out = {'input':[], 'middle':[], 'output': []}
if control_input is not None:
for i in range(len(control_input)):
key = 'input'
x = control_input[i]
if x is not None:
x *= self.strength
if x.dtype != output_dtype:
x = x.to(output_dtype)
out[key].insert(0, x)
if control_output is not None:
for key in control:
control_output = control[key]
applied_to = set()
for i in range(len(control_output)):
if i == (len(control_output) - 1):
key = 'middle'
index = 0
else:
key = 'output'
index = i
x = control_output[i]
if x is not None:
if self.global_average_pooling:
x = torch.mean(x, dim=(2, 3), keepdim=True).repeat(1, 1, x.shape[2], x.shape[3])
x *= self.strength
if x not in applied_to: #memory saving strategy, allow shared tensors and only apply strength to shared tensors once
applied_to.add(x)
x *= self.strength
if x.dtype != output_dtype:
x = x.to(output_dtype)
out[key].append(x)
if control_prev is not None:
for x in ['input', 'middle', 'output']:
o = out[x]
@ -135,19 +132,21 @@ class ControlBase:
if o[i].shape[0] < prev_val.shape[0]:
o[i] = prev_val + o[i]
else:
o[i] += prev_val
o[i] = prev_val + o[i] #TODO: change back to inplace add if shared tensors stop being an issue
return out
class ControlNet(ControlBase):
def __init__(self, control_model=None, global_average_pooling=False, device=None, load_device=None, manual_cast_dtype=None):
def __init__(self, control_model=None, global_average_pooling=False, compression_ratio=8, latent_format=None, device=None, load_device=None, manual_cast_dtype=None):
super().__init__(device)
self.control_model = control_model
self.load_device = load_device
if control_model is not None:
self.control_model_wrapped = model_patcher.ModelPatcher(self.control_model, load_device=load_device, offload_device=model_management.unet_offload_device())
self.compression_ratio = compression_ratio
self.global_average_pooling = global_average_pooling
self.model_sampling_current = None
self.manual_cast_dtype = manual_cast_dtype
self.latent_format = latent_format
def get_control(self, x_noisy, t, cond, batched_number):
control_prev = None
@ -170,7 +169,17 @@ class ControlNet(ControlBase):
if self.cond_hint is not None:
del self.cond_hint
self.cond_hint = None
self.cond_hint = utils.common_upscale(self.cond_hint_original, x_noisy.shape[3] * self.compression_ratio, x_noisy.shape[2] * self.compression_ratio, self.upscale_algorithm, "center").to(dtype).to(self.device)
compression_ratio = self.compression_ratio
if self.vae is not None:
compression_ratio *= self.vae.downscale_ratio
self.cond_hint = utils.common_upscale(self.cond_hint_original, x_noisy.shape[3] * compression_ratio, x_noisy.shape[2] * compression_ratio, self.upscale_algorithm, "center")
if self.vae is not None:
loaded_models = model_management.loaded_models(only_currently_used=True)
self.cond_hint = self.vae.encode(self.cond_hint.movedim(1, -1))
model_management.load_models_gpu(loaded_models)
if self.latent_format is not None:
self.cond_hint = self.latent_format.process_in(self.cond_hint)
self.cond_hint = self.cond_hint.to(device=self.device, dtype=dtype)
if x_noisy.shape[0] != self.cond_hint.shape[0]:
self.cond_hint = broadcast_image_to(self.cond_hint, x_noisy.shape[0], batched_number)
@ -182,7 +191,7 @@ class ControlNet(ControlBase):
x_noisy = self.model_sampling_current.calculate_input(t, x_noisy)
control = self.control_model(x=x_noisy.to(dtype), hint=self.cond_hint, timesteps=timestep.float(), context=context.to(dtype), y=y)
return self.control_merge(None, control, control_prev, output_dtype)
return self.control_merge(control, control_prev, output_dtype)
def copy(self):
c = ControlNet(None, global_average_pooling=self.global_average_pooling, load_device=self.load_device, manual_cast_dtype=self.manual_cast_dtype)
@ -322,6 +331,39 @@ class ControlLora(ControlNet):
def inference_memory_requirements(self, dtype):
return utils.calculate_parameters(self.control_weights) * model_management.dtype_size(dtype) + ControlBase.inference_memory_requirements(self, dtype)
def load_controlnet_mmdit(sd):
new_sd = model_detection.convert_diffusers_mmdit(sd, "")
model_config = model_detection.model_config_from_unet(new_sd, "", True)
num_blocks = model_detection.count_blocks(new_sd, 'joint_blocks.{}.')
for k in sd:
new_sd[k] = sd[k]
supported_inference_dtypes = model_config.supported_inference_dtypes
controlnet_config = model_config.unet_config
unet_dtype = model_management.unet_dtype(supported_dtypes=supported_inference_dtypes)
load_device = model_management.get_torch_device()
manual_cast_dtype = model_management.unet_manual_cast(unet_dtype, load_device)
if manual_cast_dtype is not None:
operations = ops.manual_cast
else:
operations = ops.disable_weight_init
control_model = mmdit.ControlNet(num_blocks=num_blocks, operations=operations, device=load_device, dtype=unet_dtype, **controlnet_config)
missing, unexpected = control_model.load_state_dict(new_sd, strict=False)
if len(missing) > 0:
logging.warning("missing controlnet keys: {}".format(missing))
if len(unexpected) > 0:
logging.debug("unexpected controlnet keys: {}".format(unexpected))
latent_format = latent_formats.SD3()
latent_format.shift_factor = 0 #SD3 controlnet weirdness
control = ControlNet(control_model, compression_ratio=1, latent_format=latent_format, load_device=load_device, manual_cast_dtype=manual_cast_dtype)
return control
def load_controlnet(ckpt_path, model=None):
controlnet_data = utils.load_torch_file(ckpt_path, safe_load=True)
if "lora_controlnet" in controlnet_data:
@ -374,6 +416,8 @@ def load_controlnet(ckpt_path, model=None):
if len(leftover_keys) > 0:
logging.warning("leftover keys: {}".format(leftover_keys))
controlnet_data = new_sd
elif "controlnet_blocks.0.weight" in controlnet_data: #SD3 diffusers format
return load_controlnet_mmdit(controlnet_data)
pth_key = 'control_model.zero_convs.0.0.weight'
pth = False
@ -490,12 +534,11 @@ class T2IAdapter(ControlBase):
self.control_input = self.t2i_model(self.cond_hint.to(x_noisy.dtype))
self.t2i_model.cpu()
control_input = list(map(lambda a: None if a is None else a.clone(), self.control_input))
mid = None
if self.t2i_model.xl == True:
mid = control_input[-1:]
control_input = control_input[:-1]
return self.control_merge(control_input, mid, control_prev, x_noisy.dtype)
control_input = {}
for k in self.control_input:
control_input[k] = list(map(lambda a: None if a is None else a.clone(), self.control_input[k]))
return self.control_merge(control_input, control_prev, x_noisy.dtype)
def copy(self):
c = T2IAdapter(self.t2i_model, self.channels_in, self.compression_ratio, self.upscale_algorithm)

121
comfy/k_diffusion/deis.py Normal file
View File

@ -0,0 +1,121 @@
#Taken from: https://github.com/zju-pi/diff-sampler/blob/main/gits-main/solver_utils.py
#under Apache 2 license
import torch
import numpy as np
# A pytorch reimplementation of DEIS (https://github.com/qsh-zh/deis).
#############################
### Utils for DEIS solver ###
#############################
#----------------------------------------------------------------------------
# Transfer from the input time (sigma) used in EDM to that (t) used in DEIS.
def edm2t(edm_steps, epsilon_s=1e-3, sigma_min=0.002, sigma_max=80):
vp_sigma = lambda beta_d, beta_min: lambda t: (np.e ** (0.5 * beta_d * (t ** 2) + beta_min * t) - 1) ** 0.5
vp_sigma_inv = lambda beta_d, beta_min: lambda sigma: ((beta_min ** 2 + 2 * beta_d * (sigma ** 2 + 1).log()).sqrt() - beta_min) / beta_d
vp_beta_d = 2 * (np.log(torch.tensor(sigma_min).cpu() ** 2 + 1) / epsilon_s - np.log(torch.tensor(sigma_max).cpu() ** 2 + 1)) / (epsilon_s - 1)
vp_beta_min = np.log(torch.tensor(sigma_max).cpu() ** 2 + 1) - 0.5 * vp_beta_d
t_steps = vp_sigma_inv(vp_beta_d.clone().detach().cpu(), vp_beta_min.clone().detach().cpu())(edm_steps.clone().detach().cpu())
return t_steps, vp_beta_min, vp_beta_d + vp_beta_min
#----------------------------------------------------------------------------
def cal_poly(prev_t, j, taus):
poly = 1
for k in range(prev_t.shape[0]):
if k == j:
continue
poly *= (taus - prev_t[k]) / (prev_t[j] - prev_t[k])
return poly
#----------------------------------------------------------------------------
# Transfer from t to alpha_t.
def t2alpha_fn(beta_0, beta_1, t):
return torch.exp(-0.5 * t ** 2 * (beta_1 - beta_0) - t * beta_0)
#----------------------------------------------------------------------------
def cal_intergrand(beta_0, beta_1, taus):
with torch.inference_mode(mode=False):
taus = taus.clone()
beta_0 = beta_0.clone()
beta_1 = beta_1.clone()
with torch.enable_grad():
taus.requires_grad_(True)
alpha = t2alpha_fn(beta_0, beta_1, taus)
log_alpha = alpha.log()
log_alpha.sum().backward()
d_log_alpha_dtau = taus.grad
integrand = -0.5 * d_log_alpha_dtau / torch.sqrt(alpha * (1 - alpha))
return integrand
#----------------------------------------------------------------------------
def get_deis_coeff_list(t_steps, max_order, N=10000, deis_mode='tab'):
"""
Get the coefficient list for DEIS sampling.
Args:
t_steps: A pytorch tensor. The time steps for sampling.
max_order: A `int`. Maximum order of the solver. 1 <= max_order <= 4
N: A `int`. Use how many points to perform the numerical integration when deis_mode=='tab'.
deis_mode: A `str`. Select between 'tab' and 'rhoab'. Type of DEIS.
Returns:
A pytorch tensor. A batch of generated samples or sampling trajectories if return_inters=True.
"""
if deis_mode == 'tab':
t_steps, beta_0, beta_1 = edm2t(t_steps)
C = []
for i, (t_cur, t_next) in enumerate(zip(t_steps[:-1], t_steps[1:])):
order = min(i+1, max_order)
if order == 1:
C.append([])
else:
taus = torch.linspace(t_cur, t_next, N) # split the interval for integral appximation
dtau = (t_next - t_cur) / N
prev_t = t_steps[[i - k for k in range(order)]]
coeff_temp = []
integrand = cal_intergrand(beta_0, beta_1, taus)
for j in range(order):
poly = cal_poly(prev_t, j, taus)
coeff_temp.append(torch.sum(integrand * poly) * dtau)
C.append(coeff_temp)
elif deis_mode == 'rhoab':
# Analytical solution, second order
def get_def_intergral_2(a, b, start, end, c):
coeff = (end**3 - start**3) / 3 - (end**2 - start**2) * (a + b) / 2 + (end - start) * a * b
return coeff / ((c - a) * (c - b))
# Analytical solution, third order
def get_def_intergral_3(a, b, c, start, end, d):
coeff = (end**4 - start**4) / 4 - (end**3 - start**3) * (a + b + c) / 3 \
+ (end**2 - start**2) * (a*b + a*c + b*c) / 2 - (end - start) * a * b * c
return coeff / ((d - a) * (d - b) * (d - c))
C = []
for i, (t_cur, t_next) in enumerate(zip(t_steps[:-1], t_steps[1:])):
order = min(i, max_order)
if order == 0:
C.append([])
else:
prev_t = t_steps[[i - k for k in range(order+1)]]
if order == 1:
coeff_cur = ((t_next - prev_t[1])**2 - (t_cur - prev_t[1])**2) / (2 * (t_cur - prev_t[1]))
coeff_prev1 = (t_next - t_cur)**2 / (2 * (prev_t[1] - t_cur))
coeff_temp = [coeff_cur, coeff_prev1]
elif order == 2:
coeff_cur = get_def_intergral_2(prev_t[1], prev_t[2], t_cur, t_next, t_cur)
coeff_prev1 = get_def_intergral_2(t_cur, prev_t[2], t_cur, t_next, prev_t[1])
coeff_prev2 = get_def_intergral_2(t_cur, prev_t[1], t_cur, t_next, prev_t[2])
coeff_temp = [coeff_cur, coeff_prev1, coeff_prev2]
elif order == 3:
coeff_cur = get_def_intergral_3(prev_t[1], prev_t[2], prev_t[3], t_cur, t_next, t_cur)
coeff_prev1 = get_def_intergral_3(t_cur, prev_t[2], prev_t[3], t_cur, t_next, prev_t[1])
coeff_prev2 = get_def_intergral_3(t_cur, prev_t[1], prev_t[3], t_cur, t_next, prev_t[2])
coeff_prev3 = get_def_intergral_3(t_cur, prev_t[1], prev_t[2], t_cur, t_next, prev_t[3])
coeff_temp = [coeff_cur, coeff_prev1, coeff_prev2, coeff_prev3]
C.append(coeff_temp)
return C

View File

@ -7,7 +7,8 @@ import torchsde
from tqdm.auto import trange, tqdm
from . import utils
from . import deis
import comfy.model_patcher
def append_zero(x):
return torch.cat([x, x.new_zeros([1])])
@ -866,3 +867,209 @@ def sample_heunpp2(model, x, sigmas, extra_args=None, callback=None, disable=Non
d_prime = w1 * d + w2 * d_2 + w3 * d_3
x = x + d_prime * dt
return x
#From https://github.com/zju-pi/diff-sampler/blob/main/diff-solvers-main/solvers.py
#under Apache 2 license
def sample_ipndm(model, x, sigmas, extra_args=None, callback=None, disable=None, max_order=4):
extra_args = {} if extra_args is None else extra_args
s_in = x.new_ones([x.shape[0]])
x_next = x
buffer_model = []
for i in trange(len(sigmas) - 1, disable=disable):
t_cur = sigmas[i]
t_next = sigmas[i + 1]
x_cur = x_next
denoised = model(x_cur, t_cur * s_in, **extra_args)
if callback is not None:
callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised})
d_cur = (x_cur - denoised) / t_cur
order = min(max_order, i+1)
if order == 1: # First Euler step.
x_next = x_cur + (t_next - t_cur) * d_cur
elif order == 2: # Use one history point.
x_next = x_cur + (t_next - t_cur) * (3 * d_cur - buffer_model[-1]) / 2
elif order == 3: # Use two history points.
x_next = x_cur + (t_next - t_cur) * (23 * d_cur - 16 * buffer_model[-1] + 5 * buffer_model[-2]) / 12
elif order == 4: # Use three history points.
x_next = x_cur + (t_next - t_cur) * (55 * d_cur - 59 * buffer_model[-1] + 37 * buffer_model[-2] - 9 * buffer_model[-3]) / 24
if len(buffer_model) == max_order - 1:
for k in range(max_order - 2):
buffer_model[k] = buffer_model[k+1]
buffer_model[-1] = d_cur
else:
buffer_model.append(d_cur)
return x_next
#From https://github.com/zju-pi/diff-sampler/blob/main/diff-solvers-main/solvers.py
#under Apache 2 license
def sample_ipndm_v(model, x, sigmas, extra_args=None, callback=None, disable=None, max_order=4):
extra_args = {} if extra_args is None else extra_args
s_in = x.new_ones([x.shape[0]])
x_next = x
t_steps = sigmas
buffer_model = []
for i in trange(len(sigmas) - 1, disable=disable):
t_cur = sigmas[i]
t_next = sigmas[i + 1]
x_cur = x_next
denoised = model(x_cur, t_cur * s_in, **extra_args)
if callback is not None:
callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised})
d_cur = (x_cur - denoised) / t_cur
order = min(max_order, i+1)
if order == 1: # First Euler step.
x_next = x_cur + (t_next - t_cur) * d_cur
elif order == 2: # Use one history point.
h_n = (t_next - t_cur)
h_n_1 = (t_cur - t_steps[i-1])
coeff1 = (2 + (h_n / h_n_1)) / 2
coeff2 = -(h_n / h_n_1) / 2
x_next = x_cur + (t_next - t_cur) * (coeff1 * d_cur + coeff2 * buffer_model[-1])
elif order == 3: # Use two history points.
h_n = (t_next - t_cur)
h_n_1 = (t_cur - t_steps[i-1])
h_n_2 = (t_steps[i-1] - t_steps[i-2])
temp = (1 - h_n / (3 * (h_n + h_n_1)) * (h_n * (h_n + h_n_1)) / (h_n_1 * (h_n_1 + h_n_2))) / 2
coeff1 = (2 + (h_n / h_n_1)) / 2 + temp
coeff2 = -(h_n / h_n_1) / 2 - (1 + h_n_1 / h_n_2) * temp
coeff3 = temp * h_n_1 / h_n_2
x_next = x_cur + (t_next - t_cur) * (coeff1 * d_cur + coeff2 * buffer_model[-1] + coeff3 * buffer_model[-2])
elif order == 4: # Use three history points.
h_n = (t_next - t_cur)
h_n_1 = (t_cur - t_steps[i-1])
h_n_2 = (t_steps[i-1] - t_steps[i-2])
h_n_3 = (t_steps[i-2] - t_steps[i-3])
temp1 = (1 - h_n / (3 * (h_n + h_n_1)) * (h_n * (h_n + h_n_1)) / (h_n_1 * (h_n_1 + h_n_2))) / 2
temp2 = ((1 - h_n / (3 * (h_n + h_n_1))) / 2 + (1 - h_n / (2 * (h_n + h_n_1))) * h_n / (6 * (h_n + h_n_1 + h_n_2))) \
* (h_n * (h_n + h_n_1) * (h_n + h_n_1 + h_n_2)) / (h_n_1 * (h_n_1 + h_n_2) * (h_n_1 + h_n_2 + h_n_3))
coeff1 = (2 + (h_n / h_n_1)) / 2 + temp1 + temp2
coeff2 = -(h_n / h_n_1) / 2 - (1 + h_n_1 / h_n_2) * temp1 - (1 + (h_n_1 / h_n_2) + (h_n_1 * (h_n_1 + h_n_2) / (h_n_2 * (h_n_2 + h_n_3)))) * temp2
coeff3 = temp1 * h_n_1 / h_n_2 + ((h_n_1 / h_n_2) + (h_n_1 * (h_n_1 + h_n_2) / (h_n_2 * (h_n_2 + h_n_3))) * (1 + h_n_2 / h_n_3)) * temp2
coeff4 = -temp2 * (h_n_1 * (h_n_1 + h_n_2) / (h_n_2 * (h_n_2 + h_n_3))) * h_n_1 / h_n_2
x_next = x_cur + (t_next - t_cur) * (coeff1 * d_cur + coeff2 * buffer_model[-1] + coeff3 * buffer_model[-2] + coeff4 * buffer_model[-3])
if len(buffer_model) == max_order - 1:
for k in range(max_order - 2):
buffer_model[k] = buffer_model[k+1]
buffer_model[-1] = d_cur.detach()
else:
buffer_model.append(d_cur.detach())
return x_next
#From https://github.com/zju-pi/diff-sampler/blob/main/diff-solvers-main/solvers.py
#under Apache 2 license
@torch.no_grad()
def sample_deis(model, x, sigmas, extra_args=None, callback=None, disable=None, max_order=3, deis_mode='tab'):
extra_args = {} if extra_args is None else extra_args
s_in = x.new_ones([x.shape[0]])
x_next = x
t_steps = sigmas
coeff_list = deis.get_deis_coeff_list(t_steps, max_order, deis_mode=deis_mode)
buffer_model = []
for i in trange(len(sigmas) - 1, disable=disable):
t_cur = sigmas[i]
t_next = sigmas[i + 1]
x_cur = x_next
denoised = model(x_cur, t_cur * s_in, **extra_args)
if callback is not None:
callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised})
d_cur = (x_cur - denoised) / t_cur
order = min(max_order, i+1)
if t_next <= 0:
order = 1
if order == 1: # First Euler step.
x_next = x_cur + (t_next - t_cur) * d_cur
elif order == 2: # Use one history point.
coeff_cur, coeff_prev1 = coeff_list[i]
x_next = x_cur + coeff_cur * d_cur + coeff_prev1 * buffer_model[-1]
elif order == 3: # Use two history points.
coeff_cur, coeff_prev1, coeff_prev2 = coeff_list[i]
x_next = x_cur + coeff_cur * d_cur + coeff_prev1 * buffer_model[-1] + coeff_prev2 * buffer_model[-2]
elif order == 4: # Use three history points.
coeff_cur, coeff_prev1, coeff_prev2, coeff_prev3 = coeff_list[i]
x_next = x_cur + coeff_cur * d_cur + coeff_prev1 * buffer_model[-1] + coeff_prev2 * buffer_model[-2] + coeff_prev3 * buffer_model[-3]
if len(buffer_model) == max_order - 1:
for k in range(max_order - 2):
buffer_model[k] = buffer_model[k+1]
buffer_model[-1] = d_cur.detach()
else:
buffer_model.append(d_cur.detach())
return x_next
@torch.no_grad()
def sample_euler_cfg_pp(model, x, sigmas, extra_args=None, callback=None, disable=None):
extra_args = {} if extra_args is None else extra_args
temp = [0]
def post_cfg_function(args):
temp[0] = args["uncond_denoised"]
return args["denoised"]
model_options = extra_args.get("model_options", {}).copy()
extra_args["model_options"] = comfy.model_patcher.set_model_options_post_cfg_function(model_options, post_cfg_function, disable_cfg1_optimization=True)
s_in = x.new_ones([x.shape[0]])
for i in trange(len(sigmas) - 1, disable=disable):
sigma_hat = sigmas[i]
denoised = model(x, sigma_hat * s_in, **extra_args)
d = to_d(x, sigma_hat, temp[0])
if callback is not None:
callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigma_hat, 'denoised': denoised})
dt = sigmas[i + 1] - sigma_hat
# Euler method
x = denoised + d * sigmas[i + 1]
return x
@torch.no_grad()
def sample_euler_ancestral_cfg_pp(model, x, sigmas, extra_args=None, callback=None, disable=None, eta=1., s_noise=1., noise_sampler=None):
"""Ancestral sampling with Euler method steps."""
extra_args = {} if extra_args is None else extra_args
noise_sampler = default_noise_sampler(x) if noise_sampler is None else noise_sampler
temp = [0]
def post_cfg_function(args):
temp[0] = args["uncond_denoised"]
return args["denoised"]
model_options = extra_args.get("model_options", {}).copy()
extra_args["model_options"] = comfy.model_patcher.set_model_options_post_cfg_function(model_options, post_cfg_function, disable_cfg1_optimization=True)
s_in = x.new_ones([x.shape[0]])
for i in trange(len(sigmas) - 1, disable=disable):
denoised = model(x, sigmas[i] * s_in, **extra_args)
sigma_down, sigma_up = get_ancestral_step(sigmas[i], sigmas[i + 1], eta=eta)
if callback is not None:
callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised})
d = to_d(x, sigmas[i], temp[0])
# Euler method
dt = sigma_down - sigmas[i]
x = denoised + d * sigma_down
if sigmas[i + 1] > 0:
x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * sigma_up
return x

View File

@ -90,4 +90,4 @@ class ControlNet(nn.Module):
proj_outputs = [None for _ in range(max(self.proj_blocks) + 1)]
for i, idx in enumerate(self.proj_blocks):
proj_outputs[idx] = self.projections[i](x)
return proj_outputs
return {"input": proj_outputs[::-1]}

View File

@ -746,6 +746,8 @@ class MMDiT(nn.Module):
qkv_bias: bool = True,
context_processor_layers = None,
context_size = 4096,
num_blocks = None,
final_layer = True,
dtype = None, #TODO
device = None,
operations = None,
@ -767,7 +769,10 @@ class MMDiT(nn.Module):
# apply magic --> this defines a head_size of 64
self.hidden_size = 64 * depth
num_heads = depth
if num_blocks is None:
num_blocks = depth
self.depth = depth
self.num_heads = num_heads
self.x_embedder = PatchEmbed(
@ -822,7 +827,7 @@ class MMDiT(nn.Module):
mlp_ratio=mlp_ratio,
qkv_bias=qkv_bias,
attn_mode=attn_mode,
pre_only=i == depth - 1,
pre_only=(i == num_blocks - 1) and final_layer,
rmsnorm=rmsnorm,
scale_mod_only=scale_mod_only,
swiglu=swiglu,
@ -831,11 +836,12 @@ class MMDiT(nn.Module):
device=device,
operations=operations
)
for i in range(depth)
for i in range(num_blocks)
]
)
self.final_layer = FinalLayer(self.hidden_size, patch_size, self.out_channels, dtype=dtype, device=device, operations=operations)
if final_layer:
self.final_layer = FinalLayer(self.hidden_size, patch_size, self.out_channels, dtype=dtype, device=device, operations=operations)
self.compile_core = compile_core
if compile_core:
@ -894,6 +900,7 @@ class MMDiT(nn.Module):
x: torch.Tensor,
c_mod: torch.Tensor,
context: Optional[torch.Tensor] = None,
control = None,
) -> torch.Tensor:
if self.compile_core:
return self.forward_core_with_concat_compiled(x, c_mod, context)
@ -908,13 +915,20 @@ class MMDiT(nn.Module):
# context is B, L', D
# x is B, L, D
for block in self.joint_blocks:
context, x = block(
blocks = len(self.joint_blocks)
for i in range(blocks):
context, x = self.joint_blocks[i](
context,
x,
c=c_mod,
use_checkpoint=self.use_checkpoint,
)
if control is not None:
control_o = control.get("output")
if i < len(control_o):
add = control_o[i]
if add is not None:
x += add
x = self.final_layer(x, c_mod) # (N, T, patch_size ** 2 * out_channels)
return x
@ -925,6 +939,7 @@ class MMDiT(nn.Module):
t: torch.Tensor,
y: Optional[torch.Tensor] = None,
context: Optional[torch.Tensor] = None,
control = None,
) -> torch.Tensor:
"""
Forward pass of DiT.
@ -946,7 +961,7 @@ class MMDiT(nn.Module):
if context is not None:
context = self.context_embedder(context)
x = self.forward_core_with_concat(x, c, context)
x = self.forward_core_with_concat(x, c, context, control)
x = self.unpatchify(x, hw=hw) # (N, out_channels, H, W)
return x[:,:,:hw[-2],:hw[-1]]
@ -959,7 +974,8 @@ class OpenAISignatureMMDITWrapper(MMDiT):
timesteps: torch.Tensor,
context: Optional[torch.Tensor] = None,
y: Optional[torch.Tensor] = None,
control = None,
**kwargs,
) -> torch.Tensor:
return super().forward(x, timesteps, context=context, y=y)
return super().forward(x, timesteps, context=context, y=y, control=control)

View File

@ -216,12 +216,21 @@ def model_lora_keys_clip(model, key_map={}):
lora_key = "lora_prior_te_text_model_encoder_layers_{}_{}".format(b, LORA_CLIP_MAP[c]) #cascade lora: TODO put lora key prefix in the model config
key_map[lora_key] = k
for k in sdk: #OneTrainer SD3 lora
if k.startswith("t5xxl.transformer.") and k.endswith(".weight"):
l_key = k[len("t5xxl.transformer."):-len(".weight")]
lora_key = "lora_te3_{}".format(l_key.replace(".", "_"))
key_map[lora_key] = k
k = "clip_g.transformer.text_projection.weight"
if k in sdk:
key_map["lora_prior_te_text_projection"] = k #cascade lora?
# key_map["text_encoder.text_projection"] = k #TODO: check if other lora have the text_projection too
# key_map["lora_te_text_projection"] = k
key_map["lora_te2_text_projection"] = k #OneTrainer SD3 lora
k = "clip_l.transformer.text_projection.weight"
if k in sdk:
key_map["lora_te1_text_projection"] = k #OneTrainer SD3 lora, not necessary but omits warning
return key_map
@ -250,15 +259,17 @@ def model_lora_keys_unet(model, key_map={}):
key_map[diffusers_lora_key] = unet_key
if isinstance(model, model_base.SD3): #Diffusers lora SD3
for i in range(model.model_config.unet_config.get("depth", 0)):
k = "transformer.transformer_blocks.{}.attn.".format(i)
qkv = "diffusion_model.joint_blocks.{}.x_block.attn.qkv.weight".format(i)
proj = "diffusion_model.joint_blocks.{}.x_block.attn.proj.weight".format(i)
if qkv in sd:
offset = sd[qkv].shape[0] // 3
key_map["{}to_q".format(k)] = (qkv, (0, 0, offset))
key_map["{}to_k".format(k)] = (qkv, (0, offset, offset))
key_map["{}to_v".format(k)] = (qkv, (0, offset * 2, offset))
key_map["{}to_out.0".format(k)] = proj
diffusers_keys = utils.mmdit_to_diffusers(model.model_config.unet_config, output_prefix="diffusion_model.")
for k in diffusers_keys:
if k.endswith(".weight"):
to = diffusers_keys[k]
key_lora = "transformer.{}".format(k[:-len(".weight")]) #regular diffusers sd3 lora format
key_map[key_lora] = to
key_lora = "base_model.model.{}".format(k[:-len(".weight")]) #format for flash-sd3 lora and others?
key_map[key_lora] = to
key_lora = "lora_transformer_{}".format(k[:-len(".weight")].replace(".", "_")) #OneTrainer lora
key_map[key_lora] = to
return key_map

View File

@ -651,3 +651,12 @@ class StableAudio1(BaseModel):
cross_attn = torch.cat([cross_attn.to(device), seconds_start_embed.repeat((cross_attn.shape[0], 1, 1)), seconds_total_embed.repeat((cross_attn.shape[0], 1, 1))], dim=1)
out['c_crossattn'] = conds.CONDRegular(cross_attn)
return out
def state_dict_for_saving(self, clip_state_dict=None, vae_state_dict=None, clip_vision_state_dict=None):
sd = super().state_dict_for_saving(clip_state_dict=clip_state_dict, vae_state_dict=vae_state_dict, clip_vision_state_dict=clip_vision_state_dict)
d = {"conditioner.conditioners.seconds_start.": self.seconds_start_embedder.state_dict(), "conditioner.conditioners.seconds_total.": self.seconds_total_embedder.state_dict()}
for k in d:
s = d[k]
for l in s:
sd["{}{}".format(k, l)] = s[l]
return sd

View File

@ -1,7 +1,8 @@
from . import supported_models
from . import supported_models, utils
from . import supported_models_base
import math
import logging
import torch
def count_blocks(state_dict_keys, prefix_string):
count = 0
@ -39,7 +40,9 @@ def detect_unet_config(state_dict, key_prefix):
unet_config["in_channels"] = state_dict['{}x_embedder.proj.weight'.format(key_prefix)].shape[1]
patch_size = state_dict['{}x_embedder.proj.weight'.format(key_prefix)].shape[2]
unet_config["patch_size"] = patch_size
unet_config["out_channels"] = state_dict['{}final_layer.linear.weight'.format(key_prefix)].shape[0] // (patch_size * patch_size)
final_layer = '{}final_layer.linear.weight'.format(key_prefix)
if final_layer in state_dict:
unet_config["out_channels"] = state_dict[final_layer].shape[0] // (patch_size * patch_size)
unet_config["depth"] = state_dict['{}x_embedder.proj.weight'.format(key_prefix)].shape[0] // 64
unet_config["input_size"] = None
@ -431,3 +434,40 @@ def model_config_from_diffusers_unet(state_dict):
if unet_config is not None:
return model_config_from_unet_config(unet_config)
return None
def convert_diffusers_mmdit(state_dict, output_prefix=""):
out_sd = None
num_blocks = count_blocks(state_dict, 'transformer_blocks.{}.')
if num_blocks > 0:
depth = state_dict["pos_embed.proj.weight"].shape[0] // 64
out_sd = {}
sd_map = utils.mmdit_to_diffusers({"depth": depth, "num_blocks": num_blocks}, output_prefix=output_prefix)
for k in sd_map:
weight = state_dict.get(k, None)
if weight is not None:
t = sd_map[k]
if not isinstance(t, str):
if len(t) > 2:
fun = t[2]
else:
fun = lambda a: a
offset = t[1]
if offset is not None:
old_weight = out_sd.get(t[0], None)
if old_weight is None:
old_weight = torch.empty_like(weight)
old_weight = old_weight.repeat([3] + [1] * (len(old_weight.shape) - 1))
w = old_weight.narrow(offset[0], offset[1], offset[2])
else:
old_weight = weight
w = weight
w[:] = fun(weight)
t = t[0]
out_sd[t] = old_weight
else:
out_sd[t] = weight
state_dict.pop(k)
return out_sd

View File

@ -54,6 +54,12 @@ def set_model_options_patch_replace(model_options, patch, name, block_name, numb
return model_options
def set_model_options_post_cfg_function(model_options, post_cfg_function, disable_cfg1_optimization=False):
model_options["sampler_post_cfg_function"] = model_options.get("sampler_post_cfg_function", []) + [post_cfg_function]
if disable_cfg1_optimization:
model_options["disable_cfg1_optimization"] = True
return model_options
class ModelPatcher(ModelManageable):
def __init__(self, model, load_device, offload_device, size=0, current_device=None, weight_inplace_update=False):
self.size = size
@ -134,9 +140,7 @@ class ModelPatcher(ModelManageable):
self.model_options["disable_cfg1_optimization"] = True
def set_model_sampler_post_cfg_function(self, post_cfg_function, disable_cfg1_optimization=False):
self.model_options["sampler_post_cfg_function"] = self.model_options.get("sampler_post_cfg_function", []) + [post_cfg_function]
if disable_cfg1_optimization:
self.model_options["disable_cfg1_optimization"] = True
self.model_options = set_model_options_post_cfg_function(self.model_options, post_cfg_function, disable_cfg1_optimization)
def set_model_unet_function_wrapper(self, unet_wrapper_function: UnetWrapperFunction):
self.model_options["model_function_wrapper"] = unet_wrapper_function
@ -222,16 +226,19 @@ class ModelPatcher(ModelManageable):
model_sd = self.model.state_dict()
for k in patches:
offset = None
function = None
if isinstance(k, str):
key = k
else:
offset = k[1]
key = k[0]
if len(k) > 2:
function = k[2]
if key in model_sd:
p.add(k)
current_patches = self.patches.get(key, [])
current_patches.append((strength_patch, patches[k], strength_model, offset))
current_patches.append((strength_patch, patches[k], strength_model, offset, function))
self.patches[key] = current_patches
self.patches_uuid = uuid.uuid4()
@ -361,6 +368,9 @@ class ModelPatcher(ModelManageable):
v = p[1]
strength_model = p[2]
offset = p[3]
function = p[4]
if function is None:
function = lambda a: a
old_weight = None
if offset is not None:
@ -387,7 +397,7 @@ class ModelPatcher(ModelManageable):
if w1.shape != weight.shape:
logging.warning("WARNING SHAPE MISMATCH {} WEIGHT NOT MERGED {} != {}".format(key, w1.shape, weight.shape))
else:
weight += strength * model_management.cast_to_device(w1, weight.device, weight.dtype)
weight += function(strength * model_management.cast_to_device(w1, weight.device, weight.dtype))
elif patch_type == "lora": # lora/locon
mat1 = model_management.cast_to_device(v[0], weight.device, torch.float32)
mat2 = model_management.cast_to_device(v[1], weight.device, torch.float32)
@ -405,9 +415,9 @@ class ModelPatcher(ModelManageable):
try:
lora_diff = torch.mm(mat1.flatten(start_dim=1), mat2.flatten(start_dim=1)).reshape(weight.shape)
if dora_scale is not None:
weight = weight_decompose(dora_scale, weight, lora_diff, alpha, strength)
weight = function(weight_decompose(dora_scale, weight, lora_diff, alpha, strength))
else:
weight += ((strength * alpha) * lora_diff).type(weight.dtype)
weight += function(((strength * alpha) * lora_diff).type(weight.dtype))
except Exception as e:
logging.error("ERROR {} {} {}".format(patch_type, key, e))
elif patch_type == "lokr":
@ -451,9 +461,9 @@ class ModelPatcher(ModelManageable):
try:
lora_diff = torch.kron(w1, w2).reshape(weight.shape)
if dora_scale is not None:
weight = weight_decompose(dora_scale, weight, lora_diff, alpha, strength)
weight = function(weight_decompose(dora_scale, weight, lora_diff, alpha, strength))
else:
weight += ((strength * alpha) * lora_diff).type(weight.dtype)
weight += function(((strength * alpha) * lora_diff).type(weight.dtype))
except Exception as e:
logging.error("ERROR {} {} {}".format(patch_type, key, e))
elif patch_type == "loha":
@ -488,9 +498,9 @@ class ModelPatcher(ModelManageable):
try:
lora_diff = (m1 * m2).reshape(weight.shape)
if dora_scale is not None:
weight = weight_decompose(dora_scale, weight, lora_diff, alpha, strength)
weight = function(weight_decompose(dora_scale, weight, lora_diff, alpha, strength))
else:
weight += ((strength * alpha) * lora_diff).type(weight.dtype)
weight += function(((strength * alpha) * lora_diff).type(weight.dtype))
except Exception as e:
logging.error("ERROR {} {} {}".format(patch_type, key, e))
elif patch_type == "glora":
@ -509,9 +519,9 @@ class ModelPatcher(ModelManageable):
try:
lora_diff = (torch.mm(b2, b1) + torch.mm(torch.mm(weight.flatten(start_dim=1), a2), a1)).reshape(weight.shape)
if dora_scale is not None:
weight = weight_decompose(dora_scale, weight, lora_diff, alpha, strength)
weight = function(weight_decompose(dora_scale, weight, lora_diff, alpha, strength))
else:
weight += ((strength * alpha) * lora_diff).type(weight.dtype)
weight += function(((strength * alpha) * lora_diff).type(weight.dtype))
except Exception as e:
logging.error("ERROR {} {} {}".format(patch_type, key, e))
else:

View File

@ -780,7 +780,7 @@ class ControlNetApplyAdvanced:
CATEGORY = "conditioning"
def apply_controlnet(self, positive, negative, control_net, image, strength, start_percent, end_percent):
def apply_controlnet(self, positive, negative, control_net, image, strength, start_percent, end_percent, vae=None):
if strength == 0:
return (positive, negative)
@ -797,7 +797,7 @@ class ControlNetApplyAdvanced:
if prev_cnet in cnets:
c_net = cnets[prev_cnet]
else:
c_net = control_net.copy().set_cond_hint(control_hint, strength, (start_percent, end_percent))
c_net = control_net.copy().set_cond_hint(control_hint, strength, (start_percent, end_percent), vae)
c_net.set_previous_controlnet(prev_cnet)
cnets[prev_cnet] = c_net

View File

@ -103,6 +103,7 @@ def _import_and_enumerate_nodes_in_module(module: types.ModuleType,
def import_all_nodes_in_workspace(vanilla_custom_nodes=True, raise_on_failure=False) -> ExportedNodes:
# now actually import the nodes, to improve control of node loading order
from comfy_extras import nodes as comfy_extras_nodes
from ..cli_args import args
from . import base_nodes
from .vanilla_node_importing import mitigated_import_of_vanilla_custom_nodes
# only load these nodes once
@ -116,6 +117,11 @@ def import_all_nodes_in_workspace(vanilla_custom_nodes=True, raise_on_failure=Fa
ExportedNodes())
custom_nodes_mappings = ExportedNodes()
if args.disable_all_custom_nodes:
logging.info("Loading custom nodes was disabled, only base and extra nodes were loaded")
_comfy_nodes.update(base_and_extra)
return _comfy_nodes
# load from entrypoints
for entry_point in entry_points().select(group='comfyui.custom_nodes'):
# Load the module associated with the current entry point

View File

@ -540,9 +540,10 @@ class Sampler:
sigma = float(sigmas[0])
return math.isclose(max_sigma, sigma, rel_tol=1e-05) or sigma > max_sigma
KSAMPLER_NAMES = ["euler", "euler_ancestral", "heun", "heunpp2","dpm_2", "dpm_2_ancestral",
KSAMPLER_NAMES = ["euler", "euler_cfg_pp", "euler_ancestral", "euler_ancestral_cfg_pp", "heun", "heunpp2","dpm_2", "dpm_2_ancestral",
"lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_ancestral", "dpmpp_sde", "dpmpp_sde_gpu",
"dpmpp_2m", "dpmpp_2m_sde", "dpmpp_2m_sde_gpu", "dpmpp_3m_sde", "dpmpp_3m_sde_gpu", "ddpm", "lcm"]
"dpmpp_2m", "dpmpp_2m_sde", "dpmpp_2m_sde_gpu", "dpmpp_3m_sde", "dpmpp_3m_sde_gpu", "ddpm", "lcm",
"ipndm", "ipndm_v", "deis"]
class KSAMPLER(Sampler):
def __init__(self, sampler_function, extra_options={}, inpaint_options={}):

View File

@ -243,7 +243,7 @@ class VAE:
self.first_stage_model = AutoencodingEngine(regularizer_config={'target': "comfy.ldm.models.autoencoder.DiagonalGaussianRegularizer"},
encoder_config={'target': "comfy.ldm.modules.diffusionmodules.model.Encoder", 'params': ddconfig},
decoder_config={'target': "comfy.ldm.modules.diffusionmodules.model.Decoder", 'params': ddconfig})
elif "decoder.layers.0.weight_v" in sd:
elif "decoder.layers.1.layers.0.beta" in sd:
self.first_stage_model = AudioOobleckVAE()
self.memory_used_encode = lambda shape, dtype: (1000 * shape[2]) * model_management.dtype_size(dtype)
self.memory_used_decode = lambda shape, dtype: (1000 * shape[2] * 2048) * model_management.dtype_size(dtype)
@ -305,6 +305,10 @@ class VAE:
/ 3.0)
return output
def decode_tiled_1d(self, samples, tile_x=128, overlap=32):
decode_fn = lambda a: self.first_stage_model.decode(a.to(self.vae_dtype).to(self.device)).float()
return utils.tiled_scale_multidim(samples, decode_fn, tile=(tile_x,), overlap=overlap, upscale_amount=self.upscale_ratio, out_channels=self.output_channels, output_device=self.output_device)
def encode_tiled_(self, pixel_samples, tile_x=512, tile_y=512, overlap=64):
steps = pixel_samples.shape[0] * utils.get_tiled_scale_steps(pixel_samples.shape[3], pixel_samples.shape[2], tile_x, tile_y, overlap)
steps += pixel_samples.shape[0] * utils.get_tiled_scale_steps(pixel_samples.shape[3], pixel_samples.shape[2], tile_x // 2, tile_y * 2, overlap)
@ -318,6 +322,10 @@ class VAE:
samples /= 3.0
return samples
def encode_tiled_1d(self, samples, tile_x=128 * 2048, overlap=32 * 2048):
encode_fn = lambda a: self.first_stage_model.encode((self.process_input(a)).to(self.vae_dtype).to(self.device)).float()
return utils.tiled_scale_multidim(samples, encode_fn, tile=(tile_x,), overlap=overlap, upscale_amount=(1/self.downscale_ratio), out_channels=self.latent_channels, output_device=self.output_device)
def decode(self, samples_in):
try:
memory_used = self.memory_used_decode(samples_in.shape, self.vae_dtype)
@ -332,7 +340,10 @@ class VAE:
pixel_samples[x:x + batch_number] = self.process_output(self.first_stage_model.decode(samples).to(self.output_device).float())
except model_management.OOM_EXCEPTION as e:
logging.warning("Warning: Ran out of memory when regular VAE decoding, retrying with tiled VAE decoding.")
pixel_samples = self.decode_tiled_(samples_in)
if len(samples_in.shape) == 3:
pixel_samples = self.decode_tiled_1d(samples_in)
else:
pixel_samples = self.decode_tiled_(samples_in)
pixel_samples = pixel_samples.to(self.output_device).movedim(1, -1)
return pixel_samples
@ -358,7 +369,10 @@ class VAE:
except model_management.OOM_EXCEPTION as e:
logging.warning("Warning: Ran out of memory when regular VAE encoding, retrying with tiled VAE encoding.")
samples = self.encode_tiled_(pixel_samples)
if len(pixel_samples.shape) == 3:
samples = self.encode_tiled_1d(pixel_samples)
else:
samples = self.encode_tiled_(pixel_samples)
return samples
@ -569,17 +583,32 @@ def load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, o
return (_model_patcher, clip, vae, clipvision)
def load_unet_state_dict(sd): # load unet in diffusers format
def load_unet_state_dict(sd): # load unet in diffusers or regular format
#Allow loading unets from checkpoint files
checkpoint = False
diffusion_model_prefix = model_detection.unet_prefix_from_state_dict(sd)
temp_sd = utils.state_dict_prefix_replace(sd, {diffusion_model_prefix: ""}, filter_keys=True)
if len(temp_sd) > 0:
sd = temp_sd
checkpoint = True
parameters = utils.calculate_parameters(sd)
unet_dtype = model_management.unet_dtype(model_params=parameters)
load_device = model_management.get_torch_device()
if "input_blocks.0.0.weight" in sd or 'clf.1.weight' in sd: # ldm or stable cascade
if checkpoint or "input_blocks.0.0.weight" in sd or 'clf.1.weight' in sd: # ldm or stable cascade
model_config = model_detection.model_config_from_unet(sd, "")
if model_config is None:
return None
new_sd = sd
elif 'transformer_blocks.0.attn.add_q_proj.weight' in sd: #MMDIT SD3
new_sd = model_detection.convert_diffusers_mmdit(sd, "")
if new_sd is None:
return None
model_config = model_detection.model_config_from_unet(new_sd, "")
if model_config is None:
return None
else: # diffusers
model_config = model_detection.model_config_from_diffusers_unet(sd)
if model_config is None:
@ -629,4 +658,9 @@ def save_checkpoint(output_path, model, clip=None, vae=None, clip_vision=None, m
for k in extra_keys:
sd[k] = extra_keys[k]
for k in sd:
t = sd[k]
if not t.is_contiguous():
sd[k] = t.contiguous()
utils.save_torch_file(sd, output_path, metadata=metadata)

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import copy
import logging
import numbers
import os
import traceback
import zipfile
@ -97,10 +98,10 @@ class SDClipModel(torch.nn.Module):
for x in tokens:
tokens_temp = []
for y in x:
if isinstance(y, int):
if isinstance(y, numbers.Integral):
if y == token_dict_size: # EOS token
y = -1
tokens_temp += [y]
tokens_temp += [int(y)]
else:
if y.shape[0] == current_embeds.weight.shape[1]:
embedding_weights += [y]

View File

@ -543,13 +543,16 @@ class StableAudio(supported_models_base.BASE):
seconds_total_sd = utils.state_dict_prefix_replace(state_dict, {"conditioner.conditioners.seconds_total.": ""}, filter_keys=True)
return model_base.StableAudio1(self, seconds_start_embedder_weights=seconds_start_sd, seconds_total_embedder_weights=seconds_total_sd, device=device)
def process_unet_state_dict(self, state_dict):
for k in list(state_dict.keys()):
if k.endswith(".cross_attend_norm.beta") or k.endswith(".ff_norm.beta") or k.endswith(".pre_norm.beta"): #These weights are all zero
state_dict.pop(k)
return state_dict
def process_unet_state_dict_for_saving(self, state_dict):
replace_prefix = {"": "model.model."}
return utils.state_dict_prefix_replace(state_dict, replace_prefix)
def clip_target(self, state_dict={}):
return supported_models_base.ClipTarget(sa_t5.SAT5Tokenizer, sa_t5.SAT5Model)

View File

@ -153,7 +153,13 @@ class Adapter(nn.Module):
features.append(None)
features.append(x)
return features
features = features[::-1]
if self.xl:
return {"input": features[1:], "middle": features[:1]}
else:
return {"input": features}
class LayerNorm(nn.LayerNorm):
@ -290,4 +296,4 @@ class Adapter_light(nn.Module):
features.append(None)
features.append(x)
return features
return {"input": features[::-1]}

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import contextlib
import itertools
import logging
import math
import os
@ -288,6 +289,91 @@ def unet_to_diffusers(unet_config):
return diffusers_unet_map
def swap_scale_shift(weight):
shift, scale = weight.chunk(2, dim=0)
new_weight = torch.cat([scale, shift], dim=0)
return new_weight
MMDIT_MAP_BASIC = {
("context_embedder.bias", "context_embedder.bias"),
("context_embedder.weight", "context_embedder.weight"),
("t_embedder.mlp.0.bias", "time_text_embed.timestep_embedder.linear_1.bias"),
("t_embedder.mlp.0.weight", "time_text_embed.timestep_embedder.linear_1.weight"),
("t_embedder.mlp.2.bias", "time_text_embed.timestep_embedder.linear_2.bias"),
("t_embedder.mlp.2.weight", "time_text_embed.timestep_embedder.linear_2.weight"),
("x_embedder.proj.bias", "pos_embed.proj.bias"),
("x_embedder.proj.weight", "pos_embed.proj.weight"),
("y_embedder.mlp.0.bias", "time_text_embed.text_embedder.linear_1.bias"),
("y_embedder.mlp.0.weight", "time_text_embed.text_embedder.linear_1.weight"),
("y_embedder.mlp.2.bias", "time_text_embed.text_embedder.linear_2.bias"),
("y_embedder.mlp.2.weight", "time_text_embed.text_embedder.linear_2.weight"),
("pos_embed", "pos_embed.pos_embed"),
("final_layer.adaLN_modulation.1.bias", "norm_out.linear.bias", swap_scale_shift),
("final_layer.adaLN_modulation.1.weight", "norm_out.linear.weight", swap_scale_shift),
("final_layer.linear.bias", "proj_out.bias"),
("final_layer.linear.weight", "proj_out.weight"),
}
MMDIT_MAP_BLOCK = {
("context_block.adaLN_modulation.1.bias", "norm1_context.linear.bias"),
("context_block.adaLN_modulation.1.weight", "norm1_context.linear.weight"),
("context_block.attn.proj.bias", "attn.to_add_out.bias"),
("context_block.attn.proj.weight", "attn.to_add_out.weight"),
("context_block.mlp.fc1.bias", "ff_context.net.0.proj.bias"),
("context_block.mlp.fc1.weight", "ff_context.net.0.proj.weight"),
("context_block.mlp.fc2.bias", "ff_context.net.2.bias"),
("context_block.mlp.fc2.weight", "ff_context.net.2.weight"),
("x_block.adaLN_modulation.1.bias", "norm1.linear.bias"),
("x_block.adaLN_modulation.1.weight", "norm1.linear.weight"),
("x_block.attn.proj.bias", "attn.to_out.0.bias"),
("x_block.attn.proj.weight", "attn.to_out.0.weight"),
("x_block.mlp.fc1.bias", "ff.net.0.proj.bias"),
("x_block.mlp.fc1.weight", "ff.net.0.proj.weight"),
("x_block.mlp.fc2.bias", "ff.net.2.bias"),
("x_block.mlp.fc2.weight", "ff.net.2.weight"),
}
def mmdit_to_diffusers(mmdit_config, output_prefix=""):
key_map = {}
depth = mmdit_config.get("depth", 0)
num_blocks = mmdit_config.get("num_blocks", depth)
for i in range(num_blocks):
block_from = "transformer_blocks.{}".format(i)
block_to = "{}joint_blocks.{}".format(output_prefix, i)
offset = depth * 64
for end in ("weight", "bias"):
k = "{}.attn.".format(block_from)
qkv = "{}.x_block.attn.qkv.{}".format(block_to, end)
key_map["{}to_q.{}".format(k, end)] = (qkv, (0, 0, offset))
key_map["{}to_k.{}".format(k, end)] = (qkv, (0, offset, offset))
key_map["{}to_v.{}".format(k, end)] = (qkv, (0, offset * 2, offset))
qkv = "{}.context_block.attn.qkv.{}".format(block_to, end)
key_map["{}add_q_proj.{}".format(k, end)] = (qkv, (0, 0, offset))
key_map["{}add_k_proj.{}".format(k, end)] = (qkv, (0, offset, offset))
key_map["{}add_v_proj.{}".format(k, end)] = (qkv, (0, offset * 2, offset))
for k in MMDIT_MAP_BLOCK:
key_map["{}.{}".format(block_from, k[1])] = "{}.{}".format(block_to, k[0])
map_basic = MMDIT_MAP_BASIC.copy()
map_basic.add(("joint_blocks.{}.context_block.adaLN_modulation.1.bias".format(depth - 1), "transformer_blocks.{}.norm1_context.linear.bias".format(depth - 1), swap_scale_shift))
map_basic.add(("joint_blocks.{}.context_block.adaLN_modulation.1.weight".format(depth - 1), "transformer_blocks.{}.norm1_context.linear.weight".format(depth - 1), swap_scale_shift))
for k in map_basic:
if len(k) > 2:
key_map[k[1]] = ("{}{}".format(output_prefix, k[0]), None, k[2])
else:
key_map[k[1]] = "{}{}".format(output_prefix, k[0])
return key_map
def repeat_to_batch_size(tensor, batch_size, dim=0):
if tensor.shape[dim] > batch_size:
return tensor.narrow(dim, 0, batch_size)
@ -476,35 +562,54 @@ def get_tiled_scale_steps(width, height, tile_x, tile_y, overlap):
@torch.inference_mode()
def tiled_scale(samples, function, tile_x=64, tile_y=64, overlap=8, upscale_amount=4, out_channels=3, output_device="cpu", pbar=None):
output = torch.empty((samples.shape[0], out_channels, round(samples.shape[2] * upscale_amount), round(samples.shape[3] * upscale_amount)), device=output_device)
def tiled_scale_multidim(samples, function, tile=(64, 64), overlap=8, upscale_amount=4, out_channels=3, output_device="cpu", pbar=None):
dims = len(tile)
output = torch.empty([samples.shape[0], out_channels] + list(map(lambda a: round(a * upscale_amount), samples.shape[2:])), device=output_device)
for b in range(samples.shape[0]):
s = samples[b:b + 1]
out = torch.zeros((s.shape[0], out_channels, round(s.shape[2] * upscale_amount), round(s.shape[3] * upscale_amount)), device=output_device)
out_div = torch.zeros((s.shape[0], out_channels, round(s.shape[2] * upscale_amount), round(s.shape[3] * upscale_amount)), device=output_device)
for y in range(0, s.shape[2], tile_y - overlap):
for x in range(0, s.shape[3], tile_x - overlap):
x = max(0, min(s.shape[-1] - overlap, x))
y = max(0, min(s.shape[-2] - overlap, y))
s_in = s[:, :, y:y + tile_y, x:x + tile_x]
out = torch.zeros([s.shape[0], out_channels] + list(map(lambda a: round(a * upscale_amount), s.shape[2:])), device=output_device)
out_div = torch.zeros([s.shape[0], out_channels] + list(map(lambda a: round(a * upscale_amount), s.shape[2:])), device=output_device)
ps = function(s_in).to(output_device)
mask = torch.ones_like(ps)
feather = round(overlap * upscale_amount)
for t in range(feather):
mask[:, :, t:1 + t, :] *= ((1.0 / feather) * (t + 1))
mask[:, :, mask.shape[2] - 1 - t: mask.shape[2] - t, :] *= ((1.0 / feather) * (t + 1))
mask[:, :, :, t:1 + t] *= ((1.0 / feather) * (t + 1))
mask[:, :, :, mask.shape[3] - 1 - t: mask.shape[3] - t] *= ((1.0 / feather) * (t + 1))
out[:, :, round(y * upscale_amount):round((y + tile_y) * upscale_amount), round(x * upscale_amount):round((x + tile_x) * upscale_amount)] += ps * mask
out_div[:, :, round(y * upscale_amount):round((y + tile_y) * upscale_amount), round(x * upscale_amount):round((x + tile_x) * upscale_amount)] += mask
if pbar is not None:
pbar.update(1)
for it in itertools.product(*map(lambda a: range(0, a[0], a[1] - overlap), zip(s.shape[2:], tile))):
s_in = s
upscaled = []
for d in range(dims):
pos = max(0, min(s.shape[d + 2] - overlap, it[d]))
l = min(tile[d], s.shape[d + 2] - pos)
s_in = s_in.narrow(d + 2, pos, l)
upscaled.append(round(pos * upscale_amount))
ps = function(s_in).to(output_device)
mask = torch.ones_like(ps)
feather = round(overlap * upscale_amount)
for t in range(feather):
for d in range(2, dims + 2):
m = mask.narrow(d, t, 1)
m *= ((1.0 / feather) * (t + 1))
m = mask.narrow(d, mask.shape[d] - 1 - t, 1)
m *= ((1.0 / feather) * (t + 1))
o = out
o_d = out_div
for d in range(dims):
o = o.narrow(d + 2, upscaled[d], mask.shape[d + 2])
o_d = o_d.narrow(d + 2, upscaled[d], mask.shape[d + 2])
o += ps * mask
o_d += mask
if pbar is not None:
pbar.update(1)
output[b:b + 1] = out / out_div
return output
def tiled_scale(samples, function, tile_x=64, tile_y=64, overlap=8, upscale_amount=4, out_channels=3, output_device="cpu", pbar=None):
return tiled_scale_multidim(samples, function, (tile_y, tile_x), overlap, upscale_amount, out_channels, output_device, pbar)
def _progress_bar_update(value: float, total: float, preview_image_or_data: Optional[Any] = None, client_id: Optional[str] = None, server: Optional[ExecutorToClientProgress] = None):
server = server or current_execution_context().server
# todo: this should really be from the context. right now the server is behaving like a context

View File

@ -63,6 +63,10 @@ const colorPalettes = {
"border-color": "#4e4e4e",
"tr-even-bg-color": "#222",
"tr-odd-bg-color": "#353535",
"content-bg": "#4e4e4e",
"content-fg": "#fff",
"content-hover-bg": "#222",
"content-hover-fg": "#fff"
}
},
},
@ -120,6 +124,10 @@ const colorPalettes = {
"border-color": "#888",
"tr-even-bg-color": "#f9f9f9",
"tr-odd-bg-color": "#fff",
"content-bg": "#e0e0e0",
"content-fg": "#222",
"content-hover-bg": "#adadad",
"content-hover-fg": "#222"
}
},
},
@ -176,6 +184,10 @@ const colorPalettes = {
"border-color": "#657b83", // Base00
"tr-even-bg-color": "#002b36",
"tr-odd-bg-color": "#073642",
"content-bg": "#657b83",
"content-fg": "#fdf6e3",
"content-hover-bg": "#002b36",
"content-hover-fg": "#fdf6e3"
}
},
},
@ -244,7 +256,11 @@ const colorPalettes = {
"error-text": "#ff4444",
"border-color": "#6e7581",
"tr-even-bg-color": "#2b2f38",
"tr-odd-bg-color": "#242730"
"tr-odd-bg-color": "#242730",
"content-bg": "#6e7581",
"content-fg": "#fff",
"content-hover-bg": "#2b2f38",
"content-hover-fg": "#fff"
}
},
},
@ -313,7 +329,11 @@ const colorPalettes = {
"error-text": "#ff4444",
"border-color": "#545d70",
"tr-even-bg-color": "#2e3440",
"tr-odd-bg-color": "#161b22"
"tr-odd-bg-color": "#161b22",
"content-bg": "#545d70",
"content-fg": "#e5eaf0",
"content-hover-bg": "#2e3440",
"content-hover-fg": "#e5eaf0"
}
},
},
@ -382,7 +402,11 @@ const colorPalettes = {
"error-text": "#ff4444",
"border-color": "#30363d",
"tr-even-bg-color": "#161b22",
"tr-odd-bg-color": "#13171d"
"tr-odd-bg-color": "#13171d",
"content-bg": "#30363d",
"content-fg": "#e5eaf0",
"content-hover-bg": "#161b22",
"content-hover-fg": "#e5eaf0"
}
},
}

View File

@ -1,177 +0,0 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"
const MAX_HISTORY = 50;
let undo = [];
let redo = [];
let activeState = null;
let isOurLoad = false;
function checkState() {
const currentState = app.graph.serialize();
if (!graphEqual(activeState, currentState)) {
undo.push(activeState);
if (undo.length > MAX_HISTORY) {
undo.shift();
}
activeState = clone(currentState);
redo.length = 0;
api.dispatchEvent(new CustomEvent("graphChanged", { detail: activeState }));
}
}
const loadGraphData = app.loadGraphData;
app.loadGraphData = async function () {
const v = await loadGraphData.apply(this, arguments);
if (isOurLoad) {
isOurLoad = false;
} else {
checkState();
}
return v;
};
function clone(obj) {
try {
if (typeof structuredClone !== "undefined") {
return structuredClone(obj);
}
} catch (error) {
// structuredClone is stricter than using JSON.parse/stringify so fallback to that
}
return JSON.parse(JSON.stringify(obj));
}
function graphEqual(a, b, root = true) {
if (a === b) return true;
if (typeof a == "object" && a && typeof b == "object" && b) {
const keys = Object.getOwnPropertyNames(a);
if (keys.length != Object.getOwnPropertyNames(b).length) {
return false;
}
for (const key of keys) {
let av = a[key];
let bv = b[key];
if (root && key === "nodes") {
// Nodes need to be sorted as the order changes when selecting nodes
av = [...av].sort((a, b) => a.id - b.id);
bv = [...bv].sort((a, b) => a.id - b.id);
}
if (!graphEqual(av, bv, false)) {
return false;
}
}
return true;
}
return false;
}
const undoRedo = async (e) => {
const updateState = async (source, target) => {
const prevState = source.pop();
if (prevState) {
target.push(activeState);
isOurLoad = true;
await app.loadGraphData(prevState, false);
activeState = prevState;
}
}
if (e.ctrlKey || e.metaKey) {
if (e.key === "y") {
updateState(redo, undo);
return true;
} else if (e.key === "z") {
updateState(undo, redo);
return true;
}
}
};
const bindInput = (activeEl) => {
if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") {
for (const evt of ["change", "input", "blur"]) {
if (`on${evt}` in activeEl) {
const listener = () => {
checkState();
activeEl.removeEventListener(evt, listener);
};
activeEl.addEventListener(evt, listener);
return true;
}
}
}
};
let keyIgnored = false;
window.addEventListener(
"keydown",
(e) => {
requestAnimationFrame(async () => {
let activeEl;
// If we are auto queue in change mode then we do want to trigger on inputs
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
activeEl = document.activeElement;
if (activeEl?.tagName === "INPUT" || activeEl?.type === "textarea") {
// Ignore events on inputs, they have their native history
return;
}
}
keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta";
if (keyIgnored) return;
// Check if this is a ctrl+z ctrl+y
if (await undoRedo(e)) return;
// If our active element is some type of input then handle changes after they're done
if (bindInput(activeEl)) return;
checkState();
});
},
true
);
window.addEventListener("keyup", (e) => {
if (keyIgnored) {
keyIgnored = false;
checkState();
}
});
// Handle clicking DOM elements (e.g. widgets)
window.addEventListener("mouseup", () => {
checkState();
});
// Handle prompt queue event for dynamic widget changes
api.addEventListener("promptQueued", () => {
checkState();
});
// Handle litegraph clicks
const processMouseUp = LGraphCanvas.prototype.processMouseUp;
LGraphCanvas.prototype.processMouseUp = function (e) {
const v = processMouseUp.apply(this, arguments);
checkState();
return v;
};
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
LGraphCanvas.prototype.processMouseDown = function (e) {
const v = processMouseDown.apply(this, arguments);
checkState();
return v;
};
// Handle litegraph context menu for COMBO widgets
const close = LiteGraph.ContextMenu.prototype.close;
LiteGraph.ContextMenu.prototype.close = function(e) {
const v = close.apply(this, arguments);
checkState();
return v;
}

View File

@ -0,0 +1,178 @@
import { app } from "../../scripts/app.js"
import { api } from "../../scripts/api.js"
function splitFilePath(path) {
const folder_separator = path.lastIndexOf("/")
if (folder_separator === -1) {
return ["", path]
}
return [
path.substring(0, folder_separator),
path.substring(folder_separator + 1)
]
}
function getResourceURL(subfolder, filename, type = "input") {
const params = [
"filename=" + encodeURIComponent(filename),
"type=" + type,
"subfolder=" + subfolder,
app.getPreviewFormatParam().substring(1),
app.getRandParam().substring(1)
].join("&")
return `/view?${params}`
}
async function uploadFile(
audioWidget,
audioUIWidget,
file,
updateNode,
pasted = false
) {
try {
// Wrap file in formdata so it includes filename
const body = new FormData()
body.append("image", file)
if (pasted) body.append("subfolder", "pasted")
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body
})
if (resp.status === 200) {
const data = await resp.json()
// Add the file to the dropdown list and update the widget value
let path = data.name
if (data.subfolder) path = data.subfolder + "/" + path
if (!audioWidget.options.values.includes(path)) {
audioWidget.options.values.push(path)
}
if (updateNode) {
audioUIWidget.element.src = api.apiURL(
getResourceURL(...splitFilePath(path))
)
audioWidget.value = path
}
} else {
alert(resp.status + " - " + resp.statusText)
}
} catch (error) {
alert(error)
}
}
// AudioWidget MUST be registered first, as AUDIOUPLOAD depends on AUDIO_UI to be
// present.
app.registerExtension({
name: "Comfy.AudioWidget",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass)) {
nodeData.input.required.audioUI = ["AUDIO_UI"]
}
},
getCustomWidgets() {
return {
AUDIO_UI(node, inputName) {
const audio = document.createElement("audio")
audio.controls = true
audio.classList.add("comfy-audio")
audio.setAttribute("name", "media")
const audioUIWidget = node.addDOMWidget(
inputName,
/* name=*/ "audioUI",
audio
)
// @ts-ignore
// TODO: Sort out the DOMWidget type.
audioUIWidget.serialize = false
const isOutputNode = node.constructor.nodeData.output_node
if (isOutputNode) {
// Hide the audio widget when there is no audio initially.
audioUIWidget.element.classList.add("empty-audio-widget")
// Populate the audio widget UI on node execution.
const onExecuted = node.onExecuted
node.onExecuted = function(message) {
onExecuted?.apply(this, arguments)
const audios = message.audio
if (!audios) return
const audio = audios[0]
audioUIWidget.element.src = api.apiURL(
getResourceURL(audio.subfolder, audio.filename, audio.type)
)
audioUIWidget.element.classList.remove("empty-audio-widget")
}
}
return { widget: audioUIWidget }
}
}
},
onNodeOutputsUpdated(nodeOutputs) {
for (const [nodeId, output] of Object.entries(nodeOutputs)) {
const node = app.graph.getNodeById(Number.parseInt(nodeId));
if ("audio" in output) {
const audioUIWidget = node.widgets.find((w) => w.name === "audioUI");
const audio = output.audio[0];
audioUIWidget.element.src = api.apiURL(getResourceURL(audio.subfolder, audio.filename, audio.type));
audioUIWidget.element.classList.remove("empty-audio-widget");
}
}
},
})
app.registerExtension({
name: "Comfy.UploadAudio",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.input?.required?.audio?.[1]?.audio_upload === true) {
nodeData.input.required.upload = ["AUDIOUPLOAD"]
}
},
getCustomWidgets() {
return {
AUDIOUPLOAD(node, inputName) {
// The widget that allows user to select file.
const audioWidget = node.widgets.find(w => w.name === "audio")
const audioUIWidget = node.widgets.find(w => w.name === "audioUI")
const onAudioWidgetUpdate = () => {
audioUIWidget.element.src = api.apiURL(
getResourceURL(...splitFilePath(audioWidget.value))
)
}
// Initially load default audio file to audioUIWidget.
if (audioWidget.value) {
onAudioWidgetUpdate()
}
audioWidget.callback = onAudioWidgetUpdate
const fileInput = document.createElement("input")
fileInput.type = "file"
fileInput.accept = "audio/*"
fileInput.style.display = "none"
fileInput.onchange = () => {
if (fileInput.files.length) {
uploadFile(audioWidget, audioUIWidget, fileInput.files[0], true)
}
}
// The widget to pop up the upload dialog.
const uploadWidget = node.addWidget(
"button",
inputName,
/* value=*/ "",
() => {
fileInput.click()
}
)
uploadWidget.label = "choose file to upload"
uploadWidget.serialize = false
return { widget: uploadWidget }
}
}
}
})

Binary file not shown.

View File

@ -5,6 +5,7 @@
<title>ComfyUI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" type="text/css" href="./lib/litegraph.css" />
<link rel="stylesheet" type="text/css" href="./lib/materialdesignicons.min.css" />
<link rel="stylesheet" type="text/css" href="./style.css" />
<link rel="stylesheet" type="text/css" href="./user.css" />
<script type="text/javascript" src="./lib/litegraph.core.js"></script>

View File

@ -4,7 +4,9 @@
"paths": {
"/*": ["./*"]
},
"lib": ["DOM", "ES2022"]
"lib": ["DOM", "ES2022", "DOM.Iterable"],
"target": "ES2015",
"module": "es2020"
},
"include": ["."]
}

File diff suppressed because one or more lines are too long

View File

@ -394,7 +394,7 @@ class ComfyApi extends EventTarget {
* Gets a user data file for the current user
* @param { string } file The name of the userdata file to load
* @param { RequestInit } [options]
* @returns { Promise<unknown> } The fetch response object
* @returns { Promise<Response> } The fetch response object
*/
async getUserData(file, options) {
return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options);
@ -404,18 +404,75 @@ class ComfyApi extends EventTarget {
* Stores a user data file for the current user
* @param { string } file The name of the userdata file to save
* @param { unknown } data The data to save to the file
* @param { RequestInit & { stringify?: boolean, throwOnError?: boolean } } [options]
* @returns { Promise<void> }
* @param { RequestInit & { overwrite?: boolean, stringify?: boolean, throwOnError?: boolean } } [options]
* @returns { Promise<Response> }
*/
async storeUserData(file, data, options = { stringify: true, throwOnError: true }) {
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, {
async storeUserData(file, data, options = { overwrite: true, stringify: true, throwOnError: true }) {
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}?overwrite=${options?.overwrite}`, {
method: "POST",
body: options?.stringify ? JSON.stringify(data) : data,
...options,
});
if (resp.status !== 200) {
if (resp.status !== 200 && options?.throwOnError !== false) {
throw new Error(`Error storing user data file '${file}': ${resp.status} ${(await resp).statusText}`);
}
return resp;
}
/**
* Deletes a user data file for the current user
* @param { string } file The name of the userdata file to delete
*/
async deleteUserData(file) {
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, {
method: "DELETE",
});
if (resp.status !== 204) {
throw new Error(`Error removing user data file '${file}': ${resp.status} ${(resp).statusText}`);
}
}
/**
* Move a user data file for the current user
* @param { string } source The userdata file to move
* @param { string } dest The destination for the file
*/
async moveUserData(source, dest, options = { overwrite: false }) {
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options?.overwrite}`, {
method: "POST",
});
return resp;
}
/**
* @overload
* Lists user data files for the current user
* @param { string } dir The directory in which to list files
* @param { boolean } [recurse] If the listing should be recursive
* @param { true } [split] If the paths should be split based on the os path separator
* @returns { Promise<string[][]>> } The list of split file paths in the format [fullPath, ...splitPath]
*/
/**
* @overload
* Lists user data files for the current user
* @param { string } dir The directory in which to list files
* @param { boolean } [recurse] If the listing should be recursive
* @param { false | undefined } [split] If the paths should be split based on the os path separator
* @returns { Promise<string[]>> } The list of files
*/
async listUserData(dir, recurse, split) {
const resp = await this.fetchApi(
`/userdata?${new URLSearchParams({
recurse,
dir,
split,
})}`
);
if (resp.status === 404) return [];
if (resp.status !== 200) {
throw new Error(`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`);
}
return resp.json();
}
}

View File

@ -3,11 +3,13 @@ import { ComfyWidgets, initWidgets } from "./widgets.js";
import { ComfyUI, $el } from "./ui.js";
import { api } from "./api.js";
import { defaultGraph } from "./defaultGraph.js";
import { getPngMetadata, getWebpMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
import { getPngMetadata, getWebpMetadata, getFlacMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
import { addDomClippingSetting } from "./domWidget.js";
import { createImageHost, calculateImageGrid } from "./ui/imagePreview.js"
export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview"
import { createImageHost, calculateImageGrid } from "./ui/imagePreview.js";
import { ComfyAppMenu } from "./ui/menu/index.js";
import { getStorageValue, setStorageValue } from "./utils.js";
import { ComfyWorkflowManager } from "./workflows.js";
export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview";
function sanitizeNodeName(string) {
let entityMap = {
@ -52,6 +54,12 @@ export class ComfyApp {
constructor() {
this.ui = new ComfyUI(this);
this.logging = new ComfyLogging(this);
this.workflowManager = new ComfyWorkflowManager(this);
this.bodyTop = $el("div.comfyui-body-top", { parent: document.body });
this.bodyLeft = $el("div.comfyui-body-left", { parent: document.body });
this.bodyRight = $el("div.comfyui-body-right", { parent: document.body });
this.bodyBottom = $el("div.comfyui-body-bottom", { parent: document.body });
this.menu = new ComfyAppMenu(this);
/**
* List of extensions that are registered with the app
@ -63,7 +71,7 @@ export class ComfyApp {
* Stores the execution output data for each node
* @type {Record<string, any>}
*/
this.nodeOutputs = {};
this._nodeOutputs = {};
/**
* Stores the preview image data for each node
@ -78,6 +86,15 @@ export class ComfyApp {
this.shiftDown = false;
}
get nodeOutputs() {
return this._nodeOutputs;
}
set nodeOutputs(value) {
this._nodeOutputs = value;
this.#invokeExtensions("onNodeOutputsUpdated", value);
}
getPreviewFormatParam() {
let preview_format = this.ui.settings.getSettingValue("Comfy.PreviewFormat");
if(preview_format)
@ -1067,7 +1084,7 @@ export class ComfyApp {
if (e.type == "keydown" && !e.repeat) {
// Ctrl + M mute/unmute
if (e.key === 'm' && e.ctrlKey) {
if (e.key === 'm' && (e.metaKey || e.ctrlKey)) {
if (this.selected_nodes) {
for (var i in this.selected_nodes) {
if (this.selected_nodes[i].mode === 2) { // never
@ -1081,7 +1098,7 @@ export class ComfyApp {
}
// Ctrl + B bypass
if (e.key === 'b' && e.ctrlKey) {
if (e.key === 'b' && (e.metaKey || e.ctrlKey)) {
if (this.selected_nodes) {
for (var i in this.selected_nodes) {
if (this.selected_nodes[i].mode === 4) { // never
@ -1313,11 +1330,15 @@ export class ComfyApp {
});
api.addEventListener("progress", ({ detail }) => {
if (this.workflowManager.activePrompt?.workflow
&& this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return;
this.progress = detail;
this.graph.setDirtyCanvas(true, false);
});
api.addEventListener("executing", ({ detail }) => {
if (this.workflowManager.activePrompt ?.workflow
&& this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return;
this.progress = null;
this.runningNodeId = detail;
this.graph.setDirtyCanvas(true, false);
@ -1325,6 +1346,8 @@ export class ComfyApp {
});
api.addEventListener("executed", ({ detail }) => {
if (this.workflowManager.activePrompt ?.workflow
&& this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return;
const output = this.nodeOutputs[detail.node];
if (detail.merge && output) {
for (const k in detail.output ?? {}) {
@ -1433,6 +1456,11 @@ export class ComfyApp {
});
await Promise.all(extensionPromises);
try {
this.menu.workflows.registerExtension(this);
} catch (error) {
console.error(error);
}
}
async #migrateSettings() {
@ -1520,15 +1548,17 @@ export class ComfyApp {
*/
async setup() {
await this.#setUser();
await this.ui.settings.load();
await this.#loadExtensions();
// Create and mount the LiteGraph in the DOM
const mainCanvas = document.createElement("canvas")
mainCanvas.style.touchAction = "none"
const canvasEl = (this.canvasEl = Object.assign(mainCanvas, { id: "graph-canvas" }));
canvasEl.tabIndex = "1";
document.body.prepend(canvasEl);
document.body.append(canvasEl);
this.resizeCanvas();
await Promise.all([this.workflowManager.loadWorkflows(), this.ui.settings.load()]);
await this.#loadExtensions();
addDomClippingSetting();
this.#addProcessMouseHandler();
@ -1541,7 +1571,7 @@ export class ComfyApp {
this.#addAfterConfigureHandler();
const canvas = (this.canvas = new LGraphCanvas(canvasEl, this.graph));
this.canvas = new LGraphCanvas(canvasEl, this.graph);
this.ctx = canvasEl.getContext("2d");
LiteGraph.release_link_on_empty_shows_menu = true;
@ -1549,19 +1579,14 @@ export class ComfyApp {
this.graph.start();
function resizeCanvas() {
// Limit minimal scale to 1, see https://github.com/comfyanonymous/ComfyUI/pull/845
const scale = Math.max(window.devicePixelRatio, 1);
const { width, height } = canvasEl.getBoundingClientRect();
canvasEl.width = Math.round(width * scale);
canvasEl.height = Math.round(height * scale);
canvasEl.getContext("2d").scale(scale, scale);
canvas.draw(true, true);
}
// Ensure the canvas fills the window
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
this.resizeCanvas();
window.addEventListener("resize", () => this.resizeCanvas());
const ro = new ResizeObserver(() => this.resizeCanvas());
ro.observe(this.bodyTop);
ro.observe(this.bodyLeft);
ro.observe(this.bodyRight);
ro.observe(this.bodyBottom);
await this.#invokeExtensionsAsync("init");
await this.registerNodes();
@ -1573,7 +1598,8 @@ export class ComfyApp {
const loadWorkflow = async (json) => {
if (json) {
const workflow = JSON.parse(json);
await this.loadGraphData(workflow);
const workflowName = getStorageValue("Comfy.PreviousWorkflow");
await this.loadGraphData(workflow, true, workflowName);
return true;
}
};
@ -1609,6 +1635,19 @@ export class ComfyApp {
await this.#invokeExtensionsAsync("setup");
}
resizeCanvas() {
// Limit minimal scale to 1, see https://github.com/comfyanonymous/ComfyUI/pull/845
const scale = Math.max(window.devicePixelRatio, 1);
// Clear fixed width and height while calculating rect so it uses 100% instead
this.canvasEl.height = this.canvasEl.width = "";
const { width, height } = this.canvasEl.getBoundingClientRect();
this.canvasEl.width = Math.round(width * scale);
this.canvasEl.height = Math.round(height * scale);
this.canvasEl.getContext("2d").scale(scale, scale);
this.canvas?.draw(true, true);
}
/**
* Registers nodes with the graph
*/
@ -1795,12 +1834,29 @@ export class ComfyApp {
});
}
async changeWorkflow(callback, workflow = null) {
try {
this.workflowManager.activeWorkflow?.changeTracker?.store()
} catch (error) {
console.error(error);
}
await callback();
try {
this.workflowManager.setWorkflow(workflow);
this.workflowManager.activeWorkflow?.track()
} catch (error) {
console.error(error);
}
}
/**
* Populates the graph with the specified workflow data
* @param {*} graphData A serialized graph object
* @param { boolean } clean If the graph state, e.g. images, should be cleared
* @param { boolean } restore_view If the graph position should be restored
* @param { import("./workflows.js").ComfyWorkflowInstance | null } workflow The workflow
*/
async loadGraphData(graphData, clean = true, restore_view = true) {
async loadGraphData(graphData, clean = true, restore_view = true, workflow = null) {
if (clean !== false) {
this.clean();
}
@ -1819,6 +1875,12 @@ export class ComfyApp {
graphData = structuredClone(graphData);
}
try {
this.workflowManager.setWorkflow(workflow);
} catch (error) {
console.error(error);
}
const missingNodeTypes = [];
await this.#invokeExtensionsAsync("beforeConfigureGraph", graphData, missingNodeTypes);
for (let n of graphData.nodes) {
@ -1840,6 +1902,11 @@ export class ComfyApp {
this.canvas.ds.offset = graphData.extra.ds.offset;
this.canvas.ds.scale = graphData.extra.ds.scale;
}
try {
this.workflowManager.activeWorkflow?.track()
} catch (error) {
}
} catch (error) {
let errorHint = [];
// Try extracting filename to see if it was caused by an extension script
@ -1899,6 +1966,14 @@ export class ComfyApp {
if (widget.value.startsWith("sample_")) {
widget.value = widget.value.slice(7);
}
if (widget.value === "euler_pp" || widget.value === "euler_ancestral_pp") {
widget.value = widget.value.slice(0, -3);
for (let w of node.widgets) {
if (w.name == "cfg") {
w.value *= 2.0;
}
}
}
}
}
if (node.type == "KSampler" || node.type == "KSamplerAdvanced" || node.type == "PrimitiveNode") {
@ -1927,14 +2002,17 @@ export class ComfyApp {
this.showMissingNodesError(missingNodeTypes);
}
await this.#invokeExtensionsAsync("afterConfigureGraph", missingNodeTypes);
requestAnimationFrame(() => {
this.graph.setDirtyCanvas(true, true);
});
}
/**
* Converts the current graph workflow for sending to the API
* @returns The workflow and node links
*/
async graphToPrompt() {
for (const outerNode of this.graph.computeExecutionOrder(false)) {
async graphToPrompt(graph = this.graph, clean = true) {
for (const outerNode of graph.computeExecutionOrder(false)) {
if (outerNode.widgets) {
for (const widget of outerNode.widgets) {
// Allow widgets to run callbacks before a prompt has been queued
@ -1954,10 +2032,10 @@ export class ComfyApp {
}
}
const workflow = this.graph.serialize();
const workflow = graph.serialize();
const output = {};
// Process nodes in order of execution
for (const outerNode of this.graph.computeExecutionOrder(false)) {
for (const outerNode of graph.computeExecutionOrder(false)) {
const skipNode = outerNode.mode === 2 || outerNode.mode === 4;
const innerNodes = (!skipNode && outerNode.getInnerNodes) ? outerNode.getInnerNodes() : [outerNode];
for (const node of innerNodes) {
@ -2049,13 +2127,14 @@ export class ComfyApp {
}
// Remove inputs connected to removed nodes
for (const o in output) {
for (const i in output[o].inputs) {
if (Array.isArray(output[o].inputs[i])
&& output[o].inputs[i].length === 2
&& !output[output[o].inputs[i][0]]) {
delete output[o].inputs[i];
if(clean) {
for (const o in output) {
for (const i in output[o].inputs) {
if (Array.isArray(output[o].inputs[i])
&& output[o].inputs[i].length === 2
&& !output[output[o].inputs[i][0]]) {
delete output[o].inputs[i];
}
}
}
}
@ -2123,6 +2202,14 @@ export class ComfyApp {
this.lastNodeErrors = res.node_errors;
if (this.lastNodeErrors.length > 0) {
this.canvas.draw(true, true);
} else {
try {
this.workflowManager.storePrompt({
id: res.prompt_id,
nodes: Object.keys(p.output)
});
} catch (error) {
}
}
} catch (error) {
const formattedError = this.#formatPromptError(error)
@ -2155,6 +2242,7 @@ export class ComfyApp {
this.#processingQueue = false;
}
api.dispatchEvent(new CustomEvent("promptQueued", { detail: { number, batchCount } }));
return !this.lastNodeErrors;
}
showErrorOnFileLoad(file) {
@ -2170,14 +2258,24 @@ export class ComfyApp {
* @param {File} file
*/
async handleFile(file) {
const removeExt = f => {
if(!f) return f;
const p = f.lastIndexOf(".");
if(p === -1) return f;
return f.substring(0, p);
};
const fileName = removeExt(file.name);
if (file.type === "image/png") {
const pngInfo = await getPngMetadata(file);
if (pngInfo?.workflow) {
await this.loadGraphData(JSON.parse(pngInfo.workflow));
await this.loadGraphData(JSON.parse(pngInfo.workflow), true, true, fileName);
} else if (pngInfo?.prompt) {
this.loadApiJson(JSON.parse(pngInfo.prompt));
this.loadApiJson(JSON.parse(pngInfo.prompt), fileName);
} else if (pngInfo?.parameters) {
importA1111(this.graph, pngInfo.parameters);
this.changeWorkflow(() => {
importA1111(this.graph, pngInfo.parameters);
}, fileName)
} else {
this.showErrorOnFileLoad(file);
}
@ -2188,9 +2286,22 @@ export class ComfyApp {
const prompt = pngInfo?.prompt || pngInfo?.Prompt;
if (workflow) {
this.loadGraphData(JSON.parse(workflow));
this.loadGraphData(JSON.parse(workflow), true, true, fileName);
} else if (prompt) {
this.loadApiJson(JSON.parse(prompt));
this.loadApiJson(JSON.parse(prompt), fileName);
} else {
this.showErrorOnFileLoad(file);
}
} else if (file.type === "audio/flac" || file.type === "audio/x-flac") {
const pngInfo = await getFlacMetadata(file);
// Support loading workflows from that webp custom node.
const workflow = pngInfo?.workflow;
const prompt = pngInfo?.prompt;
if (workflow) {
this.loadGraphData(JSON.parse(workflow), true, true, fileName);
} else if (prompt) {
this.loadApiJson(JSON.parse(prompt), fileName);
} else {
this.showErrorOnFileLoad(file);
}
@ -2201,16 +2312,16 @@ export class ComfyApp {
if (jsonContent?.templates) {
this.loadTemplateData(jsonContent);
} else if(this.isApiJson(jsonContent)) {
this.loadApiJson(jsonContent);
this.loadApiJson(jsonContent, fileName);
} else {
await this.loadGraphData(jsonContent);
await this.loadGraphData(jsonContent, true, true, fileName);
}
};
reader.readAsText(file);
} else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) {
const info = await getLatentMetadata(file);
if (info.workflow) {
await this.loadGraphData(JSON.parse(info.workflow));
await this.loadGraphData(JSON.parse(info.workflow), true, true, fileName);
} else if (info.prompt) {
this.loadApiJson(JSON.parse(info.prompt));
} else {
@ -2225,7 +2336,7 @@ export class ComfyApp {
return Object.values(data).every((v) => v.class_type);
}
loadApiJson(apiData) {
loadApiJson(apiData, fileName) {
const missingNodeTypes = Object.values(apiData).filter((n) => !LiteGraph.registered_node_types[n.class_type]);
if (missingNodeTypes.length) {
this.showMissingNodesError(missingNodeTypes.map(t => t.class_type), false);
@ -2242,38 +2353,39 @@ export class ComfyApp {
app.graph.add(node);
}
for (const id of ids) {
const data = apiData[id];
const node = app.graph.getNodeById(id);
for (const input in data.inputs ?? {}) {
const value = data.inputs[input];
if (value instanceof Array) {
const [fromId, fromSlot] = value;
const fromNode = app.graph.getNodeById(fromId);
let toSlot = node.inputs?.findIndex((inp) => inp.name === input);
if (toSlot == null || toSlot === -1) {
try {
// Target has no matching input, most likely a converted widget
const widget = node.widgets?.find((w) => w.name === input);
if (widget && node.convertWidgetToInput?.(widget)) {
toSlot = node.inputs?.length - 1;
}
} catch (error) {}
}
if (toSlot != null || toSlot !== -1) {
fromNode.connect(fromSlot, node, toSlot);
}
} else {
const widget = node.widgets?.find((w) => w.name === input);
if (widget) {
widget.value = value;
widget.callback?.(value);
this.changeWorkflow(() => {
for (const id of ids) {
const data = apiData[id];
const node = app.graph.getNodeById(id);
for (const input in data.inputs ?? {}) {
const value = data.inputs[input];
if (value instanceof Array) {
const [fromId, fromSlot] = value;
const fromNode = app.graph.getNodeById(fromId);
let toSlot = node.inputs?.findIndex((inp) => inp.name === input);
if (toSlot == null || toSlot === -1) {
try {
// Target has no matching input, most likely a converted widget
const widget = node.widgets?.find((w) => w.name === input);
if (widget && node.convertWidgetToInput?.(widget)) {
toSlot = node.inputs?.length - 1;
}
} catch (error) {}
}
if (toSlot != null || toSlot !== -1) {
fromNode.connect(fromSlot, node, toSlot);
}
} else {
const widget = node.widgets?.find((w) => w.name === input);
if (widget) {
widget.value = value;
widget.callback?.(value);
}
}
}
}
}
app.graph.arrange();
app.graph.arrange();
}, fileName);
}
/**

View File

@ -0,0 +1,254 @@
// @ts-check
import { api } from "./api.js";
import { clone } from "./utils.js";
export class ChangeTracker {
static MAX_HISTORY = 50;
#app;
undo = [];
redo = [];
activeState = null;
isOurLoad = false;
/** @type { import("./workflows").ComfyWorkflow | null } */
workflow;
ds;
nodeOutputs;
get app() {
return this.#app ?? this.workflow.manager.app;
}
constructor(workflow) {
this.workflow = workflow;
}
#setApp(app) {
this.#app = app;
}
store() {
this.ds = { scale: this.app.canvas.ds.scale, offset: [...this.app.canvas.ds.offset] };
}
restore() {
if (this.ds) {
this.app.canvas.ds.scale = this.ds.scale;
this.app.canvas.ds.offset = this.ds.offset;
}
if (this.nodeOutputs) {
this.app.nodeOutputs = this.nodeOutputs;
}
}
checkState() {
if (!this.app.graph) return;
const currentState = this.app.graph.serialize();
if (!this.activeState) {
this.activeState = clone(currentState);
return;
}
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
this.undo.push(this.activeState);
if (this.undo.length > ChangeTracker.MAX_HISTORY) {
this.undo.shift();
}
this.activeState = clone(currentState);
this.redo.length = 0;
this.workflow.unsaved = true;
api.dispatchEvent(new CustomEvent("graphChanged", { detail: this.activeState }));
}
}
async updateState(source, target) {
const prevState = source.pop();
if (prevState) {
target.push(this.activeState);
this.isOurLoad = true;
await this.app.loadGraphData(prevState, false, false, this.workflow);
this.activeState = prevState;
}
}
async undoRedo(e) {
if (e.ctrlKey || e.metaKey) {
if (e.key === "y") {
this.updateState(this.redo, this.undo);
return true;
} else if (e.key === "z") {
this.updateState(this.undo, this.redo);
return true;
}
}
}
/** @param { import("./app.js").ComfyApp } app */
static init(app) {
const changeTracker = () => app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker;
globalTracker.#setApp(app);
const loadGraphData = app.loadGraphData;
app.loadGraphData = async function () {
const v = await loadGraphData.apply(this, arguments);
const ct = changeTracker();
if (ct.isOurLoad) {
ct.isOurLoad = false;
} else {
ct.checkState();
}
return v;
};
let keyIgnored = false;
window.addEventListener(
"keydown",
(e) => {
requestAnimationFrame(async () => {
let activeEl;
// If we are auto queue in change mode then we do want to trigger on inputs
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
activeEl = document.activeElement;
if (activeEl?.tagName === "INPUT" || activeEl?.["type"] === "textarea") {
// Ignore events on inputs, they have their native history
return;
}
}
keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta";
if (keyIgnored) return;
// Check if this is a ctrl+z ctrl+y
if (await changeTracker().undoRedo(e)) return;
// If our active element is some type of input then handle changes after they're done
if (ChangeTracker.bindInput(activeEl)) return;
changeTracker().checkState();
});
},
true
);
window.addEventListener("keyup", (e) => {
if (keyIgnored) {
keyIgnored = false;
changeTracker().checkState();
}
});
// Handle clicking DOM elements (e.g. widgets)
window.addEventListener("mouseup", () => {
changeTracker().checkState();
});
// Handle prompt queue event for dynamic widget changes
api.addEventListener("promptQueued", () => {
changeTracker().checkState();
});
// Handle litegraph clicks
const processMouseUp = LGraphCanvas.prototype.processMouseUp;
LGraphCanvas.prototype.processMouseUp = function (e) {
const v = processMouseUp.apply(this, arguments);
changeTracker().checkState();
return v;
};
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
LGraphCanvas.prototype.processMouseDown = function (e) {
const v = processMouseDown.apply(this, arguments);
changeTracker().checkState();
return v;
};
// Handle litegraph context menu for COMBO widgets
const close = LiteGraph.ContextMenu.prototype.close;
LiteGraph.ContextMenu.prototype.close = function (e) {
const v = close.apply(this, arguments);
changeTracker().checkState();
return v;
};
// Detects nodes being added via the node search dialog
const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded;
LiteGraph.LGraph.prototype.onNodeAdded = function () {
const v = onNodeAdded?.apply(this, arguments);
if (!app?.configuringGraph) {
const ct = changeTracker();
if (!ct.isOurLoad) {
ct.checkState();
}
}
return v;
};
// Store node outputs
api.addEventListener("executed", ({ detail }) => {
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id];
if (!prompt?.workflow) return;
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {});
const output = nodeOutputs[detail.node];
if (detail.merge && output) {
for (const k in detail.output ?? {}) {
const v = output[k];
if (v instanceof Array) {
output[k] = v.concat(detail.output[k]);
} else {
output[k] = detail.output[k];
}
}
} else {
nodeOutputs[detail.node] = detail.output;
}
});
}
static bindInput(app, activeEl) {
if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") {
for (const evt of ["change", "input", "blur"]) {
if (`on${evt}` in activeEl) {
const listener = () => {
app.workflowManager.activeWorkflow.changeTracker.checkState();
activeEl.removeEventListener(evt, listener);
};
activeEl.addEventListener(evt, listener);
return true;
}
}
}
}
static graphEqual(a, b, path = "") {
if (a === b) return true;
if (typeof a == "object" && a && typeof b == "object" && b) {
const keys = Object.getOwnPropertyNames(a);
if (keys.length != Object.getOwnPropertyNames(b).length) {
return false;
}
for (const key of keys) {
let av = a[key];
let bv = b[key];
if (!path && key === "nodes") {
// Nodes need to be sorted as the order changes when selecting nodes
av = [...av].sort((a, b) => a.id - b.id);
bv = [...bv].sort((a, b) => a.id - b.id);
} else if (path === "extra.ds") {
// Ignore view changes
continue;
}
if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) {
return false;
}
}
return true;
}
return false;
}
}
const globalTracker = new ChangeTracker({});

View File

@ -34,8 +34,8 @@ function getClipPath(node, element) {
}
const widgetRect = element.getBoundingClientRect();
const clipX = intersection[0] - widgetRect.x / scale + "px";
const clipY = intersection[1] - widgetRect.y / scale + "px";
const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px";
const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px";
const clipWidth = intersection[2] + "px";
const clipHeight = intersection[3] + "px";
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`;
@ -210,6 +210,8 @@ LGraphNode.prototype.addDOMWidget = function (name, type, element, options) {
if (!element.parentElement) {
document.body.append(element);
}
element.hidden = true;
element.style.display = "none";
let mouseDownHandler;
if (element.blur) {
@ -254,15 +256,15 @@ LGraphNode.prototype.addDOMWidget = function (name, type, element, options) {
const transform = new DOMMatrix()
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + y);
.translateSelf(margin, margin + y );
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
Object.assign(element.style, {
transformOrigin: "0 0",
transform: scale,
left: `${transform.a + transform.e}px`,
top: `${transform.d + transform.f}px`,
left: `${transform.a + transform.e + elRect.left}px`,
top: `${transform.d + transform.f + elRect.top}px`,
width: `${widgetWidth - margin * 2}px`,
height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
position: "absolute",

View File

@ -163,6 +163,78 @@ export function getLatentMetadata(file) {
});
}
function getString(dataView, offset, length) {
let string = '';
for (let i = 0; i < length; i++) {
string += String.fromCharCode(dataView.getUint8(offset + i));
}
return string;
}
// Function to parse the Vorbis Comment block
function parseVorbisComment(dataView) {
let offset = 0;
const vendorLength = dataView.getUint32(offset, true);
offset += 4;
const vendorString = getString(dataView, offset, vendorLength);
offset += vendorLength;
const userCommentListLength = dataView.getUint32(offset, true);
offset += 4;
const comments = {};
for (let i = 0; i < userCommentListLength; i++) {
const commentLength = dataView.getUint32(offset, true);
offset += 4;
const comment = getString(dataView, offset, commentLength);
offset += commentLength;
const [key, value] = comment.split('=');
comments[key] = value;
}
return comments;
}
// Function to read a FLAC file and parse Vorbis comments
export function getFlacMetadata(file) {
return new Promise((r) => {
const reader = new FileReader();
reader.onload = function(event) {
const arrayBuffer = event.target.result;
const dataView = new DataView(arrayBuffer);
// Verify the FLAC signature
const signature = String.fromCharCode(...new Uint8Array(arrayBuffer, 0, 4));
if (signature !== 'fLaC') {
console.error('Not a valid FLAC file');
return;
}
// Parse metadata blocks
let offset = 4;
let vorbisComment = null;
while (offset < dataView.byteLength) {
const isLastBlock = dataView.getUint8(offset) & 0x80;
const blockType = dataView.getUint8(offset) & 0x7F;
const blockSize = dataView.getUint32(offset, false) & 0xFFFFFF;
offset += 4;
if (blockType === 4) { // Vorbis Comment block type
vorbisComment = parseVorbisComment(new DataView(arrayBuffer, offset, blockSize));
}
offset += blockSize;
if (isLastBlock) break;
}
r(vorbisComment);
};
reader.readAsArrayBuffer(file);
});
}
export async function importA1111(graph, parameters) {
const p = parameters.lastIndexOf("\nSteps:");
if (p > -1) {

View File

@ -6,17 +6,22 @@ import { ComfySettingsDialog } from "./ui/settings.js";
export const ComfyDialog = _ComfyDialog;
/**
*
* @param { string } tag HTML Element Tag and optional classes e.g. div.class1.class2
* @param { string | Element | Element[] | {
* @template { string | (keyof HTMLElementTagNameMap) } K
* @typedef { K extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[K] : HTMLElement } ElementType
*/
/**
* @template { string | (keyof HTMLElementTagNameMap) } K
* @param { K } tag HTML Element Tag and optional classes e.g. div.class1.class2
* @param { string | Element | Element[] | ({
* parent?: Element,
* $?: (el: Element) => void,
* $?: (el: ElementType<K>) => void,
* dataset?: DOMStringMap,
* style?: CSSStyleDeclaration,
* style?: Partial<CSSStyleDeclaration>,
* for?: string
* } | undefined } propsOrChildren
* @param { Element[] | undefined } [children]
* @returns
* } & Omit<Partial<ElementType<K>>, "style">) | undefined } [propsOrChildren]
* @param { string | Element | Element[] | undefined } [children]
* @returns { ElementType<K> }
*/
export function $el(tag, propsOrChildren, children) {
const split = tag.split(".");
@ -54,7 +59,7 @@ export function $el(tag, propsOrChildren, children) {
Object.assign(element, propsOrChildren);
if (children) {
element.append(...(children instanceof Array ? children : [children]));
element.append(...(children instanceof Array ? children.filter(Boolean) : [children]));
}
if (parent) {
@ -102,6 +107,8 @@ function dragElement(dragEl, settings) {
}
function positionElement() {
if(dragEl.style.display === "none") return;
const halfWidth = document.body.clientWidth / 2;
const anchorRight = newPosX + dragEl.clientWidth / 2 > halfWidth;
@ -191,6 +198,8 @@ function dragElement(dragEl, settings) {
document.onmouseup = null;
document.onmousemove = null;
}
return restorePos;
}
class ComfyList {
@ -364,7 +373,7 @@ export class ComfyUI {
const fileInput = $el("input", {
id: "comfy-file-input",
type: "file",
accept: ".json,image/png,.latent,.safetensors,image/webp",
accept: ".json,image/png,.latent,.safetensors,image/webp,audio/flac",
style: {display: "none"},
parent: document.body,
onchange: () => {
@ -372,6 +381,8 @@ export class ComfyUI {
},
});
this.loadFile = () => fileInput.click();
const autoQueueModeEl = toggleSwitch(
"autoQueueMode",
[
@ -621,10 +632,10 @@ export class ComfyUI {
name: "Enable Dev mode Options",
type: "boolean",
defaultValue: false,
onChange: function(value) { document.getElementById("comfy-dev-save-api-button").style.display = value ? "block" : "none"},
onChange: function(value) { document.getElementById("comfy-dev-save-api-button").style.display = value ? "flex" : "none"},
});
dragElement(this.menuContainer, this.settings);
this.restoreMenuPosition = dragElement(this.menuContainer, this.settings);
this.setStatus({exec_info: {queue_remaining: "X"}});
}

View File

@ -0,0 +1,64 @@
import { ComfyDialog } from "../dialog.js";
import { $el } from "../../ui.js";
export class ComfyAsyncDialog extends ComfyDialog {
#resolve;
constructor(actions) {
super(
"dialog.comfy-dialog.comfyui-dialog",
actions?.map((opt) => {
if (typeof opt === "string") {
opt = { text: opt };
}
return $el("button.comfyui-button", {
type: "button",
textContent: opt.text,
onclick: () => this.close(opt.value ?? opt.text),
});
})
);
}
show(html) {
this.element.addEventListener("close", () => {
this.close();
});
super.show(html);
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
showModal(html) {
this.element.addEventListener("close", () => {
this.close();
});
super.show(html);
this.element.showModal();
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
close(result = null) {
this.#resolve(result);
this.element.close();
super.close();
}
static async prompt({ title = null, message, actions }) {
const dialog = new ComfyAsyncDialog(actions);
const content = [$el("span", message)];
if (title) {
content.unshift($el("h3", title));
}
const res = await dialog.showModal(content);
dialog.element.remove();
return res;
}
}

View File

@ -0,0 +1,163 @@
// @ts-check
import { $el } from "../../ui.js";
import { applyClasses, toggleElement } from "../utils.js";
import { prop } from "../../utils.js";
/**
* @typedef {{
* icon?: string;
* overIcon?: string;
* iconSize?: number;
* content?: string | HTMLElement;
* tooltip?: string;
* enabled?: boolean;
* action?: (e: Event, btn: ComfyButton) => void,
* classList?: import("../utils.js").ClassList,
* visibilitySetting?: { id: string, showValue: any },
* app?: import("../../app.js").ComfyApp
* }} ComfyButtonProps
*/
export class ComfyButton {
#over = 0;
#popupOpen = false;
isOver = false;
iconElement = $el("i.mdi");
contentElement = $el("span");
/**
* @type {import("./popup.js").ComfyPopup}
*/
popup;
/**
* @param {ComfyButtonProps} opts
*/
constructor({
icon,
overIcon,
iconSize,
content,
tooltip,
action,
classList = "comfyui-button",
visibilitySetting,
app,
enabled = true,
}) {
this.element = $el("button", {
onmouseenter: () => {
this.isOver = true;
if(this.overIcon) {
this.updateIcon();
}
},
onmouseleave: () => {
this.isOver = false;
if(this.overIcon) {
this.updateIcon();
}
}
}, [this.iconElement, this.contentElement]);
this.icon = prop(this, "icon", icon, toggleElement(this.iconElement, { onShow: this.updateIcon }));
this.overIcon = prop(this, "overIcon", overIcon, () => {
if(this.isOver) {
this.updateIcon();
}
});
this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon);
this.content = prop(
this,
"content",
content,
toggleElement(this.contentElement, {
onShow: (el, v) => {
if (typeof v === "string") {
el.textContent = v;
} else {
el.replaceChildren(v);
}
},
})
);
this.tooltip = prop(this, "tooltip", tooltip, (v) => {
if (v) {
this.element.title = v;
} else {
this.element.removeAttribute("title");
}
});
this.classList = prop(this, "classList", classList, this.updateClasses);
this.hidden = prop(this, "hidden", false, this.updateClasses);
this.enabled = prop(this, "enabled", enabled, () => {
this.updateClasses();
this.element.disabled = !this.enabled;
});
this.action = prop(this, "action", action);
this.element.addEventListener("click", (e) => {
if (this.popup) {
// we are either a touch device or triggered by click not hover
if (!this.#over) {
this.popup.toggle();
}
}
this.action?.(e, this);
});
if (visibilitySetting?.id) {
const settingUpdated = () => {
this.hidden = app.ui.settings.getSettingValue(visibilitySetting.id) !== visibilitySetting.showValue;
};
app.ui.settings.addEventListener(visibilitySetting.id + ".change", settingUpdated);
settingUpdated();
}
}
updateIcon = () => (this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`);
updateClasses = () => {
const internalClasses = [];
if (this.hidden) {
internalClasses.push("hidden");
}
if (!this.enabled) {
internalClasses.push("disabled");
}
if (this.popup) {
if (this.#popupOpen) {
internalClasses.push("popup-open");
} else {
internalClasses.push("popup-closed");
}
}
applyClasses(this.element, this.classList, ...internalClasses);
};
/**
*
* @param { import("./popup.js").ComfyPopup } popup
* @param { "click" | "hover" } mode
*/
withPopup(popup, mode = "click") {
this.popup = popup;
if (mode === "hover") {
for (const el of [this.element, this.popup.element]) {
el.addEventListener("mouseenter", () => {
this.popup.open = !!++this.#over;
});
el.addEventListener("mouseleave", () => {
this.popup.open = !!--this.#over;
});
}
}
popup.addEventListener("change", () => {
this.#popupOpen = popup.open;
this.updateClasses();
});
return this;
}
}

View File

@ -0,0 +1,45 @@
// @ts-check
import { $el } from "../../ui.js";
import { ComfyButton } from "./button.js";
import { prop } from "../../utils.js";
export class ComfyButtonGroup {
element = $el("div.comfyui-button-group");
/** @param {Array<ComfyButton | HTMLElement>} buttons */
constructor(...buttons) {
this.buttons = prop(this, "buttons", buttons, () => this.update());
}
/**
* @param {ComfyButton} button
* @param {number} index
*/
insert(button, index) {
this.buttons.splice(index, 0, button);
this.update();
}
/** @param {ComfyButton} button */
append(button) {
this.buttons.push(button);
this.update();
}
/** @param {ComfyButton|number} indexOrButton */
remove(indexOrButton) {
if (typeof indexOrButton !== "number") {
indexOrButton = this.buttons.indexOf(indexOrButton);
}
if (indexOrButton > -1) {
const r = this.buttons.splice(indexOrButton, 1);
this.update();
return r;
}
}
update() {
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
}
}

View File

@ -0,0 +1,128 @@
// @ts-check
import { prop } from "../../utils.js";
import { $el } from "../../ui.js";
import { applyClasses } from "../utils.js";
export class ComfyPopup extends EventTarget {
element = $el("div.comfyui-popup");
/**
* @param {{
* target: HTMLElement,
* container?: HTMLElement,
* classList?: import("../utils.js").ClassList,
* ignoreTarget?: boolean,
* closeOnEscape?: boolean,
* position?: "absolute" | "relative",
* horizontal?: "left" | "right"
* }} param0
* @param {...HTMLElement} children
*/
constructor(
{
target,
container = document.body,
classList = "",
ignoreTarget = true,
closeOnEscape = true,
position = "absolute",
horizontal = "left",
},
...children
) {
super();
this.target = target;
this.ignoreTarget = ignoreTarget;
this.container = container;
this.position = position;
this.closeOnEscape = closeOnEscape;
this.horizontal = horizontal;
container.append(this.element);
this.children = prop(this, "children", children, () => {
this.element.replaceChildren(...this.children);
this.update();
});
this.classList = prop(this, "classList", classList, () => applyClasses(this.element, this.classList, "comfyui-popup", horizontal));
this.open = prop(this, "open", false, (v, o) => {
if (v === o) return;
if (v) {
this.#show();
} else {
this.#hide();
}
});
}
toggle() {
this.open = !this.open;
}
#hide() {
this.element.classList.remove("open");
window.removeEventListener("resize", this.update);
window.removeEventListener("click", this.#clickHandler, { capture: true });
window.removeEventListener("keydown", this.#escHandler, { capture: true });
this.dispatchEvent(new CustomEvent("close"));
this.dispatchEvent(new CustomEvent("change"));
}
#show() {
this.element.classList.add("open");
this.update();
window.addEventListener("resize", this.update);
window.addEventListener("click", this.#clickHandler, { capture: true });
if (this.closeOnEscape) {
window.addEventListener("keydown", this.#escHandler, { capture: true });
}
this.dispatchEvent(new CustomEvent("open"));
this.dispatchEvent(new CustomEvent("change"));
}
#escHandler = (e) => {
if (e.key === "Escape") {
this.open = false;
e.preventDefault();
e.stopImmediatePropagation();
}
};
#clickHandler = (e) => {
/** @type {any} */
const target = e.target;
if (!this.element.contains(target) && this.ignoreTarget && !this.target.contains(target)) {
this.open = false;
}
};
update = () => {
const rect = this.target.getBoundingClientRect();
this.element.style.setProperty("--bottom", "unset");
if (this.position === "absolute") {
if (this.horizontal === "left") {
this.element.style.setProperty("--left", rect.left + "px");
} else {
this.element.style.setProperty("--left", rect.right - this.element.clientWidth + "px");
}
this.element.style.setProperty("--top", rect.bottom + "px");
this.element.style.setProperty("--limit", rect.bottom + "px");
} else {
this.element.style.setProperty("--left", 0 + "px");
this.element.style.setProperty("--top", rect.height + "px");
this.element.style.setProperty("--limit", rect.height + "px");
}
const thisRect = this.element.getBoundingClientRect();
if (thisRect.height < 30) {
// Move up instead
this.element.style.setProperty("--top", "unset");
this.element.style.setProperty("--bottom", rect.height + 5 + "px");
this.element.style.setProperty("--limit", rect.height + 5 + "px");
}
};
}

View File

@ -0,0 +1,43 @@
// @ts-check
import { $el } from "../../ui.js";
import { ComfyButton } from "./button.js";
import { prop } from "../../utils.js";
import { ComfyPopup } from "./popup.js";
export class ComfySplitButton {
/**
* @param {{
* primary: ComfyButton,
* mode?: "hover" | "click",
* horizontal?: "left" | "right",
* position?: "relative" | "absolute"
* }} param0
* @param {Array<ComfyButton> | Array<HTMLElement>} items
*/
constructor({ primary, mode, horizontal = "left", position = "relative" }, ...items) {
this.arrow = new ComfyButton({
icon: "chevron-down",
});
this.element = $el("div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""), [
$el("div.comfyui-split-primary", primary.element),
$el("div.comfyui-split-arrow", this.arrow.element),
]);
this.popup = new ComfyPopup({
target: this.element,
container: position === "relative" ? this.element : document.body,
classList: "comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""),
closeOnEscape: mode === "click",
position,
horizontal,
});
this.arrow.withPopup(this.popup, mode);
this.items = prop(this, "items", items, () => this.update());
}
update() {
this.popup.element.replaceChildren(...this.items.map((b) => b.element ?? b));
}
}

View File

@ -1,20 +1,26 @@
import { $el } from "../ui.js";
export class ComfyDialog {
constructor() {
this.element = $el("div.comfy-modal", { parent: document.body }, [
export class ComfyDialog extends EventTarget {
#buttons;
constructor(type = "div", buttons = null) {
super();
this.#buttons = buttons;
this.element = $el(type + ".comfy-modal", { parent: document.body }, [
$el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]),
]);
}
createButtons() {
return [
$el("button", {
type: "button",
textContent: "Close",
onclick: () => this.close(),
}),
];
return (
this.#buttons ?? [
$el("button", {
type: "button",
textContent: "Close",
onclick: () => this.close(),
}),
]
);
}
close() {
@ -25,7 +31,7 @@ export class ComfyDialog {
if (typeof html === "string") {
this.textElement.innerHTML = html;
} else {
this.textElement.replaceChildren(html);
this.textElement.replaceChildren(...(html instanceof Array ? html : [html]));
}
this.element.style.display = "flex";
}

View File

@ -0,0 +1,302 @@
// @ts-check
import { $el } from "../../ui.js";
import { downloadBlob } from "../../utils.js";
import { ComfyButton } from "../components/button.js";
import { ComfyButtonGroup } from "../components/buttonGroup.js";
import { ComfySplitButton } from "../components/splitButton.js";
import { ComfyViewHistoryButton } from "./viewHistory.js";
import { ComfyQueueButton } from "./queueButton.js";
import { ComfyWorkflowsMenu } from "./workflows.js";
import { ComfyViewQueueButton } from "./viewQueue.js";
import { getInteruptButton } from "./interruptButton.js";
const collapseOnMobile = (t) => {
(t.element ?? t).classList.add("comfyui-menu-mobile-collapse");
return t;
};
const showOnMobile = (t) => {
(t.element ?? t).classList.add("lt-lg-show");
return t;
};
export class ComfyAppMenu {
#sizeBreak = "lg";
#lastSizeBreaks = {
lg: null,
md: null,
sm: null,
xs: null,
};
#sizeBreaks = Object.keys(this.#lastSizeBreaks);
#cachedInnerSize = null;
#cacheTimeout = null;
/**
* @param { import("../../app.js").ComfyApp } app
*/
constructor(app) {
this.app = app;
this.workflows = new ComfyWorkflowsMenu(app);
const getSaveButton = (t) =>
new ComfyButton({
icon: "content-save",
tooltip: "Save the current workflow",
action: () => app.workflowManager.activeWorkflow.save(),
content: t,
});
this.logo = $el("h1.comfyui-logo.nlg-hide", { title: "ComfyUI" }, "ComfyUI");
this.saveButton = new ComfySplitButton(
{
primary: getSaveButton(),
mode: "hover",
position: "absolute",
},
getSaveButton("Save"),
new ComfyButton({
icon: "content-save-edit",
content: "Save As",
tooltip: "Save the current graph as a new workflow",
action: () => app.workflowManager.activeWorkflow.save(true),
}),
new ComfyButton({
icon: "download",
content: "Export",
tooltip: "Export the current workflow as JSON",
action: () => this.exportWorkflow("workflow", "workflow"),
}),
new ComfyButton({
icon: "api",
content: "Export (API Format)",
tooltip: "Export the current workflow as JSON for use with the ComfyUI API",
action: () => this.exportWorkflow("workflow_api", "output"),
visibilitySetting: { id: "Comfy.DevMode", showValue: true },
app,
})
);
this.actionsGroup = new ComfyButtonGroup(
new ComfyButton({
icon: "refresh",
content: "Refresh",
tooltip: "Refresh widgets in nodes to find new models or files",
action: () => app.refreshComboInNodes(),
}),
new ComfyButton({
icon: "clipboard-edit-outline",
content: "Clipspace",
tooltip: "Open Clipspace window",
action: () => app["openClipspace"](),
}),
new ComfyButton({
icon: "fit-to-page-outline",
content: "Reset View",
tooltip: "Reset the canvas view",
action: () => app.resetView(),
}),
new ComfyButton({
icon: "cancel",
content: "Clear",
tooltip: "Clears current workflow",
action: () => {
if (!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || confirm("Clear workflow?")) {
app.clean();
app.graph.clear();
}
},
})
);
this.settingsGroup = new ComfyButtonGroup(
new ComfyButton({
icon: "cog",
content: "Settings",
tooltip: "Open settings",
action: () => {
app.ui.settings.show();
},
})
);
this.viewGroup = new ComfyButtonGroup(
new ComfyViewHistoryButton(app).element,
new ComfyViewQueueButton(app).element,
getInteruptButton("nlg-hide").element
);
this.mobileMenuButton = new ComfyButton({
icon: "menu",
action: (_, btn) => {
btn.icon = this.element.classList.toggle("expanded") ? "menu-open" : "menu";
window.dispatchEvent(new Event("resize"));
},
classList: "comfyui-button comfyui-menu-button",
});
this.element = $el("nav.comfyui-menu.lg", { style: { display: "none" } }, [
this.logo,
this.workflows.element,
this.saveButton.element,
collapseOnMobile(this.actionsGroup).element,
$el("section.comfyui-menu-push"),
collapseOnMobile(this.settingsGroup).element,
collapseOnMobile(this.viewGroup).element,
getInteruptButton("lt-lg-show").element,
new ComfyQueueButton(app).element,
showOnMobile(this.mobileMenuButton).element,
]);
let resizeHandler;
this.menuPositionSetting = app.ui.settings.addSetting({
id: "Comfy.UseNewMenu",
defaultValue: "Disabled",
name: "[Beta] Use new menu and workflow management. Note: On small screens the menu will always be at the top.",
type: "combo",
options: ["Disabled", "Top", "Bottom"],
onChange: async (v) => {
if (v && v !== "Disabled") {
if (!resizeHandler) {
resizeHandler = () => {
this.calculateSizeBreak();
};
window.addEventListener("resize", resizeHandler);
}
this.updatePosition(v);
} else {
if (resizeHandler) {
window.removeEventListener("resize", resizeHandler);
resizeHandler = null;
}
document.body.style.removeProperty("display");
app.ui.menuContainer.style.removeProperty("display");
this.element.style.display = "none";
app.ui.restoreMenuPosition();
}
window.dispatchEvent(new Event("resize"));
},
});
}
updatePosition(v) {
document.body.style.display = "grid";
this.app.ui.menuContainer.style.display = "none";
this.element.style.removeProperty("display");
this.position = v;
if (v === "Bottom") {
this.app.bodyBottom.append(this.element);
} else {
this.app.bodyTop.prepend(this.element);
}
this.calculateSizeBreak();
}
updateSizeBreak(idx, prevIdx, direction) {
const newSize = this.#sizeBreaks[idx];
if (newSize === this.#sizeBreak) return;
this.#cachedInnerSize = null;
clearTimeout(this.#cacheTimeout);
this.#sizeBreak = this.#sizeBreaks[idx];
for (let i = 0; i < this.#sizeBreaks.length; i++) {
const sz = this.#sizeBreaks[i];
if (sz === this.#sizeBreak) {
this.element.classList.add(sz);
} else {
this.element.classList.remove(sz);
}
if (i < idx) {
this.element.classList.add("lt-" + sz);
} else {
this.element.classList.remove("lt-" + sz);
}
}
if (idx) {
// We're on a small screen, force the menu at the top
if (this.position !== "Top") {
this.updatePosition("Top");
}
} else if (this.position != this.menuPositionSetting.value) {
// Restore user position
this.updatePosition(this.menuPositionSetting.value);
}
// Allow multiple updates, but prevent bouncing
if (!direction) {
direction = prevIdx - idx;
} else if (direction != prevIdx - idx) {
return;
}
this.calculateSizeBreak(direction);
}
calculateSizeBreak(direction = 0) {
let idx = this.#sizeBreaks.indexOf(this.#sizeBreak);
const currIdx = idx;
const innerSize = this.calculateInnerSize(idx);
if (window.innerWidth >= this.#lastSizeBreaks[this.#sizeBreaks[idx - 1]]) {
if (idx > 0) {
idx--;
}
} else if (innerSize > this.element.clientWidth) {
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(window.innerWidth, innerSize);
// We need to shrink
if (idx < this.#sizeBreaks.length - 1) {
idx++;
}
}
this.updateSizeBreak(idx, currIdx, direction);
}
calculateInnerSize(idx) {
// Cache the inner size to prevent too much calculation when resizing the window
clearTimeout(this.#cacheTimeout);
if (this.#cachedInnerSize) {
// Extend cache time
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
} else {
let innerSize = 0;
let count = 1;
for (const c of this.element.children) {
if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push
if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) continue; // ignore collapse items
innerSize += c.clientWidth;
count++;
}
innerSize += 8 * count;
this.#cachedInnerSize = innerSize;
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
}
return this.#cachedInnerSize;
}
/**
* @param {string} defaultName
*/
getFilename(defaultName) {
if (this.app.ui.settings.getSettingValue("Comfy.PromptFilename", true)) {
defaultName = prompt("Save workflow as:", defaultName);
if (!defaultName) return;
if (!defaultName.toLowerCase().endsWith(".json")) {
defaultName += ".json";
}
}
return defaultName;
}
/**
* @param {string} [filename]
* @param { "workflow" | "output" } [promptProperty]
*/
async exportWorkflow(filename, promptProperty) {
if (this.app.workflowManager.activeWorkflow?.path) {
filename = this.app.workflowManager.activeWorkflow.name;
}
const p = await this.app.graphToPrompt();
const json = JSON.stringify(p[promptProperty], null, 2);
const blob = new Blob([json], { type: "application/json" });
const file = this.getFilename(filename);
if (!file) return;
downloadBlob(file, blob);
}
}

View File

@ -0,0 +1,23 @@
// @ts-check
import { api } from "../../api.js";
import { ComfyButton } from "../components/button.js";
export function getInteruptButton(visibility) {
const btn = new ComfyButton({
icon: "close",
tooltip: "Cancel current generation",
enabled: false,
action: () => {
api.interrupt();
},
classList: ["comfyui-button", "comfyui-interrupt-button", visibility],
});
api.addEventListener("status", ({ detail }) => {
const sz = detail?.exec_info?.queue_remaining;
btn.enabled = sz > 0;
});
return btn;
}

View File

@ -0,0 +1,705 @@
.relative {
position: relative;
}
.hidden {
display: none !important;
}
.mdi.rotate270::before {
transform: rotate(270deg);
}
/* Generic */
.comfyui-button {
display: flex;
align-items: center;
gap: 0.5em;
cursor: pointer;
border: none;
border-radius: 4px;
padding: 4px 8px;
box-sizing: border-box;
margin: 0;
transition: box-shadow 0.1s;
}
.comfyui-button:active {
box-shadow: inset 1px 1px 10px rgba(0, 0, 0, 0.5);
}
.comfyui-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.primary .comfyui-button,
.primary.comfyui-button {
background-color: var(--primary-bg) !important;
color: var(--primary-fg) !important;
}
.primary .comfyui-button:not(:disabled):hover,
.primary.comfyui-button:not(:disabled):hover {
background-color: var(--primary-hover-bg) !important;
color: var(--primary-hover-fg) !important;
}
/* Popup */
.comfyui-popup {
position: absolute;
left: var(--left);
right: var(--right);
top: var(--top);
bottom: var(--bottom);
z-index: 2000;
max-height: calc(100vh - var(--limit) - 10px);
box-shadow: 3px 3px 5px 0px rgba(0, 0, 0, 0.3);
}
.comfyui-popup:not(.open) {
display: none;
}
.comfyui-popup.right.open {
border-top-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
overflow: hidden;
}
/* Split button */
.comfyui-split-button {
position: relative;
display: flex;
}
.comfyui-split-primary {
flex: auto;
}
.comfyui-split-primary .comfyui-button {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 1px solid var(--comfy-menu-bg);
width: 100%;
}
.comfyui-split-arrow .comfyui-button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding-left: 2px;
padding-right: 2px;
}
.comfyui-split-button-popup {
white-space: nowrap;
background-color: var(--content-bg);
color: var(--content-fg);
display: flex;
flex-direction: column;
overflow: auto;
}
.comfyui-split-button-popup.hover {
z-index: 2001;
}
.comfyui-split-button-popup > .comfyui-button {
border: none;
background-color: transparent;
color: var(--fg-color);
padding: 8px 12px 8px 8px;
}
.comfyui-split-button-popup > .comfyui-button:not(:disabled):hover {
background-color: var(--comfy-input-bg);
}
/* Button group */
.comfyui-button-group {
display: flex;
border-radius: 4px;
overflow: hidden;
}
.comfyui-button-group > .comfyui-button,
.comfyui-button-group > .comfyui-button-wrapper > .comfyui-button {
padding: 4px 10px;
border-radius: 0;
}
/* Menu */
.comfyui-menu {
width: 100vw;
background: var(--comfy-menu-bg);
color: var(--fg-color);
font-family: Arial, Helvetica, sans-serif;
font-size: 0.8em;
display: flex;
padding: 4px 8px;
align-items: center;
gap: 8px;
box-sizing: border-box;
z-index: 1000;
order: 0;
grid-column: 1/-1;
overflow: auto;
max-height: 90vh;
}
.comfyui-menu>* {
flex-shrink: 0;
}
.comfyui-menu .mdi::before {
font-size: 18px;
}
.comfyui-menu .comfyui-button {
background: var(--comfy-input-bg);
color: var(--fg-color);
white-space: nowrap;
}
.comfyui-menu .comfyui-button:not(:disabled):hover {
background: var(--border-color);
color: var(--content-fg);
}
.comfyui-menu .comfyui-split-button-popup > .comfyui-button {
border-radius: 0;
background-color: transparent;
}
.comfyui-menu .comfyui-split-button-popup > .comfyui-button:not(:disabled):hover {
background-color: var(--comfy-input-bg);
}
.comfyui-menu .comfyui-split-button-popup.left {
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.comfyui-menu .comfyui-button.popup-open {
background-color: var(--content-bg);
color: var(--content-fg);
}
.comfyui-menu-push {
margin-left: -0.8em;
flex: auto;
}
.comfyui-logo {
font-size: 1.2em;
margin: 0;
user-select: none;
cursor: default;
}
/* Workflows */
.comfyui-workflows-button {
flex-direction: row-reverse;
max-width: 200px;
position: relative;
z-index: 0;
}
.comfyui-workflows-button.popup-open {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.comfyui-workflows-button.unsaved {
font-style: italic;
}
.comfyui-workflows-button-progress {
position: absolute;
top: 0;
left: 0;
background-color: green;
height: 100%;
border-radius: 4px;
z-index: -1;
}
.comfyui-workflows-button > span {
flex: auto;
text-align: left;
overflow: hidden;
}
.comfyui-workflows-button-inner {
display: flex;
align-items: center;
gap: 7px;
width: 150px;
}
.comfyui-workflows-label {
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
flex: auto;
position: relative;
}
.comfyui-workflows-button.unsaved .comfyui-workflows-label {
padding-left: 8px;
}
.comfyui-workflows-button.unsaved .comfyui-workflows-label:after {
content: "*";
position: absolute;
top: 0;
left: 0;
}
.comfyui-workflows-button-inner .mdi-graph::before {
transform: rotate(-90deg);
}
.comfyui-workflows-popup {
font-family: Arial, Helvetica, sans-serif;
font-size: 0.8em;
padding: 10px;
overflow: auto;
background-color: var(--content-bg);
color: var(--content-fg);
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
z-index: 400;
}
.comfyui-workflows-panel {
min-height: 150px;
}
.comfyui-workflows-panel .lds-ring {
transform: translate(-50%);
position: absolute;
left: 50%;
top: 75px;
}
.comfyui-workflows-panel h3 {
margin: 10px 0 10px 0;
font-size: 11px;
opacity: 0.8;
}
.comfyui-workflows-panel section header {
display: flex;
justify-content: space-between;
align-items: center;
}
.comfy-ui-workflows-search .mdi {
position: relative;
top: 2px;
pointer-events: none;
}
.comfy-ui-workflows-search input {
background-color: var(--comfy-input-bg);
color: var(--input-text);
border: none;
border-radius: 4px;
padding: 4px 10px;
margin-left: -24px;
text-indent: 18px;
}
.comfy-ui-workflows-search input:placeholder-shown {
width: 10px;
}
.comfy-ui-workflows-search input:placeholder-shown:focus {
width: auto;
}
.comfyui-workflows-actions {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.comfyui-workflows-actions .comfyui-button {
background: var(--comfy-input-bg);
color: var(--input-text);
}
.comfyui-workflows-actions .comfyui-button:not(:disabled):hover {
background: var(--primary-bg);
color: var(--primary-fg);
}
.comfyui-workflows-favorites,
.comfyui-workflows-open {
border-bottom: 1px solid var(--comfy-input-bg);
padding-bottom: 5px;
margin-bottom: 5px;
}
.comfyui-workflows-open .active {
font-weight: bold;
}
.comfyui-workflows-favorites:empty {
display: none;
}
.comfyui-workflows-tree {
padding: 0;
margin: 0;
}
.comfyui-workflows-tree:empty::after {
content: "No saved workflows";
display: block;
text-align: center;
}
.comfyui-workflows-tree > ul {
padding: 0;
}
.comfyui-workflows-tree > ul ul {
margin: 0;
padding: 0 0 0 25px;
}
.comfyui-workflows-tree:not(.filtered) .closed > ul {
display: none;
}
.comfyui-workflows-tree li,
.comfyui-workflows-tree-file {
--item-height: 32px;
list-style-type: none;
height: var(--item-height);
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
user-select: none;
}
.comfyui-workflows-tree-file.active::before,
.comfyui-workflows-tree li:hover::before,
.comfyui-workflows-tree-file:hover::before {
content: "";
position: absolute;
width: 100%;
left: 0;
height: var(--item-height);
background-color: var(--content-hover-bg);
color: var(--content-hover-fg);
z-index: -1;
}
.comfyui-workflows-tree-file.active::before {
background-color: var(--primary-bg);
color: var(--primary-fg);
}
.comfyui-workflows-tree-file.running:not(:hover)::before {
content: "";
position: absolute;
width: var(--progress, 0);
left: 0;
height: var(--item-height);
background-color: green;
z-index: -1;
}
.comfyui-workflows-tree-file.unsaved span {
font-style: italic;
}
.comfyui-workflows-tree-file span {
flex: auto;
}
.comfyui-workflows-tree-file span + .comfyui-workflows-file-action {
margin-left: 10px;
}
.comfyui-workflows-tree-file .comfyui-workflows-file-action {
background-color: transparent;
color: var(--fg-color);
padding: 2px 4px;
}
.lg ~ .comfyui-workflows-popup .comfyui-workflows-tree-file:not(:hover) .comfyui-workflows-file-action {
opacity: 0;
}
.comfyui-workflows-tree-file .comfyui-workflows-file-action:hover {
background-color: var(--primary-bg);
color: var(--primary-fg);
}
.comfyui-workflows-tree-file .comfyui-workflows-file-action-primary {
background-color: transparent;
color: var(--fg-color);
padding: 2px 4px;
margin: 0 -4px;
}
.comfyui-workflows-file-action-favorite .mdi-star {
color: orange;
}
/* View List */
.comfyui-view-list-popup {
padding: 10px;
background-color: var(--content-bg);
color: var(--content-fg);
min-width: 170px;
min-height: 435px;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}
.comfyui-view-list-popup h3 {
margin: 0 0 5px 0;
}
.comfyui-view-list-items {
width: 100%;
background: var(--comfy-menu-bg);
border-radius: 5px;
display: flex;
justify-content: center;
flex: auto;
align-items: center;
flex-direction: column;
}
.comfyui-view-list-items section {
max-height: 400px;
overflow: auto;
width: 100%;
display: grid;
grid-template-columns: auto auto auto;
align-items: center;
justify-content: center;
gap: 5px;
padding: 5px 0;
}
.comfyui-view-list-items section + section {
border-top: 1px solid var(--border-color);
margin-top: 10px;
padding-top: 5px;
}
.comfyui-view-list-items section h5 {
grid-column: 1 / 4;
text-align: center;
margin: 5px;
}
.comfyui-view-list-items span {
text-align: center;
padding: 0 2px;
}
.comfyui-view-list-popup header {
margin-bottom: 10px;
display: flex;
gap: 5px;
}
.comfyui-view-list-popup header .comfyui-button {
border: 1px solid transparent;
}
.comfyui-view-list-popup header .comfyui-button:not(:disabled):hover {
border: 1px solid var(--comfy-menu-bg);
}
/* Queue button */
.comfyui-queue-button .comfyui-split-primary .comfyui-button {
padding-right: 12px;
}
.comfyui-queue-count {
margin-left: 5px;
border-radius: 10px;
background-color: rgb(8, 80, 153);
padding: 2px 4px;
font-size: 10px;
min-width: 1em;
display: inline-block;
}
/* Queue options*/
.comfyui-queue-options {
padding: 10px;
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
display: flex;
gap: 10px;
}
.comfyui-queue-batch {
display: flex;
flex-direction: column;
border-right: 1px solid var(--comfy-menu-bg);
padding-right: 10px;
gap: 5px;
}
.comfyui-queue-batch input {
width: 145px;
}
.comfyui-queue-batch .comfyui-queue-batch-value {
width: 70px;
}
.comfyui-queue-mode {
display: flex;
flex-direction: column;
}
.comfyui-queue-mode span {
font-weight: bold;
margin-bottom: 2px;
}
.comfyui-queue-mode label {
display: flex;
flex-direction: row-reverse;
justify-content: start;
gap: 5px;
padding: 2px 0;
}
.comfyui-queue-mode label input {
padding: 0;
margin: 0;
}
/** Send to workflow widget selection dialog */
.comfy-widget-selection-dialog {
border: none;
}
.comfy-widget-selection-dialog div {
color: var(--fg-color);
font-family: Arial, Helvetica, sans-serif;
}
.comfy-widget-selection-dialog h2 {
margin-top: 0;
}
.comfy-widget-selection-dialog section {
width: fit-content;
display: flex;
flex-direction: column;
}
.comfy-widget-selection-item {
display: flex;
gap: 10px;
align-items: center;
}
.comfy-widget-selection-item span {
margin-right: auto;
}
.comfy-widget-selection-item span::before {
content: '#' attr(data-id);
opacity: 0.5;
margin-right: 5px;
}
.comfy-modal .comfy-widget-selection-item button {
font-size: 1em;
}
/***** Responsive *****/
.lg.comfyui-menu .lt-lg-show {
display: none !important;
}
.comfyui-menu:not(.lg) .nlg-hide {
display: none !important;
}
/** Large screen */
.lg.comfyui-menu>.comfyui-menu-mobile-collapse .comfyui-button span,
.lg.comfyui-menu>.comfyui-menu-mobile-collapse.comfyui-button span {
display: none;
}
.lg.comfyui-menu>.comfyui-menu-mobile-collapse .comfyui-popup .comfyui-button span {
display: unset;
}
/** Non large screen */
.lt-lg.comfyui-menu {
flex-wrap: wrap;
}
.lt-lg.comfyui-menu > *:not(.comfyui-menu-mobile-collapse) {
order: 1;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse {
order: 9999;
width: 100%;
}
.comfyui-body-bottom .lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse {
order: -1;
}
.comfyui-body-bottom .lt-lg.comfyui-menu > .comfyui-menu-button {
top: unset;
bottom: 4px;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse.comfyui-button-group {
flex-wrap: wrap;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button,
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse.comfyui-button {
padding: 10px;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button,
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button-wrapper {
width: 100%;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-popup {
position: static;
background-color: var(--comfy-input-bg);
max-width: unset;
max-height: 50vh;
overflow: auto;
}
.lt-lg.comfyui-menu:not(.expanded) > .comfyui-menu-mobile-collapse {
display: none;
}
.lt-lg .comfyui-queue-button {
margin-right: 44px;
}
.lt-lg .comfyui-menu-button {
position: absolute;
top: 4px;
right: 8px;
}
.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-view-list-popup {
border-radius: 0;
}
.lt-lg.comfyui-menu .comfyui-workflows-popup {
width: 100vw;
}
/** Small */
.lt-md .comfyui-workflows-button-inner {
width: unset !important;
}
.lt-md .comfyui-workflows-label {
display: none;
}
/** Extra small */
.lt-sm .comfyui-queue-button {
margin-right: 0;
width: 100%;
}
.lt-sm .comfyui-queue-button .comfyui-button {
justify-content: center;
}
.lt-sm .comfyui-interrupt-button {
margin-right: 45px;
}
.comfyui-body-bottom .lt-sm.comfyui-menu > .comfyui-menu-button{
bottom: 41px;
}

View File

@ -0,0 +1,93 @@
// @ts-check
import { ComfyButton } from "../components/button.js";
import { $el } from "../../ui.js";
import { api } from "../../api.js";
import { ComfySplitButton } from "../components/splitButton.js";
import { ComfyQueueOptions } from "./queueOptions.js";
import { prop } from "../../utils.js";
export class ComfyQueueButton {
element = $el("div.comfyui-queue-button");
#internalQueueSize = 0;
queuePrompt = async (e) => {
this.#internalQueueSize += this.queueOptions.batchCount;
// Hold shift to queue front, event is undefined when auto-queue is enabled
await this.app.queuePrompt(e?.shiftKey ? -1 : 0, this.queueOptions.batchCount);
};
constructor(app) {
this.app = app;
this.queueSizeElement = $el("span.comfyui-queue-count", {
textContent: "?",
});
const queue = new ComfyButton({
content: $el("div", [
$el("span", {
textContent: "Queue",
}),
this.queueSizeElement,
]),
icon: "play",
classList: "comfyui-button",
action: this.queuePrompt,
});
this.queueOptions = new ComfyQueueOptions(app);
const btn = new ComfySplitButton(
{
primary: queue,
mode: "click",
position: "absolute",
horizontal: "right",
},
this.queueOptions.element
);
btn.element.classList.add("primary");
this.element.append(btn.element);
this.autoQueueMode = prop(this, "autoQueueMode", "", () => {
switch (this.autoQueueMode) {
case "instant":
queue.icon = "infinity";
break;
case "change":
queue.icon = "auto-mode";
break;
default:
queue.icon = "play";
break;
}
});
this.queueOptions.addEventListener("autoQueueMode", (e) => (this.autoQueueMode = e["detail"]));
api.addEventListener("graphChanged", () => {
if (this.autoQueueMode === "change") {
if (this.#internalQueueSize) {
this.graphHasChanged = true;
} else {
this.graphHasChanged = false;
this.queuePrompt();
}
}
});
api.addEventListener("status", ({ detail }) => {
this.#internalQueueSize = detail?.exec_info?.queue_remaining;
if (this.#internalQueueSize != null) {
this.queueSizeElement.textContent = this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + "";
this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`;
if (!this.#internalQueueSize && !app.lastExecutionError) {
if (this.autoQueueMode === "instant" || (this.autoQueueMode === "change" && this.graphHasChanged)) {
this.graphHasChanged = false;
this.queuePrompt();
}
}
}
});
}
}

View File

@ -0,0 +1,77 @@
// @ts-check
import { $el } from "../../ui.js";
import { prop } from "../../utils.js";
export class ComfyQueueOptions extends EventTarget {
element = $el("div.comfyui-queue-options");
constructor(app) {
super();
this.app = app;
this.batchCountInput = $el("input", {
className: "comfyui-queue-batch-value",
type: "number",
min: "1",
value: "1",
oninput: () => (this.batchCount = +this.batchCountInput.value),
});
this.batchCountRange = $el("input", {
type: "range",
min: "1",
max: "100",
value: "1",
oninput: () => (this.batchCount = +this.batchCountRange.value),
});
this.element.append(
$el("div.comfyui-queue-batch", [
$el(
"label",
{
textContent: "Batch count: ",
},
this.batchCountInput
),
this.batchCountRange,
])
);
const createOption = (text, value, checked = false) =>
$el(
"label",
{ textContent: text },
$el("input", {
type: "radio",
name: "AutoQueueMode",
checked,
value,
oninput: (e) => (this.autoQueueMode = e.target["value"]),
})
);
this.autoQueueEl = $el("div.comfyui-queue-mode", [
$el("span", "Auto Queue:"),
createOption("Disabled", "", true),
createOption("Instant", "instant"),
createOption("On Change", "change"),
]);
this.element.append(this.autoQueueEl);
this.batchCount = prop(this, "batchCount", 1, () => {
this.batchCountInput.value = this.batchCount + "";
this.batchCountRange.value = this.batchCount + "";
});
this.autoQueueMode = prop(this, "autoQueueMode", "Disabled", () => {
this.dispatchEvent(
new CustomEvent("autoQueueMode", {
detail: this.autoQueueMode,
})
);
});
}
}

View File

@ -0,0 +1,27 @@
// @ts-check
import { ComfyButton } from "../components/button.js";
import { ComfyViewList, ComfyViewListButton } from "./viewList.js";
export class ComfyViewHistoryButton extends ComfyViewListButton {
constructor(app) {
super(app, {
button: new ComfyButton({
content: "View History",
icon: "history",
tooltip: "View history",
classList: "comfyui-button comfyui-history-button",
}),
list: ComfyViewHistoryList,
mode: "History",
});
}
}
export class ComfyViewHistoryList extends ComfyViewList {
async loadItems() {
const items = await super.loadItems();
items["History"].reverse();
return items;
}
}

View File

@ -0,0 +1,203 @@
// @ts-check
import { ComfyButton } from "../components/button.js";
import { $el } from "../../ui.js";
import { api } from "../../api.js";
import { ComfyPopup } from "../components/popup.js";
export class ComfyViewListButton {
get open() {
return this.popup.open;
}
set open(open) {
this.popup.open = open;
}
constructor(app, { button, list, mode }) {
this.app = app;
this.button = button;
this.element = $el("div.comfyui-button-wrapper", this.button.element);
this.popup = new ComfyPopup({
target: this.element,
container: this.element,
horizontal: "right",
});
this.list = new (list ?? ComfyViewList)(app, mode, this.popup);
this.popup.children = [this.list.element];
this.popup.addEventListener("open", () => {
this.list.update();
});
this.popup.addEventListener("close", () => {
this.list.close();
});
this.button.withPopup(this.popup);
api.addEventListener("status", () => {
if (this.popup.open) {
this.popup.update();
}
});
}
}
export class ComfyViewList {
popup;
constructor(app, mode, popup) {
this.app = app;
this.mode = mode;
this.popup = popup;
this.type = mode.toLowerCase();
this.items = $el(`div.comfyui-${this.type}-items.comfyui-view-list-items`);
this.clear = new ComfyButton({
icon: "cancel",
content: "Clear",
action: async () => {
this.showSpinner(false);
await api.clearItems(this.type);
await this.update();
},
});
this.refresh = new ComfyButton({
icon: "refresh",
content: "Refresh",
action: async () => {
await this.update(false);
},
});
this.element = $el(`div.comfyui-${this.type}-popup.comfyui-view-list-popup`, [
$el("h3", mode),
$el("header", [this.clear.element, this.refresh.element]),
this.items,
]);
api.addEventListener("status", () => {
if (this.popup.open) {
this.update();
}
});
}
async close() {
this.items.replaceChildren();
}
async update(resize = true) {
this.showSpinner(resize);
const res = await this.loadItems();
let any = false;
const names = Object.keys(res);
const sections = names
.map((section) => {
const items = res[section];
if (items?.length) {
any = true;
} else {
return;
}
const rows = [];
if (names.length > 1) {
rows.push($el("h5", section));
}
rows.push(...items.flatMap((item) => this.createRow(item, section)));
return $el("section", rows);
})
.filter(Boolean);
if (any) {
this.items.replaceChildren(...sections);
} else {
this.items.replaceChildren($el("h5", "None"));
}
this.popup.update();
this.clear.enabled = this.refresh.enabled = true;
this.element.style.removeProperty("height");
}
showSpinner(resize = true) {
// if (!this.spinner) {
// this.spinner = createSpinner();
// }
// if (!resize) {
// this.element.style.height = this.element.clientHeight + "px";
// }
// this.clear.enabled = this.refresh.enabled = false;
// this.items.replaceChildren(
// $el(
// "div",
// {
// style: {
// fontSize: "18px",
// },
// },
// this.spinner
// )
// );
// this.popup.update();
}
async loadItems() {
return await api.getItems(this.type);
}
getRow(item, section) {
return {
text: item.prompt[0] + "",
actions: [
{
text: "Load",
action: async () => {
try {
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
if (item.outputs) {
this.app.nodeOutputs = item.outputs;
}
} catch (error) {
alert("Error loading workflow: " + error.message);
console.error(error);
}
},
},
{
text: "Delete",
action: async () => {
try {
await api.deleteItem(this.type, item.prompt[1]);
this.update();
} catch (error) {}
},
},
],
};
}
createRow = (item, section) => {
const row = this.getRow(item, section);
return [
$el("span", row.text),
...row.actions.map(
(a) =>
new ComfyButton({
content: a.text,
action: async (e, btn) => {
btn.enabled = false;
try {
await a.action();
} catch (error) {
throw error;
} finally {
btn.enabled = true;
}
},
}).element
),
];
};
}

View File

@ -0,0 +1,55 @@
// @ts-check
import { ComfyButton } from "../components/button.js";
import { ComfyViewList, ComfyViewListButton } from "./viewList.js";
import { api } from "../../api.js";
export class ComfyViewQueueButton extends ComfyViewListButton {
constructor(app) {
super(app, {
button: new ComfyButton({
content: "View Queue",
icon: "format-list-numbered",
tooltip: "View queue",
classList: "comfyui-button comfyui-queue-button",
}),
list: ComfyViewQueueList,
mode: "Queue",
});
}
}
export class ComfyViewQueueList extends ComfyViewList {
getRow = (item, section) => {
if (section !== "Running") {
return super.getRow(item, section);
}
return {
text: item.prompt[0] + "",
actions: [
{
text: "Load",
action: async () => {
try {
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
if (item.outputs) {
this.app.nodeOutputs = item.outputs;
}
} catch (error) {
alert("Error loading workflow: " + error.message);
console.error(error);
}
},
},
{
text: "Cancel",
action: async () => {
try {
await api.interrupt();
} catch (error) {}
},
},
],
};
}
}

View File

@ -0,0 +1,764 @@
// @ts-check
import { ComfyButton } from "../components/button.js";
import { prop, getStorageValue, setStorageValue } from "../../utils.js";
import { $el } from "../../ui.js";
import { api } from "../../api.js";
import { ComfyPopup } from "../components/popup.js";
import { createSpinner } from "../spinner.js";
import { ComfyWorkflow, trimJsonExt } from "../../workflows.js";
import { ComfyAsyncDialog } from "../components/asyncDialog.js";
export class ComfyWorkflowsMenu {
#first = true;
element = $el("div.comfyui-workflows");
get open() {
return this.popup.open;
}
set open(open) {
this.popup.open = open;
}
/**
* @param {import("../../app.js").ComfyApp} app
*/
constructor(app) {
this.app = app;
this.#bindEvents();
const classList = {
"comfyui-workflows-button": true,
"comfyui-button": true,
unsaved: getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true",
running: false,
};
this.buttonProgress = $el("div.comfyui-workflows-button-progress");
this.workflowLabel = $el("span.comfyui-workflows-label", "");
this.button = new ComfyButton({
content: $el("div.comfyui-workflows-button-inner", [$el("i.mdi.mdi-graph"), this.workflowLabel, this.buttonProgress]),
icon: "chevron-down",
classList,
});
this.element.append(this.button.element);
this.popup = new ComfyPopup({ target: this.element, classList: "comfyui-workflows-popup" });
this.content = new ComfyWorkflowsContent(app, this.popup);
this.popup.children = [this.content.element];
this.popup.addEventListener("change", () => {
this.button.icon = "chevron-" + (this.popup.open ? "up" : "down");
});
this.button.withPopup(this.popup);
this.unsaved = prop(this, "unsaved", classList.unsaved, (v) => {
classList.unsaved = v;
this.button.classList = classList;
setStorageValue("Comfy.PreviousWorkflowUnsaved", v);
});
}
#updateProgress = () => {
const prompt = this.app.workflowManager.activePrompt;
let percent = 0;
if (this.app.workflowManager.activeWorkflow === prompt?.workflow) {
const total = Object.values(prompt.nodes);
const done = total.filter(Boolean);
percent = (done.length / total.length) * 100;
}
this.buttonProgress.style.width = percent + "%";
};
#updateActive = () => {
const active = this.app.workflowManager.activeWorkflow;
this.button.tooltip = active.path;
this.workflowLabel.textContent = active.name;
this.unsaved = active.unsaved;
if (this.#first) {
this.#first = false;
this.content.load();
}
this.#updateProgress();
};
#bindEvents() {
this.app.workflowManager.addEventListener("changeWorkflow", this.#updateActive);
this.app.workflowManager.addEventListener("rename", this.#updateActive);
this.app.workflowManager.addEventListener("delete", this.#updateActive);
this.app.workflowManager.addEventListener("save", () => {
this.unsaved = this.app.workflowManager.activeWorkflow.unsaved;
});
this.app.workflowManager.addEventListener("execute", (e) => {
this.#updateProgress();
});
api.addEventListener("graphChanged", () => {
this.unsaved = true;
});
}
#getMenuOptions(callback) {
const menu = [];
const directories = new Map();
for (const workflow of this.app.workflowManager.workflows || []) {
const path = workflow.pathParts;
if (!path) continue;
let parent = menu;
let currentPath = "";
for (let i = 0; i < path.length - 1; i++) {
currentPath += "/" + path[i];
let newParent = directories.get(currentPath);
if (!newParent) {
newParent = {
title: path[i],
has_submenu: true,
submenu: {
options: [],
},
};
parent.push(newParent);
newParent = newParent.submenu.options;
directories.set(currentPath, newParent);
}
parent = newParent;
}
parent.push({
title: trimJsonExt(path[path.length - 1]),
callback: () => callback(workflow),
});
}
return menu;
}
#getFavoriteMenuOptions(callback) {
const menu = [];
for (const workflow of this.app.workflowManager.workflows || []) {
if (workflow.isFavorite) {
menu.push({
title: "⭐ " + workflow.name,
callback: () => callback(workflow),
});
}
}
return menu;
}
/**
* @param {import("../../app.js").ComfyApp} app
*/
registerExtension(app) {
const self = this;
app.registerExtension({
name: "Comfy.Workflows",
async beforeRegisterNodeDef(nodeType) {
function getImageWidget(node) {
const inputs = { ...node.constructor?.nodeData?.input?.required, ...node.constructor?.nodeData?.input?.optional };
for (const input in inputs) {
if (inputs[input][0] === "IMAGEUPLOAD") {
const imageWidget = node.widgets.find((w) => w.name === (inputs[input]?.[1]?.widget ?? "image"));
if (imageWidget) return imageWidget;
}
}
}
function setWidgetImage(node, widget, img) {
const url = new URL(img.src);
const filename = url.searchParams.get("filename");
const subfolder = url.searchParams.get("subfolder");
const type = url.searchParams.get("type");
const imageId = `${subfolder ? subfolder + "/" : ""}${filename} [${type}]`;
widget.value = imageId;
node.imgs = [img];
app.graph.setDirtyCanvas(true, true);
}
/**
* @param {HTMLImageElement} img
* @param {ComfyWorkflow} workflow
*/
async function sendToWorkflow(img, workflow) {
await workflow.load();
let options = [];
const nodes = app.graph.computeExecutionOrder(false);
for (const node of nodes) {
const widget = getImageWidget(node);
if (widget == null) continue;
if (node.title?.toLowerCase().includes("input")) {
options = [{ widget, node }];
break;
} else {
options.push({ widget, node });
}
}
if (!options.length) {
alert("No image nodes have been found in this workflow!");
return;
} else if (options.length > 1) {
const dialog = new WidgetSelectionDialog(options);
const res = await dialog.show(app);
if (!res) return;
options = [res];
}
setWidgetImage(options[0].node, options[0].widget, img);
}
const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"];
nodeType.prototype["getExtraMenuOptions"] = function (_, options) {
const r = getExtraMenuOptions?.apply?.(this, arguments);
if (app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true) {
const t = /** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (this);
let img;
if (t.imageIndex != null) {
// An image is selected so select that
img = t.imgs?.[t.imageIndex];
} else if (t.overIndex != null) {
// No image is selected but one is hovered
img = t.img?.s[t.overIndex];
}
if (img) {
let pos = options.findIndex((o) => o.content === "Save Image");
if (pos === -1) {
pos = 0;
} else {
pos++;
}
options.splice(pos, 0, {
content: "Send to workflow",
has_submenu: true,
submenu: {
options: [
{
callback: () => sendToWorkflow(img, app.workflowManager.activeWorkflow),
title: "[Current workflow]",
},
...self.#getFavoriteMenuOptions(sendToWorkflow.bind(null, img)),
null,
...self.#getMenuOptions(sendToWorkflow.bind(null, img)),
],
},
});
}
}
return r;
};
},
});
}
}
export class ComfyWorkflowsContent {
element = $el("div.comfyui-workflows-panel");
treeState = {};
treeFiles = {};
/** @type { Map<ComfyWorkflow, WorkflowElement> } */
openFiles = new Map();
/** @type {WorkflowElement} */
activeElement = null;
/**
* @param {import("../../app.js").ComfyApp} app
* @param {ComfyPopup} popup
*/
constructor(app, popup) {
this.app = app;
this.popup = popup;
this.actions = $el("div.comfyui-workflows-actions", [
new ComfyButton({
content: "Default",
icon: "file-code",
iconSize: 18,
classList: "comfyui-button primary",
tooltip: "Load default workflow",
action: () => {
popup.open = false;
app.loadGraphData();
app.resetView();
},
}).element,
new ComfyButton({
content: "Browse",
icon: "folder",
iconSize: 18,
tooltip: "Browse for an image or exported workflow",
action: () => {
popup.open = false;
app.ui.loadFile();
},
}).element,
new ComfyButton({
content: "Blank",
icon: "plus-thick",
iconSize: 18,
tooltip: "Create a new blank workflow",
action: () => {
app.workflowManager.setWorkflow(null);
app.clean();
app.graph.clear();
app.workflowManager.activeWorkflow.track();
popup.open = false;
},
}).element,
]);
this.spinner = createSpinner();
this.element.replaceChildren(this.actions, this.spinner);
this.popup.addEventListener("open", () => this.load());
this.popup.addEventListener("close", () => this.element.replaceChildren(this.actions, this.spinner));
this.app.workflowManager.addEventListener("favorite", (e) => {
const workflow = e["detail"];
const button = this.treeFiles[workflow.path]?.primary;
if (!button) return; // Can happen when a workflow is renamed
button.icon = this.#getFavoriteIcon(workflow);
button.overIcon = this.#getFavoriteOverIcon(workflow);
this.updateFavorites();
});
for (const e of ["save", "open", "close", "changeWorkflow"]) {
// TODO: dont be lazy and just update the specific element
app.workflowManager.addEventListener(e, () => this.updateOpen());
}
this.app.workflowManager.addEventListener("rename", () => this.load());
this.app.workflowManager.addEventListener("execute", (e) => this.#updateActive());
}
async load() {
await this.app.workflowManager.loadWorkflows();
this.updateTree();
this.updateFavorites();
this.updateOpen();
this.element.replaceChildren(this.actions, this.openElement, this.favoritesElement, this.treeElement);
}
updateOpen() {
const current = this.openElement;
this.openFiles.clear();
this.openElement = $el("div.comfyui-workflows-open", [
$el("h3", "Open"),
...this.app.workflowManager.openWorkflows.map((w) => {
const wrapper = new WorkflowElement(this, w, {
primary: { element: $el("i.mdi.mdi-18px.mdi-progress-pencil") },
buttons: [
this.#getRenameButton(w),
new ComfyButton({
icon: "close",
iconSize: 18,
classList: "comfyui-button comfyui-workflows-file-action",
tooltip: "Close workflow",
action: (e) => {
e.stopImmediatePropagation();
this.app.workflowManager.closeWorkflow(w);
},
}),
],
});
if (w.unsaved) {
wrapper.element.classList.add("unsaved");
}
if(w === this.app.workflowManager.activeWorkflow) {
wrapper.element.classList.add("active");
}
this.openFiles.set(w, wrapper);
return wrapper.element;
}),
]);
this.#updateActive();
current?.replaceWith(this.openElement);
}
updateFavorites() {
const current = this.favoritesElement;
const favorites = [...this.app.workflowManager.workflows.filter((w) => w.isFavorite)];
this.favoritesElement = $el("div.comfyui-workflows-favorites", [
$el("h3", "Favorites"),
...favorites
.map((w) => {
return this.#getWorkflowElement(w).element;
})
.filter(Boolean),
]);
current?.replaceWith(this.favoritesElement);
}
filterTree() {
if (!this.filterText) {
this.treeRoot.classList.remove("filtered");
// Unfilter whole tree
for (const item of Object.values(this.treeFiles)) {
item.element.parentElement.style.removeProperty("display");
this.showTreeParents(item.element.parentElement);
}
return;
}
this.treeRoot.classList.add("filtered");
const searchTerms = this.filterText.toLocaleLowerCase().split(" ");
for (const item of Object.values(this.treeFiles)) {
const parts = item.workflow.pathParts;
let termIndex = 0;
let valid = false;
for (const part of parts) {
let currentIndex = 0;
do {
currentIndex = part.indexOf(searchTerms[termIndex], currentIndex);
if (currentIndex > -1) currentIndex += searchTerms[termIndex].length;
} while (currentIndex !== -1 && ++termIndex < searchTerms.length);
if (termIndex >= searchTerms.length) {
valid = true;
break;
}
}
if (valid) {
item.element.parentElement.style.removeProperty("display");
this.showTreeParents(item.element.parentElement);
} else {
item.element.parentElement.style.display = "none";
this.hideTreeParents(item.element.parentElement);
}
}
}
hideTreeParents(element) {
// Hide all parents if no children are visible
if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
for (let i = 1; i < element.parentElement.children.length; i++) {
const c = element.parentElement.children[i];
if (c.style.display !== "none") {
return;
}
}
element.parentElement.style.display = "none";
this.hideTreeParents(element.parentElement);
}
}
showTreeParents(element) {
if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
element.parentElement.style.removeProperty("display");
this.showTreeParents(element.parentElement);
}
}
updateTree() {
const current = this.treeElement;
const nodes = {};
let typingTimeout;
this.treeFiles = {};
this.treeRoot = $el("ul.comfyui-workflows-tree");
this.treeElement = $el("section", [
$el("header", [
$el("h3", "Browse"),
$el("div.comfy-ui-workflows-search", [
$el("i.mdi.mdi-18px.mdi-magnify"),
$el("input", {
placeholder: "Search",
value: this.filterText ?? "",
oninput: (e) => {
this.filterText = e.target["value"]?.trim();
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => this.filterTree(), 250);
},
}),
]),
]),
this.treeRoot,
]);
for (const workflow of this.app.workflowManager.workflows) {
if (!workflow.pathParts) continue;
let currentPath = "";
let currentRoot = this.treeRoot;
for (let i = 0; i < workflow.pathParts.length; i++) {
currentPath += (currentPath ? "\\" : "") + workflow.pathParts[i];
const parentNode = nodes[currentPath] ?? this.#createNode(currentPath, workflow, i, currentRoot);
nodes[currentPath] = parentNode;
currentRoot = parentNode;
}
}
current?.replaceWith(this.treeElement);
this.filterTree();
}
#expandNode(el, workflow, thisPath, i) {
const expanded = !el.classList.toggle("closed");
if (expanded) {
let c = "";
for (let j = 0; j <= i; j++) {
c += (c ? "\\" : "") + workflow.pathParts[j];
this.treeState[c] = true;
}
} else {
let c = thisPath;
for (let j = i + 1; j < workflow.pathParts.length; j++) {
c += (c ? "\\" : "") + workflow.pathParts[j];
delete this.treeState[c];
}
delete this.treeState[thisPath];
}
}
#updateActive() {
this.#removeActive();
const active = this.app.workflowManager.activePrompt;
if (!active?.workflow) return;
const open = this.openFiles.get(active.workflow);
if (!open) return;
this.activeElement = open;
const total = Object.values(active.nodes);
const done = total.filter(Boolean);
const percent = done.length / total.length;
open.element.classList.add("running");
open.element.style.setProperty("--progress", percent * 100 + "%");
open.primary.element.classList.remove("mdi-progress-pencil");
open.primary.element.classList.add("mdi-play");
}
#removeActive() {
if (!this.activeElement) return;
this.activeElement.element.classList.remove("running");
this.activeElement.element.style.removeProperty("--progress");
this.activeElement.primary.element.classList.add("mdi-progress-pencil");
this.activeElement.primary.element.classList.remove("mdi-play");
}
/** @param {ComfyWorkflow} workflow */
#getFavoriteIcon(workflow) {
return workflow.isFavorite ? "star" : "file-outline";
}
/** @param {ComfyWorkflow} workflow */
#getFavoriteOverIcon(workflow) {
return workflow.isFavorite ? "star-off" : "star-outline";
}
/** @param {ComfyWorkflow} workflow */
#getFavoriteTooltip(workflow) {
return workflow.isFavorite ? "Remove this workflow from your favorites" : "Add this workflow to your favorites";
}
/** @param {ComfyWorkflow} workflow */
#getFavoriteButton(workflow, primary) {
return new ComfyButton({
icon: this.#getFavoriteIcon(workflow),
overIcon: this.#getFavoriteOverIcon(workflow),
iconSize: 18,
classList: "comfyui-button comfyui-workflows-file-action-favorite" + (primary ? " comfyui-workflows-file-action-primary" : ""),
tooltip: this.#getFavoriteTooltip(workflow),
action: (e) => {
e.stopImmediatePropagation();
workflow.favorite(!workflow.isFavorite);
},
});
}
/** @param {ComfyWorkflow} workflow */
#getDeleteButton(workflow) {
const deleteButton = new ComfyButton({
icon: "delete",
tooltip: "Delete this workflow",
classList: "comfyui-button comfyui-workflows-file-action",
iconSize: 18,
action: async (e, btn) => {
e.stopImmediatePropagation();
if (btn.icon === "delete-empty") {
btn.enabled = false;
await workflow.delete();
await this.load();
} else {
btn.icon = "delete-empty";
btn.element.style.background = "red";
}
},
});
deleteButton.element.addEventListener("mouseleave", () => {
deleteButton.icon = "delete";
deleteButton.element.style.removeProperty("background");
});
return deleteButton;
}
/** @param {ComfyWorkflow} workflow */
#getInsertButton(workflow) {
return new ComfyButton({
icon: "file-move-outline",
iconSize: 18,
tooltip: "Insert this workflow into the current workflow",
classList: "comfyui-button comfyui-workflows-file-action",
action: (e) => {
if (!this.app.shiftDown) {
this.popup.open = false;
}
e.stopImmediatePropagation();
if (!this.app.shiftDown) {
this.popup.open = false;
}
workflow.insert();
},
});
}
/** @param {ComfyWorkflow} workflow */
#getRenameButton(workflow) {
return new ComfyButton({
icon: "pencil",
tooltip: workflow.path ? "Rename this workflow" : "This workflow can't be renamed as it hasn't been saved.",
classList: "comfyui-button comfyui-workflows-file-action",
iconSize: 18,
enabled: !!workflow.path,
action: async (e) => {
e.stopImmediatePropagation();
const newName = prompt("Enter new name", workflow.path);
if (newName) {
await workflow.rename(newName);
}
},
});
}
/** @param {ComfyWorkflow} workflow */
#getWorkflowElement(workflow) {
return new WorkflowElement(this, workflow, {
primary: this.#getFavoriteButton(workflow, true),
buttons: [this.#getInsertButton(workflow), this.#getRenameButton(workflow), this.#getDeleteButton(workflow)],
});
}
/** @param {ComfyWorkflow} workflow */
#createLeafNode(workflow) {
const fileNode = this.#getWorkflowElement(workflow);
this.treeFiles[workflow.path] = fileNode;
return fileNode;
}
#createNode(currentPath, workflow, i, currentRoot) {
const part = workflow.pathParts[i];
const parentNode = $el("ul" + (this.treeState[currentPath] ? "" : ".closed"), {
$: (el) => {
el.onclick = (e) => {
this.#expandNode(el, workflow, currentPath, i);
e.stopImmediatePropagation();
};
},
});
currentRoot.append(parentNode);
// Create a node for the current part and an inner UL for its children if it isnt a leaf node
const leaf = i === workflow.pathParts.length - 1;
let nodeElement;
if (leaf) {
nodeElement = this.#createLeafNode(workflow).element;
} else {
nodeElement = $el("li", [$el("i.mdi.mdi-18px.mdi-folder"), $el("span", part)]);
}
parentNode.append(nodeElement);
return parentNode;
}
}
class WorkflowElement {
/**
* @param { ComfyWorkflowsContent } parent
* @param { ComfyWorkflow } workflow
*/
constructor(parent, workflow, { tagName = "li", primary, buttons }) {
this.parent = parent;
this.workflow = workflow;
this.primary = primary;
this.buttons = buttons;
this.element = $el(
tagName + ".comfyui-workflows-tree-file",
{
onclick: () => {
workflow.load();
this.parent.popup.open = false;
},
title: this.workflow.path,
},
[this.primary?.element, $el("span", workflow.name), ...buttons.map((b) => b.element)]
);
}
}
class WidgetSelectionDialog extends ComfyAsyncDialog {
#options;
/**
* @param {Array<{widget: {name: string}, node: {pos: [number, number], title: string, id: string, type: string}}>} options
*/
constructor(options) {
super();
this.#options = options;
}
show(app) {
this.element.classList.add("comfy-widget-selection-dialog");
return super.show(
$el("div", [
$el("h2", "Select image target"),
$el(
"p",
"This workflow has multiple image loader nodes, you can rename a node to include 'input' in the title for it to be automatically selected, or select one below."
),
$el(
"section",
this.#options.map((opt) => {
return $el("div.comfy-widget-selection-item", [
$el("span", { dataset: { id: opt.node.id } }, `${opt.node.title ?? opt.node.type} ${opt.widget.name}`),
$el(
"button.comfyui-button",
{
onclick: () => {
app.canvas.ds.offset[0] = -opt.node.pos[0] + 50;
app.canvas.ds.offset[1] = -opt.node.pos[1] + 50;
app.canvas.selectNode(opt.node);
app.graph.setDirtyCanvas(true, true);
},
},
"Show"
),
$el(
"button.comfyui-button.primary",
{
onclick: () => {
this.close(opt);
},
},
"Select"
),
]);
})
),
])
);
}
}

View File

@ -47,6 +47,17 @@ export class ComfySettingsDialog extends ComfyDialog {
return Object.values(this.settingsLookup);
}
#dispatchChange(id, value, oldValue) {
this.dispatchEvent(
new CustomEvent(id + ".change", {
detail: {
value,
oldValue
},
})
);
}
async load() {
if (this.app.storageLocation === "browser") {
this.settingsValues = localStorage;
@ -56,7 +67,9 @@ export class ComfySettingsDialog extends ComfyDialog {
// Trigger onChange for any settings added before load
for (const id in this.settingsLookup) {
this.settingsLookup[id].onChange?.(this.settingsValues[this.getId(id)]);
const value = this.settingsValues[this.getId(id)];
this.settingsLookup[id].onChange?.(value);
this.#dispatchChange(id, value);
}
}
@ -90,6 +103,7 @@ export class ComfySettingsDialog extends ComfyDialog {
if (id in this.settingsLookup) {
this.settingsLookup[id].onChange?.(value, oldValue);
}
this.#dispatchChange(id, value, oldValue);
await api.storeSetting(id, value);
}
@ -136,6 +150,8 @@ export class ComfySettingsDialog extends ComfyDialog {
onChange,
name,
render: () => {
if (type === "hidden") return;
const setter = (v) => {
if (onChange) {
onChange(v, value);
@ -310,7 +326,7 @@ export class ComfySettingsDialog extends ComfyDialog {
},
[$el("th"), $el("th", { style: { width: "33%" } })]
),
...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render())
...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render()).filter(Boolean)
);
this.element.showModal();
}

View File

@ -0,0 +1,56 @@
/**
* @typedef { string | string[] | Record<string, boolean> } ClassList
*/
/**
* @param { HTMLElement } element
* @param { ClassList } classList
* @param { string[] } requiredClasses
*/
export function applyClasses(element, classList, ...requiredClasses) {
classList ??= "";
let str;
if (typeof classList === "string") {
str = classList;
} else if (classList instanceof Array) {
str = classList.join(" ");
} else {
str = Object.entries(classList).reduce((p, c) => {
if (c[1]) {
p += (p.length ? " " : "") + c[0];
}
return p;
}, "");
}
element.className = str;
if (requiredClasses) {
element.classList.add(...requiredClasses);
}
}
/**
* @param { HTMLElement } element
* @param { { onHide?: (el: HTMLElement) => void, onShow?: (el: HTMLElement, value) => void } } [param1]
* @returns
*/
export function toggleElement(element, { onHide, onShow } = {}) {
let placeholder;
let hidden;
return (value) => {
if (value) {
if (hidden) {
hidden = false;
placeholder.replaceWith(element);
}
onShow?.(element, value);
} else {
if (!placeholder) {
placeholder = document.createComment("");
}
hidden = true;
element.replaceWith(placeholder);
onHide?.(element);
}
};
}

View File

@ -1,4 +1,5 @@
import { $el } from "./ui.js";
import { api } from "./api.js";
// Simple date formatter
const parts = {
@ -25,6 +26,19 @@ function formatDate(text, date) {
});
}
export function clone(obj) {
try {
if (typeof structuredClone !== "undefined") {
return structuredClone(obj);
}
} catch (error) {
// structuredClone is stricter than using JSON.parse/stringify so fallback to that
}
return JSON.parse(JSON.stringify(obj));
}
export function applyTextReplacements(app, value) {
return value.replace(/%([^%]+)%/g, function (match, text) {
const split = text.split(".");
@ -86,3 +100,57 @@ export async function addStylesheet(urlOrFile, relativeTo) {
});
});
}
/**
* @param { string } filename
* @param { Blob } blob
*/
export function downloadBlob(filename, blob) {
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: filename,
style: { display: "none" },
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
}
/**
* @template T
* @param {string} name
* @param {T} [defaultValue]
* @param {(currentValue: any, previousValue: any)=>void} [onChanged]
* @returns {T}
*/
export function prop(target, name, defaultValue, onChanged) {
let currentValue;
Object.defineProperty(target, name, {
get() {
return currentValue;
},
set(newValue) {
const prevValue = currentValue;
currentValue = newValue;
onChanged?.(currentValue, prevValue, target, name);
},
});
return defaultValue;
}
export function getStorageValue(id) {
const clientId = api.clientId ?? api.initialClientId;
return (clientId && sessionStorage.getItem(`${id}:${clientId}`)) ?? localStorage.getItem(id);
}
export function setStorageValue(id, value) {
const clientId = api.clientId ?? api.initialClientId;
if (clientId) {
sessionStorage.setItem(`${id}:${clientId}`, value);
}
localStorage.setItem(id, value);
}

View File

@ -0,0 +1,450 @@
// @ts-check
import { api } from "./api.js";
import { ChangeTracker } from "./changeTracker.js";
import { ComfyAsyncDialog } from "./ui/components/asyncDialog.js";
import { getStorageValue, setStorageValue } from "./utils.js";
function appendJsonExt(path) {
if (!path.toLowerCase().endsWith(".json")) {
path += ".json";
}
return path;
}
export function trimJsonExt(path) {
return path?.replace(/\.json$/, "");
}
export class ComfyWorkflowManager extends EventTarget {
/** @type {string | null} */
#activePromptId = null;
#unsavedCount = 0;
#activeWorkflow;
/** @type {Record<string, ComfyWorkflow>} */
workflowLookup = {};
/** @type {Array<ComfyWorkflow>} */
workflows = [];
/** @type {Array<ComfyWorkflow>} */
openWorkflows = [];
/** @type {Record<string, {workflow?: ComfyWorkflow, nodes?: Record<string, boolean>}>} */
queuedPrompts = {};
get activeWorkflow() {
return this.#activeWorkflow ?? this.openWorkflows[0];
}
get activePromptId() {
return this.#activePromptId;
}
get activePrompt() {
return this.queuedPrompts[this.#activePromptId];
}
/**
* @param {import("./app.js").ComfyApp} app
*/
constructor(app) {
super();
this.app = app;
ChangeTracker.init(app);
this.#bindExecutionEvents();
}
#bindExecutionEvents() {
// TODO: on reload, set active prompt based on the latest ws message
const emit = () => this.dispatchEvent(new CustomEvent("execute", { detail: this.activePrompt }));
let executing = null;
api.addEventListener("execution_start", (e) => {
this.#activePromptId = e.detail.prompt_id;
// This event can fire before the event is stored, so put a placeholder
this.queuedPrompts[this.#activePromptId] ??= { nodes: {} };
emit();
});
api.addEventListener("execution_cached", (e) => {
if (!this.activePrompt) return;
for (const n of e.detail.nodes) {
this.activePrompt.nodes[n] = true;
}
emit();
});
api.addEventListener("executed", (e) => {
if (!this.activePrompt) return;
this.activePrompt.nodes[e.detail.node] = true;
emit();
});
api.addEventListener("executing", (e) => {
if (!this.activePrompt) return;
if (executing) {
// Seems sometimes nodes that are cached fire executing but not executed
this.activePrompt.nodes[executing] = true;
}
executing = e.detail;
if (!executing) {
delete this.queuedPrompts[this.#activePromptId];
this.#activePromptId = null;
}
emit();
});
}
async loadWorkflows() {
try {
let favorites;
const resp = await api.getUserData("workflows/.index.json");
let info;
if (resp.status === 200) {
info = await resp.json();
favorites = new Set(info?.favorites ?? []);
} else {
favorites = new Set();
}
const workflows = (await api.listUserData("workflows", true, true)).map((w) => {
let workflow = this.workflowLookup[w[0]];
if (!workflow) {
workflow = new ComfyWorkflow(this, w[0], w.slice(1), favorites.has(w[0]));
this.workflowLookup[workflow.path] = workflow;
}
return workflow;
});
this.workflows = workflows;
} catch (error) {
alert("Error loading workflows: " + (error.message ?? error));
this.workflows = [];
}
}
async saveWorkflowMetadata() {
await api.storeUserData("workflows/.index.json", {
favorites: [...this.workflows.filter((w) => w.isFavorite).map((w) => w.path)],
});
}
/**
* @param {string | ComfyWorkflow | null} workflow
*/
setWorkflow(workflow) {
if (workflow && typeof workflow === "string") {
// Selected by path, i.e. on reload of last workflow
const found = this.workflows.find((w) => w.path === workflow);
if (found) {
workflow = found;
workflow.unsaved = !workflow || getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true";
}
}
if (!(workflow instanceof ComfyWorkflow)) {
// Still not found, either reloading a deleted workflow or blank
workflow = new ComfyWorkflow(this, workflow || "Unsaved Workflow" + (this.#unsavedCount++ ? ` (${this.#unsavedCount})` : ""));
}
const index = this.openWorkflows.indexOf(workflow);
if (index === -1) {
// Opening a new workflow
this.openWorkflows.push(workflow);
}
this.#activeWorkflow = workflow;
setStorageValue("Comfy.PreviousWorkflow", this.activeWorkflow.path ?? "");
this.dispatchEvent(new CustomEvent("changeWorkflow"));
}
storePrompt({ nodes, id }) {
this.queuedPrompts[id] ??= {};
this.queuedPrompts[id].nodes = {
...nodes.reduce((p, n) => {
p[n] = false;
return p;
}, {}),
...this.queuedPrompts[id].nodes,
};
this.queuedPrompts[id].workflow = this.activeWorkflow;
}
/**
* @param {ComfyWorkflow} workflow
*/
async closeWorkflow(workflow, warnIfUnsaved = true) {
if (!workflow.isOpen) {
return true;
}
if (workflow.unsaved && warnIfUnsaved) {
const res = await ComfyAsyncDialog.prompt({
title: "Save Changes?",
message: `Do you want to save changes to "${workflow.path ?? workflow.name}" before closing?`,
actions: ["Yes", "No", "Cancel"],
});
if (res === "Yes") {
const active = this.activeWorkflow;
if (active !== workflow) {
// We need to switch to the workflow to save it
await workflow.load();
}
if (!(await workflow.save())) {
// Save was canceled, restore the previous workflow
if (active !== workflow) {
await active.load();
}
return;
}
} else if (res === "Cancel") {
return;
}
}
workflow.changeTracker = null;
this.openWorkflows.splice(this.openWorkflows.indexOf(workflow), 1);
if (this.openWorkflows.length) {
this.#activeWorkflow = this.openWorkflows[0];
await this.#activeWorkflow.load();
} else {
// Load default
await this.app.loadGraphData();
}
}
}
export class ComfyWorkflow {
#name;
#path;
#pathParts;
#isFavorite = false;
/** @type {ChangeTracker | null} */
changeTracker = null;
unsaved = false;
get name() {
return this.#name;
}
get path() {
return this.#path;
}
get pathParts() {
return this.#pathParts;
}
get isFavorite() {
return this.#isFavorite;
}
get isOpen() {
return !!this.changeTracker;
}
/**
* @overload
* @param {ComfyWorkflowManager} manager
* @param {string} path
*/
/**
* @overload
* @param {ComfyWorkflowManager} manager
* @param {string} path
* @param {string[]} pathParts
* @param {boolean} isFavorite
*/
/**
* @param {ComfyWorkflowManager} manager
* @param {string} path
* @param {string[]} [pathParts]
* @param {boolean} [isFavorite]
*/
constructor(manager, path, pathParts, isFavorite) {
this.manager = manager;
if (pathParts) {
this.#updatePath(path, pathParts);
this.#isFavorite = isFavorite;
} else {
this.#name = path;
this.unsaved = true;
}
}
/**
* @param {string} path
* @param {string[]} [pathParts]
*/
#updatePath(path, pathParts) {
this.#path = path;
if (!pathParts) {
if (!path.includes("\\")) {
pathParts = path.split("/");
} else {
pathParts = path.split("\\");
}
}
this.#pathParts = pathParts;
this.#name = trimJsonExt(pathParts[pathParts.length - 1]);
}
async getWorkflowData() {
const resp = await api.getUserData("workflows/" + this.path);
if (resp.status !== 200) {
alert(`Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}`);
return;
}
return await resp.json();
}
load = async () => {
if (this.isOpen) {
await this.manager.app.loadGraphData(this.changeTracker.activeState, true, true, this);
} else {
const data = await this.getWorkflowData();
if (!data) return;
await this.manager.app.loadGraphData(data, true, true, this);
}
};
async save(saveAs = false) {
if (!this.path || saveAs) {
return !!(await this.#save(null, false));
} else {
return !!(await this.#save(this.path, true));
}
}
/**
* @param {boolean} value
*/
async favorite(value) {
try {
if (this.#isFavorite === value) return;
this.#isFavorite = value;
await this.manager.saveWorkflowMetadata();
this.manager.dispatchEvent(new CustomEvent("favorite", { detail: this }));
} catch (error) {
alert("Error favoriting workflow " + this.path + "\n" + (error.message ?? error));
}
}
/**
* @param {string} path
*/
async rename(path) {
path = appendJsonExt(path);
let resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path);
if (resp.status === 409) {
if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return resp;
resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path, { overwrite: true });
}
if (resp.status !== 200) {
alert(`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`);
return;
}
const isFav = this.isFavorite;
if (isFav) {
await this.favorite(false);
}
path = (await resp.json()).substring("workflows/".length);
this.#updatePath(path, null);
if (isFav) {
await this.favorite(true);
}
this.manager.dispatchEvent(new CustomEvent("rename", { detail: this }));
setStorageValue("Comfy.PreviousWorkflow", this.path ?? "");
}
async insert() {
const data = await this.getWorkflowData();
if (!data) return;
const old = localStorage.getItem("litegrapheditor_clipboard");
const graph = new LGraph(data);
const canvas = new LGraphCanvas(null, graph, { skip_events: true, skip_render: true });
canvas.selectNodes();
canvas.copyToClipboard();
this.manager.app.canvas.pasteFromClipboard();
localStorage.setItem("litegrapheditor_clipboard", old);
}
async delete() {
// TODO: fix delete of current workflow - should mark workflow as unsaved and when saving use old name by default
try {
if (this.isFavorite) {
await this.favorite(false);
}
await api.deleteUserData("workflows/" + this.path);
this.unsaved = true;
this.#path = null;
this.#pathParts = null;
this.manager.workflows.splice(this.manager.workflows.indexOf(this), 1);
this.manager.dispatchEvent(new CustomEvent("delete", { detail: this }));
} catch (error) {
alert(`Error deleting workflow: ${error.message || error}`);
}
}
track() {
if (this.changeTracker) {
this.changeTracker.restore();
} else {
this.changeTracker = new ChangeTracker(this);
}
}
/**
* @param {string|null} path
* @param {boolean} overwrite
*/
async #save(path, overwrite) {
if (!path) {
path = prompt("Save workflow as:", trimJsonExt(this.path) ?? this.name ?? "workflow");
if (!path) return;
}
path = appendJsonExt(path);
const p = await this.manager.app.graphToPrompt();
const json = JSON.stringify(p.workflow, null, 2);
let resp = await api.storeUserData("workflows/" + path, json, { stringify: false, throwOnError: false, overwrite });
if (resp.status === 409) {
if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return;
resp = await api.storeUserData("workflows/" + path, json, { stringify: false });
}
if (resp.status !== 200) {
alert(`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`);
return;
}
path = (await resp.json()).substring("workflows/".length);
if (!this.path) {
// Saved new workflow, patch this instance
this.#updatePath(path, null);
await this.manager.loadWorkflows();
this.unsaved = false;
this.manager.dispatchEvent(new CustomEvent("rename", { detail: this }));
setStorageValue("Comfy.PreviousWorkflow", this.path ?? "");
} else if (path !== this.path) {
// Saved as, open the new copy
await this.manager.loadWorkflows();
const workflow = this.manager.workflowLookup[path];
await workflow.load();
} else {
// Normal save
this.unsaved = false;
this.manager.dispatchEvent(new CustomEvent("save", { detail: this }));
}
return true;
}
}

View File

@ -1,3 +1,5 @@
@import url("scripts/ui/menu/menu.css");
:root {
--fg-color: #000;
--bg-color: #fff;
@ -10,12 +12,24 @@
--border-color: #4e4e4e;
--tr-even-bg-color: #222;
--tr-odd-bg-color: #353535;
--primary-bg: #236692;
--primary-fg: #ffffff;
--primary-hover-bg: #3485bb;
--primary-hover-fg: #ffffff;
--content-bg: #e0e0e0;
--content-fg: #000;
--content-hover-bg: #adadad;
--content-hover-fg: #000;
}
@media (prefers-color-scheme: dark) {
:root {
--fg-color: #fff;
--bg-color: #202020;
--content-bg: #4e4e4e;
--content-fg: #fff;
--content-hover-bg: #222;
--content-hover-fg: #fff;
}
}
@ -26,11 +40,41 @@ body {
overflow: hidden;
background-color: var(--bg-color);
color: var(--fg-color);
grid-template-columns: auto 1fr auto;
grid-template-rows: auto auto 1fr auto;
min-height: -webkit-fill-available;
max-height: -webkit-fill-available;
min-width: -webkit-fill-available;
max-width: -webkit-fill-available;
}
.comfyui-body-top {
order: 0;
grid-column: 1/-1;
z-index: 10;
}
.comfyui-body-left {
order: 1;
z-index: 10;
}
#graph-canvas {
width: 100%;
height: 100%;
order: 2;
grid-column: 1/-1;
}
.comfyui-body-right {
order: 3;
z-index: 10;
}
.comfyui-body-bottom {
order: 4;
grid-column: 1/-1;
z-index: 10;
}
.comfy-multiline-input {
@ -364,6 +408,37 @@ dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
.comfy-dialog.comfyui-dialog {
top: 0;
}
.comfy-dialog.comfy-modal {
font-family: Arial, sans-serif;
border-color: var(--bg-color);
box-shadow: none;
border: 2px solid var(--border-color);
}
.comfy-dialog .comfy-modal-content {
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
color: var(--fg-color);
}
.comfy-dialog .comfy-modal-content h3 {
margin-top: 0;
}
.comfy-dialog .comfy-modal-content > p {
width: 100%;
}
.comfy-dialog .comfy-modal-content > .comfyui-button {
flex: 1;
justify-content: center;
}
#comfy-settings-dialog {
padding: 0;
width: 41rem;
@ -557,3 +632,7 @@ dialog::backdrop {
border-top: none;
}
}
audio.comfy-audio.empty-audio-widget {
display: none;
}

View File

@ -10,24 +10,24 @@ export interface ComfyExtension {
* Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
* @param app The ComfyUI app instance
*/
init(app: ComfyApp): Promise<void>;
init?(app: ComfyApp): Promise<void>;
/**
* Allows any additonal setup, called after the application is fully set up and running
* @param app The ComfyUI app instance
*/
setup(app: ComfyApp): Promise<void>;
setup?(app: ComfyApp): Promise<void>;
/**
* Called before nodes are registered with the graph
* @param defs The collection of node definitions, add custom ones or edit existing ones
* @param app The ComfyUI app instance
*/
addCustomNodeDefs(defs: Record<string, ComfyObjectInfo>, app: ComfyApp): Promise<void>;
addCustomNodeDefs?(defs: Record<string, ComfyObjectInfo>, app: ComfyApp): Promise<void>;
/**
* Allows the extension to add custom widgets
* @param app The ComfyUI app instance
* @returns An array of {[widget name]: widget data}
*/
getCustomWidgets(
getCustomWidgets?(
app: ComfyApp
): Promise<
Record<string, (node, inputName, inputData, app) => { widget?: IWidget; minWidth?: number; minHeight?: number }>
@ -38,12 +38,12 @@ export interface ComfyExtension {
* @param nodeData The original node object info config object
* @param app The ComfyUI app instance
*/
beforeRegisterNodeDef(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp): Promise<void>;
beforeRegisterNodeDef?(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp): Promise<void>;
/**
* Allows the extension to register additional nodes with LGraph after standard nodes are added
* @param app The ComfyUI app instance
*/
registerCustomNodes(app: ComfyApp): Promise<void>;
registerCustomNodes?(app: ComfyApp): Promise<void>;
/**
* Allows the extension to modify a node that has been reloaded onto the graph.
* If you break something in the backend and want to patch workflows in the frontend
@ -51,13 +51,13 @@ export interface ComfyExtension {
* @param node The node that has been loaded
* @param app The ComfyUI app instance
*/
loadedGraphNode(node: LGraphNode, app: ComfyApp);
loadedGraphNode?(node: LGraphNode, app: ComfyApp);
/**
* Allows the extension to run code after the constructor of the node
* @param node The node that has been created
* @param app The ComfyUI app instance
*/
nodeCreated(node: LGraphNode, app: ComfyApp);
nodeCreated?(node: LGraphNode, app: ComfyApp);
}
export type ComfyObjectInfo = {

View File

@ -56,6 +56,57 @@ class SamplerLCMUpscale:
sampler = comfy.samplers.KSAMPLER(sample_lcm_upscale, extra_options={"total_upscale": scale_ratio, "upscale_steps": scale_steps, "upscale_method": upscale_method})
return (sampler, )
from comfy.k_diffusion.sampling import to_d
import comfy.model_patcher
@torch.no_grad()
def sample_euler_pp(model, x, sigmas, extra_args=None, callback=None, disable=None):
extra_args = {} if extra_args is None else extra_args
temp = [0]
def post_cfg_function(args):
temp[0] = args["uncond_denoised"]
return args["denoised"]
model_options = extra_args.get("model_options", {}).copy()
extra_args["model_options"] = comfy.model_patcher.set_model_options_post_cfg_function(model_options, post_cfg_function, disable_cfg1_optimization=True)
s_in = x.new_ones([x.shape[0]])
for i in trange(len(sigmas) - 1, disable=disable):
sigma_hat = sigmas[i]
denoised = model(x, sigma_hat * s_in, **extra_args)
d = to_d(x - denoised + temp[0], sigmas[i], denoised)
if callback is not None:
callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigma_hat, 'denoised': denoised})
dt = sigmas[i + 1] - sigma_hat
x = x + d * dt
return x
class SamplerEulerCFGpp:
@classmethod
def INPUT_TYPES(s):
return {"required":
{"version": (["regular", "alternative"],),}
}
RETURN_TYPES = ("SAMPLER",)
# CATEGORY = "sampling/custom_sampling/samplers"
CATEGORY = "_for_testing"
FUNCTION = "get_sampler"
def get_sampler(self, version):
if version == "alternative":
sampler = comfy.samplers.KSAMPLER(sample_euler_pp)
else:
sampler = comfy.samplers.ksampler("euler_cfg_pp")
return (sampler, )
NODE_CLASS_MAPPINGS = {
"SamplerLCMUpscale": SamplerLCMUpscale,
"SamplerEulerCFGpp": SamplerEulerCFGpp,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SamplerEulerCFGpp": "SamplerEulerCFG++",
}

View File

@ -2,6 +2,11 @@ import torch
import comfy.model_management
from comfy.cmd import folder_paths
import os
import io
import json
import struct
import random
from comfy.cli_args import args
class EmptyLatentAudio:
def __init__(self):
@ -9,15 +14,16 @@ class EmptyLatentAudio:
@classmethod
def INPUT_TYPES(s):
return {"required": {}}
return {"required": {"seconds": ("FLOAT", {"default": 47.6, "min": 1.0, "max": 1000.0, "step": 0.1})}}
RETURN_TYPES = ("LATENT",)
FUNCTION = "generate"
CATEGORY = "_for_testing/audio"
def generate(self):
def generate(self, seconds):
batch_size = 1
latent = torch.zeros([batch_size, 64, 1024], device=self.device)
length = round((seconds * 44100 / 2048) / 2) * 2
latent = torch.zeros([batch_size, 64, length], device=self.device)
return ({"samples":latent, "type": "audio"}, )
class VAEEncodeAudio:
@ -30,7 +36,13 @@ class VAEEncodeAudio:
CATEGORY = "_for_testing/audio"
def encode(self, vae, audio):
t = vae.encode(audio["waveform"].movedim(1, -1))
sample_rate = audio["sample_rate"]
if 44100 != sample_rate:
waveform = torchaudio.functional.resample(audio["waveform"], sample_rate, 44100)
else:
waveform = audio["waveform"]
t = vae.encode(waveform.movedim(1, -1))
return ({"samples":t}, )
class VAEDecodeAudio:
@ -46,12 +58,66 @@ class VAEDecodeAudio:
audio = vae.decode(samples["samples"]).movedim(-1, 1)
return ({"waveform": audio, "sample_rate": 44100}, )
def create_vorbis_comment_block(comment_dict, last_block):
vendor_string = b'ComfyUI'
vendor_length = len(vendor_string)
comments = []
for key, value in comment_dict.items():
comment = f"{key}={value}".encode('utf-8')
comments.append(struct.pack('<I', len(comment)) + comment)
user_comment_list_length = len(comments)
user_comments = b''.join(comments)
comment_data = struct.pack('<I', vendor_length) + vendor_string + struct.pack('<I', user_comment_list_length) + user_comments
if last_block:
id = b'\x84'
else:
id = b'\x04'
comment_block = id + struct.pack('>I', len(comment_data))[1:] + comment_data
return comment_block
def insert_or_replace_vorbis_comment(flac_io, comment_dict):
if len(comment_dict) == 0:
return flac_io
flac_io.seek(4)
blocks = []
last_block = False
while not last_block:
header = flac_io.read(4)
last_block = (header[0] & 0x80) != 0
block_type = header[0] & 0x7F
block_length = struct.unpack('>I', b'\x00' + header[1:])[0]
block_data = flac_io.read(block_length)
if block_type == 4 or block_type == 1:
pass
else:
header = bytes([(header[0] & (~0x80))]) + header[1:]
blocks.append(header + block_data)
blocks.append(create_vorbis_comment_block(comment_dict, last_block=True))
new_flac_io = io.BytesIO()
new_flac_io.write(b'fLaC')
for block in blocks:
new_flac_io.write(block)
new_flac_io.write(flac_io.read())
return new_flac_io
class SaveAudio:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
self.prefix_append = ""
self.compress_level = 4
@classmethod
def INPUT_TYPES(s):
@ -73,11 +139,27 @@ class SaveAudio:
filename_prefix += self.prefix_append
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
results = list()
metadata = {}
if not args.disable_metadata:
if prompt is not None:
metadata["prompt"] = json.dumps(prompt)
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata[x] = json.dumps(extra_pnginfo[x])
for (batch_number, waveform) in enumerate(audio["waveform"]):
#TODO: metadata
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.flac"
torchaudio.save(os.path.join(full_output_folder, file), waveform, audio["sample_rate"], format="FLAC")
buff = io.BytesIO()
torchaudio.save(buff, waveform, audio["sample_rate"], format="FLAC")
buff = insert_or_replace_vorbis_comment(buff, metadata)
with open(os.path.join(full_output_folder, file), 'wb') as f:
f.write(buff.getbuffer())
results.append({
"filename": file,
"subfolder": subfolder,
@ -87,12 +169,32 @@ class SaveAudio:
return { "ui": { "audio": results } }
class PreviewAudio(SaveAudio):
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
@classmethod
def INPUT_TYPES(s):
return {"required":
{"audio": ("AUDIO", ), },
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
class LoadAudio:
SUPPORTED_FORMATS = ('.wav', '.mp3', '.ogg', '.flac', '.aiff', '.aif')
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
return {"required": {"audio": [sorted(files), ]}, }
files = [
f for f in os.listdir(input_dir)
if (os.path.isfile(os.path.join(input_dir, f))
and f.endswith(LoadAudio.SUPPORTED_FORMATS)
)
]
return {"required": {"audio": (sorted(files), {"audio_upload": True})}}
CATEGORY = "_for_testing/audio"
@ -128,4 +230,5 @@ NODE_CLASS_MAPPINGS = {
"VAEDecodeAudio": VAEDecodeAudio,
"SaveAudio": SaveAudio,
"LoadAudio": LoadAudio,
"PreviewAudio": PreviewAudio,
}

View File

@ -318,6 +318,25 @@ class SamplerEulerAncestral:
sampler = comfy.samplers.ksampler("euler_ancestral", {"eta": eta, "s_noise": s_noise})
return (sampler, )
class SamplerEulerAncestralCFGPP:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"eta": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step":0.01, "round": False}),
"s_noise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step":0.01, "round": False}),
}}
RETURN_TYPES = ("SAMPLER",)
CATEGORY = "sampling/custom_sampling/samplers"
FUNCTION = "get_sampler"
def get_sampler(self, eta, s_noise):
sampler = comfy.samplers.ksampler(
"euler_ancestral_cfg_pp",
{"eta": eta, "s_noise": s_noise})
return (sampler, )
class SamplerLMS:
@classmethod
def INPUT_TYPES(s):
@ -647,6 +666,7 @@ NODE_CLASS_MAPPINGS = {
"SDTurboScheduler": SDTurboScheduler,
"KSamplerSelect": KSamplerSelect,
"SamplerEulerAncestral": SamplerEulerAncestral,
"SamplerEulerAncestralCFGPP": SamplerEulerAncestralCFGPP,
"SamplerLMS": SamplerLMS,
"SamplerDPMPP_3M_SDE": SamplerDPMPP_3M_SDE,
"SamplerDPMPP_2M_SDE": SamplerDPMPP_2M_SDE,
@ -664,3 +684,7 @@ NODE_CLASS_MAPPINGS = {
"AddNoise": AddNoise,
"SamplerCustomAdvanced": SamplerCustomAdvanced,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SamplerEulerAncestralCFGPP": "SamplerEulerAncestralCFG++",
}

View File

@ -52,9 +52,32 @@ class ModelMergeSDXL(nodes_model_merging.ModelMergeBlocks):
return {"required": arg_dict}
class ModelMergeSD3_2B(comfy_extras.nodes_model_merging.ModelMergeBlocks):
CATEGORY = "advanced/model_merging/model_specific"
@classmethod
def INPUT_TYPES(s):
arg_dict = { "model1": ("MODEL",),
"model2": ("MODEL",)}
argument = ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01})
arg_dict["pos_embed."] = argument
arg_dict["x_embedder."] = argument
arg_dict["context_embedder."] = argument
arg_dict["y_embedder."] = argument
arg_dict["t_embedder."] = argument
for i in range(24):
arg_dict["joint_blocks.{}.".format(i)] = argument
arg_dict["final_layer."] = argument
return {"required": arg_dict}
NODE_CLASS_MAPPINGS = {
"ModelMergeSD1": ModelMergeSD1,
"ModelMergeSD2": ModelMergeSD1, #SD1 and SD2 have the same blocks
"ModelMergeSDXL": ModelMergeSDXL,
"ModelMergeSD3_2B": ModelMergeSD3_2B,
}

View File

@ -89,8 +89,23 @@ class CLIPTextEncodeSD3:
return ([[cond, {"pooled_output": pooled}]],)
class ControlNetApplySD3(nodes.ControlNetApplyAdvanced):
@classmethod
def INPUT_TYPES(s):
return {"required": {"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"control_net": ("CONTROL_NET", ),
"vae": ("VAE", ),
"image": ("IMAGE", ),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})
}}
CATEGORY = "_for_testing/sd3"
NODE_CLASS_MAPPINGS = {
"TripleCLIPLoader": TripleCLIPLoader,
"EmptySD3LatentImage": EmptySD3LatentImage,
"CLIPTextEncodeSD3": CLIPTextEncodeSD3,
"ControlNetApplySD3": ControlNetApplySD3,
}

369
comfy_extras/nodes_gits.py Normal file
View File

@ -0,0 +1,369 @@
# from https://github.com/zju-pi/diff-sampler/tree/main/gits-main
import numpy as np
import torch
def loglinear_interp(t_steps, num_steps):
"""
Performs log-linear interpolation of a given array of decreasing numbers.
"""
xs = np.linspace(0, 1, len(t_steps))
ys = np.log(t_steps[::-1])
new_xs = np.linspace(0, 1, num_steps)
new_ys = np.interp(new_xs, xs, ys)
interped_ys = np.exp(new_ys)[::-1].copy()
return interped_ys
NOISE_LEVELS = {
0.80: [
[14.61464119, 7.49001646, 0.02916753],
[14.61464119, 11.54541874, 6.77309084, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 3.07277966, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 2.05039096, 0.02916753],
[14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 2.05039096, 0.02916753],
[14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 3.07277966, 1.56271636, 0.02916753],
[14.61464119, 12.96784878, 11.54541874, 8.75849152, 7.49001646, 5.85520077, 3.07277966, 1.56271636, 0.02916753],
[14.61464119, 13.76078796, 12.2308979, 10.90732002, 8.75849152, 7.49001646, 5.85520077, 3.07277966, 1.56271636, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 8.75849152, 7.49001646, 5.85520077, 3.07277966, 1.56271636, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 5.85520077, 3.07277966, 1.56271636, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.07277966, 1.56271636, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.07277966, 1.56271636, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.07277966, 1.56271636, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.07277966, 1.56271636, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.1956799, 1.98035145, 0.86115354, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.1956799, 1.98035145, 0.86115354, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.07277966, 1.84880662, 0.83188516, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, 9.24142551, 8.75849152, 8.30717278, 7.88507891, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.07277966, 1.84880662, 0.83188516, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, 9.24142551, 8.75849152, 8.30717278, 7.88507891, 7.49001646, 6.77309084, 5.85520077, 4.86714602, 3.75677586, 2.84484982, 1.78698075, 0.803307, 0.02916753],
],
0.85: [
[14.61464119, 7.49001646, 0.02916753],
[14.61464119, 7.49001646, 1.84880662, 0.02916753],
[14.61464119, 11.54541874, 6.77309084, 1.56271636, 0.02916753],
[14.61464119, 11.54541874, 7.11996698, 3.07277966, 1.24153244, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.09240818, 2.84484982, 0.95350921, 0.02916753],
[14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.09240818, 2.84484982, 0.95350921, 0.02916753],
[14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.58536053, 3.1956799, 1.84880662, 0.803307, 0.02916753],
[14.61464119, 12.96784878, 11.54541874, 8.75849152, 7.49001646, 5.58536053, 3.1956799, 1.84880662, 0.803307, 0.02916753],
[14.61464119, 12.96784878, 11.54541874, 8.75849152, 7.49001646, 6.14220476, 4.65472794, 3.07277966, 1.84880662, 0.803307, 0.02916753],
[14.61464119, 13.76078796, 12.2308979, 10.90732002, 8.75849152, 7.49001646, 6.14220476, 4.65472794, 3.07277966, 1.84880662, 0.803307, 0.02916753],
[14.61464119, 13.76078796, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 4.65472794, 3.07277966, 1.84880662, 0.803307, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 4.65472794, 3.07277966, 1.84880662, 0.803307, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 4.65472794, 3.07277966, 1.84880662, 0.803307, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.60512662, 2.6383388, 1.56271636, 0.72133851, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.46139455, 2.45070267, 1.56271636, 0.72133851, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.46139455, 2.45070267, 1.56271636, 0.72133851, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.46139455, 2.45070267, 1.56271636, 0.72133851, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.46139455, 2.45070267, 1.56271636, 0.72133851, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, 9.24142551, 8.75849152, 8.30717278, 7.88507891, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.46139455, 2.45070267, 1.56271636, 0.72133851, 0.02916753],
],
0.90: [
[14.61464119, 6.77309084, 0.02916753],
[14.61464119, 7.49001646, 1.56271636, 0.02916753],
[14.61464119, 7.49001646, 3.07277966, 0.95350921, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 2.54230714, 0.89115214, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 4.86714602, 2.54230714, 0.89115214, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.09240818, 3.07277966, 1.61558151, 0.69515091, 0.02916753],
[14.61464119, 12.2308979, 8.75849152, 7.11996698, 4.86714602, 3.07277966, 1.61558151, 0.69515091, 0.02916753],
[14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 2.95596409, 1.61558151, 0.69515091, 0.02916753],
[14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.19988537, 1.24153244, 0.57119018, 0.02916753],
[14.61464119, 12.96784878, 10.90732002, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.19988537, 1.24153244, 0.57119018, 0.02916753],
[14.61464119, 12.96784878, 11.54541874, 9.24142551, 8.30717278, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.19988537, 1.24153244, 0.57119018, 0.02916753],
[14.61464119, 12.96784878, 11.54541874, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.75677586, 2.84484982, 1.84880662, 1.08895338, 0.52423614, 0.02916753],
[14.61464119, 13.76078796, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.75677586, 2.84484982, 1.84880662, 1.08895338, 0.52423614, 0.02916753],
[14.61464119, 13.76078796, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.44769001, 5.58536053, 4.45427561, 3.32507086, 2.45070267, 1.61558151, 0.95350921, 0.45573691, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.44769001, 5.58536053, 4.45427561, 3.32507086, 2.45070267, 1.61558151, 0.95350921, 0.45573691, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.86714602, 3.91689563, 3.07277966, 2.27973175, 1.56271636, 0.95350921, 0.45573691, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.86714602, 3.91689563, 3.07277966, 2.27973175, 1.56271636, 0.95350921, 0.45573691, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.86714602, 3.91689563, 3.07277966, 2.27973175, 1.56271636, 0.95350921, 0.45573691, 0.02916753],
[14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 5.09240818, 4.45427561, 3.60512662, 2.95596409, 2.19988537, 1.51179266, 0.89115214, 0.43325692, 0.02916753],
],
0.95: [
[14.61464119, 6.77309084, 0.02916753],
[14.61464119, 6.77309084, 1.56271636, 0.02916753],
[14.61464119, 7.49001646, 2.84484982, 0.89115214, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 2.36326075, 0.803307, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 2.95596409, 1.56271636, 0.64427125, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 4.86714602, 2.95596409, 1.56271636, 0.64427125, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 4.86714602, 3.07277966, 1.91321158, 1.08895338, 0.50118381, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.07277966, 1.91321158, 1.08895338, 0.50118381, 0.02916753],
[14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 3.07277966, 1.91321158, 1.08895338, 0.50118381, 0.02916753],
[14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.19988537, 1.41535246, 0.803307, 0.38853383, 0.02916753],
[14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 4.65472794, 3.46139455, 2.6383388, 1.84880662, 1.24153244, 0.72133851, 0.34370604, 0.02916753],
[14.61464119, 12.96784878, 10.90732002, 8.75849152, 7.49001646, 5.85520077, 4.65472794, 3.46139455, 2.6383388, 1.84880662, 1.24153244, 0.72133851, 0.34370604, 0.02916753],
[14.61464119, 12.96784878, 10.90732002, 8.75849152, 7.49001646, 6.14220476, 4.86714602, 3.75677586, 2.95596409, 2.19988537, 1.56271636, 1.05362725, 0.64427125, 0.32104823, 0.02916753],
[14.61464119, 12.96784878, 10.90732002, 8.75849152, 7.49001646, 6.44769001, 5.58536053, 4.65472794, 3.60512662, 2.95596409, 2.19988537, 1.56271636, 1.05362725, 0.64427125, 0.32104823, 0.02916753],
[14.61464119, 12.96784878, 11.54541874, 9.24142551, 8.30717278, 7.49001646, 6.44769001, 5.58536053, 4.65472794, 3.60512662, 2.95596409, 2.19988537, 1.56271636, 1.05362725, 0.64427125, 0.32104823, 0.02916753],
[14.61464119, 12.96784878, 11.54541874, 9.24142551, 8.30717278, 7.49001646, 6.44769001, 5.58536053, 4.65472794, 3.75677586, 3.07277966, 2.45070267, 1.78698075, 1.24153244, 0.83188516, 0.50118381, 0.22545385, 0.02916753],
[14.61464119, 12.96784878, 11.54541874, 9.24142551, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 5.09240818, 4.45427561, 3.60512662, 2.95596409, 2.36326075, 1.72759056, 1.24153244, 0.83188516, 0.50118381, 0.22545385, 0.02916753],
[14.61464119, 13.76078796, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 5.09240818, 4.45427561, 3.60512662, 2.95596409, 2.36326075, 1.72759056, 1.24153244, 0.83188516, 0.50118381, 0.22545385, 0.02916753],
[14.61464119, 13.76078796, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 5.09240818, 4.45427561, 3.75677586, 3.07277966, 2.45070267, 1.91321158, 1.46270394, 1.05362725, 0.72133851, 0.43325692, 0.19894916, 0.02916753],
],
1.00: [
[14.61464119, 1.56271636, 0.02916753],
[14.61464119, 6.77309084, 0.95350921, 0.02916753],
[14.61464119, 6.77309084, 2.36326075, 0.803307, 0.02916753],
[14.61464119, 7.11996698, 3.07277966, 1.56271636, 0.59516323, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 2.84484982, 1.41535246, 0.57119018, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 2.84484982, 1.61558151, 0.86115354, 0.38853383, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 4.86714602, 2.84484982, 1.61558151, 0.86115354, 0.38853383, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 4.86714602, 3.07277966, 1.98035145, 1.24153244, 0.72133851, 0.34370604, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.07277966, 1.98035145, 1.24153244, 0.72133851, 0.34370604, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.27973175, 1.51179266, 0.95350921, 0.54755926, 0.25053367, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.36326075, 1.61558151, 1.08895338, 0.72133851, 0.41087446, 0.17026083, 0.02916753],
[14.61464119, 11.54541874, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.36326075, 1.61558151, 1.08895338, 0.72133851, 0.41087446, 0.17026083, 0.02916753],
[14.61464119, 11.54541874, 8.75849152, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.84484982, 2.12350607, 1.56271636, 1.08895338, 0.72133851, 0.41087446, 0.17026083, 0.02916753],
[14.61464119, 11.54541874, 8.75849152, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.84484982, 2.19988537, 1.61558151, 1.162866, 0.803307, 0.50118381, 0.27464288, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 8.75849152, 7.49001646, 5.85520077, 4.65472794, 3.75677586, 3.07277966, 2.45070267, 1.84880662, 1.36964464, 1.01931262, 0.72133851, 0.45573691, 0.25053367, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 8.75849152, 7.49001646, 6.14220476, 5.09240818, 4.26497746, 3.46139455, 2.84484982, 2.19988537, 1.67050016, 1.24153244, 0.92192322, 0.64427125, 0.43325692, 0.25053367, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 8.75849152, 7.49001646, 6.14220476, 5.09240818, 4.26497746, 3.60512662, 2.95596409, 2.45070267, 1.91321158, 1.51179266, 1.12534678, 0.83188516, 0.59516323, 0.38853383, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 12.2308979, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 5.09240818, 4.26497746, 3.60512662, 2.95596409, 2.45070267, 1.91321158, 1.51179266, 1.12534678, 0.83188516, 0.59516323, 0.38853383, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 12.2308979, 9.24142551, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 5.09240818, 4.26497746, 3.60512662, 2.95596409, 2.45070267, 1.91321158, 1.51179266, 1.12534678, 0.83188516, 0.59516323, 0.38853383, 0.22545385, 0.09824532, 0.02916753],
],
1.05: [
[14.61464119, 0.95350921, 0.02916753],
[14.61464119, 6.77309084, 0.89115214, 0.02916753],
[14.61464119, 6.77309084, 2.05039096, 0.72133851, 0.02916753],
[14.61464119, 6.77309084, 2.84484982, 1.28281462, 0.52423614, 0.02916753],
[14.61464119, 6.77309084, 3.07277966, 1.61558151, 0.803307, 0.34370604, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 2.84484982, 1.56271636, 0.803307, 0.34370604, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 2.84484982, 1.61558151, 0.95350921, 0.52423614, 0.22545385, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.07277966, 1.98035145, 1.24153244, 0.74807048, 0.41087446, 0.17026083, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.27973175, 1.51179266, 0.95350921, 0.59516323, 0.34370604, 0.13792117, 0.02916753],
[14.61464119, 7.49001646, 5.09240818, 3.46139455, 2.45070267, 1.61558151, 1.08895338, 0.72133851, 0.45573691, 0.25053367, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.09240818, 3.46139455, 2.45070267, 1.61558151, 1.08895338, 0.72133851, 0.45573691, 0.25053367, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.36326075, 1.61558151, 1.08895338, 0.72133851, 0.45573691, 0.25053367, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.45070267, 1.72759056, 1.24153244, 0.86115354, 0.59516323, 0.38853383, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.84484982, 2.19988537, 1.61558151, 1.162866, 0.83188516, 0.59516323, 0.38853383, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.84484982, 2.19988537, 1.67050016, 1.28281462, 0.95350921, 0.72133851, 0.52423614, 0.34370604, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.95596409, 2.36326075, 1.84880662, 1.41535246, 1.08895338, 0.83188516, 0.61951244, 0.45573691, 0.32104823, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.95596409, 2.45070267, 1.91321158, 1.51179266, 1.20157266, 0.95350921, 0.74807048, 0.57119018, 0.43325692, 0.29807833, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 8.30717278, 7.11996698, 5.85520077, 4.65472794, 3.60512662, 2.95596409, 2.45070267, 1.91321158, 1.51179266, 1.20157266, 0.95350921, 0.74807048, 0.57119018, 0.43325692, 0.29807833, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 8.30717278, 7.11996698, 5.85520077, 4.65472794, 3.60512662, 2.95596409, 2.45070267, 1.98035145, 1.61558151, 1.32549286, 1.08895338, 0.86115354, 0.69515091, 0.54755926, 0.41087446, 0.29807833, 0.19894916, 0.09824532, 0.02916753],
],
1.10: [
[14.61464119, 0.89115214, 0.02916753],
[14.61464119, 2.36326075, 0.72133851, 0.02916753],
[14.61464119, 5.85520077, 1.61558151, 0.57119018, 0.02916753],
[14.61464119, 6.77309084, 2.45070267, 1.08895338, 0.45573691, 0.02916753],
[14.61464119, 6.77309084, 2.95596409, 1.56271636, 0.803307, 0.34370604, 0.02916753],
[14.61464119, 6.77309084, 3.07277966, 1.61558151, 0.89115214, 0.4783645, 0.19894916, 0.02916753],
[14.61464119, 6.77309084, 3.07277966, 1.84880662, 1.08895338, 0.64427125, 0.34370604, 0.13792117, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 2.84484982, 1.61558151, 0.95350921, 0.54755926, 0.27464288, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 2.95596409, 1.91321158, 1.24153244, 0.803307, 0.4783645, 0.25053367, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.07277966, 2.05039096, 1.41535246, 0.95350921, 0.64427125, 0.41087446, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.27973175, 1.61558151, 1.12534678, 0.803307, 0.54755926, 0.36617002, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.32507086, 2.45070267, 1.72759056, 1.24153244, 0.89115214, 0.64427125, 0.45573691, 0.32104823, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 5.09240818, 3.60512662, 2.84484982, 2.05039096, 1.51179266, 1.08895338, 0.803307, 0.59516323, 0.43325692, 0.29807833, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 5.09240818, 3.60512662, 2.84484982, 2.12350607, 1.61558151, 1.24153244, 0.95350921, 0.72133851, 0.54755926, 0.41087446, 0.29807833, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.45070267, 1.84880662, 1.41535246, 1.08895338, 0.83188516, 0.64427125, 0.50118381, 0.36617002, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.45070267, 1.91321158, 1.51179266, 1.20157266, 0.95350921, 0.74807048, 0.59516323, 0.45573691, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 5.85520077, 4.45427561, 3.46139455, 2.84484982, 2.19988537, 1.72759056, 1.36964464, 1.08895338, 0.86115354, 0.69515091, 0.54755926, 0.43325692, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.46139455, 2.84484982, 2.19988537, 1.72759056, 1.36964464, 1.08895338, 0.86115354, 0.69515091, 0.54755926, 0.43325692, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.46139455, 2.84484982, 2.19988537, 1.72759056, 1.36964464, 1.08895338, 0.89115214, 0.72133851, 0.59516323, 0.4783645, 0.38853383, 0.29807833, 0.22545385, 0.17026083, 0.09824532, 0.02916753],
],
1.15: [
[14.61464119, 0.83188516, 0.02916753],
[14.61464119, 1.84880662, 0.59516323, 0.02916753],
[14.61464119, 5.85520077, 1.56271636, 0.52423614, 0.02916753],
[14.61464119, 5.85520077, 1.91321158, 0.83188516, 0.34370604, 0.02916753],
[14.61464119, 5.85520077, 2.45070267, 1.24153244, 0.59516323, 0.25053367, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.51179266, 0.803307, 0.41087446, 0.17026083, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.56271636, 0.89115214, 0.50118381, 0.25053367, 0.09824532, 0.02916753],
[14.61464119, 6.77309084, 3.07277966, 1.84880662, 1.12534678, 0.72133851, 0.43325692, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 6.77309084, 3.07277966, 1.91321158, 1.24153244, 0.803307, 0.52423614, 0.34370604, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 2.95596409, 1.91321158, 1.24153244, 0.803307, 0.52423614, 0.34370604, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.07277966, 2.05039096, 1.36964464, 0.95350921, 0.69515091, 0.4783645, 0.32104823, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.07277966, 2.12350607, 1.51179266, 1.08895338, 0.803307, 0.59516323, 0.43325692, 0.29807833, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.07277966, 2.12350607, 1.51179266, 1.08895338, 0.803307, 0.59516323, 0.45573691, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.07277966, 2.19988537, 1.61558151, 1.24153244, 0.95350921, 0.74807048, 0.59516323, 0.45573691, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.45070267, 1.78698075, 1.32549286, 1.01931262, 0.803307, 0.64427125, 0.50118381, 0.38853383, 0.29807833, 0.22545385, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.45070267, 1.78698075, 1.32549286, 1.01931262, 0.803307, 0.64427125, 0.52423614, 0.41087446, 0.32104823, 0.25053367, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.45070267, 1.84880662, 1.41535246, 1.12534678, 0.89115214, 0.72133851, 0.59516323, 0.4783645, 0.38853383, 0.32104823, 0.25053367, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.45070267, 1.84880662, 1.41535246, 1.12534678, 0.89115214, 0.72133851, 0.59516323, 0.50118381, 0.41087446, 0.34370604, 0.27464288, 0.22545385, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.45070267, 1.84880662, 1.41535246, 1.12534678, 0.89115214, 0.72133851, 0.59516323, 0.50118381, 0.41087446, 0.34370604, 0.29807833, 0.25053367, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
],
1.20: [
[14.61464119, 0.803307, 0.02916753],
[14.61464119, 1.56271636, 0.52423614, 0.02916753],
[14.61464119, 2.36326075, 0.92192322, 0.36617002, 0.02916753],
[14.61464119, 2.84484982, 1.24153244, 0.59516323, 0.25053367, 0.02916753],
[14.61464119, 5.85520077, 2.05039096, 0.95350921, 0.45573691, 0.17026083, 0.02916753],
[14.61464119, 5.85520077, 2.45070267, 1.24153244, 0.64427125, 0.29807833, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.45070267, 1.36964464, 0.803307, 0.45573691, 0.25053367, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.61558151, 0.95350921, 0.59516323, 0.36617002, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.67050016, 1.08895338, 0.74807048, 0.50118381, 0.32104823, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.95596409, 1.84880662, 1.24153244, 0.83188516, 0.59516323, 0.41087446, 0.27464288, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 3.07277966, 1.98035145, 1.36964464, 0.95350921, 0.69515091, 0.50118381, 0.36617002, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 6.77309084, 3.46139455, 2.36326075, 1.56271636, 1.08895338, 0.803307, 0.59516323, 0.45573691, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 6.77309084, 3.46139455, 2.45070267, 1.61558151, 1.162866, 0.86115354, 0.64427125, 0.50118381, 0.38853383, 0.29807833, 0.22545385, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.12350607, 1.51179266, 1.08895338, 0.83188516, 0.64427125, 0.50118381, 0.38853383, 0.29807833, 0.22545385, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.12350607, 1.51179266, 1.08895338, 0.83188516, 0.64427125, 0.50118381, 0.41087446, 0.32104823, 0.25053367, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.12350607, 1.51179266, 1.08895338, 0.83188516, 0.64427125, 0.50118381, 0.41087446, 0.34370604, 0.27464288, 0.22545385, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.19988537, 1.61558151, 1.20157266, 0.92192322, 0.72133851, 0.57119018, 0.45573691, 0.36617002, 0.29807833, 0.25053367, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.19988537, 1.61558151, 1.24153244, 0.95350921, 0.74807048, 0.59516323, 0.4783645, 0.38853383, 0.32104823, 0.27464288, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.19988537, 1.61558151, 1.24153244, 0.95350921, 0.74807048, 0.59516323, 0.50118381, 0.41087446, 0.34370604, 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
],
1.25: [
[14.61464119, 0.72133851, 0.02916753],
[14.61464119, 1.56271636, 0.50118381, 0.02916753],
[14.61464119, 2.05039096, 0.803307, 0.32104823, 0.02916753],
[14.61464119, 2.36326075, 0.95350921, 0.43325692, 0.17026083, 0.02916753],
[14.61464119, 2.84484982, 1.24153244, 0.59516323, 0.27464288, 0.09824532, 0.02916753],
[14.61464119, 3.07277966, 1.51179266, 0.803307, 0.43325692, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.36326075, 1.24153244, 0.72133851, 0.41087446, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.45070267, 1.36964464, 0.83188516, 0.52423614, 0.34370604, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.61558151, 0.98595673, 0.64427125, 0.43325692, 0.27464288, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.67050016, 1.08895338, 0.74807048, 0.52423614, 0.36617002, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.72759056, 1.162866, 0.803307, 0.59516323, 0.45573691, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.95596409, 1.84880662, 1.24153244, 0.86115354, 0.64427125, 0.4783645, 0.36617002, 0.27464288, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.95596409, 1.84880662, 1.28281462, 0.92192322, 0.69515091, 0.52423614, 0.41087446, 0.32104823, 0.25053367, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.95596409, 1.91321158, 1.32549286, 0.95350921, 0.72133851, 0.54755926, 0.43325692, 0.34370604, 0.27464288, 0.22545385, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.95596409, 1.91321158, 1.32549286, 0.95350921, 0.72133851, 0.57119018, 0.45573691, 0.36617002, 0.29807833, 0.25053367, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.95596409, 1.91321158, 1.32549286, 0.95350921, 0.74807048, 0.59516323, 0.4783645, 0.38853383, 0.32104823, 0.27464288, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 3.07277966, 2.05039096, 1.41535246, 1.05362725, 0.803307, 0.61951244, 0.50118381, 0.41087446, 0.34370604, 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 3.07277966, 2.05039096, 1.41535246, 1.05362725, 0.803307, 0.64427125, 0.52423614, 0.43325692, 0.36617002, 0.32104823, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 3.07277966, 2.05039096, 1.46270394, 1.08895338, 0.83188516, 0.66947293, 0.54755926, 0.45573691, 0.38853383, 0.34370604, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
],
1.30: [
[14.61464119, 0.72133851, 0.02916753],
[14.61464119, 1.24153244, 0.43325692, 0.02916753],
[14.61464119, 1.56271636, 0.59516323, 0.22545385, 0.02916753],
[14.61464119, 1.84880662, 0.803307, 0.36617002, 0.13792117, 0.02916753],
[14.61464119, 2.36326075, 1.01931262, 0.52423614, 0.25053367, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.36964464, 0.74807048, 0.41087446, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 3.07277966, 1.56271636, 0.89115214, 0.54755926, 0.34370604, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 3.07277966, 1.61558151, 0.95350921, 0.61951244, 0.41087446, 0.27464288, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.45070267, 1.36964464, 0.83188516, 0.54755926, 0.36617002, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.45070267, 1.41535246, 0.92192322, 0.64427125, 0.45573691, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.6383388, 1.56271636, 1.01931262, 0.72133851, 0.50118381, 0.36617002, 0.27464288, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.61558151, 1.05362725, 0.74807048, 0.54755926, 0.41087446, 0.32104823, 0.25053367, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.61558151, 1.08895338, 0.77538133, 0.57119018, 0.43325692, 0.34370604, 0.27464288, 0.22545385, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.61558151, 1.08895338, 0.803307, 0.59516323, 0.45573691, 0.36617002, 0.29807833, 0.25053367, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.61558151, 1.08895338, 0.803307, 0.59516323, 0.4783645, 0.38853383, 0.32104823, 0.27464288, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.72759056, 1.162866, 0.83188516, 0.64427125, 0.50118381, 0.41087446, 0.34370604, 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.72759056, 1.162866, 0.83188516, 0.64427125, 0.52423614, 0.43325692, 0.36617002, 0.32104823, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.78698075, 1.24153244, 0.92192322, 0.72133851, 0.57119018, 0.45573691, 0.38853383, 0.34370604, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.84484982, 1.78698075, 1.24153244, 0.92192322, 0.72133851, 0.57119018, 0.4783645, 0.41087446, 0.36617002, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
],
1.35: [
[14.61464119, 0.69515091, 0.02916753],
[14.61464119, 0.95350921, 0.34370604, 0.02916753],
[14.61464119, 1.56271636, 0.57119018, 0.19894916, 0.02916753],
[14.61464119, 1.61558151, 0.69515091, 0.29807833, 0.09824532, 0.02916753],
[14.61464119, 1.84880662, 0.83188516, 0.43325692, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.162866, 0.64427125, 0.36617002, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.36964464, 0.803307, 0.50118381, 0.32104823, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.41535246, 0.83188516, 0.54755926, 0.36617002, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.56271636, 0.95350921, 0.64427125, 0.45573691, 0.32104823, 0.22545385, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.56271636, 0.95350921, 0.64427125, 0.45573691, 0.34370604, 0.25053367, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 3.07277966, 1.61558151, 1.01931262, 0.72133851, 0.52423614, 0.38853383, 0.29807833, 0.22545385, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 3.07277966, 1.61558151, 1.01931262, 0.72133851, 0.52423614, 0.41087446, 0.32104823, 0.25053367, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 3.07277966, 1.61558151, 1.05362725, 0.74807048, 0.54755926, 0.43325692, 0.34370604, 0.27464288, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 3.07277966, 1.72759056, 1.12534678, 0.803307, 0.59516323, 0.45573691, 0.36617002, 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 3.07277966, 1.72759056, 1.12534678, 0.803307, 0.59516323, 0.4783645, 0.38853383, 0.32104823, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.45070267, 1.51179266, 1.01931262, 0.74807048, 0.57119018, 0.45573691, 0.36617002, 0.32104823, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.6383388, 1.61558151, 1.08895338, 0.803307, 0.61951244, 0.50118381, 0.41087446, 0.34370604, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.6383388, 1.61558151, 1.08895338, 0.803307, 0.64427125, 0.52423614, 0.43325692, 0.36617002, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 5.85520077, 2.6383388, 1.61558151, 1.08895338, 0.803307, 0.64427125, 0.52423614, 0.45573691, 0.38853383, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
],
1.40: [
[14.61464119, 0.59516323, 0.02916753],
[14.61464119, 0.95350921, 0.34370604, 0.02916753],
[14.61464119, 1.08895338, 0.43325692, 0.13792117, 0.02916753],
[14.61464119, 1.56271636, 0.64427125, 0.27464288, 0.09824532, 0.02916753],
[14.61464119, 1.61558151, 0.803307, 0.43325692, 0.22545385, 0.09824532, 0.02916753],
[14.61464119, 2.05039096, 0.95350921, 0.54755926, 0.34370604, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.24153244, 0.72133851, 0.43325692, 0.27464288, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.24153244, 0.74807048, 0.50118381, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.28281462, 0.803307, 0.52423614, 0.36617002, 0.27464288, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.28281462, 0.803307, 0.54755926, 0.38853383, 0.29807833, 0.22545385, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.41535246, 0.86115354, 0.59516323, 0.43325692, 0.32104823, 0.25053367, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.51179266, 0.95350921, 0.64427125, 0.45573691, 0.34370604, 0.27464288, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.51179266, 0.95350921, 0.64427125, 0.4783645, 0.36617002, 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.56271636, 0.98595673, 0.69515091, 0.52423614, 0.41087446, 0.34370604, 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.56271636, 1.01931262, 0.72133851, 0.54755926, 0.43325692, 0.36617002, 0.32104823, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.61558151, 1.05362725, 0.74807048, 0.57119018, 0.45573691, 0.38853383, 0.34370604, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.61558151, 1.08895338, 0.803307, 0.61951244, 0.50118381, 0.41087446, 0.36617002, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.61558151, 1.08895338, 0.803307, 0.61951244, 0.50118381, 0.43325692, 0.38853383, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.61558151, 1.08895338, 0.803307, 0.64427125, 0.52423614, 0.45573691, 0.41087446, 0.36617002, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
],
1.45: [
[14.61464119, 0.59516323, 0.02916753],
[14.61464119, 0.803307, 0.25053367, 0.02916753],
[14.61464119, 0.95350921, 0.34370604, 0.09824532, 0.02916753],
[14.61464119, 1.24153244, 0.54755926, 0.25053367, 0.09824532, 0.02916753],
[14.61464119, 1.56271636, 0.72133851, 0.36617002, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 1.61558151, 0.803307, 0.45573691, 0.27464288, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 1.91321158, 0.95350921, 0.57119018, 0.36617002, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 2.19988537, 1.08895338, 0.64427125, 0.41087446, 0.27464288, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.24153244, 0.74807048, 0.50118381, 0.34370604, 0.25053367, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.24153244, 0.74807048, 0.50118381, 0.36617002, 0.27464288, 0.22545385, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.28281462, 0.803307, 0.54755926, 0.41087446, 0.32104823, 0.25053367, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.28281462, 0.803307, 0.57119018, 0.43325692, 0.34370604, 0.27464288, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.28281462, 0.83188516, 0.59516323, 0.45573691, 0.36617002, 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.28281462, 0.83188516, 0.59516323, 0.45573691, 0.36617002, 0.32104823, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.51179266, 0.95350921, 0.69515091, 0.52423614, 0.41087446, 0.34370604, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.51179266, 0.95350921, 0.69515091, 0.52423614, 0.43325692, 0.36617002, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.56271636, 0.98595673, 0.72133851, 0.54755926, 0.45573691, 0.38853383, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.56271636, 1.01931262, 0.74807048, 0.57119018, 0.4783645, 0.41087446, 0.36617002, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.84484982, 1.56271636, 1.01931262, 0.74807048, 0.59516323, 0.50118381, 0.43325692, 0.38853383, 0.36617002, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
],
1.50: [
[14.61464119, 0.54755926, 0.02916753],
[14.61464119, 0.803307, 0.25053367, 0.02916753],
[14.61464119, 0.86115354, 0.32104823, 0.09824532, 0.02916753],
[14.61464119, 1.24153244, 0.54755926, 0.25053367, 0.09824532, 0.02916753],
[14.61464119, 1.56271636, 0.72133851, 0.36617002, 0.19894916, 0.09824532, 0.02916753],
[14.61464119, 1.61558151, 0.803307, 0.45573691, 0.27464288, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 1.61558151, 0.83188516, 0.52423614, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753],
[14.61464119, 1.84880662, 0.95350921, 0.59516323, 0.38853383, 0.27464288, 0.19894916, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 1.84880662, 0.95350921, 0.59516323, 0.41087446, 0.29807833, 0.22545385, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 1.84880662, 0.95350921, 0.61951244, 0.43325692, 0.32104823, 0.25053367, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.19988537, 1.12534678, 0.72133851, 0.50118381, 0.36617002, 0.27464288, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.19988537, 1.12534678, 0.72133851, 0.50118381, 0.36617002, 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.36326075, 1.24153244, 0.803307, 0.57119018, 0.43325692, 0.34370604, 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.36326075, 1.24153244, 0.803307, 0.57119018, 0.43325692, 0.34370604, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.36326075, 1.24153244, 0.803307, 0.59516323, 0.45573691, 0.36617002, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.36326075, 1.24153244, 0.803307, 0.59516323, 0.45573691, 0.38853383, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.32549286, 0.86115354, 0.64427125, 0.50118381, 0.41087446, 0.36617002, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.36964464, 0.92192322, 0.69515091, 0.54755926, 0.45573691, 0.41087446, 0.36617002, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
[14.61464119, 2.45070267, 1.41535246, 0.95350921, 0.72133851, 0.57119018, 0.4783645, 0.43325692, 0.38853383, 0.36617002, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753],
],
}
class GITSScheduler:
@classmethod
def INPUT_TYPES(s):
return {"required":
{"coeff": ("FLOAT", {"default": 1.20, "min": 0.80, "max": 1.50, "step": 0.05}),
"steps": ("INT", {"default": 10, "min": 2, "max": 1000}),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
}
}
RETURN_TYPES = ("SIGMAS",)
CATEGORY = "sampling/custom_sampling/schedulers"
FUNCTION = "get_sigmas"
def get_sigmas(self, coeff, steps, denoise):
total_steps = steps
if denoise < 1.0:
if denoise <= 0.0:
return (torch.FloatTensor([]),)
total_steps = round(steps * denoise)
if steps <= 20:
sigmas = NOISE_LEVELS[round(coeff, 2)][steps-2][:]
else:
sigmas = NOISE_LEVELS[round(coeff, 2)][-1][:]
sigmas = loglinear_interp(sigmas, steps + 1)
sigmas = sigmas[-(total_steps + 1):]
sigmas[-1] = 0
return (torch.FloatTensor(sigmas), )
NODE_CLASS_MAPPINGS = {
"GITSScheduler": GITSScheduler,
}

View File

@ -12,9 +12,9 @@ class Example:
Attributes
----------
RETURN_TYPES (`tuple`):
The type of each element in the output tulple.
The type of each element in the output tuple.
RETURN_NAMES (`tuple`):
Optional: The name of each output in the output tulple.
Optional: The name of each output in the output tuple.
FUNCTION (`str`):
The name of the entry-point method. For example, if `FUNCTION = "execute"` then it will run Example().execute()
OUTPUT_NODE ([`bool`]):
@ -44,7 +44,7 @@ class Example:
* Key field_name (`string`): Name of a entry-point method's argument
* Value field_config (`tuple`):
+ First value is a string indicate the type of field or a list for selection.
+ Secound value is a config for type "INT", "STRING" or "FLOAT".
+ Second value is a config for type "INT", "STRING" or "FLOAT".
"""
return {
"required": {
@ -61,7 +61,7 @@ class Example:
"min": 0.0,
"max": 10.0,
"step": 0.01,
"round": 0.001, #The value represeting the precision to round to, will be set to the step value by default. Can be set to False to disable rounding.
"round": 0.001, #The value representing the precision to round to, will be set to the step value by default. Can be set to False to disable rounding.
"display": "number"}),
"print_to_screen": (["enable", "disable"],),
"string_field": ("STRING", {
@ -106,6 +106,16 @@ class Example:
# Set the web directory, any .js file in that directory will be loaded by the frontend as a frontend extension
# WEB_DIRECTORY = "./somejs"
# Add custom API routes, using router
from aiohttp import web
from server import PromptServer
@PromptServer.instance.routes.get("/hello")
async def get_hello(request):
return web.json_response("hello")
# A dictionary that contains all nodes you want to export with their names
# NOTE: names should be globally unique
NODE_CLASS_MAPPINGS = {

0
folder_paths.py Normal file
View File

View File

@ -53,3 +53,4 @@ wrapt>=1.16.0
certifi
spandrel
numpy>=1.26.3,<2.0.0
soundfile

View File

@ -72,6 +72,7 @@ export function mockApi(config = {}) {
storeUserData: jest.fn((file, data) => {
userData[file] = data;
}),
listUserData: jest.fn(() => [])
};
jest.mock("../../comfy/web/scripts/api", () => ({
get api() {