Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d2e03e8d5 | ||
|
|
c5606f8e8f | ||
|
|
79e9b6426f | ||
|
|
ad320a218c | ||
|
|
a310f4593b | ||
|
|
7d957dcfa7 | ||
|
|
22cfd71f95 | ||
|
|
21a2f44d4c | ||
|
|
0220252912 | ||
|
|
f447ef70fa | ||
|
|
fb27a5bda8 | ||
|
|
aa83259e66 | ||
|
|
75c632df4b | ||
|
|
a088a2dde2 | ||
|
|
fbf99f2a08 |
@@ -44,30 +44,45 @@ That is two 32-bit integers (big endian) with values 1 and 2 followed by the PNG
|
|||||||
{'type': 'executed', 'data': {'node': '<node ID>', 'output': {'images': [{'source': 'websocket', 'content-type': 'image/png', 'type': 'output'}, ...]}, 'prompt_id': '<prompt ID>}}
|
{'type': 'executed', 'data': {'node': '<node ID>', 'output': {'images': [{'source': 'websocket', 'content-type': 'image/png', 'type': 'output'}, ...]}, 'prompt_id': '<prompt ID>}}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Send Image (HTTP)
|
### Load Image from Cache
|
||||||
|
|
||||||
|
Loads an image or mask that has been uploaded previously into the workflow.
|
||||||
|
Uploaded images are temporarily stored in RAM rather than written to disk. This
|
||||||
|
method has less overhead compared to embedding images as base64 into the prompt,
|
||||||
|
but is more complex to implement.
|
||||||
|
* Inputs: id of an image that was uploaded previously
|
||||||
|
* Outputs: image (RGB) and mask (A of RGBA input, or first channel if no alpha present).
|
||||||
|
|
||||||
|
To upload an image, upload the _bytes_ of a PNG via a HTTP PUT request to
|
||||||
|
`/api/etn/image/{id}`. JPEG or other formats also work. Choose any `id` which
|
||||||
|
does not clash with other images you upload, and reference it in the node. The
|
||||||
|
request returns `201` if the image was uploaded and `200` if it was already
|
||||||
|
cached.
|
||||||
|
|
||||||
|
### Save Image to Cache
|
||||||
|
|
||||||
Stores an output image in RAM temporarily and allows retrieval over HTTP.
|
Stores an output image in RAM temporarily and allows retrieval over HTTP.
|
||||||
This is typically faster than WebSocket, especially for large images.
|
This is typically faster than WebSocket, especially for large images.
|
||||||
* Inputs: the image (RGB or RGBA), supports batches
|
* Inputs: the image (RGB or RGBA). Batches are supported.
|
||||||
|
|
||||||
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
To download the images, send a HTTP GET request to `/api/etn/image/{id}` with the image IDs from the message.
|
To download the images, send a HTTP GET request to `/api/etn/image/{id}` with
|
||||||
Images will be cached for a few minutes.
|
the image IDs from the message. Images will be cached for a few minutes.
|
||||||
|
|
||||||
|
|
||||||
## <a id="regions" href="#toc">Regions</a>
|
## <a id="regions" href="#toc">Regions</a>
|
||||||
@@ -190,6 +205,8 @@ 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:
|
||||||
@@ -203,7 +220,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, lumina2, 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, flux2, lumina2, z-image, 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`
|
||||||
|
|
||||||
@@ -213,6 +230,24 @@ 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.
|
||||||
|
|||||||
+2
-1
@@ -5,10 +5,11 @@ from . import api as api, nodes, tile, region, nsfw, translation, krita
|
|||||||
class ExternalToolingNodes(ComfyExtension):
|
class ExternalToolingNodes(ComfyExtension):
|
||||||
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
return [
|
return [
|
||||||
|
nodes.LoadImageCache,
|
||||||
|
nodes.SaveImageCache,
|
||||||
nodes.LoadImageBase64,
|
nodes.LoadImageBase64,
|
||||||
nodes.LoadMaskBase64,
|
nodes.LoadMaskBase64,
|
||||||
nodes.SendImageWebSocket,
|
nodes.SendImageWebSocket,
|
||||||
nodes.SendImageHTTP,
|
|
||||||
nodes.ApplyMaskToImage,
|
nodes.ApplyMaskToImage,
|
||||||
nodes.ReferenceImage,
|
nodes.ReferenceImage,
|
||||||
nodes.ApplyReferenceImages,
|
nodes.ApplyReferenceImages,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from typing import NamedTuple
|
from typing import Any, NamedTuple
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
@@ -44,6 +44,8 @@ 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",
|
||||||
@@ -54,6 +56,7 @@ model_names = {
|
|||||||
"ACEStep": "ace-step",
|
"ACEStep": "ace-step",
|
||||||
"Omnigen2": "omnigen2",
|
"Omnigen2": "omnigen2",
|
||||||
"QwenImage": "qwen-image",
|
"QwenImage": "qwen-image",
|
||||||
|
"Flux2": "flux2",
|
||||||
}
|
}
|
||||||
|
|
||||||
gguf_architectures = {
|
gguf_architectures = {
|
||||||
@@ -123,7 +126,7 @@ def inspect_safetensors(filename: str, model_type: str, is_checkpoint: bool):
|
|||||||
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 = {"base_model": base_model_name}
|
result: dict[str, Any] = {"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"
|
||||||
@@ -142,6 +145,7 @@ 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}"}
|
||||||
|
|
||||||
@@ -151,12 +155,16 @@ 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)
|
||||||
return comfy_config.get("model_class")
|
if model_class := comfy_config.get("model_class"):
|
||||||
model_class = md.get("model_class")
|
return model_class
|
||||||
if model_class == "NunchakuFluxTransformer2dModel":
|
|
||||||
return "Flux"
|
match md.get("model_class"):
|
||||||
if model_class == "NunchakuQwenImageTransformer2DModel":
|
case "NunchakuFluxTransformer2dModel":
|
||||||
return "QwenImage"
|
return "Flux"
|
||||||
|
case "NunchakuQwenImageTransformer2DModel":
|
||||||
|
return "QwenImage"
|
||||||
|
case "NunchakuZImageTransformer2DModel":
|
||||||
|
return "ZImage"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -168,6 +176,9 @@ 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:
|
||||||
@@ -178,19 +189,29 @@ 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}
|
||||||
|
|
||||||
|
# Detect Chroma (modified Flux)
|
||||||
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
|
||||||
|
|
||||||
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,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
result["quant"] = reader.get_field("general.file_type").lower()
|
if file_type := reader.get_field("general.file_type"):
|
||||||
except Exception as e:
|
result["quant"] = file_type.contents().lower()
|
||||||
|
except Exception:
|
||||||
result["quant"] = "gguf"
|
result["quant"] = "gguf"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -205,17 +226,22 @@ 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):
|
def inspect_models(model_type: str, params: dict[str, 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
|
for filename in files_range
|
||||||
}
|
}
|
||||||
|
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()
|
||||||
@@ -261,11 +287,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)
|
return inspect_models(folder_name, request.rel_url.query)
|
||||||
|
|
||||||
@_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")
|
return inspect_models("checkpoints", request.rel_url.query)
|
||||||
|
|
||||||
@_server.routes.get("/api/etn/languages")
|
@_server.routes.get("/api/etn/languages")
|
||||||
async def languages(request):
|
async def languages(request):
|
||||||
@@ -301,6 +327,23 @@ 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):
|
||||||
|
try:
|
||||||
|
id = request.match_info.get("id", "")
|
||||||
|
if id in image_cache:
|
||||||
|
return web.json_response(dict(status="cached"), status=200)
|
||||||
|
|
||||||
|
content_type = request.headers.get("Content-Type", "application/octet-stream")
|
||||||
|
data = bytearray()
|
||||||
|
async for chunk, _ in request.content.iter_chunks():
|
||||||
|
data.extend(chunk)
|
||||||
|
|
||||||
|
image_cache.insert(id, bytes(data), content_type)
|
||||||
|
return web.json_response(dict(status="success"), status=201)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response(dict(error=str(e)), status=500)
|
||||||
|
|
||||||
@_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", "")
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ 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
|
||||||
@@ -77,7 +76,8 @@ function defaultParameterType(widgetType, connectedNode, connectedWidget) {
|
|||||||
if (connectedNode.comfyClass === "CLIPTextEncode") {
|
if (connectedNode.comfyClass === "CLIPTextEncode") {
|
||||||
paramType = "prompt (positive)"
|
paramType = "prompt (positive)"
|
||||||
}
|
}
|
||||||
if (connectedWidget.options?.round === 1) {
|
const round = connectedWidget.options?.round
|
||||||
|
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 === 0 ? fallback : result
|
return result === null || result === -1e10 || result === 1e10 ? fallback : result
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeWidgets(node, type, connectedNode, connectedWidget) {
|
function changeWidgets(node, type, connectedNode, connectedWidget) {
|
||||||
@@ -206,8 +206,6 @@ 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") {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import torch
|
import torch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, NamedTuple
|
from typing import Any, NamedTuple
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -73,6 +74,13 @@ 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):
|
||||||
@@ -80,13 +88,41 @@ 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=[io.Image.Input("images")],
|
inputs=[
|
||||||
|
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(cls, images: torch.Tensor):
|
def execute(
|
||||||
return SendImageWebSocket.execute(images, "PNG")
|
cls,
|
||||||
|
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):
|
||||||
@@ -141,6 +177,21 @@ class KritaCanvas(io.ComfyNode):
|
|||||||
return io.NodeOutput(_placeholder_image(), 512, 512, 0)
|
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):
|
||||||
@classmethod
|
@classmethod
|
||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
@@ -148,12 +199,26 @@ class KritaSelection(io.ComfyNode):
|
|||||||
node_id="ETN_KritaSelection",
|
node_id="ETN_KritaSelection",
|
||||||
display_name="Krita Selection",
|
display_name="Krita Selection",
|
||||||
category="krita",
|
category="krita",
|
||||||
outputs=[io.Mask.Output(display_name="mask"), io.Boolean.Output(display_name="active")],
|
inputs=[
|
||||||
|
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):
|
def execute(cls, **kwargs):
|
||||||
return io.NodeOutput(torch.ones(1, 512, 512), False)
|
return io.NodeOutput(torch.ones(1, 512, 512), False, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
class KritaImageLayer(io.ComfyNode):
|
class KritaImageLayer(io.ComfyNode):
|
||||||
@@ -217,8 +282,8 @@ 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=0.0, min=-_fmax, max=_fmax, optional=True),
|
io.Float.Input("min", default=-1e10, min=-_fmax, max=_fmax, optional=True),
|
||||||
io.Float.Input("max", default=1.0, min=-_fmax, max=_fmax, optional=True),
|
io.Float.Input("max", default=1e10, min=-_fmax, max=_fmax, optional=True),
|
||||||
],
|
],
|
||||||
outputs=[io.AnyType.Output(display_name="value")],
|
outputs=[io.AnyType.Output(display_name="value")],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ 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
|
||||||
@@ -114,8 +117,15 @@ 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
|
||||||
@@ -123,42 +133,101 @@ class ImageCache:
|
|||||||
image.save(output, format=format, quality=95, compress_level=1)
|
image.save(output, format=format, quality=95, compress_level=1)
|
||||||
image_data = output.getvalue()
|
image_data = output.getvalue()
|
||||||
|
|
||||||
|
self.insert(key, image_data, f"image/{format.lower()}")
|
||||||
|
return key
|
||||||
|
|
||||||
|
def insert(self, key: str, data: bytes, content_type: str):
|
||||||
self.images[key] = ImageCache.Entry(
|
self.images[key] = ImageCache.Entry(
|
||||||
data=image_data,
|
data=data,
|
||||||
content_type=f"image/{format.lower()}",
|
content_type=content_type,
|
||||||
timestamp=time.time(),
|
timestamp=time.time(),
|
||||||
retrieved=0,
|
retrieved=0,
|
||||||
)
|
)
|
||||||
return key
|
|
||||||
|
|
||||||
def get(self, key: str):
|
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
|
||||||
self.prune()
|
|
||||||
entry.retrieved += 1
|
entry.retrieved += 1
|
||||||
|
if extend:
|
||||||
|
entry.timestamp = time.time()
|
||||||
|
self.prune()
|
||||||
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()
|
||||||
keys_to_delete = []
|
for key, entry in sorted_entries:
|
||||||
for key, entry in self.images.items():
|
age = now - entry.timestamp
|
||||||
d = now - entry.timestamp
|
if age > self.timeout or (age > 60 and entry.retrieved > 0):
|
||||||
if (d > 60 and entry.retrieved > 1) or d > 600:
|
self.old[key] = ImageCache.OldEntry(
|
||||||
keys_to_delete.append(key)
|
entry.timestamp, now, len(entry.data), entry.retrieved
|
||||||
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):
|
||||||
|
return key in self.images
|
||||||
|
|
||||||
|
|
||||||
image_cache = ImageCache()
|
image_cache = ImageCache()
|
||||||
|
|
||||||
|
|
||||||
class SendImageHTTP(io.ComfyNode):
|
class LoadImageCache(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="ETN_SendImageHTTP",
|
node_id="ETN_LoadImageCache",
|
||||||
display_name="Send Image (HTTP)",
|
display_name="Load Image from Cache",
|
||||||
|
category="external_tooling",
|
||||||
|
inputs=[io.String.Input("id", multiline=False)],
|
||||||
|
outputs=[io.Image.Output(display_name="image"), io.Mask.Output(display_name="mask")],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, id: str):
|
||||||
|
image_data, content_type = image_cache.get(id, extend=True)
|
||||||
|
if image_data is None:
|
||||||
|
raise ValueError(f"Image with ID {id} not found in cache.")
|
||||||
|
|
||||||
|
img = Image.open(BytesIO(image_data))
|
||||||
|
w, h = img.size
|
||||||
|
c = len(img.getbands())
|
||||||
|
normalized = np.array(img).astype(np.float32) / 255.0
|
||||||
|
tensor = torch.from_numpy(normalized).reshape(1, h, w, c)
|
||||||
|
match c:
|
||||||
|
case 1:
|
||||||
|
image = tensor.expand(1, h, w, 3)
|
||||||
|
mask = tensor.reshape(1, h, w)
|
||||||
|
case 3:
|
||||||
|
image = tensor
|
||||||
|
mask = tensor[..., 0]
|
||||||
|
case 4:
|
||||||
|
image = tensor[..., :3]
|
||||||
|
mask = tensor[..., 3]
|
||||||
|
|
||||||
|
return io.NodeOutput(image, mask)
|
||||||
|
|
||||||
|
|
||||||
|
class SaveImageCache(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="ETN_SaveImageCache",
|
||||||
|
display_name="Save Image to Cache",
|
||||||
category="external_tooling",
|
category="external_tooling",
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Image.Input("images"),
|
io.Image.Input("images"),
|
||||||
|
|||||||
+1
-1
@@ -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.0.0"
|
version = "3.1.0"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
Reference in New Issue
Block a user