mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-10 06:10:50 +08:00
Merge upstream
This commit is contained in:
commit
a13088ccec
45
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
45
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal 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
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
||||
32
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
32
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal 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
32
.github/ISSUE_TEMPLATE/user-support.yml
vendored
Normal 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
63
.github/workflows/test-browser.yml
vendored
Normal 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
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -12,6 +12,7 @@
|
||||
**/put*here
|
||||
/extra_model_paths.yaml
|
||||
/.vs
|
||||
.vscode/
|
||||
.idea/
|
||||
venv/
|
||||
/web/extensions/*
|
||||
@ -173,4 +174,5 @@ cython_debug/
|
||||
.openapi-generator/
|
||||
|
||||
/tests-ui/data/object_info.json
|
||||
/user/
|
||||
/user/
|
||||
*.log
|
||||
41
CONTRIBUTING.md
Normal file
41
CONTRIBUTING.md
Normal 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.
|
||||
@ -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/)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
96
comfy/cldm/mmdit.py
Normal 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}
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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
121
comfy/k_diffusion/deis.py
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]}
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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={}):
|
||||
|
||||
46
comfy/sd.py
46
comfy/sd.py
@ -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)
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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]}
|
||||
|
||||
147
comfy/utils.py
147
comfy/utils.py
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@ -1278,4 +1278,4 @@ const ext = {
|
||||
}
|
||||
};
|
||||
|
||||
app.registerExtension(ext);
|
||||
app.registerExtension(ext);
|
||||
@ -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;
|
||||
}
|
||||
178
comfy/web/extensions/core/uploadAudio.js
Normal file
178
comfy/web/extensions/core/uploadAudio.js
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
BIN
comfy/web/fonts/materialdesignicons-webfont.woff2
Normal file
BIN
comfy/web/fonts/materialdesignicons-webfont.woff2
Normal file
Binary file not shown.
@ -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>
|
||||
|
||||
@ -4,7 +4,9 @@
|
||||
"paths": {
|
||||
"/*": ["./*"]
|
||||
},
|
||||
"lib": ["DOM", "ES2022"]
|
||||
"lib": ["DOM", "ES2022", "DOM.Iterable"],
|
||||
"target": "ES2015",
|
||||
"module": "es2020"
|
||||
},
|
||||
"include": ["."]
|
||||
}
|
||||
|
||||
3
comfy/web/lib/materialdesignicons.min.css
vendored
Normal file
3
comfy/web/lib/materialdesignicons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -327,7 +327,7 @@ class ComfyApi extends EventTarget {
|
||||
|
||||
/**
|
||||
* Gets user configuration data and where data should be stored
|
||||
* @returns { Promise<{ storage: "server" | "browser", users?: Promise<string, unknown>, migrated?: boolean }> }
|
||||
* @returns { Promise<{ storage: "server" | "browser", users?: Promise<string, unknown>, migrated?: boolean }> }
|
||||
*/
|
||||
async getUserConfig() {
|
||||
return (await this.fetchApi("/users")).json();
|
||||
@ -335,7 +335,7 @@ class ComfyApi extends EventTarget {
|
||||
|
||||
/**
|
||||
* Creates a new user
|
||||
* @param { string } username
|
||||
* @param { string } username
|
||||
* @returns The fetch response
|
||||
*/
|
||||
createUser(username) {
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -1818,6 +1874,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);
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
254
comfy/web/scripts/changeTracker.js
Normal file
254
comfy/web/scripts/changeTracker.js
Normal 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({});
|
||||
@ -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,7 +210,9 @@ 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) {
|
||||
mouseDownHandler = (event) => {
|
||||
@ -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",
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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"}});
|
||||
}
|
||||
|
||||
64
comfy/web/scripts/ui/components/asyncDialog.js
Normal file
64
comfy/web/scripts/ui/components/asyncDialog.js
Normal 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;
|
||||
}
|
||||
}
|
||||
163
comfy/web/scripts/ui/components/button.js
Normal file
163
comfy/web/scripts/ui/components/button.js
Normal 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;
|
||||
}
|
||||
}
|
||||
45
comfy/web/scripts/ui/components/buttonGroup.js
Normal file
45
comfy/web/scripts/ui/components/buttonGroup.js
Normal 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));
|
||||
}
|
||||
}
|
||||
128
comfy/web/scripts/ui/components/popup.js
Normal file
128
comfy/web/scripts/ui/components/popup.js
Normal 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");
|
||||
}
|
||||
};
|
||||
}
|
||||
43
comfy/web/scripts/ui/components/splitButton.js
Normal file
43
comfy/web/scripts/ui/components/splitButton.js
Normal 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));
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
|
||||
302
comfy/web/scripts/ui/menu/index.js
Normal file
302
comfy/web/scripts/ui/menu/index.js
Normal 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);
|
||||
}
|
||||
}
|
||||
23
comfy/web/scripts/ui/menu/interruptButton.js
Normal file
23
comfy/web/scripts/ui/menu/interruptButton.js
Normal 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;
|
||||
}
|
||||
705
comfy/web/scripts/ui/menu/menu.css
Normal file
705
comfy/web/scripts/ui/menu/menu.css
Normal 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;
|
||||
}
|
||||
93
comfy/web/scripts/ui/menu/queueButton.js
Normal file
93
comfy/web/scripts/ui/menu/queueButton.js
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
77
comfy/web/scripts/ui/menu/queueOptions.js
Normal file
77
comfy/web/scripts/ui/menu/queueOptions.js
Normal 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,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
27
comfy/web/scripts/ui/menu/viewHistory.js
Normal file
27
comfy/web/scripts/ui/menu/viewHistory.js
Normal 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;
|
||||
}
|
||||
}
|
||||
203
comfy/web/scripts/ui/menu/viewList.js
Normal file
203
comfy/web/scripts/ui/menu/viewList.js
Normal 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
|
||||
),
|
||||
];
|
||||
};
|
||||
}
|
||||
55
comfy/web/scripts/ui/menu/viewQueue.js
Normal file
55
comfy/web/scripts/ui/menu/viewQueue.js
Normal 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) {}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
764
comfy/web/scripts/ui/menu/workflows.js
Normal file
764
comfy/web/scripts/ui/menu/workflows.js
Normal 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"
|
||||
),
|
||||
]);
|
||||
})
|
||||
),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
56
comfy/web/scripts/ui/utils.js
Normal file
56
comfy/web/scripts/ui/utils.js
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
450
comfy/web/scripts/workflows.js
Normal file
450
comfy/web/scripts/workflows.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
16
comfy/web/types/comfy.d.ts
vendored
16
comfy/web/types/comfy.d.ts
vendored
@ -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 = {
|
||||
|
||||
@ -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++",
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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++",
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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
369
comfy_extras/nodes_gits.py
Normal 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,
|
||||
}
|
||||
@ -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
0
folder_paths.py
Normal file
@ -52,4 +52,5 @@ huggingface_extra_chat_templates @ git+https://github.com/AppMana/appmana-comfyu
|
||||
wrapt>=1.16.0
|
||||
certifi
|
||||
spandrel
|
||||
numpy>=1.26.3,<2.0.0
|
||||
numpy>=1.26.3,<2.0.0
|
||||
soundfile
|
||||
@ -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() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user