Compare commits

..

3 Commits

Author SHA1 Message Date
comfyanonymous
75c1c757d9 ComfyUI version v0.3.27 2025-03-21 20:09:54 -04:00
Chenlei Hu
ce9b084279 [nit] Format error strings (#7345) 2025-03-21 19:08:25 -04:00
Terry Jia
2206246055 support output normal and lineart once (#7290) 2025-03-21 16:24:13 -04:00
7 changed files with 89 additions and 112 deletions

View File

@@ -22,28 +22,46 @@ import app.logger
# The path to the requirements.txt file # The path to the requirements.txt file
req_path = Path(__file__).parents[1] / "requirements.txt" req_path = Path(__file__).parents[1] / "requirements.txt"
def frontend_install_warning_message(): def frontend_install_warning_message():
"""The warning message to display when the frontend version is not up to date.""" """The warning message to display when the frontend version is not up to date."""
extra = "" extra = ""
if sys.flags.no_user_site: if sys.flags.no_user_site:
extra = "-s " extra = "-s "
return f"Please install the updated requirements.txt file by running:\n{sys.executable} {extra}-m pip install -r {req_path}\n\nThis error is happening because the ComfyUI frontend is no longer shipped as part of the main repo but as a pip package instead.\n\nIf you are on the portable package you can run: update\\update_comfyui.bat to solve this problem" return f"""
Please install the updated requirements.txt file by running:
{sys.executable} {extra}-m pip install -r {req_path}
This error is happening because the ComfyUI frontend is no longer shipped as part of the main repo but as a pip package instead.
def parse_version(version: str) -> tuple[int, int, int]: If you are on the portable package you can run: update\\update_comfyui.bat to solve this problem
return tuple(map(int, version.split("."))) """.strip()
def check_frontend_version(): def check_frontend_version():
"""Check if the frontend version is up to date.""" """Check if the frontend version is up to date."""
def parse_version(version: str) -> tuple[int, int, int]:
return tuple(map(int, version.split(".")))
try: try:
frontend_version_str = version("comfyui-frontend-package") frontend_version_str = version("comfyui-frontend-package")
frontend_version = parse_version(frontend_version_str) frontend_version = parse_version(frontend_version_str)
with open(req_path, "r", encoding="utf-8") as f: with open(req_path, "r", encoding="utf-8") as f:
required_frontend = parse_version(f.readline().split("=")[-1]) required_frontend = parse_version(f.readline().split("=")[-1])
if frontend_version < required_frontend: if frontend_version < required_frontend:
app.logger.log_startup_warning("________________________________________________________________________\nWARNING WARNING WARNING WARNING WARNING\n\nInstalled frontend version {} is lower than the recommended version {}.\n\n{}\n________________________________________________________________________".format('.'.join(map(str, frontend_version)), '.'.join(map(str, required_frontend)), frontend_install_warning_message())) app.logger.log_startup_warning(
f"""
________________________________________________________________________
WARNING WARNING WARNING WARNING WARNING
Installed frontend version {".".join(map(str, frontend_version))} is lower than the recommended version {".".join(map(str, required_frontend))}.
{frontend_install_warning_message()}
________________________________________________________________________
""".strip()
)
else: else:
logging.info("ComfyUI frontend version: {}".format(frontend_version_str)) logging.info("ComfyUI frontend version: {}".format(frontend_version_str))
except Exception as e: except Exception as e:
@@ -73,11 +91,6 @@ class FrontEndProvider:
owner: str owner: str
repo: str repo: str
@property
def is_official(self) -> bool:
"""Check if the provider is the default official one."""
return self.owner == "Comfy-Org" and self.repo == "ComfyUI_frontend"
@property @property
def folder_name(self) -> str: def folder_name(self) -> str:
return f"{self.owner}_{self.repo}" return f"{self.owner}_{self.repo}"
@@ -148,26 +161,27 @@ def download_release_asset_zip(release: Release, destination_path: str) -> None:
zip_ref.extractall(destination_path) zip_ref.extractall(destination_path)
class FrontendInit(TypedDict):
web_root: str
""" The path to the initialized frontend. """
version: tuple[int, int, int] | None
""" The version of the initialized frontend. None for unrecognized version."""
class FrontendManager: class FrontendManager:
CUSTOM_FRONTENDS_ROOT = str(Path(__file__).parents[1] / "web_custom_versions") CUSTOM_FRONTENDS_ROOT = str(Path(__file__).parents[1] / "web_custom_versions")
@classmethod @classmethod
def init_default_frontend(cls) -> FrontendInit: def default_frontend_path(cls) -> str:
check_frontend_version()
try: try:
import comfyui_frontend_package import comfyui_frontend_package
return FrontendInit(
web_root=str(importlib.resources.files(comfyui_frontend_package) / "static"), return str(importlib.resources.files(comfyui_frontend_package) / "static")
version=parse_version(version("comfyui-frontend-package")),
)
except ImportError: except ImportError:
logging.error(f"\n\n********** ERROR ***********\n\ncomfyui-frontend-package is not installed. {frontend_install_warning_message()}\n********** ERROR **********\n") logging.error(
f"""
********** ERROR ***********
comfyui-frontend-package is not installed.
{frontend_install_warning_message()}
********** ERROR ***********
""".strip()
)
sys.exit(-1) sys.exit(-1)
@classmethod @classmethod
@@ -190,7 +204,9 @@ class FrontendManager:
return match_result.group(1), match_result.group(2), match_result.group(3) return match_result.group(1), match_result.group(2), match_result.group(3)
@classmethod @classmethod
def init_frontend_unsafe(cls, version_string: str, provider: Optional[FrontEndProvider] = None) -> FrontendInit: def init_frontend_unsafe(
cls, version_string: str, provider: Optional[FrontEndProvider] = None
) -> str:
""" """
Initializes the frontend for the specified version. Initializes the frontend for the specified version.
@@ -206,17 +222,26 @@ class FrontendManager:
main error source might be request timeout or invalid URL. main error source might be request timeout or invalid URL.
""" """
if version_string == DEFAULT_VERSION_STRING: if version_string == DEFAULT_VERSION_STRING:
return cls.init_default_frontend() check_frontend_version()
return cls.default_frontend_path()
repo_owner, repo_name, version = cls.parse_version_string(version_string) repo_owner, repo_name, version = cls.parse_version_string(version_string)
if version.startswith("v"): if version.startswith("v"):
expected_path = str(Path(cls.CUSTOM_FRONTENDS_ROOT) / f"{repo_owner}_{repo_name}" / version.lstrip("v")) expected_path = str(
Path(cls.CUSTOM_FRONTENDS_ROOT)
/ f"{repo_owner}_{repo_name}"
/ version.lstrip("v")
)
if os.path.exists(expected_path): if os.path.exists(expected_path):
logging.info(f"Using existing copy of specific frontend version tag: {repo_owner}/{repo_name}@{version}") logging.info(
f"Using existing copy of specific frontend version tag: {repo_owner}/{repo_name}@{version}"
)
return expected_path return expected_path
logging.info(f"Initializing frontend: {repo_owner}/{repo_name}@{version}, requesting version details from GitHub...") logging.info(
f"Initializing frontend: {repo_owner}/{repo_name}@{version}, requesting version details from GitHub..."
)
provider = provider or FrontEndProvider(repo_owner, repo_name) provider = provider or FrontEndProvider(repo_owner, repo_name)
release = provider.get_release(version) release = provider.get_release(version)
@@ -241,13 +266,10 @@ class FrontendManager:
if not os.listdir(web_root): if not os.listdir(web_root):
os.rmdir(web_root) os.rmdir(web_root)
return FrontendInit( return web_root
web_root=web_root,
version=parse_version(semantic_version) if provider.is_official else None,
)
@classmethod @classmethod
def init_frontend(cls, version_string: str) -> FrontendInit: def init_frontend(cls, version_string: str) -> str:
""" """
Initializes the frontend with the specified version string. Initializes the frontend with the specified version string.
@@ -262,4 +284,5 @@ class FrontendManager:
except Exception as e: except Exception as e:
logging.error("Failed to initialize frontend: %s", e) logging.error("Failed to initialize frontend: %s", e)
logging.info("Falling back to the default frontend.") logging.info("Falling back to the default frontend.")
return cls.init_default_frontend() check_frontend_version()
return cls.default_frontend_path()

View File

@@ -220,13 +220,6 @@ class ComfyNodeABC(ABC):
"""Flags a node as experimental, informing users that it may change or not work as expected.""" """Flags a node as experimental, informing users that it may change or not work as expected."""
DEPRECATED: bool DEPRECATED: bool
"""Flags a node as deprecated, indicating to users that they should find alternatives to this node.""" """Flags a node as deprecated, indicating to users that they should find alternatives to this node."""
REQUIRED_FRONTEND_VERSION: str | None
"""The minimum version of the ComfyUI frontend required to load this node.
Usage::
REQUIRED_FRONTEND_VERSION = "1.9.7"
"""
@classmethod @classmethod
@abstractmethod @abstractmethod

View File

@@ -21,8 +21,8 @@ class Load3D():
"height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), "height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}),
}} }}
RETURN_TYPES = ("IMAGE", "MASK", "STRING") RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "IMAGE")
RETURN_NAMES = ("image", "mask", "mesh_path") RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "lineart")
FUNCTION = "process" FUNCTION = "process"
EXPERIMENTAL = True EXPERIMENTAL = True
@@ -32,12 +32,16 @@ class Load3D():
def process(self, model_file, image, **kwargs): def process(self, model_file, image, **kwargs):
image_path = folder_paths.get_annotated_filepath(image['image']) image_path = folder_paths.get_annotated_filepath(image['image'])
mask_path = folder_paths.get_annotated_filepath(image['mask']) mask_path = folder_paths.get_annotated_filepath(image['mask'])
normal_path = folder_paths.get_annotated_filepath(image['normal'])
lineart_path = folder_paths.get_annotated_filepath(image['lineart'])
load_image_node = nodes.LoadImage() load_image_node = nodes.LoadImage()
output_image, ignore_mask = load_image_node.load_image(image=image_path) output_image, ignore_mask = load_image_node.load_image(image=image_path)
ignore_image, output_mask = load_image_node.load_image(image=mask_path) ignore_image, output_mask = load_image_node.load_image(image=mask_path)
normal_image, ignore_mask2 = load_image_node.load_image(image=normal_path)
lineart_image, ignore_mask3 = load_image_node.load_image(image=lineart_path)
return output_image, output_mask, model_file, return output_image, output_mask, model_file, normal_image, lineart_image
class Load3DAnimation(): class Load3DAnimation():
@classmethod @classmethod
@@ -55,8 +59,8 @@ class Load3DAnimation():
"height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), "height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}),
}} }}
RETURN_TYPES = ("IMAGE", "MASK", "STRING") RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE")
RETURN_NAMES = ("image", "mask", "mesh_path") RETURN_NAMES = ("image", "mask", "mesh_path", "normal")
FUNCTION = "process" FUNCTION = "process"
EXPERIMENTAL = True EXPERIMENTAL = True
@@ -66,12 +70,14 @@ class Load3DAnimation():
def process(self, model_file, image, **kwargs): def process(self, model_file, image, **kwargs):
image_path = folder_paths.get_annotated_filepath(image['image']) image_path = folder_paths.get_annotated_filepath(image['image'])
mask_path = folder_paths.get_annotated_filepath(image['mask']) mask_path = folder_paths.get_annotated_filepath(image['mask'])
normal_path = folder_paths.get_annotated_filepath(image['normal'])
load_image_node = nodes.LoadImage() load_image_node = nodes.LoadImage()
output_image, ignore_mask = load_image_node.load_image(image=image_path) output_image, ignore_mask = load_image_node.load_image(image=image_path)
ignore_image, output_mask = load_image_node.load_image(image=mask_path) ignore_image, output_mask = load_image_node.load_image(image=mask_path)
normal_image, ignore_mask2 = load_image_node.load_image(image=normal_path)
return output_image, output_mask, model_file, return output_image, output_mask, model_file, normal_image
class Preview3D(): class Preview3D():
@classmethod @classmethod

View File

@@ -1,3 +1,3 @@
# This file is automatically generated by the build process when version is # This file is automatically generated by the build process when version is
# updated in pyproject.toml. # updated in pyproject.toml.
__version__ = "0.3.26" __version__ = "0.3.27"

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "ComfyUI" name = "ComfyUI"
version = "0.3.26" version = "0.3.27"
readme = "README.md" readme = "README.md"
license = { file = "LICENSE" } license = { file = "LICENSE" }
requires-python = ">=3.9" requires-python = ">=3.9"

View File

@@ -1,4 +1,3 @@
from __future__ import annotations
import os import os
import sys import sys
import asyncio import asyncio
@@ -25,12 +24,11 @@ import logging
import mimetypes import mimetypes
from comfy.cli_args import args from comfy.cli_args import args
from comfy.comfy_types.node_typing import ComfyNodeABC
import comfy.utils import comfy.utils
import comfy.model_management import comfy.model_management
import node_helpers import node_helpers
from comfyui_version import __version__ from comfyui_version import __version__
from app.frontend_management import FrontendInit, FrontendManager, parse_version from app.frontend_management import FrontendManager
from app.user_manager import UserManager from app.user_manager import UserManager
from app.model_manager import ModelFileManager from app.model_manager import ModelFileManager
from app.custom_node_manager import CustomNodeManager from app.custom_node_manager import CustomNodeManager
@@ -148,11 +146,6 @@ def create_origin_only_middleware():
return origin_only_middleware return origin_only_middleware
class PromptServer(): class PromptServer():
web_root: str
"""The path to the initialized frontend assets."""
frontend_version: tuple[int, int, int] | None = None
"""The version of the initialized frontend. None for unrecognized version."""
def __init__(self, loop): def __init__(self, loop):
PromptServer.instance = self PromptServer.instance = self
@@ -183,19 +176,12 @@ class PromptServer():
max_upload_size = round(args.max_upload_size * 1024 * 1024) max_upload_size = round(args.max_upload_size * 1024 * 1024)
self.app = web.Application(client_max_size=max_upload_size, middlewares=middlewares) self.app = web.Application(client_max_size=max_upload_size, middlewares=middlewares)
self.sockets = dict() self.sockets = dict()
self.web_root = (
if args.front_end_root: FrontendManager.init_frontend(args.front_end_version)
frontend_init = FrontendInit( if args.front_end_root is None
web_root=args.front_end_root, else args.front_end_root
version=None, )
)
else:
frontend_init = FrontendManager.init_frontend(args.front_end_version)
self.frontend_version = frontend_init["version"]
self.web_root = frontend_init["web_root"]
logging.info(f"[Prompt Server] web root: {self.web_root}") logging.info(f"[Prompt Server] web root: {self.web_root}")
routes = web.RouteTableDef() routes = web.RouteTableDef()
self.routes = routes self.routes = routes
self.last_node_id = None self.last_node_id = None
@@ -601,9 +587,6 @@ class PromptServer():
with folder_paths.cache_helper: with folder_paths.cache_helper:
out = {} out = {}
for x in nodes.NODE_CLASS_MAPPINGS: for x in nodes.NODE_CLASS_MAPPINGS:
if not self.node_is_supported(x):
continue
try: try:
out[x] = node_info(x) out[x] = node_info(x)
except Exception: except Exception:
@@ -615,11 +598,7 @@ class PromptServer():
async def get_object_info_node(request): async def get_object_info_node(request):
node_class = request.match_info.get("node_class", None) node_class = request.match_info.get("node_class", None)
out = {} out = {}
if ( if (node_class is not None) and (node_class in nodes.NODE_CLASS_MAPPINGS):
node_class is not None
and node_class in nodes.NODE_CLASS_MAPPINGS
and self.node_is_supported(node_class)
):
out[node_class] = node_info(node_class) out[node_class] = node_info(node_class)
return web.json_response(out) return web.json_response(out)
@@ -884,15 +863,3 @@ class PromptServer():
logging.warning(traceback.format_exc()) logging.warning(traceback.format_exc())
return json_data return json_data
def node_is_supported(self, node_class: ComfyNodeABC) -> bool:
"""Check if the node is supported by the frontend."""
# For unrecognized frontend version, we assume the node is supported.
if self.frontend_version is None:
return True
# Check if the node is supported by the frontend.
if node_class.REQUIRED_FRONTEND_VERSION is None:
return True
return parse_version(node_class.REQUIRED_FRONTEND_VERSION) <= self.frontend_version

View File

@@ -69,10 +69,8 @@ def test_get_release_invalid_version(mock_provider):
def test_init_frontend_default(): def test_init_frontend_default():
version_string = DEFAULT_VERSION_STRING version_string = DEFAULT_VERSION_STRING
frontend_init = FrontendManager.init_frontend(version_string) frontend_path = FrontendManager.init_frontend(version_string)
assert isinstance(frontend_init, dict) assert frontend_path == FrontendManager.default_frontend_path()
assert "web_root" in frontend_init
assert "version" in frontend_init
def test_init_frontend_invalid_version(): def test_init_frontend_invalid_version():
@@ -140,47 +138,37 @@ def test_parse_version_string_invalid():
def test_init_frontend_default_with_mocks(): def test_init_frontend_default_with_mocks():
# Arrange # Arrange
version_string = DEFAULT_VERSION_STRING version_string = DEFAULT_VERSION_STRING
mock_path = "/mocked/path"
mock_version = (1, 0, 0)
# Act # Act
with ( with (
patch("app.frontend_management.check_frontend_version") as mock_check, patch("app.frontend_management.check_frontend_version") as mock_check,
patch.object( patch.object(
FrontendManager, FrontendManager, "default_frontend_path", return_value="/mocked/path"
"init_default_frontend",
return_value={"web_root": mock_path, "version": mock_version},
), ),
): ):
frontend_init = FrontendManager.init_frontend(version_string) frontend_path = FrontendManager.init_frontend(version_string)
# Assert # Assert
assert frontend_init["web_root"] == mock_path assert frontend_path == "/mocked/path"
assert frontend_init["version"] == mock_version mock_check.assert_called_once()
mock_check.assert_not_called() # check_frontend_version is now called inside init_default_frontend
def test_init_frontend_fallback_on_error(): def test_init_frontend_fallback_on_error():
# Arrange # Arrange
version_string = "test-owner/test-repo@1.0.0" version_string = "test-owner/test-repo@1.0.0"
mock_path = "/default/path"
mock_version = (1, 0, 0)
# Act # Act
with ( with (
patch.object( patch.object(
FrontendManager, FrontendManager, "init_frontend_unsafe", side_effect=Exception("Test error")
"init_frontend_unsafe",
side_effect=Exception("Test error")
), ),
patch("app.frontend_management.check_frontend_version") as mock_check,
patch.object( patch.object(
FrontendManager, FrontendManager, "default_frontend_path", return_value="/default/path"
"init_default_frontend",
return_value={"web_root": mock_path, "version": mock_version},
), ),
): ):
frontend_init = FrontendManager.init_frontend(version_string) frontend_path = FrontendManager.init_frontend(version_string)
# Assert # Assert
assert frontend_init["web_root"] == mock_path assert frontend_path == "/default/path"
assert frontend_init["version"] == mock_version mock_check.assert_called_once()