mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-10 06:10:50 +08:00
Merge branch 'master' of github.com:comfyanonymous/ComfyUI
This commit is contained in:
commit
ffb4ed9cf2
25
.github/workflows/test-unit.yml
vendored
Normal file
25
.github/workflows/test-unit.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Unit Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
pip install -r tests-unit/requirements.txt
|
||||
python -m pytest tests-unit
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,6 +15,7 @@
|
||||
.vscode/
|
||||
.idea/
|
||||
venv/
|
||||
.venv/
|
||||
/web/extensions/*
|
||||
!/web/extensions/logging.js.example
|
||||
!/web/extensions/core/
|
||||
|
||||
@ -631,6 +631,8 @@ The default installation includes a fast latent preview method that's low-resolu
|
||||
| Alt + `+` | Canvas Zoom in |
|
||||
| Alt + `-` | Canvas Zoom out |
|
||||
| Ctrl + Shift + LMB + Vertical drag | Canvas Zoom in/out |
|
||||
| P | Pin/Unpin selected nodes |
|
||||
| Ctrl + G | Group selected nodes |
|
||||
| Q | Toggle visibility of the queue |
|
||||
| H | Toggle visibility of history |
|
||||
| R | Refresh graph |
|
||||
|
||||
@ -4,16 +4,21 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from urllib import parse
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from .app_settings import AppSettings
|
||||
from ..cli_args import args
|
||||
from ..cmd.folder_paths import user_directory
|
||||
from ..cmd import folder_paths
|
||||
|
||||
default_user = "default"
|
||||
|
||||
|
||||
class UserManager():
|
||||
def __init__(self):
|
||||
user_directory = folder_paths.get_user_directory()
|
||||
|
||||
self.default_user = "default"
|
||||
self.users_file = os.path.join(user_directory, "users.json")
|
||||
self.settings = AppSettings(self)
|
||||
@ -21,14 +26,17 @@ class UserManager():
|
||||
os.mkdir(user_directory)
|
||||
|
||||
if args.multi_user:
|
||||
if os.path.isfile(self.users_file):
|
||||
with open(self.users_file) as f:
|
||||
if os.path.isfile(self.get_users_file()):
|
||||
with open(self.get_users_file()) as f:
|
||||
self.users = json.load(f)
|
||||
else:
|
||||
self.users = {}
|
||||
else:
|
||||
self.users = {"default": "default"}
|
||||
|
||||
def get_users_file(self):
|
||||
return os.path.join(folder_paths.get_user_directory(), "users.json")
|
||||
|
||||
def get_request_user_id(self, request):
|
||||
user = "default"
|
||||
if args.multi_user and "comfy-user" in request.headers:
|
||||
@ -40,6 +48,7 @@ class UserManager():
|
||||
return user
|
||||
|
||||
def get_request_user_filepath(self, request, file, type="userdata", create_dir=True):
|
||||
user_directory = folder_paths.get_user_directory()
|
||||
|
||||
if type == "userdata":
|
||||
root_dir = user_directory
|
||||
@ -54,6 +63,10 @@ class UserManager():
|
||||
raise PermissionError()
|
||||
|
||||
if file is not None:
|
||||
# Check if filename is url encoded
|
||||
if "%" in file:
|
||||
file = parse.unquote(file)
|
||||
|
||||
# prevent leaving /{type}/{user}
|
||||
path = os.path.abspath(os.path.join(user_root, file))
|
||||
if os.path.commonpath((user_root, path)) != user_root:
|
||||
@ -75,7 +88,7 @@ class UserManager():
|
||||
|
||||
self.users[user_id] = name
|
||||
|
||||
with open(self.users_file, "w") as f:
|
||||
with open(self.get_users_file(), "w") as f:
|
||||
json.dump(self.users, f)
|
||||
|
||||
return user_id
|
||||
@ -108,23 +121,40 @@ class UserManager():
|
||||
async def listuserdata(request):
|
||||
directory = request.rel_url.query.get('dir', '')
|
||||
if not directory:
|
||||
return web.Response(status=400)
|
||||
|
||||
return web.Response(status=400, text="Directory not provided")
|
||||
path = self.get_request_user_filepath(request, directory)
|
||||
if not path:
|
||||
return web.Response(status=403)
|
||||
|
||||
return web.Response(status=403, text="Invalid directory")
|
||||
if not os.path.exists(path):
|
||||
return web.Response(status=404)
|
||||
|
||||
return web.Response(status=404, text="Directory not found")
|
||||
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)]
|
||||
full_info = request.rel_url.query.get('full_info', '').lower() == "true"
|
||||
|
||||
# Use different patterns based on whether we're recursing or not
|
||||
if recurse:
|
||||
pattern = os.path.join(glob.escape(path), '**', '*')
|
||||
else:
|
||||
pattern = os.path.join(glob.escape(path), '*')
|
||||
|
||||
results = glob.glob(pattern, recursive=recurse)
|
||||
|
||||
if full_info:
|
||||
results = [
|
||||
{
|
||||
'path': os.path.relpath(x, path).replace(os.sep, '/'),
|
||||
'size': os.path.getsize(x),
|
||||
'modified': os.path.getmtime(x)
|
||||
} for x in results if os.path.isfile(x)
|
||||
]
|
||||
else:
|
||||
results = [
|
||||
os.path.relpath(x, path).replace(os.sep, '/')
|
||||
for x in results
|
||||
if os.path.isfile(x)
|
||||
]
|
||||
split_path = request.rel_url.query.get('split', '').lower() == "true"
|
||||
if split_path:
|
||||
results = [[x] + x.split(os.sep) for x in results]
|
||||
if split_path and not full_info:
|
||||
results = [[x] + x.split('/') for x in results]
|
||||
|
||||
return web.json_response(results)
|
||||
|
||||
|
||||
@ -71,6 +71,7 @@ class CacheKeySetInputSignature(CacheKeySet):
|
||||
super().__init__(dynprompt, node_ids, is_changed_cache)
|
||||
self.dynprompt = dynprompt
|
||||
self.is_changed_cache = is_changed_cache
|
||||
self.immediate_node_signature = {}
|
||||
self.add_keys(node_ids)
|
||||
|
||||
def include_node_id_in_input(self) -> bool:
|
||||
@ -98,11 +99,13 @@ class CacheKeySetInputSignature(CacheKeySet):
|
||||
if not dynprompt.has_node(node_id):
|
||||
# This node doesn't exist -- we can't cache it.
|
||||
return [float("NaN")]
|
||||
if node_id in self.immediate_node_signature: # reduce repeated calls of ancestors
|
||||
return self.immediate_node_signature[node_id]
|
||||
node = dynprompt.get_node(node_id)
|
||||
class_type = node["class_type"]
|
||||
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
|
||||
signature = [class_type, self.is_changed_cache.get(node_id)]
|
||||
if self.include_node_id_in_input() or (hasattr(class_def, "NOT_IDEMPOTENT") and class_def.NOT_IDEMPOTENT):
|
||||
if self.include_node_id_in_input() or (hasattr(class_def, "NOT_IDEMPOTENT") and class_def.NOT_IDEMPOTENT) or "UNIQUE_ID" in class_def.INPUT_TYPES().get("hidden", {}).values():
|
||||
signature.append(node_id)
|
||||
inputs = node["inputs"]
|
||||
for key in sorted(inputs.keys()):
|
||||
@ -112,6 +115,7 @@ class CacheKeySetInputSignature(CacheKeySet):
|
||||
signature.append((key, ("ANCESTOR", ancestor_index, ancestor_socket)))
|
||||
else:
|
||||
signature.append((key, inputs[key]))
|
||||
self.immediate_node_signature[node_id] = signature
|
||||
return signature
|
||||
|
||||
# This function returns a list of all ancestors of the given node. The order of the list is
|
||||
|
||||
@ -215,6 +215,8 @@ def _create_parser() -> EnhancedConfigArgParser:
|
||||
default=None
|
||||
)
|
||||
|
||||
parser.add_argument("--user-directory", type=is_valid_directory, default=None, help="Set the ComfyUI user directory with an absolute path.")
|
||||
|
||||
# now give plugins a chance to add configuration
|
||||
for entry_point in entry_points().select(group='comfyui.custom_config'):
|
||||
try:
|
||||
|
||||
@ -112,6 +112,7 @@ class Configuration(dict):
|
||||
executor_factory (str): Either ThreadPoolExecutor or ProcessPoolExecutor, defaulting to ThreadPoolExecutor
|
||||
preview_size (int): Sets the maximum preview size for sampler nodes. Defaults to 512.
|
||||
openai_api_key (str): Configures the OpenAI API Key for the OpenAI nodes
|
||||
user_directory (Optional[str]): Set the ComfyUI user directory with an absolute path.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@ -200,6 +201,7 @@ class Configuration(dict):
|
||||
|
||||
self.executor_factory: str = "ThreadPoolExecutor"
|
||||
self.openai_api_key: Optional[str] = None
|
||||
self.user_directory: Optional[str] = None
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item not in self:
|
||||
|
||||
@ -229,7 +229,13 @@ def merge_result_data(results, obj):
|
||||
# merge node execution results
|
||||
for i, is_list in zip(range(len(results[0])), output_is_list):
|
||||
if is_list:
|
||||
output.append([x for o in results for x in o[i]])
|
||||
value = []
|
||||
for o in results:
|
||||
if isinstance(o[i], ExecutionBlocker):
|
||||
value.append(o[i])
|
||||
else:
|
||||
value.extend(o[i])
|
||||
output.append(value)
|
||||
else:
|
||||
output.append([o[i] for o in results])
|
||||
return output
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import time
|
||||
from typing import Optional, List, Final
|
||||
from typing import Optional, List, Final, Literal
|
||||
|
||||
from .folder_paths_pre import get_base_path
|
||||
from ..component_model.files import get_package_as_path
|
||||
from ..component_model.folder_path_types import FolderPathsTuple, FolderNames, SaveImagePathResponse
|
||||
from ..component_model.folder_path_types import extension_mimetypes_cache as _extension_mimetypes_cache
|
||||
from ..component_model.folder_path_types import supported_pt_extensions as _supported_pt_extensions
|
||||
from ..component_model.module_property import module_property
|
||||
|
||||
supported_pt_extensions: Final[frozenset[str]] = _supported_pt_extensions
|
||||
extension_mimetypes_cache: Final[dict[str, str]] = _extension_mimetypes_cache
|
||||
|
||||
|
||||
# todo: this needs to be wrapped in a context and configurable
|
||||
@ -87,6 +90,15 @@ def get_input_directory():
|
||||
return input_directory
|
||||
|
||||
|
||||
def get_user_directory() -> str:
|
||||
return user_directory
|
||||
|
||||
|
||||
def set_user_directory(user_dir: str) -> None:
|
||||
global user_directory
|
||||
user_directory = user_dir
|
||||
|
||||
|
||||
# NOTE: used in http server so don't put folders that should not be accessed remotely
|
||||
def get_directory_by_type(type_name):
|
||||
if type_name == "output":
|
||||
@ -277,18 +289,25 @@ def get_filename_list(folder_name):
|
||||
|
||||
|
||||
def get_save_image_path(filename_prefix, output_dir, image_width=0, image_height=0):
|
||||
def map_filename(filename):
|
||||
def map_filename(filename: str) -> tuple[int, str]:
|
||||
prefix_len = len(os.path.basename(filename_prefix))
|
||||
prefix = filename[:prefix_len + 1]
|
||||
try:
|
||||
digits = int(filename[prefix_len + 1:].split('_')[0])
|
||||
except:
|
||||
digits = 0
|
||||
return (digits, prefix)
|
||||
return digits, prefix
|
||||
|
||||
def compute_vars(input, image_width, image_height):
|
||||
def compute_vars(input: str, image_width: int, image_height: int) -> str:
|
||||
input = input.replace("%width%", str(image_width))
|
||||
input = input.replace("%height%", str(image_height))
|
||||
now = time.localtime()
|
||||
input = input.replace("%year%", str(now.tm_year))
|
||||
input = input.replace("%month%", str(now.tm_mon).zfill(2))
|
||||
input = input.replace("%day%", str(now.tm_mday).zfill(2))
|
||||
input = input.replace("%hour%", str(now.tm_hour).zfill(2))
|
||||
input = input.replace("%minute%", str(now.tm_min).zfill(2))
|
||||
input = input.replace("%second%", str(now.tm_sec).zfill(2))
|
||||
return input
|
||||
|
||||
filename_prefix = compute_vars(filename_prefix, image_width, image_height)
|
||||
@ -328,3 +347,27 @@ def create_directories():
|
||||
def invalidate_cache(folder_name):
|
||||
global _filename_list_cache
|
||||
_filename_list_cache.pop(folder_name, None)
|
||||
|
||||
|
||||
def filter_files_content_types(files: list[str], content_types: Literal["image", "video", "audio"]) -> list[str]:
|
||||
"""
|
||||
Example:
|
||||
files = os.listdir(folder_paths.get_input_directory())
|
||||
filter_files_content_types(files, ["image", "audio", "video"])
|
||||
"""
|
||||
global extension_mimetypes_cache
|
||||
result = []
|
||||
for file in files:
|
||||
extension = file.split('.')[-1]
|
||||
if extension not in extension_mimetypes_cache:
|
||||
mime_type, _ = mimetypes.guess_type(file, strict=False)
|
||||
if not mime_type:
|
||||
continue
|
||||
content_type = mime_type.split('/')[0]
|
||||
extension_mimetypes_cache[extension] = content_type
|
||||
else:
|
||||
content_type = extension_mimetypes_cache[extension]
|
||||
|
||||
if content_type in content_types:
|
||||
result.append(file)
|
||||
return result
|
||||
|
||||
@ -125,6 +125,11 @@ async def main(from_script_dir: Optional[Path] = None):
|
||||
folder_paths.set_temp_directory(temp_dir)
|
||||
cleanup_temp()
|
||||
|
||||
if args.user_directory:
|
||||
user_dir = os.path.abspath(args.user_directory)
|
||||
logging.info(f"Setting user directory to: {user_dir}")
|
||||
folder_paths.set_user_directory(user_dir)
|
||||
|
||||
# configure extra model paths earlier
|
||||
try:
|
||||
extra_model_paths_config_path = os.path.join(os_getcwd, "extra_model_paths.yaml")
|
||||
|
||||
@ -2,13 +2,16 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import glob
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import traceback
|
||||
import urllib
|
||||
import uuid
|
||||
from asyncio import Future, AbstractEventLoop
|
||||
from enum import Enum
|
||||
@ -102,6 +105,69 @@ def create_cors_middleware(allowed_origin: str):
|
||||
return cors_middleware
|
||||
|
||||
|
||||
def is_loopback(host):
|
||||
if host is None:
|
||||
return False
|
||||
try:
|
||||
if ipaddress.ip_address(host).is_loopback:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
loopback = False
|
||||
for family in (socket.AF_INET, socket.AF_INET6):
|
||||
try:
|
||||
r = socket.getaddrinfo(host, None, family, socket.SOCK_STREAM)
|
||||
for family, _, _, _, sockaddr in r:
|
||||
if not ipaddress.ip_address(sockaddr[0]).is_loopback:
|
||||
return loopback
|
||||
else:
|
||||
loopback = True
|
||||
except socket.gaierror:
|
||||
pass
|
||||
|
||||
return loopback
|
||||
|
||||
|
||||
def create_origin_only_middleware():
|
||||
@web.middleware
|
||||
async def origin_only_middleware(request: web.Request, handler):
|
||||
# this code is used to prevent the case where a random website can queue comfy workflows by making a POST to 127.0.0.1 which browsers don't prevent for some dumb reason.
|
||||
# in that case the Host and Origin hostnames won't match
|
||||
# I know the proper fix would be to add a cookie but this should take care of the problem in the meantime
|
||||
if 'Host' in request.headers and 'Origin' in request.headers:
|
||||
host = request.headers['Host']
|
||||
origin = request.headers['Origin']
|
||||
host_domain = host.lower()
|
||||
parsed = urllib.parse.urlparse(origin)
|
||||
origin_domain = parsed.netloc.lower()
|
||||
host_domain_parsed = urllib.parse.urlsplit('//' + host_domain)
|
||||
|
||||
# limit the check to when the host domain is localhost, this makes it slightly less safe but should still prevent the exploit
|
||||
loopback = is_loopback(host_domain_parsed.hostname)
|
||||
|
||||
if parsed.port is None: # if origin doesn't have a port strip it from the host to handle weird browsers, same for host
|
||||
host_domain = host_domain_parsed.hostname
|
||||
if host_domain_parsed.port is None:
|
||||
origin_domain = parsed.hostname
|
||||
|
||||
if loopback and host_domain is not None and origin_domain is not None and len(host_domain) > 0 and len(origin_domain) > 0:
|
||||
if host_domain != origin_domain:
|
||||
logging.warning("WARNING: request with non matching host and origin {} != {}, returning 403".format(host_domain, origin_domain))
|
||||
return web.Response(status=403)
|
||||
|
||||
if request.method == "OPTIONS":
|
||||
response = web.Response()
|
||||
else:
|
||||
response = await handler(request)
|
||||
|
||||
return response
|
||||
|
||||
return origin_only_middleware
|
||||
|
||||
|
||||
class PromptServer(ExecutorToClientProgress):
|
||||
instance: Optional['PromptServer'] = None
|
||||
|
||||
@ -129,6 +195,8 @@ class PromptServer(ExecutorToClientProgress):
|
||||
middlewares = [cache_control]
|
||||
if args.enable_cors_header:
|
||||
middlewares.append(create_cors_middleware(args.enable_cors_header))
|
||||
else:
|
||||
middlewares.append(create_origin_only_middleware())
|
||||
|
||||
max_upload_size = round(args.max_upload_size * 1024 * 1024)
|
||||
self.app: web.Application = web.Application(client_max_size=max_upload_size,
|
||||
|
||||
@ -5,6 +5,9 @@ import os
|
||||
from typing import List, Set, Any, Iterator, Sequence, Dict, NamedTuple
|
||||
|
||||
supported_pt_extensions = frozenset(['.ckpt', '.pt', '.bin', '.pth', '.safetensors', '.pkl', '.sft'])
|
||||
extension_mimetypes_cache = {
|
||||
"webp": "image",
|
||||
}
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
|
||||
@ -513,7 +513,9 @@ def load_controlnet_flux_instantx(sd):
|
||||
if union_cnet in new_sd:
|
||||
num_union_modes = new_sd[union_cnet].shape[0]
|
||||
|
||||
control_model = controlnet_flux.ControlNetFlux(latent_input=True, num_union_modes=num_union_modes, operations=operations, device=offload_device, dtype=unet_dtype, **model_config.unet_config)
|
||||
control_latent_channels = new_sd.get("pos_embed_input.weight").shape[1] // 4
|
||||
|
||||
control_model = controlnet_flux.ControlNetFlux(latent_input=True, num_union_modes=num_union_modes, control_latent_channels=control_latent_channels, operations=operations, device=offload_device, dtype=unet_dtype, **model_config.unet_config)
|
||||
control_model = controlnet_load_state_dict(control_model, new_sd)
|
||||
|
||||
latent_format = latent_formats.Flux()
|
||||
|
||||
@ -93,30 +93,44 @@ class TopologicalSort:
|
||||
self.add_strong_link(from_node_id, from_socket, to_node_id)
|
||||
|
||||
def add_strong_link(self, from_node_id, from_socket, to_node_id):
|
||||
self.add_node(from_node_id)
|
||||
if to_node_id not in self.blocking[from_node_id]:
|
||||
self.blocking[from_node_id][to_node_id] = {}
|
||||
self.blockCount[to_node_id] += 1
|
||||
self.blocking[from_node_id][to_node_id][from_socket] = True
|
||||
if not self.is_cached(from_node_id):
|
||||
self.add_node(from_node_id)
|
||||
if to_node_id not in self.blocking[from_node_id]:
|
||||
self.blocking[from_node_id][to_node_id] = {}
|
||||
self.blockCount[to_node_id] += 1
|
||||
self.blocking[from_node_id][to_node_id][from_socket] = True
|
||||
|
||||
def add_node(self, unique_id, include_lazy=False, subgraph_nodes=None):
|
||||
if unique_id in self.pendingNodes:
|
||||
return
|
||||
self.pendingNodes[unique_id] = True
|
||||
self.blockCount[unique_id] = 0
|
||||
self.blocking[unique_id] = {}
|
||||
def add_node(self, node_unique_id, include_lazy=False, subgraph_nodes=None):
|
||||
node_ids = [node_unique_id]
|
||||
links = []
|
||||
|
||||
inputs = self.dynprompt.get_node(unique_id)["inputs"]
|
||||
for input_name in inputs:
|
||||
value = inputs[input_name]
|
||||
if is_link(value):
|
||||
from_node_id, from_socket = value
|
||||
if subgraph_nodes is not None and from_node_id not in subgraph_nodes:
|
||||
continue
|
||||
input_type, input_category, input_info = self.get_input_info(unique_id, input_name)
|
||||
is_lazy = input_info is not None and "lazy" in input_info and input_info["lazy"]
|
||||
if include_lazy or not is_lazy:
|
||||
self.add_strong_link(from_node_id, from_socket, unique_id)
|
||||
while len(node_ids) > 0:
|
||||
unique_id = node_ids.pop()
|
||||
if unique_id in self.pendingNodes:
|
||||
continue
|
||||
|
||||
self.pendingNodes[unique_id] = True
|
||||
self.blockCount[unique_id] = 0
|
||||
self.blocking[unique_id] = {}
|
||||
|
||||
inputs = self.dynprompt.get_node(unique_id)["inputs"]
|
||||
for input_name in inputs:
|
||||
value = inputs[input_name]
|
||||
if is_link(value):
|
||||
from_node_id, from_socket = value
|
||||
if subgraph_nodes is not None and from_node_id not in subgraph_nodes:
|
||||
continue
|
||||
input_type, input_category, input_info = self.get_input_info(unique_id, input_name)
|
||||
is_lazy = input_info is not None and "lazy" in input_info and input_info["lazy"]
|
||||
if (include_lazy or not is_lazy) and not self.is_cached(from_node_id):
|
||||
node_ids.append(from_node_id)
|
||||
links.append((from_node_id, from_socket, unique_id))
|
||||
|
||||
for link in links:
|
||||
self.add_strong_link(*link)
|
||||
|
||||
def is_cached(self, node_id):
|
||||
return False
|
||||
|
||||
def get_ready_nodes(self):
|
||||
return [node_id for node_id in self.pendingNodes if self.blockCount[node_id] == 0]
|
||||
@ -142,11 +156,8 @@ class ExecutionList(TopologicalSort):
|
||||
self.output_cache = output_cache
|
||||
self.staged_node_id = None
|
||||
|
||||
def add_strong_link(self, from_node_id, from_socket, to_node_id):
|
||||
if self.output_cache.get(from_node_id) is not None:
|
||||
# Nothing to do
|
||||
return
|
||||
super().add_strong_link(from_node_id, from_socket, to_node_id)
|
||||
def is_cached(self, node_id):
|
||||
return self.output_cache.get(node_id) is not None
|
||||
|
||||
def stage_node_execution(self):
|
||||
assert self.staged_node_id is None
|
||||
|
||||
@ -1126,3 +1126,45 @@ def sample_euler_ancestral_cfg_pp(model, x, sigmas, extra_args=None, callback=No
|
||||
if sigmas[i + 1] > 0:
|
||||
x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * sigma_up
|
||||
return x
|
||||
@torch.no_grad()
|
||||
def sample_dpmpp_2s_ancestral_cfg_pp(model, x, sigmas, extra_args=None, callback=None, disable=None, eta=1., s_noise=1., noise_sampler=None):
|
||||
"""Ancestral sampling with DPM-Solver++(2S) second-order 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]])
|
||||
sigma_fn = lambda t: t.neg().exp()
|
||||
t_fn = lambda sigma: sigma.log().neg()
|
||||
|
||||
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})
|
||||
if sigma_down == 0:
|
||||
# Euler method
|
||||
d = to_d(x, sigmas[i], temp[0])
|
||||
dt = sigma_down - sigmas[i]
|
||||
x = denoised + d * sigma_down
|
||||
else:
|
||||
# DPM-Solver++(2S)
|
||||
t, t_next = t_fn(sigmas[i]), t_fn(sigma_down)
|
||||
# r = torch.sinh(1 + (2 - eta) * (t_next - t) / (t - t_fn(sigma_up))) works only on non-cfgpp, weird
|
||||
r = 1 / 2
|
||||
h = t_next - t
|
||||
s = t + r * h
|
||||
x_2 = (sigma_fn(s) / sigma_fn(t)) * (x + (denoised - temp[0])) - (-h * r).expm1() * denoised
|
||||
denoised_2 = model(x_2, sigma_fn(s) * s_in, **extra_args)
|
||||
x = (sigma_fn(t_next) / sigma_fn(t)) * (x + (denoised - temp[0])) - (-h).expm1() * denoised_2
|
||||
# Noise addition
|
||||
if sigmas[i + 1] > 0:
|
||||
x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * sigma_up
|
||||
return x
|
||||
|
||||
@ -16,7 +16,7 @@ except:
|
||||
rms_norm_torch = None
|
||||
|
||||
def rms_norm(x, weight, eps=1e-6):
|
||||
if rms_norm_torch is not None:
|
||||
if rms_norm_torch is not None and not (torch.jit.is_tracing() or torch.jit.is_scripting()):
|
||||
return rms_norm_torch(x, weight.shape, weight=ops.cast_to(weight, dtype=x.dtype, device=x.device), eps=eps)
|
||||
else:
|
||||
rrms = torch.rsqrt(torch.mean(x**2, dim=-1, keepdim=True) + eps)
|
||||
|
||||
@ -53,7 +53,7 @@ class MistolineControlnetBlock(nn.Module):
|
||||
|
||||
|
||||
class ControlNetFlux(Flux):
|
||||
def __init__(self, latent_input=False, num_union_modes=0, mistoline=False, image_model=None, dtype=None, device=None, operations=None, **kwargs):
|
||||
def __init__(self, latent_input=False, num_union_modes=0, mistoline=False, control_latent_channels=None, image_model=None, dtype=None, device=None, operations=None, **kwargs):
|
||||
super().__init__(final_layer=False, dtype=dtype, device=device, operations=operations, **kwargs)
|
||||
|
||||
self.main_model_double = 19
|
||||
@ -81,7 +81,12 @@ class ControlNetFlux(Flux):
|
||||
|
||||
self.gradient_checkpointing = False
|
||||
self.latent_input = latent_input
|
||||
self.pos_embed_input = operations.Linear(self.in_channels, self.hidden_size, bias=True, dtype=dtype, device=device)
|
||||
if control_latent_channels is None:
|
||||
control_latent_channels = self.in_channels
|
||||
else:
|
||||
control_latent_channels *= 2 * 2 #patch size
|
||||
|
||||
self.pos_embed_input = operations.Linear(control_latent_channels, self.hidden_size, bias=True, dtype=dtype, device=device)
|
||||
if not self.latent_input:
|
||||
if self.mistoline:
|
||||
self.input_cond_block = MistolineCondDownsamplBlock(dtype=dtype, device=device, operations=operations)
|
||||
|
||||
@ -199,9 +199,13 @@ def load_lora(lora, to_load):
|
||||
|
||||
def model_lora_keys_clip(model, key_map={}):
|
||||
sdk = model.state_dict().keys()
|
||||
for k in sdk:
|
||||
if k.endswith(".weight"):
|
||||
key_map["text_encoders.{}".format(k[:-len(".weight")])] = k #generic lora format without any weird key names
|
||||
|
||||
text_model_lora_key = "lora_te_text_model_encoder_layers_{}_{}"
|
||||
clip_l_present = False
|
||||
clip_g_present = False
|
||||
for b in range(32): # TODO: clean up
|
||||
for c in LORA_CLIP_MAP:
|
||||
k = "clip_h.transformer.text_model.encoder.layers.{}.{}.weight".format(b, c)
|
||||
@ -225,6 +229,7 @@ def model_lora_keys_clip(model, key_map={}):
|
||||
|
||||
k = "clip_g.transformer.text_model.encoder.layers.{}.{}.weight".format(b, c)
|
||||
if k in sdk:
|
||||
clip_g_present = True
|
||||
if clip_l_present:
|
||||
lora_key = "lora_te2_text_model_encoder_layers_{}_{}".format(b, LORA_CLIP_MAP[c]) # SDXL base
|
||||
key_map[lora_key] = k
|
||||
@ -240,10 +245,18 @@ def model_lora_keys_clip(model, key_map={}):
|
||||
|
||||
for k in sdk:
|
||||
if k.endswith(".weight"):
|
||||
if k.startswith("t5xxl.transformer."): # OneTrainer SD3 lora
|
||||
if k.startswith("t5xxl.transformer."): # OneTrainer SD3 and Flux lora
|
||||
l_key = k[len("t5xxl.transformer."):-len(".weight")]
|
||||
lora_key = "lora_te3_{}".format(l_key.replace(".", "_"))
|
||||
key_map[lora_key] = k
|
||||
t5_index = 1
|
||||
if clip_g_present:
|
||||
t5_index += 1
|
||||
if clip_l_present:
|
||||
t5_index += 1
|
||||
if t5_index == 2:
|
||||
key_map["lora_te{}_{}".format(t5_index, l_key.replace(".", "_"))] = k #OneTrainer Flux
|
||||
t5_index += 1
|
||||
|
||||
key_map["lora_te{}_{}".format(t5_index, l_key.replace(".", "_"))] = k
|
||||
elif k.startswith("hydit_clip.transformer.bert."): # HunyuanDiT Lora
|
||||
l_key = k[len("hydit_clip.transformer.bert."):-len(".weight")]
|
||||
lora_key = "lora_te1_{}".format(l_key.replace(".", "_"))
|
||||
|
||||
@ -30,7 +30,7 @@ from . import utils
|
||||
from .float import stochastic_rounding
|
||||
from .model_base import BaseModel
|
||||
from .model_management_types import ModelManageable, MemoryMeasurements
|
||||
from .types import UnetWrapperFunction
|
||||
from .comfy_types import UnetWrapperFunction
|
||||
|
||||
def string_to_seed(data):
|
||||
crc = 0xFFFFFFFF
|
||||
|
||||
@ -504,6 +504,7 @@ class CheckpointLoader:
|
||||
FUNCTION = "load_checkpoint"
|
||||
|
||||
CATEGORY = "advanced/loaders"
|
||||
DEPRECATED = True
|
||||
|
||||
def load_checkpoint(self, config_name, ckpt_name):
|
||||
config_path = folder_paths.get_full_path("configs", config_name)
|
||||
|
||||
@ -5,7 +5,7 @@ import collections
|
||||
from . import model_management
|
||||
import math
|
||||
import logging
|
||||
import scipy
|
||||
import scipy.stats
|
||||
import numpy
|
||||
from . import sampler_helpers
|
||||
|
||||
@ -573,7 +573,7 @@ class Sampler:
|
||||
return math.isclose(max_sigma, sigma, rel_tol=1e-05) or sigma > max_sigma
|
||||
|
||||
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",
|
||||
"lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_ancestral", "dpmpp_2s_ancestral_cfg_pp", "dpmpp_sde", "dpmpp_sde_gpu",
|
||||
"dpmpp_2m", "dpmpp_2m_sde", "dpmpp_2m_sde_gpu", "dpmpp_3m_sde", "dpmpp_3m_sde_gpu", "ddpm", "lcm",
|
||||
"ipndm", "ipndm_v", "deis"]
|
||||
|
||||
|
||||
1
comfy/web/assets/index-BD-Ia1C4.js.map
generated
vendored
1
comfy/web/assets/index-BD-Ia1C4.js.map
generated
vendored
File diff suppressed because one or more lines are too long
0
comfy/web/assets/index-DjWyclij.css → comfy/web/assets/index-BRhY6FpL.css
generated
vendored
0
comfy/web/assets/index-DjWyclij.css → comfy/web/assets/index-BRhY6FpL.css
generated
vendored
10
comfy/web/assets/index-BD-Ia1C4.js → comfy/web/assets/index-CrROdkG4.js
generated
vendored
10
comfy/web/assets/index-BD-Ia1C4.js → comfy/web/assets/index-CrROdkG4.js
generated
vendored
@ -1,6 +1,6 @@
|
||||
var __defProp = Object.defineProperty;
|
||||
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
||||
import { C as ComfyDialog, $ as $el, a as ComfyApp, b as app, L as LGraphCanvas, c as LiteGraph, d as LGraphNode, e as applyTextReplacements, f as ComfyWidgets, g as addValueControlWidgets, D as DraggableList, h as api, u as useToastStore, i as LGraphGroup } from "./index-CI3N807S.js";
|
||||
import { C as ComfyDialog, $ as $el, a as ComfyApp, b as app, L as LGraphCanvas, c as LiteGraph, d as LGraphNode, e as applyTextReplacements, f as ComfyWidgets, g as addValueControlWidgets, D as DraggableList, h as api, i as LGraphGroup, u as useToastStore } from "./index-Dfv2aLsq.js";
|
||||
class ClipspaceDialog extends ComfyDialog {
|
||||
static {
|
||||
__name(this, "ClipspaceDialog");
|
||||
@ -3650,7 +3650,7 @@ app.registerExtension({
|
||||
content: "Add Group For Selected Nodes",
|
||||
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
||||
callback: /* @__PURE__ */ __name(() => {
|
||||
var group2 = new LiteGraph.LGraphGroup();
|
||||
const group2 = new LGraphGroup();
|
||||
addNodesToGroup(group2, this.selected_nodes);
|
||||
app.canvas.graph.add(group2);
|
||||
this.graph.change();
|
||||
@ -5088,7 +5088,7 @@ app.registerExtension({
|
||||
data = JSON.parse(data);
|
||||
const nodeIds = Object.keys(app.canvas.selected_nodes);
|
||||
for (let i = 0; i < nodeIds.length; i++) {
|
||||
const node = app.graph.getNodeById(Number.parseInt(nodeIds[i]));
|
||||
const node = app.graph.getNodeById(nodeIds[i]);
|
||||
const nodeData = node?.constructor.nodeData;
|
||||
let groupData = GroupNodeHandler.getGroupData(node);
|
||||
if (groupData) {
|
||||
@ -5955,7 +5955,7 @@ app.registerExtension({
|
||||
},
|
||||
onNodeOutputsUpdated(nodeOutputs) {
|
||||
for (const [nodeId, output] of Object.entries(nodeOutputs)) {
|
||||
const node = app.graph.getNodeById(Number.parseInt(nodeId));
|
||||
const node = app.graph.getNodeById(nodeId);
|
||||
if ("audio" in output) {
|
||||
const audioUIWidget = node.widgets.find(
|
||||
(w) => w.name === "audioUI"
|
||||
@ -6026,4 +6026,4 @@ app.registerExtension({
|
||||
};
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=index-BD-Ia1C4.js.map
|
||||
//# sourceMappingURL=index-CrROdkG4.js.map
|
||||
1
comfy/web/assets/index-CrROdkG4.js.map
generated
vendored
Normal file
1
comfy/web/assets/index-CrROdkG4.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
15027
comfy/web/assets/index-CI3N807S.js → comfy/web/assets/index-Dfv2aLsq.js
generated
vendored
15027
comfy/web/assets/index-CI3N807S.js → comfy/web/assets/index-Dfv2aLsq.js
generated
vendored
File diff suppressed because one or more lines are too long
2
comfy/web/assets/index-CI3N807S.js.map → comfy/web/assets/index-Dfv2aLsq.js.map
generated
vendored
2
comfy/web/assets/index-CI3N807S.js.map → comfy/web/assets/index-Dfv2aLsq.js.map
generated
vendored
File diff suppressed because one or more lines are too long
144
comfy/web/assets/index-_5czGnTA.css → comfy/web/assets/index-W4jP-SrU.css
generated
vendored
144
comfy/web/assets/index-_5czGnTA.css → comfy/web/assets/index-W4jP-SrU.css
generated
vendored
@ -1475,21 +1475,21 @@
|
||||
width: 5rem !important;
|
||||
}
|
||||
|
||||
.info-chip[data-v-25bd5f50] {
|
||||
.info-chip[data-v-ffbfdf57] {
|
||||
background: transparent;
|
||||
}
|
||||
.setting-item[data-v-25bd5f50] {
|
||||
.setting-item[data-v-ffbfdf57] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.setting-label[data-v-25bd5f50] {
|
||||
.setting-label[data-v-ffbfdf57] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
.setting-input[data-v-25bd5f50] {
|
||||
.setting-input[data-v-ffbfdf57] {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@ -1497,19 +1497,19 @@
|
||||
}
|
||||
|
||||
/* Ensure PrimeVue components take full width of their container */
|
||||
.setting-input[data-v-25bd5f50] .p-inputtext,
|
||||
.setting-input[data-v-25bd5f50] .input-slider,
|
||||
.setting-input[data-v-25bd5f50] .p-select,
|
||||
.setting-input[data-v-25bd5f50] .p-togglebutton {
|
||||
.setting-input[data-v-ffbfdf57] .p-inputtext,
|
||||
.setting-input[data-v-ffbfdf57] .input-slider,
|
||||
.setting-input[data-v-ffbfdf57] .p-select,
|
||||
.setting-input[data-v-ffbfdf57] .p-togglebutton {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
.setting-input[data-v-25bd5f50] .p-inputtext {
|
||||
.setting-input[data-v-ffbfdf57] .p-inputtext {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
/* Special case for ToggleSwitch to align it to the right */
|
||||
.setting-input[data-v-25bd5f50] .p-toggleswitch {
|
||||
.setting-input[data-v-ffbfdf57] .p-toggleswitch {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@ -1655,21 +1655,21 @@
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.comfy-error-report[data-v-12539d86] {
|
||||
.comfy-error-report[data-v-a103fd62] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.action-container[data-v-12539d86] {
|
||||
.action-container[data-v-a103fd62] {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.wrapper-pre[data-v-12539d86] {
|
||||
.wrapper-pre[data-v-a103fd62] {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.no-results-placeholder[data-v-12539d86] {
|
||||
.no-results-placeholder[data-v-a103fd62] {
|
||||
padding-top: 0;
|
||||
}
|
||||
.lds-ring {
|
||||
@ -3158,7 +3158,7 @@ body {
|
||||
overflow: hidden;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
background-color: var(--bg-color);
|
||||
background: var(--bg-color) var(--bg-img);
|
||||
color: var(--fg-color);
|
||||
min-height: -webkit-fill-available;
|
||||
max-height: -webkit-fill-available;
|
||||
@ -3833,17 +3833,19 @@ audio.comfy-audio.empty-audio-widget {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.node-title-editor[data-v-77799b26] {
|
||||
.group-title-editor.node-title-editor[data-v-f0cbabc5] {
|
||||
z-index: 9999;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
[data-v-77799b26] .editable-text {
|
||||
[data-v-f0cbabc5] .editable-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
[data-v-77799b26] .editable-text input {
|
||||
[data-v-f0cbabc5] .editable-text input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* Override the default font size */
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.side-bar-button-icon {
|
||||
@ -4086,26 +4088,26 @@ audio.comfy-audio.empty-audio-widget {
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.comfy-vue-node-search-container[data-v-077af1a9] {
|
||||
.comfy-vue-node-search-container[data-v-d28bffc4] {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 24rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.comfy-vue-node-search-container[data-v-077af1a9] * {
|
||||
.comfy-vue-node-search-container[data-v-d28bffc4] * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.comfy-vue-node-preview-container[data-v-077af1a9] {
|
||||
.comfy-vue-node-preview-container[data-v-d28bffc4] {
|
||||
position: absolute;
|
||||
left: -350px;
|
||||
top: 50px;
|
||||
}
|
||||
.comfy-vue-node-search-box[data-v-077af1a9] {
|
||||
.comfy-vue-node-search-box[data-v-d28bffc4] {
|
||||
z-index: 10;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.option-container[data-v-077af1a9] {
|
||||
.option-container[data-v-d28bffc4] {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
@ -4117,12 +4119,12 @@ audio.comfy-audio.empty-audio-widget {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.option-display-name[data-v-077af1a9] {
|
||||
.option-display-name[data-v-d28bffc4] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 600;
|
||||
}
|
||||
.option-category[data-v-077af1a9] {
|
||||
.option-category[data-v-d28bffc4] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.875rem;
|
||||
@ -4133,7 +4135,7 @@ audio.comfy-audio.empty-audio-widget {
|
||||
/* Keeps the text on a single line by default */
|
||||
white-space: nowrap;
|
||||
}
|
||||
[data-v-077af1a9] .highlight {
|
||||
[data-v-d28bffc4] .highlight {
|
||||
background-color: var(--p-primary-color);
|
||||
color: var(--p-primary-contrast-color);
|
||||
font-weight: bold;
|
||||
@ -4141,10 +4143,10 @@ audio.comfy-audio.empty-audio-widget {
|
||||
padding: 0rem 0.125rem;
|
||||
margin: -0.125rem 0.125rem;
|
||||
}
|
||||
._filter-button[data-v-077af1a9] {
|
||||
._filter-button[data-v-d28bffc4] {
|
||||
z-index: 10;
|
||||
}
|
||||
._dialog[data-v-077af1a9] {
|
||||
._dialog[data-v-d28bffc4] {
|
||||
min-width: 24rem;
|
||||
}
|
||||
|
||||
@ -4353,28 +4355,60 @@ img.galleria-image {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.node-tree-leaf[data-v-adf5f221] {
|
||||
.tree-node[data-v-d4b7b060] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.node-content[data-v-adf5f221] {
|
||||
.leaf-count-badge[data-v-d4b7b060] {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.node-content[data-v-d4b7b060] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.node-label[data-v-adf5f221] {
|
||||
.leaf-label[data-v-d4b7b060] {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.bookmark-button[data-v-adf5f221] {
|
||||
width: unset;
|
||||
padding: 0.25rem;
|
||||
[data-v-d4b7b060] .editable-text span {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.node-tree-folder[data-v-f2d72e9b] {
|
||||
[data-v-9d3310b9] .tree-explorer-node-label {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: var(--p-tree-node-gap);
|
||||
flex-grow: 1;
|
||||
}
|
||||
/*
|
||||
* The following styles are necessary to avoid layout shift when dragging nodes over folders.
|
||||
* By setting the position to relative on the parent and using an absolutely positioned pseudo-element,
|
||||
* we can create a visual indicator for the drop target without affecting the layout of other elements.
|
||||
*/
|
||||
[data-v-9d3310b9] .p-tree-node-content:has(.tree-folder) {
|
||||
position: relative;
|
||||
}
|
||||
[data-v-9d3310b9] .p-tree-node-content:has(.tree-folder.can-drop)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 1px solid var(--p-content-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.node-lib-node-container[data-v-3238e135] {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.bookmark-button[data-v-3238e135] {
|
||||
width: unset;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.p-selectbutton .p-button[data-v-91077f2a] {
|
||||
@ -4394,45 +4428,33 @@ img.galleria-image {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.node-lib-tree-node-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: var(--p-tree-node-gap);
|
||||
flex-grow: 1;
|
||||
}
|
||||
.node-lib-filter-popup {
|
||||
margin-left: -13px;
|
||||
}
|
||||
|
||||
[data-v-87967891] .node-lib-search-box {
|
||||
[data-v-85688f44] .node-lib-search-box {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
[data-v-87967891] .comfy-vue-side-bar-body {
|
||||
[data-v-85688f44] .comfy-vue-side-bar-body {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
|
||||
/*
|
||||
* The following styles are necessary to avoid layout shift when dragging nodes over folders.
|
||||
* By setting the position to relative on the parent and using an absolutely positioned pseudo-element,
|
||||
* we can create a visual indicator for the drop target without affecting the layout of other elements.
|
||||
*/
|
||||
[data-v-87967891] .p-tree-node-content:has(.node-tree-folder) {
|
||||
position: relative;
|
||||
[data-v-85688f44] .node-lib-bookmark-tree-explorer {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
[data-v-87967891] .p-tree-node-content:has(.node-tree-folder.can-drop)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 1px solid var(--p-content-color);
|
||||
pointer-events: none;
|
||||
[data-v-85688f44] .node-lib-tree-explorer {
|
||||
padding-top: 2px;
|
||||
}
|
||||
[data-v-85688f44] .p-divider {
|
||||
margin: var(--comfy-tree-explorer-item-padding) 0px;
|
||||
}
|
||||
|
||||
.spinner[data-v-8616e7a1] {
|
||||
.p-tree-node-content {
|
||||
padding: var(--comfy-tree-explorer-item-padding) !important;
|
||||
}
|
||||
|
||||
.spinner[data-v-75e4840f] {
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
display: flex;
|
||||
@ -1,6 +1,6 @@
|
||||
var __defProp = Object.defineProperty;
|
||||
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
||||
import { j as createSpinner, h as api, $ as $el } from "./index-CI3N807S.js";
|
||||
import { j as createSpinner, h as api, $ as $el } from "./index-Dfv2aLsq.js";
|
||||
class UserSelectionScreen {
|
||||
static {
|
||||
__name(this, "UserSelectionScreen");
|
||||
@ -117,4 +117,4 @@ window.comfyAPI.userSelection.UserSelectionScreen = UserSelectionScreen;
|
||||
export {
|
||||
UserSelectionScreen
|
||||
};
|
||||
//# sourceMappingURL=userSelection-CyXKCVy3.js.map
|
||||
//# sourceMappingURL=userSelection-DSpF-zVD.js.map
|
||||
File diff suppressed because one or more lines are too long
2
comfy/web/extensions/core/clipspace.js
vendored
2
comfy/web/extensions/core/clipspace.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for extensions\core\clipspace.ts
|
||||
// Shim for extensions/core/clipspace.ts
|
||||
export const ClipspaceDialog = window.comfyAPI.clipspace.ClipspaceDialog;
|
||||
|
||||
2
comfy/web/extensions/core/groupNode.js
vendored
2
comfy/web/extensions/core/groupNode.js
vendored
@ -1,3 +1,3 @@
|
||||
// Shim for extensions\core\groupNode.ts
|
||||
// Shim for extensions/core/groupNode.ts
|
||||
export const GroupNodeConfig = window.comfyAPI.groupNode.GroupNodeConfig;
|
||||
export const GroupNodeHandler = window.comfyAPI.groupNode.GroupNodeHandler;
|
||||
|
||||
2
comfy/web/extensions/core/groupNodeManage.js
vendored
2
comfy/web/extensions/core/groupNodeManage.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for extensions\core\groupNodeManage.ts
|
||||
// Shim for extensions/core/groupNodeManage.ts
|
||||
export const ManageGroupDialog = window.comfyAPI.groupNodeManage.ManageGroupDialog;
|
||||
|
||||
2
comfy/web/extensions/core/widgetInputs.js
vendored
2
comfy/web/extensions/core/widgetInputs.js
vendored
@ -1,4 +1,4 @@
|
||||
// Shim for extensions\core\widgetInputs.ts
|
||||
// Shim for extensions/core/widgetInputs.ts
|
||||
export const getWidgetConfig = window.comfyAPI.widgetInputs.getWidgetConfig;
|
||||
export const setWidgetConfig = window.comfyAPI.widgetInputs.setWidgetConfig;
|
||||
export const mergeIfValid = window.comfyAPI.widgetInputs.mergeIfValid;
|
||||
|
||||
100
comfy/web/index.html
vendored
100
comfy/web/index.html
vendored
@ -1,50 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ComfyUI</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<!-- Browser Test Fonts -->
|
||||
<!-- <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Roboto Mono', 'Noto Color Emoji';
|
||||
}
|
||||
</style> -->
|
||||
<link rel="stylesheet" type="text/css" href="user.css" />
|
||||
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
|
||||
<script type="module" crossorigin src="./assets/index-CI3N807S.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-_5czGnTA.css">
|
||||
</head>
|
||||
<body class="litegraph">
|
||||
<div id="vue-app"></div>
|
||||
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
|
||||
<main class="comfy-user-selection-inner">
|
||||
<h1>ComfyUI</h1>
|
||||
<form>
|
||||
<section>
|
||||
<label>New user:
|
||||
<input placeholder="Enter a username" />
|
||||
</label>
|
||||
</section>
|
||||
<div class="comfy-user-existing">
|
||||
<span class="or-separator">OR</span>
|
||||
<section>
|
||||
<label>
|
||||
Existing user:
|
||||
<select>
|
||||
<option hidden disabled selected value> Select a user </option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
<footer>
|
||||
<span class="comfy-user-error"> </span>
|
||||
<button class="comfy-btn comfy-user-button-next">Next</button>
|
||||
</footer>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ComfyUI</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<!-- Browser Test Fonts -->
|
||||
<!-- <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Roboto Mono', 'Noto Color Emoji';
|
||||
}
|
||||
</style> -->
|
||||
<link rel="stylesheet" type="text/css" href="user.css" />
|
||||
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
|
||||
<script type="module" crossorigin src="./assets/index-Dfv2aLsq.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-W4jP-SrU.css">
|
||||
</head>
|
||||
<body class="litegraph">
|
||||
<div id="vue-app"></div>
|
||||
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
|
||||
<main class="comfy-user-selection-inner">
|
||||
<h1>ComfyUI</h1>
|
||||
<form>
|
||||
<section>
|
||||
<label>New user:
|
||||
<input placeholder="Enter a username" />
|
||||
</label>
|
||||
</section>
|
||||
<div class="comfy-user-existing">
|
||||
<span class="or-separator">OR</span>
|
||||
<section>
|
||||
<label>
|
||||
Existing user:
|
||||
<select>
|
||||
<option hidden disabled selected value> Select a user </option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
<footer>
|
||||
<span class="comfy-user-error"> </span>
|
||||
<button class="comfy-btn comfy-user-button-next">Next</button>
|
||||
</footer>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
2
comfy/web/scripts/api.js
vendored
2
comfy/web/scripts/api.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\api.ts
|
||||
// Shim for scripts/api.ts
|
||||
export const api = window.comfyAPI.api.api;
|
||||
|
||||
2
comfy/web/scripts/app.js
vendored
2
comfy/web/scripts/app.js
vendored
@ -1,4 +1,4 @@
|
||||
// Shim for scripts\app.ts
|
||||
// Shim for scripts/app.ts
|
||||
export const ANIM_PREVIEW_WIDGET = window.comfyAPI.app.ANIM_PREVIEW_WIDGET;
|
||||
export const ComfyApp = window.comfyAPI.app.ComfyApp;
|
||||
export const app = window.comfyAPI.app.app;
|
||||
|
||||
2
comfy/web/scripts/changeTracker.js
vendored
2
comfy/web/scripts/changeTracker.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\changeTracker.ts
|
||||
// Shim for scripts/changeTracker.ts
|
||||
export const ChangeTracker = window.comfyAPI.changeTracker.ChangeTracker;
|
||||
|
||||
2
comfy/web/scripts/defaultGraph.js
vendored
2
comfy/web/scripts/defaultGraph.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\defaultGraph.ts
|
||||
// Shim for scripts/defaultGraph.ts
|
||||
export const defaultGraph = window.comfyAPI.defaultGraph.defaultGraph;
|
||||
|
||||
2
comfy/web/scripts/domWidget.js
vendored
2
comfy/web/scripts/domWidget.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\domWidget.ts
|
||||
// Shim for scripts/domWidget.ts
|
||||
export const addDomClippingSetting = window.comfyAPI.domWidget.addDomClippingSetting;
|
||||
|
||||
2
comfy/web/scripts/logging.js
vendored
2
comfy/web/scripts/logging.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\logging.ts
|
||||
// Shim for scripts/logging.ts
|
||||
export const ComfyLogging = window.comfyAPI.logging.ComfyLogging;
|
||||
|
||||
2
comfy/web/scripts/metadata/flac.js
vendored
2
comfy/web/scripts/metadata/flac.js
vendored
@ -1,3 +1,3 @@
|
||||
// Shim for scripts\metadata\flac.ts
|
||||
// Shim for scripts/metadata/flac.ts
|
||||
export const getFromFlacBuffer = window.comfyAPI.flac.getFromFlacBuffer;
|
||||
export const getFromFlacFile = window.comfyAPI.flac.getFromFlacFile;
|
||||
|
||||
2
comfy/web/scripts/metadata/png.js
vendored
2
comfy/web/scripts/metadata/png.js
vendored
@ -1,3 +1,3 @@
|
||||
// Shim for scripts\metadata\png.ts
|
||||
// Shim for scripts/metadata/png.ts
|
||||
export const getFromPngBuffer = window.comfyAPI.png.getFromPngBuffer;
|
||||
export const getFromPngFile = window.comfyAPI.png.getFromPngFile;
|
||||
|
||||
2
comfy/web/scripts/pnginfo.js
vendored
2
comfy/web/scripts/pnginfo.js
vendored
@ -1,4 +1,4 @@
|
||||
// Shim for scripts\pnginfo.ts
|
||||
// Shim for scripts/pnginfo.ts
|
||||
export const getPngMetadata = window.comfyAPI.pnginfo.getPngMetadata;
|
||||
export const getFlacMetadata = window.comfyAPI.pnginfo.getFlacMetadata;
|
||||
export const getWebpMetadata = window.comfyAPI.pnginfo.getWebpMetadata;
|
||||
|
||||
2
comfy/web/scripts/ui.js
vendored
2
comfy/web/scripts/ui.js
vendored
@ -1,4 +1,4 @@
|
||||
// Shim for scripts\ui.ts
|
||||
// Shim for scripts/ui.ts
|
||||
export const ComfyDialog = window.comfyAPI.ui.ComfyDialog;
|
||||
export const $el = window.comfyAPI.ui.$el;
|
||||
export const ComfyUI = window.comfyAPI.ui.ComfyUI;
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\components\asyncDialog.ts
|
||||
// Shim for scripts/ui/components/asyncDialog.ts
|
||||
export const ComfyAsyncDialog = window.comfyAPI.asyncDialog.ComfyAsyncDialog;
|
||||
|
||||
2
comfy/web/scripts/ui/components/button.js
vendored
2
comfy/web/scripts/ui/components/button.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\components\button.ts
|
||||
// Shim for scripts/ui/components/button.ts
|
||||
export const ComfyButton = window.comfyAPI.button.ComfyButton;
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\components\buttonGroup.ts
|
||||
// Shim for scripts/ui/components/buttonGroup.ts
|
||||
export const ComfyButtonGroup = window.comfyAPI.buttonGroup.ComfyButtonGroup;
|
||||
|
||||
2
comfy/web/scripts/ui/components/popup.js
vendored
2
comfy/web/scripts/ui/components/popup.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\components\popup.ts
|
||||
// Shim for scripts/ui/components/popup.ts
|
||||
export const ComfyPopup = window.comfyAPI.popup.ComfyPopup;
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\components\splitButton.ts
|
||||
// Shim for scripts/ui/components/splitButton.ts
|
||||
export const ComfySplitButton = window.comfyAPI.splitButton.ComfySplitButton;
|
||||
|
||||
2
comfy/web/scripts/ui/dialog.js
vendored
2
comfy/web/scripts/ui/dialog.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\dialog.ts
|
||||
// Shim for scripts/ui/dialog.ts
|
||||
export const ComfyDialog = window.comfyAPI.dialog.ComfyDialog;
|
||||
|
||||
2
comfy/web/scripts/ui/draggableList.js
vendored
2
comfy/web/scripts/ui/draggableList.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\draggableList.ts
|
||||
// Shim for scripts/ui/draggableList.ts
|
||||
export const DraggableList = window.comfyAPI.draggableList.DraggableList;
|
||||
|
||||
2
comfy/web/scripts/ui/imagePreview.js
vendored
2
comfy/web/scripts/ui/imagePreview.js
vendored
@ -1,3 +1,3 @@
|
||||
// Shim for scripts\ui\imagePreview.ts
|
||||
// Shim for scripts/ui/imagePreview.ts
|
||||
export const calculateImageGrid = window.comfyAPI.imagePreview.calculateImageGrid;
|
||||
export const createImageHost = window.comfyAPI.imagePreview.createImageHost;
|
||||
|
||||
2
comfy/web/scripts/ui/menu/index.js
vendored
2
comfy/web/scripts/ui/menu/index.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\menu\index.ts
|
||||
// Shim for scripts/ui/menu/index.ts
|
||||
export const ComfyAppMenu = window.comfyAPI.index.ComfyAppMenu;
|
||||
|
||||
2
comfy/web/scripts/ui/menu/interruptButton.js
vendored
2
comfy/web/scripts/ui/menu/interruptButton.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\menu\interruptButton.ts
|
||||
// Shim for scripts/ui/menu/interruptButton.ts
|
||||
export const getInterruptButton = window.comfyAPI.interruptButton.getInterruptButton;
|
||||
|
||||
2
comfy/web/scripts/ui/menu/queueButton.js
vendored
2
comfy/web/scripts/ui/menu/queueButton.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\menu\queueButton.ts
|
||||
// Shim for scripts/ui/menu/queueButton.ts
|
||||
export const ComfyQueueButton = window.comfyAPI.queueButton.ComfyQueueButton;
|
||||
|
||||
2
comfy/web/scripts/ui/menu/queueOptions.js
vendored
2
comfy/web/scripts/ui/menu/queueOptions.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\menu\queueOptions.ts
|
||||
// Shim for scripts/ui/menu/queueOptions.ts
|
||||
export const ComfyQueueOptions = window.comfyAPI.queueOptions.ComfyQueueOptions;
|
||||
|
||||
2
comfy/web/scripts/ui/menu/workflows.js
vendored
2
comfy/web/scripts/ui/menu/workflows.js
vendored
@ -1,3 +1,3 @@
|
||||
// Shim for scripts\ui\menu\workflows.ts
|
||||
// Shim for scripts/ui/menu/workflows.ts
|
||||
export const ComfyWorkflowsMenu = window.comfyAPI.workflows.ComfyWorkflowsMenu;
|
||||
export const ComfyWorkflowsContent = window.comfyAPI.workflows.ComfyWorkflowsContent;
|
||||
|
||||
2
comfy/web/scripts/ui/settings.js
vendored
2
comfy/web/scripts/ui/settings.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\settings.ts
|
||||
// Shim for scripts/ui/settings.ts
|
||||
export const ComfySettingsDialog = window.comfyAPI.settings.ComfySettingsDialog;
|
||||
|
||||
2
comfy/web/scripts/ui/spinner.js
vendored
2
comfy/web/scripts/ui/spinner.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\spinner.ts
|
||||
// Shim for scripts/ui/spinner.ts
|
||||
export const createSpinner = window.comfyAPI.spinner.createSpinner;
|
||||
|
||||
2
comfy/web/scripts/ui/toggleSwitch.js
vendored
2
comfy/web/scripts/ui/toggleSwitch.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\toggleSwitch.ts
|
||||
// Shim for scripts/ui/toggleSwitch.ts
|
||||
export const toggleSwitch = window.comfyAPI.toggleSwitch.toggleSwitch;
|
||||
|
||||
2
comfy/web/scripts/ui/userSelection.js
vendored
2
comfy/web/scripts/ui/userSelection.js
vendored
@ -1,2 +1,2 @@
|
||||
// Shim for scripts\ui\userSelection.ts
|
||||
// Shim for scripts/ui/userSelection.ts
|
||||
export const UserSelectionScreen = window.comfyAPI.userSelection.UserSelectionScreen;
|
||||
|
||||
2
comfy/web/scripts/ui/utils.js
vendored
2
comfy/web/scripts/ui/utils.js
vendored
@ -1,3 +1,3 @@
|
||||
// Shim for scripts\ui\utils.ts
|
||||
// Shim for scripts/ui/utils.ts
|
||||
export const applyClasses = window.comfyAPI.utils.applyClasses;
|
||||
export const toggleElement = window.comfyAPI.utils.toggleElement;
|
||||
|
||||
2
comfy/web/scripts/utils.js
vendored
2
comfy/web/scripts/utils.js
vendored
@ -1,4 +1,4 @@
|
||||
// Shim for scripts\utils.ts
|
||||
// Shim for scripts/utils.ts
|
||||
export const clone = window.comfyAPI.utils.clone;
|
||||
export const applyTextReplacements = window.comfyAPI.utils.applyTextReplacements;
|
||||
export const addStylesheet = window.comfyAPI.utils.addStylesheet;
|
||||
|
||||
2
comfy/web/scripts/widgets.js
vendored
2
comfy/web/scripts/widgets.js
vendored
@ -1,4 +1,4 @@
|
||||
// Shim for scripts\widgets.ts
|
||||
// Shim for scripts/widgets.ts
|
||||
export const updateControlWidgetLabel = window.comfyAPI.widgets.updateControlWidgetLabel;
|
||||
export const addValueControlWidget = window.comfyAPI.widgets.addValueControlWidget;
|
||||
export const addValueControlWidgets = window.comfyAPI.widgets.addValueControlWidgets;
|
||||
|
||||
2
comfy/web/scripts/workflows.js
vendored
2
comfy/web/scripts/workflows.js
vendored
@ -1,4 +1,4 @@
|
||||
// Shim for scripts\workflows.ts
|
||||
// Shim for scripts/workflows.ts
|
||||
export const trimJsonExt = window.comfyAPI.workflows.trimJsonExt;
|
||||
export const ComfyWorkflowManager = window.comfyAPI.workflows.ComfyWorkflowManager;
|
||||
export const ComfyWorkflow = window.comfyAPI.workflows.ComfyWorkflow;
|
||||
|
||||
@ -206,17 +206,10 @@ class PreviewAudio(SaveAudio):
|
||||
|
||||
|
||||
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))
|
||||
and f.endswith(LoadAudio.SUPPORTED_FORMATS)
|
||||
)
|
||||
]
|
||||
files = folder_paths.filter_files_content_types(os.listdir(input_dir), ["audio", "video"])
|
||||
return {"required": {"audio": (sorted(files), {"audio_upload": True})}}
|
||||
|
||||
CATEGORY = "audio"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
from enum import Enum
|
||||
|
||||
import torch
|
||||
|
||||
@ -41,6 +42,41 @@ def extract_lora(diff, rank):
|
||||
return (U, Vh)
|
||||
|
||||
|
||||
class LORAType(Enum):
|
||||
STANDARD = 0
|
||||
FULL_DIFF = 1
|
||||
|
||||
|
||||
LORA_TYPES = {"standard": LORAType.STANDARD,
|
||||
"full_diff": LORAType.FULL_DIFF}
|
||||
|
||||
|
||||
def calc_lora_model(model_diff, rank, prefix_model, prefix_lora, output_sd, lora_type, bias_diff=False):
|
||||
comfy.model_management.load_models_gpu([model_diff], force_patch_weights=True)
|
||||
sd = model_diff.model_state_dict(filter_prefix=prefix_model)
|
||||
|
||||
for k in sd:
|
||||
if k.endswith(".weight"):
|
||||
weight_diff = sd[k]
|
||||
if lora_type == LORAType.STANDARD:
|
||||
if weight_diff.ndim < 2:
|
||||
if bias_diff:
|
||||
output_sd["{}{}.diff".format(prefix_lora, k[len(prefix_model):-7])] = weight_diff.contiguous().half().cpu()
|
||||
continue
|
||||
try:
|
||||
out = extract_lora(weight_diff, rank)
|
||||
output_sd["{}{}.lora_up.weight".format(prefix_lora, k[len(prefix_model):-7])] = out[0].contiguous().half().cpu()
|
||||
output_sd["{}{}.lora_down.weight".format(prefix_lora, k[len(prefix_model):-7])] = out[1].contiguous().half().cpu()
|
||||
except:
|
||||
logging.warning("Could not generate lora weights for key {}, is the weight difference a zero?".format(k))
|
||||
elif lora_type == LORAType.FULL_DIFF:
|
||||
output_sd["{}{}.diff".format(prefix_lora, k[len(prefix_model):-7])] = weight_diff.contiguous().half().cpu()
|
||||
|
||||
elif bias_diff and k.endswith(".bias"):
|
||||
output_sd["{}{}.diff_b".format(prefix_lora, k[len(prefix_model):-5])] = sd[k].contiguous().half().cpu()
|
||||
return output_sd
|
||||
|
||||
|
||||
class LoraSave:
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
@ -48,9 +84,12 @@ class LoraSave:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {"filename_prefix": ("STRING", {"default": "loras/ComfyUI_extracted_lora"}),
|
||||
"rank": ("INT", {"default": 8, "min": 1, "max": 1024, "step": 1}),
|
||||
"rank": ("INT", {"default": 8, "min": 1, "max": 4096, "step": 1}),
|
||||
"lora_type": (tuple(LORA_TYPES.keys()),),
|
||||
"bias_diff": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {"model_diff": ("MODEL",), },
|
||||
"optional": {"model_diff": ("MODEL",),
|
||||
"text_encoder_diff": ("CLIP",)},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
@ -59,30 +98,18 @@ class LoraSave:
|
||||
|
||||
CATEGORY = "_for_testing"
|
||||
|
||||
def save(self, filename_prefix, rank, model_diff=None):
|
||||
if model_diff is None:
|
||||
def save(self, filename_prefix, rank, lora_type, bias_diff, model_diff=None, text_encoder_diff=None):
|
||||
if model_diff is None and text_encoder_diff is None:
|
||||
return {}
|
||||
|
||||
lora_type = LORA_TYPES.get(lora_type)
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
|
||||
|
||||
output_sd = {}
|
||||
prefix_key = "diffusion_model."
|
||||
stored = set()
|
||||
|
||||
comfy.model_management.load_models_gpu([model_diff], force_patch_weights=True)
|
||||
sd = model_diff.model_state_dict(filter_prefix=prefix_key)
|
||||
|
||||
for k in sd:
|
||||
if k.endswith(".weight"):
|
||||
weight_diff = sd[k]
|
||||
if weight_diff.ndim < 2:
|
||||
continue
|
||||
try:
|
||||
out = extract_lora(weight_diff, rank)
|
||||
output_sd["{}.lora_up.weight".format(k[:-7])] = out[0].contiguous().half().cpu()
|
||||
output_sd["{}.lora_down.weight".format(k[:-7])] = out[1].contiguous().half().cpu()
|
||||
except:
|
||||
logging.warning("Could not generate lora weights for key {}, is the weight difference a zero?".format(k))
|
||||
if model_diff is not None:
|
||||
output_sd = calc_lora_model(model_diff, rank, "diffusion_model.", "diffusion_model.", output_sd, lora_type, bias_diff=bias_diff)
|
||||
if text_encoder_diff is not None:
|
||||
output_sd = calc_lora_model(text_encoder_diff.patcher, rank, "", "text_encoders.", output_sd, lora_type, bias_diff=bias_diff)
|
||||
|
||||
output_checkpoint = f"{filename}_{counter:05}_.safetensors"
|
||||
output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
|
||||
|
||||
@ -24,6 +24,7 @@ class PerpNeg:
|
||||
FUNCTION = "patch"
|
||||
|
||||
CATEGORY = "_for_testing"
|
||||
DEPRECATED = True
|
||||
|
||||
def patch(self, model, empty_conditioning, neg_scale):
|
||||
m = model.clone()
|
||||
|
||||
21
comfy_extras/nodes_torch_compile.py
Normal file
21
comfy_extras/nodes_torch_compile.py
Normal file
@ -0,0 +1,21 @@
|
||||
import torch
|
||||
|
||||
class TorchCompileModel:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": { "model": ("MODEL",),
|
||||
}}
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
FUNCTION = "patch"
|
||||
|
||||
CATEGORY = "_for_testing"
|
||||
EXPERIMENTAL = True
|
||||
|
||||
def patch(self, model):
|
||||
m = model.clone()
|
||||
m.add_object_patch("diffusion_model", torch.compile(model=m.get_model_object("diffusion_model")))
|
||||
return (m, )
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"TorchCompileModel": TorchCompileModel,
|
||||
}
|
||||
66
tests-unit/comfy_test/folder_path_test.py
Normal file
66
tests-unit/comfy_test/folder_path_test.py
Normal file
@ -0,0 +1,66 @@
|
||||
### 🗻 This file is created through the spirit of Mount Fuji at its peak
|
||||
# TODO(yoland): clean up this after I get back down
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
|
||||
import folder_paths
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir():
|
||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||
yield tmpdirname
|
||||
|
||||
|
||||
def test_get_directory_by_type():
|
||||
test_dir = "/test/dir"
|
||||
folder_paths.set_output_directory(test_dir)
|
||||
assert folder_paths.get_directory_by_type("output") == test_dir
|
||||
assert folder_paths.get_directory_by_type("invalid") is None
|
||||
|
||||
def test_annotated_filepath():
|
||||
assert folder_paths.annotated_filepath("test.txt") == ("test.txt", None)
|
||||
assert folder_paths.annotated_filepath("test.txt [output]") == ("test.txt", folder_paths.get_output_directory())
|
||||
assert folder_paths.annotated_filepath("test.txt [input]") == ("test.txt", folder_paths.get_input_directory())
|
||||
assert folder_paths.annotated_filepath("test.txt [temp]") == ("test.txt", folder_paths.get_temp_directory())
|
||||
|
||||
def test_get_annotated_filepath():
|
||||
default_dir = "/default/dir"
|
||||
assert folder_paths.get_annotated_filepath("test.txt", default_dir) == os.path.join(default_dir, "test.txt")
|
||||
assert folder_paths.get_annotated_filepath("test.txt [output]") == os.path.join(folder_paths.get_output_directory(), "test.txt")
|
||||
|
||||
def test_add_model_folder_path():
|
||||
folder_paths.add_model_folder_path("test_folder", "/test/path")
|
||||
assert "/test/path" in folder_paths.get_folder_paths("test_folder")
|
||||
|
||||
def test_recursive_search(temp_dir):
|
||||
os.makedirs(os.path.join(temp_dir, "subdir"))
|
||||
open(os.path.join(temp_dir, "file1.txt"), "w").close()
|
||||
open(os.path.join(temp_dir, "subdir", "file2.txt"), "w").close()
|
||||
|
||||
files, dirs = folder_paths.recursive_search(temp_dir)
|
||||
assert set(files) == {"file1.txt", os.path.join("subdir", "file2.txt")}
|
||||
assert len(dirs) == 2 # temp_dir and subdir
|
||||
|
||||
def test_filter_files_extensions():
|
||||
files = ["file1.txt", "file2.jpg", "file3.png", "file4.txt"]
|
||||
assert folder_paths.filter_files_extensions(files, [".txt"]) == ["file1.txt", "file4.txt"]
|
||||
assert folder_paths.filter_files_extensions(files, [".jpg", ".png"]) == ["file2.jpg", "file3.png"]
|
||||
assert folder_paths.filter_files_extensions(files, []) == files
|
||||
|
||||
@patch("folder_paths.recursive_search")
|
||||
@patch("folder_paths.folder_names_and_paths")
|
||||
def test_get_filename_list(mock_folder_names_and_paths, mock_recursive_search):
|
||||
mock_folder_names_and_paths.__getitem__.return_value = (["/test/path"], {".txt"})
|
||||
mock_recursive_search.return_value = (["file1.txt", "file2.jpg"], {})
|
||||
assert folder_paths.get_filename_list("test_folder") == ["file1.txt"]
|
||||
|
||||
def test_get_save_image_path(temp_dir):
|
||||
with patch("folder_paths.output_directory", temp_dir):
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path("test", temp_dir, 100, 100)
|
||||
assert os.path.samefile(full_output_folder, temp_dir)
|
||||
assert filename == "test"
|
||||
assert counter == 1
|
||||
assert subfolder == ""
|
||||
assert filename_prefix == "test"
|
||||
0
tests-unit/folder_paths_test/__init__.py
Normal file
0
tests-unit/folder_paths_test/__init__.py
Normal file
52
tests-unit/folder_paths_test/filter_by_content_types_test.py
Normal file
52
tests-unit/folder_paths_test/filter_by_content_types_test.py
Normal file
@ -0,0 +1,52 @@
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
from folder_paths import filter_files_content_types
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def file_extensions():
|
||||
return {
|
||||
'image': ['bmp', 'cdr', 'gif', 'heif', 'ico', 'jpeg', 'jpg', 'pcx', 'png', 'pnm', 'ppm', 'psd', 'sgi', 'svg', 'tiff', 'webp', 'xbm', 'xcf', 'xpm'],
|
||||
'audio': ['aif', 'aifc', 'aiff', 'au', 'awb', 'flac', 'm4a', 'mp2', 'mp3', 'ogg', 'sd2', 'smp', 'snd', 'wav'],
|
||||
'video': ['avi', 'flv', 'm2v', 'm4v', 'mj2', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'ogv', 'qt', 'webm', 'wmv']
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def mock_dir(file_extensions):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
for content_type, extensions in file_extensions.items():
|
||||
for extension in extensions:
|
||||
with open(f"{directory}/sample_{content_type}.{extension}", "w") as f:
|
||||
f.write(f"Sample {content_type} file in {extension} format")
|
||||
yield directory
|
||||
|
||||
|
||||
def test_categorizes_all_correctly(mock_dir, file_extensions):
|
||||
files = os.listdir(mock_dir)
|
||||
for content_type, extensions in file_extensions.items():
|
||||
filtered_files = filter_files_content_types(files, [content_type])
|
||||
for extension in extensions:
|
||||
assert f"sample_{content_type}.{extension}" in filtered_files
|
||||
|
||||
|
||||
def test_categorizes_all_uniquely(mock_dir, file_extensions):
|
||||
files = os.listdir(mock_dir)
|
||||
for content_type, extensions in file_extensions.items():
|
||||
filtered_files = filter_files_content_types(files, [content_type])
|
||||
assert len(filtered_files) == len(extensions)
|
||||
|
||||
|
||||
def test_handles_bad_extensions():
|
||||
files = ["file1.txt", "file2.py", "file3.example", "file4.pdf", "file5.ini", "file6.doc", "file7.md"]
|
||||
assert filter_files_content_types(files, ["image", "audio", "video"]) == []
|
||||
|
||||
|
||||
def test_handles_no_extension():
|
||||
files = ["file1", "file2", "file3", "file4", "file5", "file6", "file7"]
|
||||
assert filter_files_content_types(files, ["image", "audio", "video"]) == []
|
||||
|
||||
|
||||
def test_handles_no_files():
|
||||
files = []
|
||||
assert filter_files_content_types(files, ["image", "audio", "video"]) == []
|
||||
124
tests-unit/utils/extra_config_test.py
Normal file
124
tests-unit/utils/extra_config_test.py
Normal file
@ -0,0 +1,124 @@
|
||||
import pytest
|
||||
import yaml
|
||||
import os
|
||||
from unittest.mock import Mock, patch, mock_open
|
||||
|
||||
from utils.extra_config import load_extra_path_config
|
||||
import folder_paths
|
||||
|
||||
@pytest.fixture
|
||||
def mock_yaml_content():
|
||||
return {
|
||||
'test_config': {
|
||||
'base_path': '~/App/',
|
||||
'checkpoints': 'subfolder1',
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_expanded_home():
|
||||
return '/home/user'
|
||||
|
||||
@pytest.fixture
|
||||
def yaml_config_with_appdata():
|
||||
return """
|
||||
test_config:
|
||||
base_path: '%APPDATA%/ComfyUI'
|
||||
checkpoints: 'models/checkpoints'
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_yaml_content_appdata(yaml_config_with_appdata):
|
||||
return yaml.safe_load(yaml_config_with_appdata)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_expandvars_appdata():
|
||||
mock = Mock()
|
||||
mock.side_effect = lambda path: path.replace('%APPDATA%', 'C:/Users/TestUser/AppData/Roaming')
|
||||
return mock
|
||||
|
||||
@pytest.fixture
|
||||
def mock_add_model_folder_path():
|
||||
return Mock()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_expanduser(mock_expanded_home):
|
||||
def _expanduser(path):
|
||||
if path.startswith('~/'):
|
||||
return os.path.join(mock_expanded_home, path[2:])
|
||||
return path
|
||||
return _expanduser
|
||||
|
||||
@pytest.fixture
|
||||
def mock_yaml_safe_load(mock_yaml_content):
|
||||
return Mock(return_value=mock_yaml_content)
|
||||
|
||||
@patch('builtins.open', new_callable=mock_open, read_data="dummy file content")
|
||||
def test_load_extra_model_paths_expands_userpath(
|
||||
mock_file,
|
||||
monkeypatch,
|
||||
mock_add_model_folder_path,
|
||||
mock_expanduser,
|
||||
mock_yaml_safe_load,
|
||||
mock_expanded_home
|
||||
):
|
||||
# Attach mocks used by load_extra_path_config
|
||||
monkeypatch.setattr(folder_paths, 'add_model_folder_path', mock_add_model_folder_path)
|
||||
monkeypatch.setattr(os.path, 'expanduser', mock_expanduser)
|
||||
monkeypatch.setattr(yaml, 'safe_load', mock_yaml_safe_load)
|
||||
|
||||
dummy_yaml_file_name = 'dummy_path.yaml'
|
||||
load_extra_path_config(dummy_yaml_file_name)
|
||||
|
||||
expected_calls = [
|
||||
('checkpoints', os.path.join(mock_expanded_home, 'App', 'subfolder1')),
|
||||
]
|
||||
|
||||
assert mock_add_model_folder_path.call_count == len(expected_calls)
|
||||
|
||||
# Check if add_model_folder_path was called with the correct arguments
|
||||
for actual_call, expected_call in zip(mock_add_model_folder_path.call_args_list, expected_calls):
|
||||
assert actual_call.args == expected_call
|
||||
|
||||
# Check if yaml.safe_load was called
|
||||
mock_yaml_safe_load.assert_called_once()
|
||||
|
||||
# Check if open was called with the correct file path
|
||||
mock_file.assert_called_once_with(dummy_yaml_file_name, 'r')
|
||||
|
||||
@patch('builtins.open', new_callable=mock_open)
|
||||
def test_load_extra_model_paths_expands_appdata(
|
||||
mock_file,
|
||||
monkeypatch,
|
||||
mock_add_model_folder_path,
|
||||
mock_expandvars_appdata,
|
||||
yaml_config_with_appdata,
|
||||
mock_yaml_content_appdata
|
||||
):
|
||||
# Set the mock_file to return yaml with appdata as a variable
|
||||
mock_file.return_value.read.return_value = yaml_config_with_appdata
|
||||
|
||||
# Attach mocks
|
||||
monkeypatch.setattr(folder_paths, 'add_model_folder_path', mock_add_model_folder_path)
|
||||
monkeypatch.setattr(os.path, 'expandvars', mock_expandvars_appdata)
|
||||
monkeypatch.setattr(yaml, 'safe_load', Mock(return_value=mock_yaml_content_appdata))
|
||||
|
||||
# Mock expanduser to do nothing (since we're not testing it here)
|
||||
monkeypatch.setattr(os.path, 'expanduser', lambda x: x)
|
||||
|
||||
dummy_yaml_file_name = 'dummy_path.yaml'
|
||||
load_extra_path_config(dummy_yaml_file_name)
|
||||
|
||||
expected_base_path = 'C:/Users/TestUser/AppData/Roaming/ComfyUI'
|
||||
expected_calls = [
|
||||
('checkpoints', os.path.join(expected_base_path, 'models/checkpoints')),
|
||||
]
|
||||
|
||||
assert mock_add_model_folder_path.call_count == len(expected_calls)
|
||||
|
||||
# Check the base path variable was expanded
|
||||
for actual_call, expected_call in zip(mock_add_model_folder_path.call_args_list, expected_calls):
|
||||
assert actual_call.args == expected_call
|
||||
|
||||
# Verify that expandvars was called
|
||||
assert mock_expandvars_appdata.called
|
||||
@ -429,3 +429,29 @@ class TestExecution:
|
||||
assert len(images) == 1, "Should have 1 image"
|
||||
assert numpy.array(images[0]).min() == 63 and numpy.array(images[0]).max() == 63, "Image should have value 0.25"
|
||||
assert not result.did_run(test_node), "The execution should have been cached"
|
||||
|
||||
# This tests that nodes with OUTPUT_IS_LIST function correctly when they receive an ExecutionBlocker
|
||||
# as input. We also test that when that list (containing an ExecutionBlocker) is passed to a node,
|
||||
# only that one entry in the list is blocked.
|
||||
def test_execution_block_list_output(self, client: ComfyClient, builder: GraphBuilder):
|
||||
g = builder
|
||||
image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
||||
image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
||||
image3 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
||||
image_list = g.node("TestMakeListNode", value1=image1.out(0), value2=image2.out(0), value3=image3.out(0))
|
||||
int1 = g.node("StubInt", value=1)
|
||||
int2 = g.node("StubInt", value=2)
|
||||
int3 = g.node("StubInt", value=3)
|
||||
int_list = g.node("TestMakeListNode", value1=int1.out(0), value2=int2.out(0), value3=int3.out(0))
|
||||
compare = g.node("TestIntConditions", a=int_list.out(0), b=2, operation="==")
|
||||
blocker = g.node("TestExecutionBlocker", input=image_list.out(0), block=compare.out(0), verbose=False)
|
||||
|
||||
list_output = g.node("TestMakeListNode", value1=blocker.out(0))
|
||||
output = g.node("PreviewImage", images=list_output.out(0))
|
||||
|
||||
result = client.run(g)
|
||||
assert result.did_run(output), "The execution should have run"
|
||||
images = result.get_images(output)
|
||||
assert len(images) == 2, "Should have 2 images"
|
||||
assert numpy.array(images[0]).min() == 0 and numpy.array(images[0]).max() == 0, "First image should be black"
|
||||
assert numpy.array(images[1]).min() == 0 and numpy.array(images[1]).max() == 0, "Second image should also be black"
|
||||
|
||||
122
tests/unit/test_user_manager.py
Normal file
122
tests/unit/test_user_manager.py
Normal file
@ -0,0 +1,122 @@
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from aiohttp import web
|
||||
|
||||
from comfy.app.user_manager import UserManager
|
||||
|
||||
pytestmark = (
|
||||
pytest.mark.asyncio
|
||||
) # This applies the asyncio mark to all test functions in the module
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_manager(tmp_path):
|
||||
um = UserManager()
|
||||
um.get_request_user_filepath = lambda req, file, **kwargs: os.path.join(
|
||||
tmp_path, file
|
||||
)
|
||||
return um
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(user_manager):
|
||||
app = web.Application()
|
||||
routes = web.RouteTableDef()
|
||||
user_manager.add_routes(routes)
|
||||
app.add_routes(routes)
|
||||
return app
|
||||
|
||||
|
||||
async def test_listuserdata_empty_directory(aiohttp_client, app, tmp_path):
|
||||
client = await aiohttp_client(app)
|
||||
resp = await client.get("/userdata?dir=test_dir")
|
||||
assert resp.status == 404
|
||||
|
||||
|
||||
async def test_listuserdata_with_files(aiohttp_client, app, tmp_path):
|
||||
os.makedirs(tmp_path / "test_dir")
|
||||
with open(tmp_path / "test_dir" / "file1.txt", "w") as f:
|
||||
f.write("test content")
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
resp = await client.get("/userdata?dir=test_dir")
|
||||
assert resp.status == 200
|
||||
assert await resp.json() == ["file1.txt"]
|
||||
|
||||
|
||||
async def test_listuserdata_recursive(aiohttp_client, app, tmp_path):
|
||||
os.makedirs(tmp_path / "test_dir" / "subdir")
|
||||
with open(tmp_path / "test_dir" / "file1.txt", "w") as f:
|
||||
f.write("test content")
|
||||
with open(tmp_path / "test_dir" / "subdir" / "file2.txt", "w") as f:
|
||||
f.write("test content")
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
resp = await client.get("/userdata?dir=test_dir&recurse=true")
|
||||
assert resp.status == 200
|
||||
assert set(await resp.json()) == {"file1.txt", "subdir/file2.txt"}
|
||||
|
||||
|
||||
async def test_listuserdata_full_info(aiohttp_client, app, tmp_path):
|
||||
os.makedirs(tmp_path / "test_dir")
|
||||
with open(tmp_path / "test_dir" / "file1.txt", "w") as f:
|
||||
f.write("test content")
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
resp = await client.get("/userdata?dir=test_dir&full_info=true")
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
assert len(result) == 1
|
||||
assert result[0]["path"] == "file1.txt"
|
||||
assert "size" in result[0]
|
||||
assert "modified" in result[0]
|
||||
|
||||
|
||||
async def test_listuserdata_split_path(aiohttp_client, app, tmp_path):
|
||||
os.makedirs(tmp_path / "test_dir" / "subdir")
|
||||
with open(tmp_path / "test_dir" / "subdir" / "file1.txt", "w") as f:
|
||||
f.write("test content")
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
resp = await client.get("/userdata?dir=test_dir&recurse=true&split=true")
|
||||
assert resp.status == 200
|
||||
assert await resp.json() == [
|
||||
["subdir/file1.txt", "subdir", "file1.txt"]
|
||||
]
|
||||
|
||||
|
||||
async def test_listuserdata_invalid_directory(aiohttp_client, app):
|
||||
client = await aiohttp_client(app)
|
||||
resp = await client.get("/userdata?dir=")
|
||||
assert resp.status == 400
|
||||
|
||||
|
||||
async def test_listuserdata_normalized_separator(aiohttp_client, app, tmp_path):
|
||||
os_sep = "\\"
|
||||
with patch("os.sep", os_sep):
|
||||
with patch("os.path.sep", os_sep):
|
||||
os.makedirs(tmp_path / "test_dir" / "subdir")
|
||||
with open(tmp_path / "test_dir" / "subdir" / "file1.txt", "w") as f:
|
||||
f.write("test content")
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
resp = await client.get("/userdata?dir=test_dir&recurse=true")
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
assert len(result) == 1
|
||||
assert "/" in result[0] # Ensure forward slash is used
|
||||
assert "\\" not in result[0] # Ensure backslash is not present
|
||||
assert result[0] == "subdir/file1.txt"
|
||||
|
||||
# Test with full_info
|
||||
resp = await client.get(
|
||||
"/userdata?dir=test_dir&recurse=true&full_info=true"
|
||||
)
|
||||
assert resp.status == 200
|
||||
result = await resp.json()
|
||||
assert len(result) == 1
|
||||
assert "/" in result[0]["path"] # Ensure forward slash is used
|
||||
assert "\\" not in result[0]["path"] # Ensure backslash is not present
|
||||
assert result[0]["path"] == "subdir/file1.txt"
|
||||
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
25
utils/extra_config.py
Normal file
25
utils/extra_config.py
Normal file
@ -0,0 +1,25 @@
|
||||
import os
|
||||
import yaml
|
||||
import folder_paths
|
||||
import logging
|
||||
|
||||
def load_extra_path_config(yaml_path):
|
||||
with open(yaml_path, 'r') as stream:
|
||||
config = yaml.safe_load(stream)
|
||||
for c in config:
|
||||
conf = config[c]
|
||||
if conf is None:
|
||||
continue
|
||||
base_path = None
|
||||
if "base_path" in conf:
|
||||
base_path = conf.pop("base_path")
|
||||
base_path = os.path.expandvars(os.path.expanduser(base_path))
|
||||
for x in conf:
|
||||
for y in conf[x].split("\n"):
|
||||
if len(y) == 0:
|
||||
continue
|
||||
full_path = y
|
||||
if base_path is not None:
|
||||
full_path = os.path.join(base_path, full_path)
|
||||
logging.info("Adding extra search path {} {}".format(x, full_path))
|
||||
folder_paths.add_model_folder_path(x, full_path)
|
||||
Loading…
Reference in New Issue
Block a user