Merge branch 'comfyanonymous:master' into master

This commit is contained in:
patientx 2024-08-21 09:49:29 +03:00 committed by GitHub
commit ac75d4e4e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 33010 additions and 21615 deletions

0
api_server/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,3 @@
# ComfyUI Internal Routes
All routes under the `/internal` path are designated for **internal use by ComfyUI only**. These routes are not intended for use by external applications may change at any time without notice.

View File

View File

@ -0,0 +1,40 @@
from aiohttp import web
from typing import Optional
from folder_paths import models_dir, user_directory, output_directory
from api_server.services.file_service import FileService
class InternalRoutes:
'''
The top level web router for internal routes: /internal/*
The endpoints here should NOT be depended upon. It is for ComfyUI frontend use only.
Check README.md for more information.
'''
def __init__(self):
self.routes: web.RouteTableDef = web.RouteTableDef()
self._app: Optional[web.Application] = None
self.file_service = FileService({
"models": models_dir,
"user": user_directory,
"output": output_directory
})
def setup_routes(self):
@self.routes.get('/files')
async def list_files(request):
directory_key = request.query.get('directory', '')
try:
file_list = self.file_service.list_files(directory_key)
return web.json_response({"files": file_list})
except ValueError as e:
return web.json_response({"error": str(e)}, status=400)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
def get_app(self):
if self._app is None:
self._app = web.Application()
self.setup_routes()
self._app.add_routes(self.routes)
return self._app

View File

View File

@ -0,0 +1,13 @@
from typing import Dict, List, Optional
from api_server.utils.file_operations import FileSystemOperations, FileSystemItem
class FileService:
def __init__(self, allowed_directories: Dict[str, str], file_system_ops: Optional[FileSystemOperations] = None):
self.allowed_directories: Dict[str, str] = allowed_directories
self.file_system_ops: FileSystemOperations = file_system_ops or FileSystemOperations()
def list_files(self, directory_key: str) -> List[FileSystemItem]:
if directory_key not in self.allowed_directories:
raise ValueError("Invalid directory key")
directory_path: str = self.allowed_directories[directory_key]
return self.file_system_ops.walk_directory(directory_path)

View File

@ -0,0 +1,42 @@
import os
from typing import List, Union, TypedDict, Literal
from typing_extensions import TypeGuard
class FileInfo(TypedDict):
name: str
path: str
type: Literal["file"]
size: int
class DirectoryInfo(TypedDict):
name: str
path: str
type: Literal["directory"]
FileSystemItem = Union[FileInfo, DirectoryInfo]
def is_file_info(item: FileSystemItem) -> TypeGuard[FileInfo]:
return item["type"] == "file"
class FileSystemOperations:
@staticmethod
def walk_directory(directory: str) -> List[FileSystemItem]:
file_list: List[FileSystemItem] = []
for root, dirs, files in os.walk(directory):
for name in files:
file_path = os.path.join(root, name)
relative_path = os.path.relpath(file_path, directory)
file_list.append({
"name": name,
"path": relative_path,
"type": "file",
"size": os.path.getsize(file_path)
})
for name in dirs:
dir_path = os.path.join(root, name)
relative_path = os.path.relpath(dir_path, directory)
file_list.append({
"name": name,
"path": relative_path,
"type": "directory"
})
return file_list

View File

@ -383,7 +383,7 @@ def minimum_inference_memory():
EXTRA_RESERVED_VRAM = 200 * 1024 * 1024
if any(platform.win32_ver()):
EXTRA_RESERVED_VRAM = 400 * 1024 * 1024 #Windows is higher because of the shared vram issue
EXTRA_RESERVED_VRAM = 500 * 1024 * 1024 #Windows is higher because of the shared vram issue
if args.reserve_vram is not None:
EXTRA_RESERVED_VRAM = args.reserve_vram * 1024 * 1024 * 1024

View File

@ -250,17 +250,16 @@ def fp8_linear(self, input):
return None
if len(input.shape) == 3:
out = torch.empty((input.shape[0], input.shape[1], self.weight.shape[0]), device=input.device, dtype=input.dtype)
inn = input.to(dtype)
inn = input.view(-1, input.shape[2]).to(dtype)
non_blocking = comfy.model_management.device_supports_non_blocking(input.device)
w = cast_to(self.weight, device=input.device, non_blocking=non_blocking).t()
for i in range(input.shape[0]):
if self.bias is not None:
o, _ = torch._scaled_mm(inn[i], w, out_dtype=input.dtype, bias=cast_to_input(self.bias, input, non_blocking=non_blocking))
else:
o, _ = torch._scaled_mm(inn[i], w, out_dtype=input.dtype)
out[i] = o
return out
if self.bias is not None:
o, _ = torch._scaled_mm(inn, w, out_dtype=input.dtype, bias=cast_to_input(self.bias, input, non_blocking=non_blocking))
else:
o, _ = torch._scaled_mm(inn, w, out_dtype=input.dtype)
return o.view((-1, input.shape[1], self.weight.shape[0]))
return None
class fp8_ops(manual_cast):

View File

@ -4,14 +4,14 @@ class Example:
Class methods
-------------
INPUT_TYPES (dict):
INPUT_TYPES (dict):
Tell the main program input parameters of nodes.
IS_CHANGED:
optional method to control when the node is re executed.
Attributes
----------
RETURN_TYPES (`tuple`):
RETURN_TYPES (`tuple`):
The type of each element in the output tuple.
RETURN_NAMES (`tuple`):
Optional: The name of each output in the output tuple.
@ -23,13 +23,19 @@ class Example:
Assumed to be False if not present.
CATEGORY (`str`):
The category the node should appear in the UI.
DEPRECATED (`bool`):
Indicates whether the node is deprecated. Deprecated nodes are hidden by default in the UI, but remain
functional in existing workflows that use them.
EXPERIMENTAL (`bool`):
Indicates whether the node is experimental. Experimental nodes are marked as such in the UI and may be subject to
significant changes or removal in future versions. Use with caution in production workflows.
execute(s) -> tuple || None:
The entry point method. The name of this method must be the same as the value of property `FUNCTION`.
For example, if `FUNCTION = "execute"` then this method's name must be `execute`, if `FUNCTION = "foo"` then it must be `foo`.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
"""

View File

@ -29,6 +29,8 @@ from app.frontend_management import FrontendManager
from app.user_manager import UserManager
from model_filemanager import download_model, DownloadModelStatus
from typing import Optional
from api_server.routes.internal.internal_routes import InternalRoutes
class BinaryEventTypes:
PREVIEW_IMAGE = 1
@ -72,6 +74,7 @@ class PromptServer():
mimetypes.types_map['.js'] = 'application/javascript; charset=utf-8'
self.user_manager = UserManager()
self.internal_routes = InternalRoutes()
self.supports = ["custom_nodes_from_web"]
self.prompt_queue = None
self.loop = loop
@ -139,6 +142,14 @@ class PromptServer():
embeddings = folder_paths.get_filename_list("embeddings")
return web.json_response(list(map(lambda a: os.path.splitext(a)[0], embeddings)))
@routes.get("/models/{folder}")
async def get_models(request):
folder = request.match_info.get("folder", None)
if not folder in folder_paths.folder_names_and_paths:
return web.Response(status=404)
files = folder_paths.get_filename_list(folder)
return web.json_response(files)
@routes.get("/extensions")
async def get_extensions(request):
files = glob.glob(os.path.join(
@ -442,6 +453,11 @@ class PromptServer():
if hasattr(obj_class, 'OUTPUT_TOOLTIPS'):
info['output_tooltips'] = obj_class.OUTPUT_TOOLTIPS
if getattr(obj_class, "DEPRECATED", False):
info['deprecated'] = True
if getattr(obj_class, "EXPERIMENTAL", False):
info['experimental'] = True
return info
@routes.get("/object_info")
@ -597,6 +613,7 @@ class PromptServer():
def add_routes(self):
self.user_manager.add_routes(self.routes)
self.app.add_subapp('/internal', self.internal_routes.get_app())
# Prefix every route with /api for easier matching for delegation.
# This is very useful for frontend dev server, which need to forward

View File

@ -0,0 +1,115 @@
import pytest
from aiohttp import web
from unittest.mock import MagicMock, patch
from api_server.routes.internal.internal_routes import InternalRoutes
from api_server.services.file_service import FileService
from folder_paths import models_dir, user_directory, output_directory
@pytest.fixture
def internal_routes():
return InternalRoutes()
@pytest.fixture
def aiohttp_client_factory(aiohttp_client, internal_routes):
async def _get_client():
app = internal_routes.get_app()
return await aiohttp_client(app)
return _get_client
@pytest.mark.asyncio
async def test_list_files_valid_directory(aiohttp_client_factory, internal_routes):
mock_file_list = [
{"name": "file1.txt", "path": "file1.txt", "type": "file", "size": 100},
{"name": "dir1", "path": "dir1", "type": "directory"}
]
internal_routes.file_service.list_files = MagicMock(return_value=mock_file_list)
client = await aiohttp_client_factory()
resp = await client.get('/files?directory=models')
assert resp.status == 200
data = await resp.json()
assert 'files' in data
assert len(data['files']) == 2
assert data['files'] == mock_file_list
# Check other valid directories
resp = await client.get('/files?directory=user')
assert resp.status == 200
resp = await client.get('/files?directory=output')
assert resp.status == 200
@pytest.mark.asyncio
async def test_list_files_invalid_directory(aiohttp_client_factory, internal_routes):
internal_routes.file_service.list_files = MagicMock(side_effect=ValueError("Invalid directory key"))
client = await aiohttp_client_factory()
resp = await client.get('/files?directory=invalid')
assert resp.status == 400
data = await resp.json()
assert 'error' in data
assert data['error'] == "Invalid directory key"
@pytest.mark.asyncio
async def test_list_files_exception(aiohttp_client_factory, internal_routes):
internal_routes.file_service.list_files = MagicMock(side_effect=Exception("Unexpected error"))
client = await aiohttp_client_factory()
resp = await client.get('/files?directory=models')
assert resp.status == 500
data = await resp.json()
assert 'error' in data
assert data['error'] == "Unexpected error"
@pytest.mark.asyncio
async def test_list_files_no_directory_param(aiohttp_client_factory, internal_routes):
mock_file_list = []
internal_routes.file_service.list_files = MagicMock(return_value=mock_file_list)
client = await aiohttp_client_factory()
resp = await client.get('/files')
assert resp.status == 200
data = await resp.json()
assert 'files' in data
assert len(data['files']) == 0
def test_setup_routes(internal_routes):
internal_routes.setup_routes()
routes = internal_routes.routes
assert any(route.method == 'GET' and str(route.path) == '/files' for route in routes)
def test_get_app(internal_routes):
app = internal_routes.get_app()
assert isinstance(app, web.Application)
assert internal_routes._app is not None
def test_get_app_reuse(internal_routes):
app1 = internal_routes.get_app()
app2 = internal_routes.get_app()
assert app1 is app2
@pytest.mark.asyncio
async def test_routes_added_to_app(aiohttp_client_factory, internal_routes):
client = await aiohttp_client_factory()
try:
resp = await client.get('/files')
print(f"Response received: status {resp.status}")
except Exception as e:
print(f"Exception occurred during GET request: {e}")
raise
assert resp.status != 404, "Route /files does not exist"
@pytest.mark.asyncio
async def test_file_service_initialization():
with patch('api_server.routes.internal.internal_routes.FileService') as MockFileService:
# Create a mock instance
mock_file_service_instance = MagicMock(spec=FileService)
MockFileService.return_value = mock_file_service_instance
internal_routes = InternalRoutes()
# Check if FileService was initialized with the correct parameters
MockFileService.assert_called_once_with({
"models": models_dir,
"user": user_directory,
"output": output_directory
})
# Verify that the file_service attribute of InternalRoutes is set
assert internal_routes.file_service == mock_file_service_instance

View File

@ -0,0 +1,54 @@
import pytest
from unittest.mock import MagicMock
from api_server.services.file_service import FileService
@pytest.fixture
def mock_file_system_ops():
return MagicMock()
@pytest.fixture
def file_service(mock_file_system_ops):
allowed_directories = {
"models": "/path/to/models",
"user": "/path/to/user",
"output": "/path/to/output"
}
return FileService(allowed_directories, file_system_ops=mock_file_system_ops)
def test_list_files_valid_directory(file_service, mock_file_system_ops):
mock_file_system_ops.walk_directory.return_value = [
{"name": "file1.txt", "path": "file1.txt", "type": "file", "size": 100},
{"name": "dir1", "path": "dir1", "type": "directory"}
]
result = file_service.list_files("models")
assert len(result) == 2
assert result[0]["name"] == "file1.txt"
assert result[1]["name"] == "dir1"
mock_file_system_ops.walk_directory.assert_called_once_with("/path/to/models")
def test_list_files_invalid_directory(file_service):
# Does not support walking directories outside of the allowed directories
with pytest.raises(ValueError, match="Invalid directory key"):
file_service.list_files("invalid_key")
def test_list_files_empty_directory(file_service, mock_file_system_ops):
mock_file_system_ops.walk_directory.return_value = []
result = file_service.list_files("models")
assert len(result) == 0
mock_file_system_ops.walk_directory.assert_called_once_with("/path/to/models")
@pytest.mark.parametrize("directory_key", ["models", "user", "output"])
def test_list_files_all_allowed_directories(file_service, mock_file_system_ops, directory_key):
mock_file_system_ops.walk_directory.return_value = [
{"name": f"file_{directory_key}.txt", "path": f"file_{directory_key}.txt", "type": "file", "size": 100}
]
result = file_service.list_files(directory_key)
assert len(result) == 1
assert result[0]["name"] == f"file_{directory_key}.txt"
mock_file_system_ops.walk_directory.assert_called_once_with(f"/path/to/{directory_key}")

View File

@ -0,0 +1,42 @@
import pytest
from typing import List
from api_server.utils.file_operations import FileSystemOperations, FileSystemItem, is_file_info
@pytest.fixture
def temp_directory(tmp_path):
# Create a temporary directory structure
dir1 = tmp_path / "dir1"
dir2 = tmp_path / "dir2"
dir1.mkdir()
dir2.mkdir()
(dir1 / "file1.txt").write_text("content1")
(dir2 / "file2.txt").write_text("content2")
(tmp_path / "file3.txt").write_text("content3")
return tmp_path
def test_walk_directory(temp_directory):
result: List[FileSystemItem] = FileSystemOperations.walk_directory(str(temp_directory))
assert len(result) == 5 # 2 directories and 3 files
files = [item for item in result if item['type'] == 'file']
dirs = [item for item in result if item['type'] == 'directory']
assert len(files) == 3
assert len(dirs) == 2
file_names = {file['name'] for file in files}
assert file_names == {'file1.txt', 'file2.txt', 'file3.txt'}
dir_names = {dir['name'] for dir in dirs}
assert dir_names == {'dir1', 'dir2'}
def test_walk_directory_empty(tmp_path):
result = FileSystemOperations.walk_directory(str(tmp_path))
assert len(result) == 0
def test_walk_directory_file_size(temp_directory):
result: List[FileSystemItem] = FileSystemOperations.walk_directory(str(temp_directory))
files = [item for item in result if is_file_info(item)]
for file in files:
assert file['size'] > 0 # Assuming all files have some content

1
web/assets/index--0nRVkuV.js.map generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
web/assets/index-CaD4RONs.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

1
web/assets/index-D8Zp4vRl.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,8 @@
@font-face {
font-family: 'primeicons';
font-display: block;
src: url('/assets/primeicons-DMOk5skT.eot');
src: url('/assets/primeicons-DMOk5skT.eot?#iefix') format('embedded-opentype'), url('/assets/primeicons-C6QP2o4f.woff2') format('woff2'), url('/assets/primeicons-WjwUDZjB.woff') format('woff'), url('/assets/primeicons-MpK4pl85.ttf') format('truetype'), url('/assets/primeicons-Dr5RGzOO.svg?#primeicons') format('svg');
src: url('./primeicons-DMOk5skT.eot');
src: url('./primeicons-DMOk5skT.eot?#iefix') format('embedded-opentype'), url('./primeicons-C6QP2o4f.woff2') format('woff2'), url('./primeicons-WjwUDZjB.woff') format('woff'), url('./primeicons-MpK4pl85.ttf') format('truetype'), url('./primeicons-Dr5RGzOO.svg?#primeicons') format('svg');
font-weight: normal;
font-style: normal;
}
@ -1330,6 +1330,184 @@
.comfyui-body-right .side-bar-button.side-bar-button-selected[data-v-7a0b94a3]:hover {
border-right: 4px solid var(--p-button-text-primary-color);
}
:root {
--red-600: #dc3545;
}
.comfy-missing-nodes[data-v-286402f2] {
font-family: monospace;
color: var(--red-600);
padding: 1.5rem;
background-color: var(--surface-ground);
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
}
.warning-title[data-v-286402f2] {
margin-top: 0;
margin-bottom: 1rem;
}
.warning-description[data-v-286402f2] {
margin-bottom: 1rem;
}
.missing-nodes-list[data-v-286402f2] {
max-height: 300px;
overflow-y: auto;
}
.missing-nodes-list.maximized[data-v-286402f2] {
max-height: unset;
}
.missing-node-item[data-v-286402f2] {
display: flex;
align-items: center;
padding: 0.5rem;
}
.node-type[data-v-286402f2] {
font-weight: 600;
color: var(--text-color);
}
.node-hint[data-v-286402f2] {
margin-left: 0.5rem;
font-style: italic;
color: var(--text-color-secondary);
}
[data-v-286402f2] .p-button {
margin-left: auto;
}
.added-nodes-warning[data-v-286402f2] {
margin-top: 1rem;
font-style: italic;
}
.input-slider[data-v-fbaf7a8c] {
display: flex;
align-items: center;
gap: 1rem;
}
.slider-part[data-v-fbaf7a8c] {
flex-grow: 1;
}
.input-part[data-v-fbaf7a8c] {
width: 5rem !important;
}
.info-chip[data-v-6361f2fb] {
background: transparent;
}
.setting-item[data-v-6361f2fb] {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.setting-label[data-v-6361f2fb] {
display: flex;
align-items: center;
flex: 1;
}
.setting-input[data-v-6361f2fb] {
flex: 1;
display: flex;
justify-content: flex-end;
margin-left: 1rem;
}
/* Ensure PrimeVue components take full width of their container */
.setting-input[data-v-6361f2fb] .p-inputtext,
.setting-input[data-v-6361f2fb] .input-slider,
.setting-input[data-v-6361f2fb] .p-select,
.setting-input[data-v-6361f2fb] .p-togglebutton {
width: 100%;
max-width: 200px;
}
.setting-input[data-v-6361f2fb] .p-inputtext {
max-width: unset;
}
/* Special case for ToggleSwitch to align it to the right */
.setting-input[data-v-6361f2fb] .p-toggleswitch {
margin-left: auto;
}
.search-box-input[data-v-8160f15b] {
width: 100%;
}
.no-results-placeholder[data-v-5a7d148a] {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 2rem;
}
.no-results-placeholder[data-v-5a7d148a] .p-card {
background-color: var(--surface-ground);
text-align: center;
}
.no-results-placeholder h3[data-v-5a7d148a] {
color: var(--text-color);
margin-bottom: 0.5rem;
}
.no-results-placeholder p[data-v-5a7d148a] {
color: var(--text-color-secondary);
margin-bottom: 1rem;
}
/* Remove after we have tailwind setup */
.border-none {
border: none !important;
}
.settings-tab-panels {
padding-top: 0px !important;
}
.settings-container[data-v-29723d1f] {
display: flex;
height: 70vh;
width: 60vw;
max-width: 1000px;
overflow: hidden;
/* Prevents container from scrolling */
}
.settings-sidebar[data-v-29723d1f] {
width: 250px;
flex-shrink: 0;
/* Prevents sidebar from shrinking */
overflow-y: auto;
padding: 10px;
}
.settings-search-box[data-v-29723d1f] {
width: 100%;
margin-bottom: 10px;
}
.settings-content[data-v-29723d1f] {
flex-grow: 1;
overflow-y: auto;
/* Allows vertical scrolling */
}
/* Ensure the Listbox takes full width of the sidebar */
.settings-sidebar[data-v-29723d1f] .p-listbox {
width: 100%;
}
/* Optional: Style scrollbars for webkit browsers */
.settings-sidebar[data-v-29723d1f]::-webkit-scrollbar,
.settings-content[data-v-29723d1f]::-webkit-scrollbar {
width: 1px;
}
.settings-sidebar[data-v-29723d1f]::-webkit-scrollbar-thumb,
.settings-content[data-v-29723d1f]::-webkit-scrollbar-thumb {
background-color: transparent;
}
.pi-cog[data-v-969a1066] {
font-size: 1.25rem;
margin-right: 0.5rem;
}
.version-tag[data-v-969a1066] {
margin-left: 0.5rem;
}
.lds-ring {
display: inline-block;
position: relative;
@ -2881,6 +3059,7 @@ body {
#graph-canvas {
width: 100%;
height: 100%;
touch-action: none;
}
.comfyui-body-right {
@ -3482,184 +3661,6 @@ audio.comfy-audio.empty-audio-widget {
max-width: 25vw;
}
:root {
--red-600: #dc3545;
}
.comfy-missing-nodes[data-v-286402f2] {
font-family: monospace;
color: var(--red-600);
padding: 1.5rem;
background-color: var(--surface-ground);
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
}
.warning-title[data-v-286402f2] {
margin-top: 0;
margin-bottom: 1rem;
}
.warning-description[data-v-286402f2] {
margin-bottom: 1rem;
}
.missing-nodes-list[data-v-286402f2] {
max-height: 300px;
overflow-y: auto;
}
.missing-nodes-list.maximized[data-v-286402f2] {
max-height: unset;
}
.missing-node-item[data-v-286402f2] {
display: flex;
align-items: center;
padding: 0.5rem;
}
.node-type[data-v-286402f2] {
font-weight: 600;
color: var(--text-color);
}
.node-hint[data-v-286402f2] {
margin-left: 0.5rem;
font-style: italic;
color: var(--text-color-secondary);
}
[data-v-286402f2] .p-button {
margin-left: auto;
}
.added-nodes-warning[data-v-286402f2] {
margin-top: 1rem;
font-style: italic;
}
.input-slider[data-v-fbaf7a8c] {
display: flex;
align-items: center;
gap: 1rem;
}
.slider-part[data-v-fbaf7a8c] {
flex-grow: 1;
}
.input-part[data-v-fbaf7a8c] {
width: 5rem !important;
}
.info-chip[data-v-4feeb3d2] {
background: transparent;
}
.setting-item[data-v-4feeb3d2] {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.setting-label[data-v-4feeb3d2] {
display: flex;
align-items: center;
flex: 1;
}
.setting-input[data-v-4feeb3d2] {
flex: 1;
display: flex;
justify-content: flex-end;
margin-left: 1rem;
}
/* Ensure PrimeVue components take full width of their container */
.setting-input[data-v-4feeb3d2] .p-inputtext,
.setting-input[data-v-4feeb3d2] .input-slider,
.setting-input[data-v-4feeb3d2] .p-select,
.setting-input[data-v-4feeb3d2] .p-togglebutton {
width: 100%;
max-width: 200px;
}
.setting-input[data-v-4feeb3d2] .p-inputtext {
max-width: unset;
}
/* Special case for ToggleSwitch to align it to the right */
.setting-input[data-v-4feeb3d2] .p-toggleswitch {
margin-left: auto;
}
.search-box-input[data-v-3bbe5335] {
width: 100%;
}
.no-results-placeholder[data-v-5a7d148a] {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 2rem;
}
.no-results-placeholder[data-v-5a7d148a] .p-card {
background-color: var(--surface-ground);
text-align: center;
}
.no-results-placeholder h3[data-v-5a7d148a] {
color: var(--text-color);
margin-bottom: 0.5rem;
}
.no-results-placeholder p[data-v-5a7d148a] {
color: var(--text-color-secondary);
margin-bottom: 1rem;
}
/* Remove after we have tailwind setup */
.border-none {
border: none !important;
}
.settings-tab-panels {
padding-top: 0px !important;
}
.settings-container[data-v-833dbfbb] {
display: flex;
height: 70vh;
width: 60vw;
max-width: 1000px;
overflow: hidden;
/* Prevents container from scrolling */
}
.settings-sidebar[data-v-833dbfbb] {
width: 250px;
flex-shrink: 0;
/* Prevents sidebar from shrinking */
overflow-y: auto;
padding: 10px;
}
.settings-search-box[data-v-833dbfbb] {
width: 100%;
margin-bottom: 10px;
}
.settings-content[data-v-833dbfbb] {
flex-grow: 1;
overflow-y: auto;
/* Allows vertical scrolling */
}
/* Ensure the Listbox takes full width of the sidebar */
.settings-sidebar[data-v-833dbfbb] .p-listbox {
width: 100%;
}
/* Optional: Style scrollbars for webkit browsers */
.settings-sidebar[data-v-833dbfbb]::-webkit-scrollbar,
.settings-content[data-v-833dbfbb]::-webkit-scrollbar {
width: 1px;
}
.settings-sidebar[data-v-833dbfbb]::-webkit-scrollbar-thumb,
.settings-content[data-v-833dbfbb]::-webkit-scrollbar-thumb {
background-color: transparent;
}
.pi-cog[data-v-969a1066] {
font-size: 1.25rem;
margin-right: 0.5rem;
}
.version-tag[data-v-969a1066] {
margin-left: 0.5rem;
}
:root {
--sidebar-width: 64px;
--sidebar-icon-size: 1.5rem;
@ -3869,77 +3870,81 @@ audio.comfy-audio.empty-audio-widget {
color: var(--error-text);
}
.comfy-vue-node-search-container[data-v-b8a4ffdc] {
.comfy-vue-node-search-container[data-v-ba2c5897] {
display: flex;
width: 100%;
min-width: 24rem;
align-items: center;
justify-content: center;
}
.comfy-vue-node-search-container[data-v-b8a4ffdc] * {
.comfy-vue-node-search-container[data-v-ba2c5897] * {
pointer-events: auto;
}
.comfy-vue-node-preview-container[data-v-b8a4ffdc] {
.comfy-vue-node-preview-container[data-v-ba2c5897] {
position: absolute;
left: -350px;
top: 50px;
}
.comfy-vue-node-search-box[data-v-b8a4ffdc] {
.comfy-vue-node-search-box[data-v-ba2c5897] {
z-index: 10;
flex-grow: 1;
}
.option-container[data-v-b8a4ffdc] {
.option-container[data-v-ba2c5897] {
display: flex;
width: 100%;
cursor: pointer;
flex-direction: column;
align-items: center;
justify-content: space-between;
overflow: hidden;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0px;
padding-bottom: 0px;
}
.option-display-name[data-v-b8a4ffdc] {
.option-display-name[data-v-ba2c5897] {
display: flex;
flex-direction: column;
font-weight: 600;
}
.option-category[data-v-b8a4ffdc] {
.option-category[data-v-ba2c5897] {
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 300;
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
/* Keeps the text on a single line by default */
white-space: nowrap;
}
.i-badge[data-v-b8a4ffdc] {
.i-badge[data-v-ba2c5897] {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.o-badge[data-v-b8a4ffdc] {
.o-badge[data-v-ba2c5897] {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.c-badge[data-v-b8a4ffdc] {
.c-badge[data-v-ba2c5897] {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.s-badge[data-v-b8a4ffdc] {
.s-badge[data-v-ba2c5897] {
--tw-bg-opacity: 1;
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
}
[data-v-b8a4ffdc] .highlight {
[data-v-ba2c5897] .highlight {
background-color: var(--p-primary-color);
color: var(--p-primary-contrast-color);
font-weight: bold;
border-radius: 0.25rem;
padding: 0.125rem 0.25rem;
padding: 0rem 0.125rem;
margin: -0.125rem 0.125rem;
}
@ -3971,46 +3976,57 @@ audio.comfy-audio.empty-audio-widget {
z-index: 99999;
}
.result-container[data-v-0fac61d9] {
.broken-image[data-v-1a883642] {
display: none;
}
.broken-image-placeholder[data-v-1a883642] {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
margin: 2rem;
}
.broken-image-placeholder i[data-v-1a883642] {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.result-container[data-v-7ceacc88] {
width: 100%;
height: 100%;
aspect-ratio: 1 / 1;
overflow: hidden;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
[data-v-0fac61d9] img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
[data-v-7ceacc88] .task-output-image {
width: 100%;
height: 100%;
-o-object-fit: cover;
object-fit: cover;
-o-object-position: center;
object-position: center;
}
.p-image-preview[data-v-0fac61d9] {
position: static;
display: contents;
}
[data-v-0fac61d9] .image-preview-mask {
.image-preview-mask[data-v-7ceacc88] {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: auto;
height: auto;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
padding: 10px;
cursor: pointer;
background: rgba(0, 0, 0, 0.5);
color: var(--p-image-preview-mask-color);
transition:
opacity var(--p-image-transition-duration),
background var(--p-image-transition-duration);
border-radius: 50%;
transition: opacity 0.3s ease;
}
.result-container:hover .image-preview-mask[data-v-7ceacc88] {
opacity: 1;
}
.task-result-preview[data-v-6cf8179c] {
.task-result-preview[data-v-7c099cb7] {
aspect-ratio: 1 / 1;
overflow: hidden;
display: flex;
@ -4019,18 +4035,18 @@ audio.comfy-audio.empty-audio-widget {
width: 100%;
height: 100%;
}
.task-result-preview i[data-v-6cf8179c],
.task-result-preview span[data-v-6cf8179c] {
.task-result-preview i[data-v-7c099cb7],
.task-result-preview span[data-v-7c099cb7] {
font-size: 2rem;
}
.task-item[data-v-6cf8179c] {
.task-item[data-v-7c099cb7] {
display: flex;
flex-direction: column;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.task-item-details[data-v-6cf8179c] {
.task-item-details[data-v-7c099cb7] {
position: absolute;
bottom: 0;
padding: 0.6rem;
@ -4041,12 +4057,23 @@ audio.comfy-audio.empty-audio-widget {
/* In dark mode, transparent background color for tags is not ideal for tags that
are floating on top of images. */
.tag-wrapper[data-v-6cf8179c] {
.tag-wrapper[data-v-7c099cb7] {
background-color: var(--p-primary-contrast-color);
border-radius: 6px;
display: inline-flex;
}
/* PrimeVue's galleria teleports the fullscreen gallery out of subtree so we
cannot use scoped style here. */
img.galleria-image {
max-width: 100vw;
max-height: 100vh;
-o-object-fit: contain;
object-fit: contain;
/* Set z-index so the close button doesn't get hidden behind the image when image is large */
z-index: -1;
}
.comfy-vue-side-bar-container[data-v-bde767d2] {
display: flex;
flex-direction: column;
@ -4078,14 +4105,33 @@ are floating on top of images. */
background-color: transparent;
}
.queue-grid[data-v-7f831ee9] {
.scroll-container[data-v-bd027c46] {
height: 100%;
overflow-y: auto;
}
.queue-grid[data-v-bd027c46] {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
padding: 0.5rem;
gap: 0.5rem;
}
.spinner[data-v-40c18658] {
.node-lib-tree-node-label {
display: flex;
align-items: center;
margin-left: var(--p-tree-node-gap);
}
[data-v-11e12183] .node-lib-search-box {
margin-left: 1rem;
margin-right: 1rem;
margin-top: 1rem;
}
[data-v-11e12183] .comfy-vue-side-bar-body {
background: var(--p-tree-background);
}
.spinner[data-v-afce9bd6] {
position: absolute;
inset: 0px;
display: flex;

View File

@ -48,7 +48,7 @@ var __async = (__this, __arguments, generator) => {
});
};
var _PrimitiveNode_instances, onFirstConnection_fn, createWidget_fn, mergeWidgetConfig_fn, isValidConnection_fn, removeWidgets_fn, _convertedToProcess;
import { C as ComfyDialog, $ as $el, a as ComfyApp, b as app, L as LGraphCanvas, c as LiteGraph, d as applyTextReplacements, e as ComfyWidgets, f as addValueControlWidgets, D as DraggableList, g as api, h as LGraphGroup, i as LGraphNode } from "./index-D8Zp4vRl.js";
import { C as ComfyDialog, $ as $el, a as ComfyApp, b as app, L as LGraphCanvas, c as LiteGraph, d as applyTextReplacements, e as ComfyWidgets, f as addValueControlWidgets, D as DraggableList, g as api, u as useToastStore, h as LGraphGroup, i as LGraphNode } from "./index-CaD4RONs.js";
const _ClipspaceDialog = class _ClipspaceDialog extends ComfyDialog {
static registerButton(name, contextPredicate, callback) {
const item = $el("button", {
@ -891,6 +891,7 @@ app.registerExtension({
});
app.ui.settings.addSetting({
id: id$4,
category: ["Comfy", "ColorPalette"],
name: "Color Palette",
type: /* @__PURE__ */ __name((name, setter, value) => {
const options = [
@ -923,12 +924,6 @@ app.registerExtension({
options
);
return $el("tr", [
$el("td", [
$el("label", {
for: id$4.replaceAll(".", "-"),
textContent: "Color palette"
})
]),
$el("td", [
els.select,
$el(
@ -1221,7 +1216,6 @@ app.registerExtension({
const floatWeight = parseFloat(weight);
if (isNaN(floatWeight)) return weight;
const newWeight = floatWeight + delta;
if (newWeight < 0) return "0";
return String(Number(newWeight.toFixed(10)));
}
__name(incrementWeight, "incrementWeight");
@ -1302,7 +1296,7 @@ app.registerExtension({
selectedText = addWeightToParentheses(selectedText);
const weightDelta = event.key === "ArrowUp" ? delta : -delta;
const updatedText = selectedText.replace(
/\((.*):(\d+(?:\.\d+)?)\)/,
/\((.*):([+-]?\d+(?:\.\d+)?)\)/,
(match, text, weight) => {
weight = incrementWeight(weight, weightDelta);
if (weight == 1) {
@ -1620,9 +1614,9 @@ function getWidgetConfig(slot) {
}
__name(getWidgetConfig, "getWidgetConfig");
function getConfig(widgetName) {
var _a, _b, _c, _d;
var _a, _b, _c, _d, _e;
const { nodeData } = this.constructor;
return (_d = (_a = nodeData == null ? void 0 : nodeData.input) == null ? void 0 : _a.required[widgetName]) != null ? _d : (_c = (_b = nodeData == null ? void 0 : nodeData.input) == null ? void 0 : _b.optional) == null ? void 0 : _c[widgetName];
return (_e = (_b = (_a = nodeData == null ? void 0 : nodeData.input) == null ? void 0 : _a.required) == null ? void 0 : _b[widgetName]) != null ? _e : (_d = (_c = nodeData == null ? void 0 : nodeData.input) == null ? void 0 : _c.optional) == null ? void 0 : _d[widgetName];
}
__name(getConfig, "getConfig");
function isConvertibleWidget(widget, config) {
@ -1854,8 +1848,7 @@ app.registerExtension({
init() {
useConversionSubmenusSetting = app.ui.settings.addSetting({
id: "Comfy.NodeInputConversionSubmenus",
name: "Node widget/input conversion sub-menus",
tooltip: "In the node context menu, place the entries that convert between input/widget in sub-menus.",
name: "In the node context menu, place the entries that convert between input/widget in sub-menus.",
type: "boolean",
defaultValue: true
});
@ -3957,7 +3950,8 @@ app.registerExtension({
}, "replace");
app.ui.settings.addSetting({
id: id$2,
name: "Invert Menu Scrolling",
category: ["Comfy", "Graph", "InvertMenuScrolling"],
name: "Invert Context Menu Scrolling",
type: "boolean",
defaultValue: false,
onChange(value) {
@ -3974,58 +3968,66 @@ app.registerExtension({
name: "Comfy.Keybinds",
init() {
const keybindListener = /* @__PURE__ */ __name(function(event) {
const modifierPressed = event.ctrlKey || event.metaKey;
if (modifierPressed && event.key === "Enter") {
if (event.altKey) {
api.interrupt();
return __async(this, null, function* () {
const modifierPressed = event.ctrlKey || event.metaKey;
if (modifierPressed && event.key === "Enter") {
if (event.altKey) {
yield api.interrupt();
useToastStore().add({
severity: "info",
summary: "Interrupted",
detail: "Execution has been interrupted",
life: 1e3
});
return;
}
app.queuePrompt(event.shiftKey ? -1 : 0).then();
return;
}
app.queuePrompt(event.shiftKey ? -1 : 0).then();
return;
}
const target = event.composedPath()[0];
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
return;
}
const modifierKeyIdMap = {
s: "#comfy-save-button",
o: "#comfy-file-input",
Backspace: "#comfy-clear-button",
d: "#comfy-load-default-button"
};
const modifierKeybindId = modifierKeyIdMap[event.key];
if (modifierPressed && modifierKeybindId) {
event.preventDefault();
const elem = document.querySelector(modifierKeybindId);
elem.click();
return;
}
if (event.ctrlKey || event.altKey || event.metaKey) {
return;
}
if (event.key === "Escape") {
const modals = document.querySelectorAll(".comfy-modal");
const modal = Array.from(modals).find(
(modal2) => window.getComputedStyle(modal2).getPropertyValue("display") !== "none"
);
if (modal) {
modal.style.display = "none";
const target = event.composedPath()[0];
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
return;
}
;
[...document.querySelectorAll("dialog")].forEach((d) => {
d.close();
});
}
const keyIdMap = {
q: ".queue-tab-button.side-bar-button",
h: ".queue-tab-button.side-bar-button",
r: "#comfy-refresh-button"
};
const buttonId = keyIdMap[event.key];
if (buttonId) {
const button = document.querySelector(buttonId);
button.click();
}
const modifierKeyIdMap = {
s: "#comfy-save-button",
o: "#comfy-file-input",
Backspace: "#comfy-clear-button",
d: "#comfy-load-default-button"
};
const modifierKeybindId = modifierKeyIdMap[event.key];
if (modifierPressed && modifierKeybindId) {
event.preventDefault();
const elem = document.querySelector(modifierKeybindId);
elem.click();
return;
}
if (event.ctrlKey || event.altKey || event.metaKey) {
return;
}
if (event.key === "Escape") {
const modals = document.querySelectorAll(".comfy-modal");
const modal = Array.from(modals).find(
(modal2) => window.getComputedStyle(modal2).getPropertyValue("display") !== "none"
);
if (modal) {
modal.style.display = "none";
}
;
[...document.querySelectorAll("dialog")].forEach((d) => {
d.close();
});
}
const keyIdMap = {
q: ".queue-tab-button.side-bar-button",
h: ".queue-tab-button.side-bar-button",
r: "#comfy-refresh-button"
};
const buttonId = keyIdMap[event.key];
if (buttonId) {
const button = document.querySelector(buttonId);
button.click();
}
});
}, "keybindListener");
window.addEventListener("keydown", keybindListener, true);
}
@ -4037,6 +4039,7 @@ const ext = {
return __async(this, null, function* () {
app2.ui.settings.addSetting({
id: id$1,
category: ["Comfy", "Graph", "LinkRenderMode"],
name: "Link Render Mode",
defaultValue: 2,
type: "combo",
@ -5684,7 +5687,9 @@ app.registerExtension({
LiteGraph.middle_click_slot_add_default_node = true;
this.suggestionsNumber = app.ui.settings.addSetting({
id: "Comfy.NodeSuggestions.number",
category: ["Comfy", "Node Search Box", "NodeSuggestions"],
name: "Number of nodes suggestions",
tooltip: "Only for litegraph searchbox/context menu",
type: "slider",
attrs: {
min: 1,
@ -5766,7 +5771,8 @@ app.registerExtension({
init() {
app.ui.settings.addSetting({
id: "Comfy.SnapToGrid.GridSize",
name: "Grid Size",
category: ["Comfy", "Graph", "GridSize"],
name: "Snap to gird size",
type: "slider",
attrs: {
min: 1,
@ -6176,4 +6182,4 @@ app.registerExtension({
};
}
});
//# sourceMappingURL=index--0nRVkuV.js.map
//# sourceMappingURL=index-DkvOTKox.js.map

1
web/assets/index-DkvOTKox.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -20,7 +20,7 @@ var __async = (__this, __arguments, generator) => {
step((generator = generator.apply(__this, __arguments)).next());
});
};
import { j as createSpinner, g as api, $ as $el } from "./index-D8Zp4vRl.js";
import { j as createSpinner, g as api, $ as $el } from "./index-CaD4RONs.js";
const _UserSelectionScreen = class _UserSelectionScreen {
show(users, user) {
return __async(this, null, function* () {
@ -139,4 +139,4 @@ window.comfyAPI.userSelection.UserSelectionScreen = UserSelectionScreen;
export {
UserSelectionScreen
};
//# sourceMappingURL=userSelection-CH4RQEqW.js.map
//# sourceMappingURL=userSelection-GRU1gtOt.js.map

File diff suppressed because one or more lines are too long

4
web/index.html vendored
View File

@ -14,8 +14,8 @@
</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-D8Zp4vRl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BHzRuMlR.css">
<script type="module" crossorigin src="./assets/index-CaD4RONs.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DAK31IJJ.css">
</head>
<body class="litegraph">
<div id="vue-app"></div>