Author SHA1 Message Date
Acly 00ccb7dcf0 Add LoadImageCached and SaveImageCached (renamed from SendImageHTTP)
* short-lived in-memory cache for image transfers
* upload images via HTTP to cache and load/reference them in workflows
* save/store images in workflows to cache and download them via HTTP
2025-10-20 13:43:25 +02:00
9 changed files with 65 additions and 274 deletions
+8 -28
View File
@@ -68,15 +68,15 @@ This is typically faster than WebSocket, especially for large images.
This node will send a JSON message over WebSocket when an image is ready: This node will send a JSON message over WebSocket when an image is ready:
```json ```json
{ {
"type": "executed", 'type': 'executed',
"data": { 'data': {
"node": "<node ID>", 'node': '<node ID>',
"output": { 'output': {
"images": [ 'images': [
{"source": "http", "id": "<image ID>", "content-type": "image/png", "type": "output"} {'source': 'http', 'id': '<image ID>', 'content-type': 'image/png', 'type': 'output'}
] ]
}, },
"prompt_id": "prompt ID" 'prompt_id': 'prompt ID'
} }
} }
``` ```
@@ -205,8 +205,6 @@ There are various types of models that can be loaded as checkpoint, LoRA, Contro
#### Paramters #### Paramters
* `folder_name`: sub-directory in ComfyUI's models folder. * `folder_name`: sub-directory in ComfyUI's models folder.
Supported model types: `checkpoints`, `diffusion_models`, `unet`, `unet_gguf` Supported model types: `checkpoints`, `diffusion_models`, `unet`, `unet_gguf`
* `limit=n`: (query parameter, optional) inspect at `n` models
* `offset=i`: (query parameter, optional) start with the `i`th model
#### Output #### Output
Lists available models with additional classification info: Lists available models with additional classification info:
@@ -220,7 +218,7 @@ Lists available models with additional classification info:
... ...
} }
``` ```
Possible values for base model: `sd15, sd20, sd21, sd3, sdxl, sdxl-refiner, ssd1b, svd, cascade-b, cascade-c, aura-flow, hunyuan-dit, flux, flux-schnell, flux2, lumina2, z-image, chroma, qwen-image` Possible values for base model: `sd15, sd20, sd21, sd3, sdxl, sdxl-refiner, ssd1b, svd, cascade-b, cascade-c, aura-flow, hunyuan-dit, flux, flux-schnell, lumina2, chroma, qwen-image`
If base model is `sdxl`, the `type` attribute is set with possible values: `eps, edm, v-prediction, v-prediction-edm` If base model is `sdxl`, the `type` attribute is set with possible values: `eps, edm, v-prediction, v-prediction-edm`
@@ -230,24 +228,6 @@ Detection supports quantized models:
Returns an entry `{"base_model": "unknown"}` for models with unknown format or which do not match any of the known base models. Returns an entry `{"base_model": "unknown"}` for models with unknown format or which do not match any of the known base models.
#### Pagination
The query parameters limit and offset allow inspecting a subset of models per request.
Usually inspection is quite fast (it only looks at model headers), but it can be slow
in some cases due to anti-virus or slow harddrives.
```
GET /api/etn/model_info/checkpoints?limit=10&offset=20
```
This will return at most 10 models, starting with the 20th model in the list.
It also returns a special `_meta` entry in the output JSON:
```json
{
"checkpoint_20.safetensors": { ... },
"_meta": { "offset": 20, "count": 1, "total": 21 }
}
```
### GET /api/etn/languages ### GET /api/etn/languages
Returns a list of available languages for translation. Returns a list of available languages for translation.
-1
View File
@@ -32,7 +32,6 @@ class ExternalToolingNodes(ComfyExtension):
krita.KritaMaskLayer, krita.KritaMaskLayer,
krita.Parameter, krita.Parameter,
krita.KritaStyle, krita.KritaStyle,
krita.KritaStyleAndPrompt,
] ]
+15 -73
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from aiohttp import web from aiohttp import web
from typing import Any, NamedTuple from typing import NamedTuple
from pathlib import Path from pathlib import Path
import json import json
import traceback import traceback
@@ -44,8 +44,6 @@ model_names = {
"CosmosI2V": "cosmos", "CosmosI2V": "cosmos",
"CosmosT2IPredict2": "cosmos-predict2", "CosmosT2IPredict2": "cosmos-predict2",
"CosmosI2VPredict2": "cosmos-predict2", "CosmosI2VPredict2": "cosmos-predict2",
"ZImage": "z-image",
"Lumina2": "lumina2",
"WAN21_T2V": "wan21", "WAN21_T2V": "wan21",
"WAN21_I2V": "wan21", "WAN21_I2V": "wan21",
"WAN21_FunControl2V": "wan21-fun", "WAN21_FunControl2V": "wan21-fun",
@@ -56,9 +54,6 @@ model_names = {
"ACEStep": "ace-step", "ACEStep": "ace-step",
"Omnigen2": "omnigen2", "Omnigen2": "omnigen2",
"QwenImage": "qwen-image", "QwenImage": "qwen-image",
"ErnieImage": "ernie-image",
"Flux2": "flux2",
"Anima": "anima",
} }
gguf_architectures = { gguf_architectures = {
@@ -123,15 +118,12 @@ def inspect_safetensors(filename: str, model_type: str, is_checkpoint: bool):
raw_name = base_model.__class__.__name__ raw_name = base_model.__class__.__name__
if raw_name == "SDXL": if raw_name == "SDXL":
model_type = base_model.model_type(cfg).name.lower().replace("_", "-") model_type = base_model.model_type(cfg).name.lower().replace("_", "-")
if raw_name == "Flux2":
hidden_size = unet_config.get("hidden_size", 0)
model_type = {3072: "klein-4b", 4096: "klein-9b"}.get(hidden_size, "dev")
if not raw_name: if not raw_name:
return {"base_model": "unknown"} return {"base_model": "unknown"}
base_model_name = model_names.get(raw_name, "unknown") base_model_name = model_names.get(raw_name, "unknown")
result: dict[str, Any] = {"base_model": base_model_name} result = {"base_model": base_model_name}
result["is_inpaint"] = ( result["is_inpaint"] = (
base_model_name in ["sd15", "sdxl"] and input_count > 4 base_model_name in ["sd15", "sdxl"] and input_count > 4
) or raw_name == "FluxInpaint" ) or raw_name == "FluxInpaint"
@@ -150,7 +142,6 @@ def inspect_safetensors(filename: str, model_type: str, is_checkpoint: bool):
return result return result
return {"base_model": "unknown"} return {"base_model": "unknown"}
except Exception as e: except Exception as e:
print("[comfyui-tooling-nodes] Error inspecting file", filename)
traceback.print_exc() traceback.print_exc()
return {"base_model": "unknown", "error": f"Failed to detect base model: {e}"} return {"base_model": "unknown", "error": f"Failed to detect base model: {e}"}
@@ -160,16 +151,12 @@ def detect_svdq(cfg: dict) -> str | None:
if comfy_config := md.get("comfy_config"): if comfy_config := md.get("comfy_config"):
if isinstance(comfy_config, str): if isinstance(comfy_config, str):
comfy_config = json.loads(comfy_config) comfy_config = json.loads(comfy_config)
if model_class := comfy_config.get("model_class"): return comfy_config.get("model_class")
return model_class model_class = md.get("model_class")
if model_class == "NunchakuFluxTransformer2dModel":
match md.get("model_class"): return "Flux"
case "NunchakuFluxTransformer2dModel": if model_class == "NunchakuQwenImageTransformer2DModel":
return "Flux" return "QwenImage"
case "NunchakuQwenImageTransformer2DModel":
return "QwenImage"
case "NunchakuZImageTransformer2DModel":
return "ZImage"
return None return None
@@ -181,9 +168,6 @@ def inspect_gguf(filename: str, model_type: str):
try: try:
path = folder_paths.get_full_path(model_type, filename) path = folder_paths.get_full_path(model_type, filename)
if path is None:
raise Exception(f"Could not find full path for {model_type}/{filename}")
reader = gguf.GGUFReader(path) reader = gguf.GGUFReader(path)
arch_field = reader.get_field("general.architecture") arch_field = reader.get_field("general.architecture")
if arch_field is not None: if arch_field is not None:
@@ -194,45 +178,19 @@ def inspect_gguf(filename: str, model_type: str):
arch_str = str(arch_field.parts[arch_field.data[-1]], encoding="utf-8") arch_str = str(arch_field.parts[arch_field.data[-1]], encoding="utf-8")
else: # stable-diffusion.cpp, requires conversion. not handled for now else: # stable-diffusion.cpp, requires conversion. not handled for now
return {"base_model": "flux", "is_inpaint": False} return {"base_model": "flux", "is_inpaint": False}
if arch_str == "flux" and any( if arch_str == "flux" and any(
t.name.startswith("distilled_guidance_layer") t.name.startswith("distilled_guidance_layer")
for t in itertools.islice(reader.tensors, 5) for t in itertools.islice(reader.tensors, 5)
): ):
arch_str = "chroma" arch_str = "chroma"
# Detect Z-Image (modified Lumina2)
if arch_str == "lumina2":
for t in reader.tensors:
if t.name == "cap_embedder.1.bias" and t.shape[0] == 3840:
arch_str = "z-image"
break
# Detect Flux variants
result_type = None
if arch_str == "flux":
for t in reader.tensors:
if t.name.startswith("distilled_guidance_layer"):
arch_str = "chroma"
break
elif t.name == "double_stream_modulation_img.lin.weight":
arch_str = "flux2"
if t.shape[0] == 3072:
result_type = "klein-4b"
elif t.shape[0] == 4096:
result_type = "klein-9b"
break
result = { result = {
"base_model": gguf_architectures.get(arch_str, arch_str), "base_model": gguf_architectures.get(arch_str, arch_str),
"is_inpaint": False, "is_inpaint": False,
} }
if result_type is not None:
result["type"] = result_type
try: try:
if file_type := reader.get_field("general.file_type"): result["quant"] = reader.get_field("general.file_type").lower()
result["quant"] = file_type.contents().lower() except Exception as e:
except Exception:
result["quant"] = "gguf" result["quant"] = "gguf"
return result return result
@@ -247,22 +205,17 @@ def inspect_diffusion_model(filename: str, model_type: str, is_checkpoint: bool)
return inspect_safetensors(filename, model_type, is_checkpoint) return inspect_safetensors(filename, model_type, is_checkpoint)
def inspect_models(model_type: str, params: dict[str, str]): def inspect_models(model_type: str):
try: try:
try: try:
files = folder_paths.get_filename_list(model_type) files = folder_paths.get_filename_list(model_type)
except KeyError: except KeyError:
return web.json_response({"error": f"Model folder not found: {model_type}"}) return web.json_response({"error": f"Model folder not found: {model_type}"})
limit = int(params.get("limit", "1000"))
offset = int(params.get("offset", "0"))
files_range = files[offset : offset + limit]
is_checkpoint = model_type == "checkpoints" is_checkpoint = model_type == "checkpoints"
info = { info = {
filename: inspect_diffusion_model(filename, model_type, is_checkpoint) filename: inspect_diffusion_model(filename, model_type, is_checkpoint)
for filename in files_range for filename in files
} }
if "limit" in params:
info["_meta"] = dict(offset=offset, count=len(files_range), total=len(files))
return web.json_response(info) return web.json_response(info)
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
@@ -308,11 +261,11 @@ if _server is not None:
error = has_invalid_folder_name(folder_name) error = has_invalid_folder_name(folder_name)
if error is not None: if error is not None:
return error return error
return inspect_models(folder_name, request.rel_url.query) return inspect_models(folder_name)
@_server.routes.get("/api/etn/model_info") @_server.routes.get("/api/etn/model_info")
async def api_model_info(request): async def api_model_info(request):
return inspect_models("checkpoints", request.rel_url.query) return inspect_models("checkpoints")
@_server.routes.get("/api/etn/languages") @_server.routes.get("/api/etn/languages")
async def languages(request): async def languages(request):
@@ -348,11 +301,11 @@ if _server is not None:
except Exception as e: except Exception as e:
return web.json_response(dict(error=str(e)), status=500) return web.json_response(dict(error=str(e)), status=500)
@_server.routes.put("/api/etn/image/{id}")
async def put_image(request: web.Request): async def put_image(request: web.Request):
try: try:
id = request.match_info.get("id", "") id = request.match_info.get("id", "")
if id in image_cache: if id in image_cache:
await request.release() # Consume and discard the data to avoid connection abort
return web.json_response(dict(status="cached"), status=200) return web.json_response(dict(status="cached"), status=200)
content_type = request.headers.get("Content-Type", "application/octet-stream") content_type = request.headers.get("Content-Type", "application/octet-stream")
@@ -365,17 +318,6 @@ if _server is not None:
except Exception as e: except Exception as e:
return web.json_response(dict(error=str(e)), status=500) return web.json_response(dict(error=str(e)), status=500)
async def _put_image_expect_handler(request: web.Request):
if request.match_info.get("id", "") in image_cache:
# Skip "100 Continue" since we don't need the data, return 200 immediately.
return web.json_response(dict(status="cached"), status=200)
# otherwise run default aiohttp handler
return None
_server.app.router.add_route(
"PUT", "/api/etn/image/{id}", put_image, expect_handler=_put_image_expect_handler
)
@_server.routes.put("/api/etn/upload/{folder_name}/{filename}") @_server.routes.put("/api/etn/upload/{folder_name}/{filename}")
async def upload(request: web.Request): async def upload(request: web.Request):
folder_name = request.match_info.get("folder_name", "") folder_name = request.match_info.get("folder_name", "")
+5 -3
View File
@@ -32,6 +32,7 @@ function loadImage(base64) {
} }
const canvasIcon = loadImage("data:image/webp;base64,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") const canvasIcon = loadImage("data:image/webp;base64,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")
const outputIcon = loadImage("data:image/webp;base64,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")
function setIconImage(nodeType, image, size, padRows, padCols) { function setIconImage(nodeType, image, size, padRows, padCols) {
const onAdded = nodeType.prototype.onAdded const onAdded = nodeType.prototype.onAdded
@@ -76,8 +77,7 @@ function defaultParameterType(widgetType, connectedNode, connectedWidget) {
if (connectedNode.comfyClass === "CLIPTextEncode") { if (connectedNode.comfyClass === "CLIPTextEncode") {
paramType = "prompt (positive)" paramType = "prompt (positive)"
} }
const round = connectedWidget.options?.round if (connectedWidget.options?.round === 1) {
if ((paramType == "number" && round === undefined) || round === 1) {
paramType = "number (integer)" paramType = "number (integer)"
} }
return paramType return paramType
@@ -96,7 +96,7 @@ function valueMatchesType(value, type, options) {
function optionalWidgetValue(widgets, index, fallback) { function optionalWidgetValue(widgets, index, fallback) {
const result = widgets.length > index ? widgets[index].value : null const result = widgets.length > index ? widgets[index].value : null
return result === null || result === -1e10 || result === 1e10 ? fallback : result return result === null || result === 0 ? fallback : result
} }
function changeWidgets(node, type, connectedNode, connectedWidget) { function changeWidgets(node, type, connectedNode, connectedWidget) {
@@ -206,6 +206,8 @@ app.registerExtension({
beforeRegisterNodeDef(nodeType /*typeof LGraphNode*/, nodeData /*ComfyObjectInfo*/, app) { beforeRegisterNodeDef(nodeType /*typeof LGraphNode*/, nodeData /*ComfyObjectInfo*/, app) {
if (nodeData.name === "ETN_KritaCanvas") { if (nodeData.name === "ETN_KritaCanvas") {
setIconImage(nodeType, canvasIcon, [200, 100], 0, 2) setIconImage(nodeType, canvasIcon, [200, 100], 0, 2)
} else if (nodeData.name === "ETN_KritaOutput") {
setIconImage(nodeType, outputIcon, [200, 100], 1, 0)
} else if (nodeData.name === "ETN_Parameter") { } else if (nodeData.name === "ETN_Parameter") {
setupParameterNode(nodeType) setupParameterNode(nodeType)
} else if (nodeData.name === "ETN_SendText") { } else if (nodeData.name === "ETN_SendText") {
+21 -120
View File
@@ -1,16 +1,14 @@
import sys import sys
from enum import Enum import torch
import numpy as np
from pathlib import Path from pathlib import Path
from typing import Any, NamedTuple from typing import Any, NamedTuple
import comfy.samplers
import numpy as np
import server
import torch
from comfy.comfy_types.node_typing import IO
from comfy_api.latest import io
from PIL import Image from PIL import Image
import server
import comfy.samplers
from comfy.comfy_types.node_typing import IO
from comfy_api.latest import io
from .nodes import SendImageWebSocket from .nodes import SendImageWebSocket
@@ -75,13 +73,6 @@ class _BasicTypes(str):
BasicTypes = _BasicTypes("BASIC") BasicTypes = _BasicTypes("BASIC")
class OutputBatchMode(Enum):
default = "default"
images = "images"
animation = "animation"
layers = "layers"
class KritaOutput(io.ComfyNode): class KritaOutput(io.ComfyNode):
@classmethod @classmethod
def define_schema(cls): def define_schema(cls):
@@ -89,41 +80,13 @@ class KritaOutput(io.ComfyNode):
node_id="ETN_KritaOutput", node_id="ETN_KritaOutput",
display_name="Krita Output", display_name="Krita Output",
category="krita", category="krita",
inputs=[ inputs=[io.Image.Input("images")],
io.Image.Input("images"),
io.Int.Input("x", "offset x", default=0),
io.Int.Input("y", "offset y", default=0),
io.String.Input("name", default=""),
io.Combo.Input(
"batch_mode", OutputBatchMode, "batch mode", default=OutputBatchMode.default
),
io.Boolean.Input("resize_canvas", "resize canvas", default=False),
],
is_output_node=True, is_output_node=True,
) )
@classmethod @classmethod
def execute( # type: ignore def execute(cls, images: torch.Tensor):
cls, return SendImageWebSocket.execute(images, "PNG")
images: torch.Tensor,
x: int = 0,
y: int = 0,
name="",
batch_mode: OutputBatchMode | str = OutputBatchMode.default,
resize_canvas=False,
):
batch_mode = batch_mode.value if isinstance(batch_mode, OutputBatchMode) else batch_mode
info = {
"name": name,
"offset_x": x,
"offset_y": y,
"batch_mode": batch_mode,
"resize_canvas": resize_canvas,
}
output = SendImageWebSocket.execute(images, "PNG")
assert isinstance(output.ui, dict)
output.ui["info"] = [info]
return output
class KritaSendText(io.ComfyNode): class KritaSendText(io.ComfyNode):
@@ -142,7 +105,7 @@ class KritaSendText(io.ComfyNode):
) )
@classmethod @classmethod
def execute(cls, value: Any, name: str, type: str): # type: ignore def execute(cls, value: Any, name: str, type: str):
mime = { mime = {
"text": "text/plain", "text": "text/plain",
"markdown": "text/markdown", "markdown": "text/markdown",
@@ -170,28 +133,12 @@ class KritaCanvas(io.ComfyNode):
io.Int.Output(display_name="width"), io.Int.Output(display_name="width"),
io.Int.Output(display_name="height"), io.Int.Output(display_name="height"),
io.Int.Output(display_name="seed"), io.Int.Output(display_name="seed"),
io.Mask.Output(display_name="mask"),
], ],
) )
@classmethod @classmethod
def execute(cls, **kwargs): def execute(cls):
return io.NodeOutput(_placeholder_image(), 512, 512, 0, torch.ones(1, 512, 512)) return io.NodeOutput(_placeholder_image(), 512, 512, 0)
class SelectionContext(Enum):
automatic = "automatic"
entire_image = "entire image"
mask_bounds = "mask bounds"
_selection_context_help = """
Determines the section (crop bounding box) of the image and mask to transmit:
- automatic: area around the selection determined by Krita settings
- entire image: always use the entire canvas area
- mask bounds: tight bounding box of the current selection
This affects the Selection and Canvas nodes. The offset x/y outputs indicate the top-left corner of the context area relative to the full canvas."""
class KritaSelection(io.ComfyNode): class KritaSelection(io.ComfyNode):
@@ -201,26 +148,12 @@ class KritaSelection(io.ComfyNode):
node_id="ETN_KritaSelection", node_id="ETN_KritaSelection",
display_name="Krita Selection", display_name="Krita Selection",
category="krita", category="krita",
inputs=[ outputs=[io.Mask.Output(display_name="mask"), io.Boolean.Output(display_name="active")],
io.Combo.Input(
"context",
options=SelectionContext,
default=SelectionContext.entire_image,
tooltip=_selection_context_help,
),
io.Int.Input("padding", "padding", default=0, min=0),
],
outputs=[
io.Mask.Output("mask", "mask"),
io.Boolean.Output("active", "active"),
io.Int.Output("x", "offset x"),
io.Int.Output("y", "offset y"),
],
) )
@classmethod @classmethod
def execute(cls, **kwargs): def execute(cls):
return io.NodeOutput(torch.ones(1, 512, 512), False, 0, 0) return io.NodeOutput(torch.ones(1, 512, 512), False)
class KritaImageLayer(io.ComfyNode): class KritaImageLayer(io.ComfyNode):
@@ -238,7 +171,7 @@ class KritaImageLayer(io.ComfyNode):
) )
@classmethod @classmethod
def execute(cls, name: str): # type: ignore def execute(cls, name: str):
return io.NodeOutput(_placeholder_image(), torch.ones(1, 512, 512)) return io.NodeOutput(_placeholder_image(), torch.ones(1, 512, 512))
@@ -256,7 +189,7 @@ class KritaMaskLayer(io.ComfyNode):
) )
@classmethod @classmethod
def execute(cls, name: str): # type: ignore def execute(cls, name: str):
return io.NodeOutput(torch.ones(1, 512, 512)) return io.NodeOutput(torch.ones(1, 512, 512))
@@ -284,14 +217,14 @@ class Parameter(io.ComfyNode):
io.String.Input("name", default="Parameter"), io.String.Input("name", default="Parameter"),
io.Combo.Input("type", options=_param_types, default="auto"), io.Combo.Input("type", options=_param_types, default="auto"),
io.String.Input("default", default=""), io.String.Input("default", default=""),
io.Float.Input("min", default=-1e10, min=-_fmax, max=_fmax, optional=True), io.Float.Input("min", default=0.0, min=-_fmax, max=_fmax, optional=True),
io.Float.Input("max", default=1e10, min=-_fmax, max=_fmax, optional=True), io.Float.Input("max", default=1.0, min=-_fmax, max=_fmax, optional=True),
], ],
outputs=[io.AnyType.Output(display_name="value")], outputs=[io.AnyType.Output(display_name="value")],
) )
@classmethod @classmethod
def execute(cls, name: str, type: str, default, min=0.0, max=1.0): # type: ignore def execute(cls, name: str, type: str, default, min=0.0, max=1.0):
if type == "number": if type == "number":
return io.NodeOutput(float(default)) return io.NodeOutput(float(default))
elif type == "number (integer)": elif type == "number (integer)":
@@ -328,37 +261,5 @@ class KritaStyle(io.ComfyNode):
) )
@classmethod @classmethod
def execute(cls, name: str, sampler_preset: str): # type: ignore def execute(cls, name: str, sampler_preset: str):
raise NotImplementedError("This workflow must be started from Krita!")
class KritaStyleAndPrompt(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ETN_KritaStyleAndPrompt",
display_name="Krita Style & Prompt",
category="krita",
inputs=[
io.Combo.Input("sampler_preset", options=["auto", "regular", "live"]),
],
outputs=[
io.Model.Output(display_name="model (with loras)"),
io.Clip.Output(display_name="clip"),
io.Vae.Output(display_name="vae"),
io.String.Output(display_name="positive prompt (evaluated)"),
io.String.Output(display_name="negative prompt (evaluated)"),
io.Combo.Output(
display_name="sampler name", options=comfy.samplers.KSampler.SAMPLERS
),
io.Combo.Output(
display_name="scheduler", options=comfy.samplers.KSampler.SCHEDULERS
),
io.Int.Output(display_name="steps"),
io.Float.Output(display_name="guidance"),
],
)
@classmethod
def execute(cls, name: str, sampler_preset: str): # type: ignore
raise NotImplementedError("This workflow must be started from Krita!") raise NotImplementedError("This workflow must be started from Krita!")
+7 -32
View File
@@ -107,9 +107,6 @@ class SendImageWebSocket(io.ComfyNode):
class ImageCache: class ImageCache:
timeout = 600 # 10 minutes
max_size = 100 * 1024 * 1024 # 100 MB
@dataclass @dataclass
class Entry: class Entry:
data: bytes data: bytes
@@ -117,15 +114,8 @@ class ImageCache:
timestamp: float timestamp: float
retrieved: int retrieved: int
class OldEntry(NamedTuple):
last_used: float
deleted: float
size: int
retrieved: int
def __init__(self): def __init__(self):
self.images: dict[str, ImageCache.Entry] = {} self.images: dict[str, ImageCache.Entry] = {}
self.old: dict[str, ImageCache.OldEntry] = {}
def add(self, image: Image.Image, format: str): def add(self, image: Image.Image, format: str):
key = uuid4().hex key = uuid4().hex
@@ -147,13 +137,6 @@ class ImageCache:
def get(self, key: str, extend: bool = False): def get(self, key: str, extend: bool = False):
entry = self.images.get(key) entry = self.images.get(key)
if entry is None: if entry is None:
if old := self.old.get(key):
now = time.time()
print(
f"[comfyui-tooling-nodes] requested image {key} has been deleted ",
f"(last used {now - old.last_used:.0f}s ago, deleted {now - old.deleted:.0f}s ago, "
f"size {old.size / 1024**2:.1f}MB, retrieved {old.retrieved} times)",
)
return None, None return None, None
entry.retrieved += 1 entry.retrieved += 1
if extend: if extend:
@@ -162,22 +145,14 @@ class ImageCache:
return entry.data, entry.content_type return entry.data, entry.content_type
def prune(self): def prune(self):
total_size = sum(len(entry.data) for entry in self.images.values())
if total_size <= self.max_size:
return
# Remove least recently used entries until under max size
sorted_entries = sorted(self.images.items(), key=lambda item: item[1].timestamp)
now = time.time() now = time.time()
for key, entry in sorted_entries: keys_to_delete = []
age = now - entry.timestamp for key, entry in self.images.items():
if age > self.timeout or (age > 60 and entry.retrieved > 0): d = now - entry.timestamp
self.old[key] = ImageCache.OldEntry( if (d > 60 and entry.retrieved > 1) or d > 600:
entry.timestamp, now, len(entry.data), entry.retrieved keys_to_delete.append(key)
) for key in keys_to_delete:
del self.images[key] del self.images[key]
total_size -= len(entry.data)
if total_size <= self.max_size:
break
def __contains__(self, key: str): def __contains__(self, key: str):
return key in self.images return key in self.images
+1 -4
View File
@@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from weakref import ref as WeakRef
from pathlib import Path from pathlib import Path
from tqdm import tqdm from tqdm import tqdm
import torch import torch
@@ -38,10 +39,6 @@ class CLIPSafetyChecker(PreTrainedModel):
self.concept_embeds_weights = nn.Parameter(torch.ones(17), requires_grad=False) self.concept_embeds_weights = nn.Parameter(torch.ones(17), requires_grad=False)
self.special_care_embeds_weights = nn.Parameter(torch.ones(3), requires_grad=False) self.special_care_embeds_weights = nn.Parameter(torch.ones(3), requires_grad=False)
# Model requires post_init after transformers v4.57.3
if hasattr(self, "post_init"):
self.post_init()
def forward(self, clip_input, images: Tensor, sensitivity: float): def forward(self, clip_input, images: Tensor, sensitivity: float):
with torch.no_grad(): with torch.no_grad():
image_batch = self.vision_model(clip_input)[1] image_batch = self.vision_model(clip_input)[1]
+2 -2
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-tooling-nodes" name = "comfyui-tooling-nodes"
description = "Provides nodes and server API extensions geared towards using ComfyUI as a backend for external tools." description = "Provides nodes and server API extensions geared towards using ComfyUI as a backend for external tools."
version = "3.1.4" version = "3.0.0"
license = { file = "LICENSE" } license = { file = "LICENSE" }
[project.urls] [project.urls]
@@ -13,7 +13,7 @@ line-length = 100
preview = true preview = true
[tool.ruff.lint] [tool.ruff.lint]
ignore = ["E741", "BLE001"] ignore = ["E741"]
[tool.black] [tool.black]
line-length = 100 line-length = 100
+6 -11
View File
@@ -9,13 +9,9 @@ IntArray = npt.NDArray[np.int_]
class TileLayout: class TileLayout:
def __init__( def __init__(self, image: Tensor, min_tile_size: int, padding: int, blending: int):
self, image: Tensor, min_tile_size: int, padding: int, blending: int, multiple: int assert all([x % 8 == 0 for x in image.shape[-3:-1]]), "Image size must be divisible by 8"
): assert min_tile_size % 8 == 0, "Tile size must be divisible by 8"
assert all([x % multiple == 0 for x in image.shape[-3:-1]]), (
"Image size must be divisible by multiple"
)
assert min_tile_size % multiple == 0, "Tile size must be divisible by multiple"
assert blending <= padding, "Blending must be smaller than padding" assert blending <= padding, "Blending must be smaller than padding"
self.image_size: IntArray = np.array(image.shape[-3:-1]) self.image_size: IntArray = np.array(image.shape[-3:-1])
@@ -25,7 +21,7 @@ class TileLayout:
image_size_with_overlap = self.image_size + (self.tile_count - 1) * 2 * padding image_size_with_overlap = self.image_size + (self.tile_count - 1) * 2 * padding
tile_size = np.ceil(image_size_with_overlap / self.tile_count) tile_size = np.ceil(image_size_with_overlap / self.tile_count)
self.tile_size: IntArray = (np.ceil(tile_size / multiple) * multiple).astype(int) self.tile_size: IntArray = (np.ceil(tile_size / 8) * 8).astype(int)
def size(self, coord: IntArray): def size(self, coord: IntArray):
return self.end(coord) - self.start(coord) return self.end(coord) - self.start(coord)
@@ -88,14 +84,13 @@ class CreateTileLayout(io.ComfyNode):
io.Int.Input("min_tile_size", default=512, min=64, max=8192, step=8), io.Int.Input("min_tile_size", default=512, min=64, max=8192, step=8),
io.Int.Input("padding", default=32, min=0, max=8192, step=8), io.Int.Input("padding", default=32, min=0, max=8192, step=8),
io.Int.Input("blending", default=8, min=0, max=256, step=8), io.Int.Input("blending", default=8, min=0, max=256, step=8),
io.Int.Input("multiple", default=8, min=1, max=1024, step=1),
], ],
outputs=[io.Custom("TileLayout").Output(display_name="layout")], outputs=[io.Custom("TileLayout").Output(display_name="layout")],
) )
@classmethod @classmethod
def execute(cls, image: Tensor, min_tile_size: int, padding: int, blending: int, multiple: int): def execute(cls, image: Tensor, min_tile_size: int, padding: int, blending: int):
return io.NodeOutput(TileLayout(image, min_tile_size, padding, blending, multiple)) return io.NodeOutput(TileLayout(image, min_tile_size, padding, blending))
class ExtractImageTile(io.ComfyNode): class ExtractImageTile(io.ComfyNode):