mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-24 21:30:15 +08:00
Merge branch 'comfyanonymous:master' into master
This commit is contained in:
commit
a12b26c630
@ -95,6 +95,7 @@ import aiohttp
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
import socket
|
import socket
|
||||||
from aiohttp.client_exceptions import ClientError, ClientResponseError
|
from aiohttp.client_exceptions import ClientError, ClientResponseError
|
||||||
from typing import Dict, Type, Optional, Any, TypeVar, Generic, Callable, Tuple
|
from typing import Dict, Type, Optional, Any, TypeVar, Generic, Callable, Tuple
|
||||||
@ -499,7 +500,9 @@ class ApiClient:
|
|||||||
else:
|
else:
|
||||||
raise ValueError("File must be BytesIO or str path")
|
raise ValueError("File must be BytesIO or str path")
|
||||||
|
|
||||||
operation_id = f"upload_{upload_url.split('/')[-1]}_{uuid.uuid4().hex[:8]}"
|
parsed = urlparse(upload_url)
|
||||||
|
basename = os.path.basename(parsed.path) or parsed.netloc or "upload"
|
||||||
|
operation_id = f"upload_{basename}_{uuid.uuid4().hex[:8]}"
|
||||||
request_logger.log_request_response(
|
request_logger.log_request_response(
|
||||||
operation_id=operation_id,
|
operation_id=operation_id,
|
||||||
request_method="PUT",
|
request_method="PUT",
|
||||||
|
|||||||
@ -4,16 +4,18 @@ import os
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
import hashlib
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import folder_paths
|
import folder_paths
|
||||||
|
|
||||||
# Get the logger instance
|
# Get the logger instance
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_log_directory():
|
def get_log_directory():
|
||||||
"""
|
"""Ensures the API log directory exists within ComfyUI's temp directory and returns its path."""
|
||||||
Ensures the API log directory exists within ComfyUI's temp directory
|
|
||||||
and returns its path.
|
|
||||||
"""
|
|
||||||
base_temp_dir = folder_paths.get_temp_directory()
|
base_temp_dir = folder_paths.get_temp_directory()
|
||||||
log_dir = os.path.join(base_temp_dir, "api_logs")
|
log_dir = os.path.join(base_temp_dir, "api_logs")
|
||||||
try:
|
try:
|
||||||
@ -24,42 +26,77 @@ def get_log_directory():
|
|||||||
return base_temp_dir
|
return base_temp_dir
|
||||||
return log_dir
|
return log_dir
|
||||||
|
|
||||||
def _format_data_for_logging(data):
|
|
||||||
|
def _sanitize_filename_component(name: str) -> str:
|
||||||
|
if not name:
|
||||||
|
return "log"
|
||||||
|
sanitized = re.sub(r"[^A-Za-z0-9._-]+", "_", name) # Replace disallowed characters with underscore
|
||||||
|
sanitized = sanitized.strip(" ._") # Windows: trailing dots or spaces are not allowed
|
||||||
|
if not sanitized:
|
||||||
|
sanitized = "log"
|
||||||
|
return sanitized
|
||||||
|
|
||||||
|
|
||||||
|
def _short_hash(*parts: str, length: int = 10) -> str:
|
||||||
|
return hashlib.sha1(("|".join(parts)).encode("utf-8")).hexdigest()[:length]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_log_filepath(log_dir: str, operation_id: str, request_url: str) -> str:
|
||||||
|
"""Build log filepath. We keep it well under common path length limits aiming for <= 240 characters total."""
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||||
|
slug = _sanitize_filename_component(operation_id) # Best-effort human-readable slug from operation_id
|
||||||
|
h = _short_hash(operation_id or "", request_url or "") # Short hash ties log to the full operation and URL
|
||||||
|
|
||||||
|
# Compute how much room we have for the slug given the directory length
|
||||||
|
# Keep total path length reasonably below ~260 on Windows.
|
||||||
|
max_total_path = 240
|
||||||
|
prefix = f"{timestamp}_"
|
||||||
|
suffix = f"_{h}.log"
|
||||||
|
if not slug:
|
||||||
|
slug = "op"
|
||||||
|
max_filename_len = max(60, max_total_path - len(log_dir) - 1)
|
||||||
|
max_slug_len = max(8, max_filename_len - len(prefix) - len(suffix))
|
||||||
|
if len(slug) > max_slug_len:
|
||||||
|
slug = slug[:max_slug_len].rstrip(" ._-")
|
||||||
|
return os.path.join(log_dir, f"{prefix}{slug}{suffix}")
|
||||||
|
|
||||||
|
|
||||||
|
def _format_data_for_logging(data: Any) -> str:
|
||||||
"""Helper to format data (dict, str, bytes) for logging."""
|
"""Helper to format data (dict, str, bytes) for logging."""
|
||||||
if isinstance(data, bytes):
|
if isinstance(data, bytes):
|
||||||
try:
|
try:
|
||||||
return data.decode('utf-8') # Try to decode as text
|
return data.decode("utf-8") # Try to decode as text
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
return f"[Binary data of length {len(data)} bytes]"
|
return f"[Binary data of length {len(data)} bytes]"
|
||||||
elif isinstance(data, (dict, list)):
|
elif isinstance(data, (dict, list)):
|
||||||
try:
|
try:
|
||||||
return json.dumps(data, indent=2, ensure_ascii=False)
|
return json.dumps(data, indent=2, ensure_ascii=False)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return str(data) # Fallback for non-serializable objects
|
return str(data) # Fallback for non-serializable objects
|
||||||
return str(data)
|
return str(data)
|
||||||
|
|
||||||
|
|
||||||
def log_request_response(
|
def log_request_response(
|
||||||
operation_id: str,
|
operation_id: str,
|
||||||
request_method: str,
|
request_method: str,
|
||||||
request_url: str,
|
request_url: str,
|
||||||
request_headers: dict | None = None,
|
request_headers: dict | None = None,
|
||||||
request_params: dict | None = None,
|
request_params: dict | None = None,
|
||||||
request_data: any = None,
|
request_data: Any = None,
|
||||||
response_status_code: int | None = None,
|
response_status_code: int | None = None,
|
||||||
response_headers: dict | None = None,
|
response_headers: dict | None = None,
|
||||||
response_content: any = None,
|
response_content: Any = None,
|
||||||
error_message: str | None = None
|
error_message: str | None = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Logs API request and response details to a file in the temp/api_logs directory.
|
Logs API request and response details to a file in the temp/api_logs directory.
|
||||||
|
Filenames are sanitized and length-limited for cross-platform safety.
|
||||||
|
If we still fail to write, we fall back to appending into api.log.
|
||||||
"""
|
"""
|
||||||
log_dir = get_log_directory()
|
log_dir = get_log_directory()
|
||||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
filepath = _build_log_filepath(log_dir, operation_id, request_url)
|
||||||
filename = f"{timestamp}_{operation_id.replace('/', '_').replace(':', '_')}.log"
|
|
||||||
filepath = os.path.join(log_dir, filename)
|
|
||||||
|
|
||||||
log_content = []
|
|
||||||
|
|
||||||
|
log_content: list[str] = []
|
||||||
log_content.append(f"Timestamp: {datetime.datetime.now().isoformat()}")
|
log_content.append(f"Timestamp: {datetime.datetime.now().isoformat()}")
|
||||||
log_content.append(f"Operation ID: {operation_id}")
|
log_content.append(f"Operation ID: {operation_id}")
|
||||||
log_content.append("-" * 30 + " REQUEST " + "-" * 30)
|
log_content.append("-" * 30 + " REQUEST " + "-" * 30)
|
||||||
@ -69,7 +106,7 @@ def log_request_response(
|
|||||||
log_content.append(f"Headers:\n{_format_data_for_logging(request_headers)}")
|
log_content.append(f"Headers:\n{_format_data_for_logging(request_headers)}")
|
||||||
if request_params:
|
if request_params:
|
||||||
log_content.append(f"Params:\n{_format_data_for_logging(request_params)}")
|
log_content.append(f"Params:\n{_format_data_for_logging(request_params)}")
|
||||||
if request_data:
|
if request_data is not None:
|
||||||
log_content.append(f"Data/Body:\n{_format_data_for_logging(request_data)}")
|
log_content.append(f"Data/Body:\n{_format_data_for_logging(request_data)}")
|
||||||
|
|
||||||
log_content.append("\n" + "-" * 30 + " RESPONSE " + "-" * 30)
|
log_content.append("\n" + "-" * 30 + " RESPONSE " + "-" * 30)
|
||||||
@ -77,7 +114,7 @@ def log_request_response(
|
|||||||
log_content.append(f"Status Code: {response_status_code}")
|
log_content.append(f"Status Code: {response_status_code}")
|
||||||
if response_headers:
|
if response_headers:
|
||||||
log_content.append(f"Headers:\n{_format_data_for_logging(response_headers)}")
|
log_content.append(f"Headers:\n{_format_data_for_logging(response_headers)}")
|
||||||
if response_content:
|
if response_content is not None:
|
||||||
log_content.append(f"Content:\n{_format_data_for_logging(response_content)}")
|
log_content.append(f"Content:\n{_format_data_for_logging(response_content)}")
|
||||||
if error_message:
|
if error_message:
|
||||||
log_content.append(f"Error:\n{error_message}")
|
log_content.append(f"Error:\n{error_message}")
|
||||||
@ -89,6 +126,7 @@ def log_request_response(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error writing API log to {filepath}: {e}")
|
logger.error(f"Error writing API log to {filepath}: {e}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Example usage (for testing the logger directly)
|
# Example usage (for testing the logger directly)
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|||||||
@ -1,24 +1,34 @@
|
|||||||
import torch
|
import torch
|
||||||
import comfy.model_management
|
import comfy.model_management
|
||||||
|
from typing_extensions import override
|
||||||
|
from comfy_api.latest import ComfyExtension, io
|
||||||
|
|
||||||
from kornia.morphology import dilation, erosion, opening, closing, gradient, top_hat, bottom_hat
|
from kornia.morphology import dilation, erosion, opening, closing, gradient, top_hat, bottom_hat
|
||||||
import kornia.color
|
import kornia.color
|
||||||
|
|
||||||
|
|
||||||
class Morphology:
|
class Morphology(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {"image": ("IMAGE",),
|
return io.Schema(
|
||||||
"operation": (["erode", "dilate", "open", "close", "gradient", "bottom_hat", "top_hat"],),
|
node_id="Morphology",
|
||||||
"kernel_size": ("INT", {"default": 3, "min": 3, "max": 999, "step": 1}),
|
display_name="ImageMorphology",
|
||||||
}}
|
category="image/postprocessing",
|
||||||
|
inputs=[
|
||||||
|
io.Image.Input("image"),
|
||||||
|
io.Combo.Input(
|
||||||
|
"operation",
|
||||||
|
options=["erode", "dilate", "open", "close", "gradient", "bottom_hat", "top_hat"],
|
||||||
|
),
|
||||||
|
io.Int.Input("kernel_size", default=3, min=3, max=999, step=1),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Image.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
@classmethod
|
||||||
FUNCTION = "process"
|
def execute(cls, image, operation, kernel_size) -> io.NodeOutput:
|
||||||
|
|
||||||
CATEGORY = "image/postprocessing"
|
|
||||||
|
|
||||||
def process(self, image, operation, kernel_size):
|
|
||||||
device = comfy.model_management.get_torch_device()
|
device = comfy.model_management.get_torch_device()
|
||||||
kernel = torch.ones(kernel_size, kernel_size, device=device)
|
kernel = torch.ones(kernel_size, kernel_size, device=device)
|
||||||
image_k = image.to(device).movedim(-1, 1)
|
image_k = image.to(device).movedim(-1, 1)
|
||||||
@ -39,49 +49,63 @@ class Morphology:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Invalid operation {operation} for morphology. Must be one of 'erode', 'dilate', 'open', 'close', 'gradient', 'tophat', 'bottomhat'")
|
raise ValueError(f"Invalid operation {operation} for morphology. Must be one of 'erode', 'dilate', 'open', 'close', 'gradient', 'tophat', 'bottomhat'")
|
||||||
img_out = output.to(comfy.model_management.intermediate_device()).movedim(1, -1)
|
img_out = output.to(comfy.model_management.intermediate_device()).movedim(1, -1)
|
||||||
return (img_out,)
|
return io.NodeOutput(img_out)
|
||||||
|
|
||||||
|
|
||||||
class ImageRGBToYUV:
|
class ImageRGBToYUV(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": { "image": ("IMAGE",),
|
return io.Schema(
|
||||||
}}
|
node_id="ImageRGBToYUV",
|
||||||
|
category="image/batch",
|
||||||
|
inputs=[
|
||||||
|
io.Image.Input("image"),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Image.Output(display_name="Y"),
|
||||||
|
io.Image.Output(display_name="U"),
|
||||||
|
io.Image.Output(display_name="V"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE")
|
@classmethod
|
||||||
RETURN_NAMES = ("Y", "U", "V")
|
def execute(cls, image) -> io.NodeOutput:
|
||||||
FUNCTION = "execute"
|
|
||||||
|
|
||||||
CATEGORY = "image/batch"
|
|
||||||
|
|
||||||
def execute(self, image):
|
|
||||||
out = kornia.color.rgb_to_ycbcr(image.movedim(-1, 1)).movedim(1, -1)
|
out = kornia.color.rgb_to_ycbcr(image.movedim(-1, 1)).movedim(1, -1)
|
||||||
return (out[..., 0:1].expand_as(image), out[..., 1:2].expand_as(image), out[..., 2:3].expand_as(image))
|
return io.NodeOutput(out[..., 0:1].expand_as(image), out[..., 1:2].expand_as(image), out[..., 2:3].expand_as(image))
|
||||||
|
|
||||||
class ImageYUVToRGB:
|
class ImageYUVToRGB(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def define_schema(cls):
|
||||||
return {"required": {"Y": ("IMAGE",),
|
return io.Schema(
|
||||||
"U": ("IMAGE",),
|
node_id="ImageYUVToRGB",
|
||||||
"V": ("IMAGE",),
|
category="image/batch",
|
||||||
}}
|
inputs=[
|
||||||
|
io.Image.Input("Y"),
|
||||||
|
io.Image.Input("U"),
|
||||||
|
io.Image.Input("V"),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Image.Output(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
RETURN_TYPES = ("IMAGE",)
|
@classmethod
|
||||||
FUNCTION = "execute"
|
def execute(cls, Y, U, V) -> io.NodeOutput:
|
||||||
|
|
||||||
CATEGORY = "image/batch"
|
|
||||||
|
|
||||||
def execute(self, Y, U, V):
|
|
||||||
image = torch.cat([torch.mean(Y, dim=-1, keepdim=True), torch.mean(U, dim=-1, keepdim=True), torch.mean(V, dim=-1, keepdim=True)], dim=-1)
|
image = torch.cat([torch.mean(Y, dim=-1, keepdim=True), torch.mean(U, dim=-1, keepdim=True), torch.mean(V, dim=-1, keepdim=True)], dim=-1)
|
||||||
out = kornia.color.ycbcr_to_rgb(image.movedim(-1, 1)).movedim(1, -1)
|
out = kornia.color.ycbcr_to_rgb(image.movedim(-1, 1)).movedim(1, -1)
|
||||||
return (out,)
|
return io.NodeOutput(out)
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"Morphology": Morphology,
|
|
||||||
"ImageRGBToYUV": ImageRGBToYUV,
|
|
||||||
"ImageYUVToRGB": ImageYUVToRGB,
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
class MorphologyExtension(ComfyExtension):
|
||||||
"Morphology": "ImageMorphology",
|
@override
|
||||||
}
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
Morphology,
|
||||||
|
ImageRGBToYUV,
|
||||||
|
ImageYUVToRGB,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> MorphologyExtension:
|
||||||
|
return MorphologyExtension()
|
||||||
|
|
||||||
|
|||||||
1
main.py
1
main.py
@ -115,6 +115,7 @@ if os.name == "nt":
|
|||||||
os.environ['MIMALLOC_PURGE_DELAY'] = '0'
|
os.environ['MIMALLOC_PURGE_DELAY'] = '0'
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
os.environ['TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL'] = '1'
|
||||||
if args.default_device is not None:
|
if args.default_device is not None:
|
||||||
default_dev = args.default_device
|
default_dev = args.default_device
|
||||||
devices = list(range(32))
|
devices = list(range(32))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user