Files
Acly-comfyui-tooling-nodes/krita.py
T

259 lines
6.3 KiB
Python

import torch
import numpy as np
from pathlib import Path
from typing import Any, NamedTuple
from PIL import Image
import server
import comfy.samplers
from .nodes import SendImageWebSocket
class Publisher(NamedTuple):
name: str
id: str
workflow: dict
class WorkflowExchange:
def __init__(self, server: server.PromptServer):
self._server = server
self._publishers: dict[str, Publisher] = {}
self._subscribers: list[str] = []
async def publish(self, publisher_name: str, publisher_id: str, workflow: dict):
publisher = Publisher(publisher_name, publisher_id, workflow)
for client_id in self._subscribers:
await self._notify(client_id, publisher)
self._publishers[publisher_id] = publisher
async def subscribe(self, client_id: str):
if client_id in self._subscribers:
raise KeyError("Already subscribed")
self._subscribers.append(client_id)
for publisher in self._publishers.values():
await self._notify(client_id, publisher)
async def unsubscribe(self, client_id: str):
if client_id in self._subscribers:
self._subscribers.remove(client_id)
else:
raise KeyError("No subscriber found with id " + client_id)
async def _notify(self, client_id: str, publisher: Publisher):
data = {
"publisher": {"name": publisher.name, "id": publisher.id},
"workflow": publisher.workflow,
}
await self._server.send_json("etn_workflow_published", data, client_id)
def _placeholder_image():
path = Path(__file__).parent / "data" / "external-image-placeholder.webp"
image = Image.open(path).convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
return torch.from_numpy(image)[None,]
class _AnyType(str):
def __ne__(self, other):
return False
class _BasicTypes(str):
basic_types = ["INT", "FLOAT", "STRING", "BOOL"]
def __eq__(self, other):
return other in self.basic_types or isinstance(other, (list, _BasicTypes))
def __ne__(self, other):
return not self.__eq__(other)
AnyType = _AnyType("*")
BasicTypes = _BasicTypes("BASIC")
class KritaOutput:
@classmethod
def INPUT_TYPES(s):
return {"required": {"images": ("IMAGE",)}}
RETURN_TYPES = ()
FUNCTION = "send_images"
OUTPUT_NODE = True
CATEGORY = "krita"
def send_images(self, images):
return SendImageWebSocket().send_images(images, "PNG")
class KritaSendText:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (AnyType, {}),
"name": ("STRING", {"default": "Output"}),
"type": (["text", "markdown", "html"], {"default": "text"}),
}
}
RETURN_TYPES = ()
FUNCTION = "send"
OUTPUT_NODE = True
CATEGORY = "krita"
def send(self, value: Any, name: str, type: str):
mime = {
"text": "text/plain",
"markdown": "text/markdown",
"html": "text/html",
}[type]
text = "None"
if value is not None:
try:
text = str(value)
except Exception as e:
text = f"Could not convert to text: {e}"
print(f"Sending text: {name} = {text}")
return {"ui": {"text": [{"name": name, "text": text, "content-type": mime}]}}
class KritaCanvas:
@classmethod
def INPUT_TYPES(cls):
return {}
RETURN_TYPES = ("IMAGE", "INT", "INT", "INT")
RETURN_NAMES = ("image", "width", "height", "seed")
FUNCTION = "placeholder"
CATEGORY = "krita"
def placeholder(self):
return (_placeholder_image(), 512, 512, 0)
class KritaSelection:
@classmethod
def INPUT_TYPES(cls):
return {}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = "placeholder"
CATEGORY = "krita"
def placeholder(self):
return (torch.ones(1, 512, 512),)
class KritaImageLayer:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"name": ("STRING", {"default": "Image"}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "placeholder"
CATEGORY = "krita"
def placeholder(self, name: str):
return (_placeholder_image(),)
class KritaMaskLayer:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"name": ("STRING", {"default": "Mask"}),
}
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = "placeholder"
CATEGORY = "krita"
def placeholder(self, name: str):
return (torch.ones(1, 512, 512),)
_param_types = [
"auto",
"number",
"number (integer)",
"toggle",
"choice",
"text",
"prompt (positive)",
"prompt (negative)",
]
class Parameter:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"name": ("STRING", {"default": "Parameter"}),
"type": (_param_types, {"default": "auto"}),
"default": ("STRING", {"default": ""}),
"min": ("FLOAT", {"default": 0.0}),
"max": ("FLOAT", {"default": 0.0}),
}
}
RETURN_TYPES = (BasicTypes,)
RETURN_NAMES = ("value",)
FUNCTION = "placeholder"
CATEGORY = "krita"
def placeholder(self, name: str, type: str, default, min, max):
return (default,)
class KritaStyle:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"name": ("STRING", {"default": "Style"}),
"sampler_preset": (["auto", "regular", "live"],),
}
}
RETURN_TYPES = (
"MODEL",
"CLIP",
"VAE",
"STRING",
"STRING",
comfy.samplers.KSampler.SAMPLERS,
comfy.samplers.KSampler.SCHEDULERS,
"INT",
"FLOAT",
)
RETURN_NAMES = (
"model",
"clip",
"vae",
"positive prompt",
"negative prompt",
"sampler name",
"scheduler",
"steps",
"guidance",
)
FUNCTION = "placeholder"
CATEGORY = "krita"
def placeholder(self, name: str, sampler_preset: str):
raise NotImplementedError("This workflow must be started from Krita!")