Files
without-ordinary-openoutpai…/py/nodes.py
T
Without Ordinary 6d906ded9e Added error handling on server start
Fixed server stop
Fixed port still in use on restart if done "too quickly"
2025-06-13 02:36:01 -07:00

582 lines
17 KiB
Python

import json
from comfy_execution.graph import ExecutionBlocker
from .utils import get_category, base64_to_image, base64_to_mask, image_to_base64, images_to_base64
from .api_server import POSTPATHS, OpenOutpainterServingManager
# global server manager
# current does not support more than a single active API workflow
oop_serving = OpenOutpainterServingManager()
########################
# Serving Node #
########################
class OpenOutpainterServing:
CLASSNAME = "OpenOutpainterServingV1"
NAME = "OpenOutpainter Serving"
CATEGORY = get_category()
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"run_server": ("BOOLEAN", {"default": False}),
"server_address": ("STRING",{"default": "127.0.0.1"}),
"port": ("INT", {"default": 7860, "min": 1, "max": 65535}),
"enable_cross_origin_requests": ("BOOLEAN", {"default": False}),
"request_id": ("INT", {"default": -1, "min": -1, "max": 1125899906842624}),
},
"hidden": {"unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("OOP_REQUEST", "STRING")
RETURN_NAMES = ("oop_request", "Server status")
FUNCTION = "serve"
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
def serve(self, run_server, server_address, port, enable_cross_origin_requests, request_id, unique_id):
print(f"{self.NAME} start - unique_id: {unique_id}")
# server settings changed, restart
if oop_serving.http_running and (
not run_server or
oop_serving.server_address != server_address or
oop_serving.port != port or
oop_serving.enable_cross_origin_requests != enable_cross_origin_requests
):
oop_serving.stop_server()
# start server
if run_server and not oop_serving.http_running:
oop_serving.start_server(
server_address=server_address,
port=port,
enable_cross_origin_requests=enable_cross_origin_requests,
node_type=OpenOutpainterServing.CLASSNAME,
node_id=unique_id,
)
data = oop_serving.get_data(request_id=request_id)
return (data, oop_serving.server_status)
########################
# Upscale Nodes #
########################
# upscale image: '/sdapi/v1/extra-single-image/'
# request
# {
# "resize-mode": 0,
# "upscaling_resize": "2",
# "upscaler_1": "Lanczos",
# "image": "data:image/png;base64,iVBORw0KGgoAAAANkJggg=="
# }
# response
# {
# "html_info": "<p>Postprocess upscale by: 2.0, Postprocess upscaler: Lanczos</p>",
# "image": "iVBORw0KGgoAAAANSUhEUgAACAAJggg=="
# }
# only "image" is needed
class OpenOutpainterServingInputUpscale:
CLASSNAME = "OpenOutpainterServingInputUpscaleV1"
NAME = "OpenOutpainter Serving Input Upscale"
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"oop_request": ("OOP_REQUEST", {"lazy": True}),
}
}
RETURN_TYPES = ("IMAGE", "FLOAT")
RETURN_NAMES = ("image", "scale_by")
FUNCTION = "out"
def __init__(self):
self.command_name = POSTPATHS.PATH_UPSCALE
pass
def check_lazy_status(self, oop_request):
if oop_request is None:
return ["oop_request"]
if oop_request.is_command(self.command_name):
return ["oop_request"]
return None
def out(self, oop_request):
# lazy eval does not work for some reason
if not oop_request.is_command(self.command_name):
return (ExecutionBlocker(None), ExecutionBlocker(None))
return (
base64_to_image(oop_request.request_data["image"]),
float(oop_request.request_data["upscaling_resize"]),
)
class OpenOutpainterServingOutputUpscale:
CLASSNAME = "OpenOutpainterServingOutputUpscaleV1"
NAME = "OpenOutpainter Serving Output Upscale"
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"oop_request": ("OOP_REQUEST", {"lazy": True}),
"image": ("IMAGE", {"lazy": True}),
},
}
FUNCTION = "out"
RETURN_TYPES = ()
OUTPUT_NODE = True
def __init__(self):
self.command_name = POSTPATHS.PATH_UPSCALE
pass
def check_lazy_status(self, oop_request, image=None):
if oop_request is None:
return ["oop_request"]
if oop_request.is_command(self.command_name):
return ["image", "oop_request"]
return None
def out(self, oop_request, image=None):
print(f"{self.NAME} out '{self.command_name}' image: {bool(image is not None)}")
if image is not None and oop_request.is_command(self.command_name):
response = {"image": image_to_base64(image)}
oop_request.finalize(response)
return {}
############################
# Interrogate Nodes #
############################
# caption image: '/sdapi/v1/interrogate':
# request
# {"image":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABggg==","model":"clip"}
# model is hardcoded to always return "clip"
# response
# {"caption":"caption text here"}
class OpenOutpainterServingInputInterrogate:
CLASSNAME = "OpenOutpainterServingInputInterrogateV1"
NAME = "OpenOutpainter Serving Input Interrogate"
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"oop_request": ("OOP_REQUEST", {"lazy": True}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "out"
def __init__(self):
self.command_name = POSTPATHS.PATH_INTERROGATE
pass
def check_lazy_status(self, oop_request):
if oop_request is None:
return ["oop_request"]
if oop_request.is_command(self.command_name):
return ["oop_request"]
return None
def out(self, oop_request):
# lazy eval does not work for some reason
if not oop_request.is_command(self.command_name):
return (ExecutionBlocker(None),)
return (base64_to_image(oop_request.request_data["image"]),)
class OpenOutpainterServingOutputInterrogate:
CLASSNAME = "OpenOutpainterServingOutputInterrogateV1"
NAME = "OpenOutpainter Serving Output Interrogate"
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"oop_request": ("OOP_REQUEST", {"lazy": True}),
"caption": ("STRING", {"lazy": True, "forceInput": True}),
},
}
FUNCTION = "out"
RETURN_TYPES = ()
OUTPUT_NODE = True
def __init__(self):
self.command_name = POSTPATHS.PATH_INTERROGATE
pass
def check_lazy_status(self, oop_request, caption=None):
if oop_request is None:
return ["oop_request"]
if oop_request.is_command(self.command_name):
return ["caption", "oop_request"]
return None
def out(self, oop_request, caption=None):
print(f"{self.NAME} out '{self.command_name}' caption: {caption}")
if caption is not None and oop_request.is_command(self.command_name):
response = {"caption": caption}
oop_request.finalize(response)
return {}
########################
# TXT2IMG Nodes #
########################
# txt2img: '/sdapi/v1/txt2img':
# request
# {
# "prompt": "This is a prompt",
# "negative_prompt": "This is a negative prompt",
# "seed": "-1",
# "cfg_scale": 7,
# "sampler_index": "DDIM",
# "steps": 30,
# "denoising_strength": 1,
# "mask_blur": 8,
# "batch_size": 1,
# "width": 128,
# "height": 128,
# "n_iter": 3,
# "mask": "",
# "init_images": [],
# "inpaint_full_res": false,
# "inpainting_fill": 1,
# "outpainting_fill": 2,
# "enable_hr": false,
# "restore_faces": false,
# "hr_scale": 2,
# "hr_upscaler": "None",
# "hr_second_pass_steps": 0,
# "hr_resize_x": 0,
# "hr_resize_y": 0,
# "hr_square_aspect": false,
# "styles": [],
# "upscale_x": 2,
# "hr_denoising_strength": 0.7,
# "hr_fix_lock_px": 0,
# "enable_refiner": false,
# "alwayson_scripts": {}
# }
# response
# {
# "images": [
# "iVBORw0KGgoAAAANSUhEUgAAAIAAAACAC5DU117AAAAAElFTkSuQmCC",
# "iVBORw0KGgoAAAANSUhEUgAAAIAAAACAC5DU117AAAAAElFTkSuQmCC",
# "iVBORw0KGgoAAAANSUhEUgAAAIAAAACAC5DU117AAAAAElFTkSuQmCC"
# ],
# "parameters": {}, # not used
# "info": {
# "all_seeds": [
# 2742961909,
# 2742961910,
# 2742961911
# ],
# }
# }
class OpenOutpainterServingInputTXT2IMG:
CLASSNAME = "OpenOutpainterServingInputTXT2IMGV1"
NAME = "OpenOutpainter Serving Input TXT2IMG"
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"oop_request": ("OOP_REQUEST", {"lazy": True}),
}
}
OUTPUTS = {
"prompt": "STRING",
"negative_prompt": "STRING",
"width": "INT",
"height": "INT",
"SEED": "INT",
"steps": "INT",
"cfg_scale": "FLOAT",
"batch_size": "INT",
"n_iter": "INT",
}
RETURN_TYPES = tuple(OUTPUTS.values())
RETURN_NAMES = tuple(OUTPUTS.keys())
FUNCTION = "out"
def __init__(self):
self.command_name = POSTPATHS.PATH_TXT2IMG
pass
def check_lazy_status(self, oop_request):
if oop_request is None:
return ["oop_request"]
if oop_request.is_command(self.command_name):
return ["oop_request"]
return None
def out(self, oop_request):
# lazy eval does not work for some reason
if not oop_request.is_command(self.command_name):
return (ExecutionBlocker(None),) * len(self.OUTPUTS)
request_data = oop_request.request_data
return (
request_data["prompt"],
request_data["negative_prompt"],
int(request_data["width"]),
int(request_data["height"]),
int(request_data["seed"]),
int(request_data["steps"]),
float(request_data["cfg_scale"]),
int(request_data["batch_size"]),
int(request_data["n_iter"]),
)
class OpenOutpainterServingOutputTXT2IMG:
CLASSNAME = "OpenOutpainterServingOutputTXT2IMGV1"
NAME = "OpenOutpainter Serving Output TXT2IMG"
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"oop_request": ("OOP_REQUEST", {"lazy": True}),
"images": ("IMAGE", {"lazy": True}),
"SEEDS": ("INT", {"lazy": True, "forceInput": True}),
},
}
FUNCTION = "out"
RETURN_TYPES = ()
OUTPUT_NODE = True
INPUT_IS_LIST = True
def __init__(self):
self.command_name = POSTPATHS.PATH_TXT2IMG
pass
def check_lazy_status(self, oop_request, images=None, SEEDS=None):
if oop_request is None:
return ["oop_request"]
if oop_request[0] is None:
return ["oop_request"]
if oop_request[0].is_command(self.command_name):
return ["SEEDS", "images", "oop_request"]
return None
def out(self, oop_request, images=None, SEEDS=None):
print(f"{self.NAME} out '{self.command_name}' images: {bool(images is not None)}")
if images is not None and SEEDS is not None and oop_request[0].is_command(self.command_name):
response = {
"images": images_to_base64(images),
"info": json.dumps({"all_seeds": SEEDS}, default=lambda o: None)
}
oop_request[0].finalize(response)
return {}
########################
# IMG2IMG Nodes #
########################
# txt2img: '/sdapi/v1/img2img':
# request
# {
# "prompt": "This is a prompt",
# "negative_prompt": "people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry",
# "seed": "-1",
# "cfg_scale": 7,
# "steps": 30,
# "denoising_strength": 0.5,
# "mask_blur": 8,
# "batch_size": 1,
# "width": 512,
# "height": 512,
# "n_iter": 3,
# "mask": "data:image/png;base64,iVBORw0KGgoAAPQAAAABJRU5ErkJggg==",
# "init_images": ["data:image/png;base64,iVBORwo9jTl4TD/Er4/f5by6KwHISuQmCC"],
# "image_cfg_scale": 8,
# }
# response
# {
# "images": ["iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABAAElFTkSuQmCC"],
# "parameters": {}, # not used
# "info": {
# "all_subseeds": [
# 3242546751,
# 3242546752,
# 3242546753
# ],
# }
# }
class OpenOutpainterServingInputIMG2IMG:
CLASSNAME = "OpenOutpainterServingInputIMG2IMGV1"
NAME = "OpenOutpainter Serving Input IMG2IMG"
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"oop_request": ("OOP_REQUEST", {"lazy": True}),
}
}
OUTPUTS = {
"init_images": "IMAGE",
"mask": "MASK",
"prompt": "STRING",
"negative_prompt": "STRING",
"width": "INT",
"height": "INT",
"SEED": "INT", # seed
"steps": "INT",
"cfg_scale": "FLOAT",
"denoising_strength": "FLOAT",
"mask_blur": "INT",
"ip2p_image_cfg_scale": "FLOAT", # image_cfg_scale
"batch_size": "INT",
"n_iter": "INT",
}
RETURN_TYPES = tuple(OUTPUTS.values())
RETURN_NAMES = tuple(OUTPUTS.keys())
FUNCTION = "out"
def __init__(self):
self.command_name = POSTPATHS.PATH_IMG2IMG
pass
def check_lazy_status(self, oop_request):
if oop_request is None:
return ["oop_request"]
if oop_request.is_command(self.command_name):
return ["oop_request"]
return None
def out(self, oop_request):
# lazy eval does not work for some reason
if not oop_request.is_command(self.command_name):
return (ExecutionBlocker(None),) * len(self.OUTPUTS)
request_data = oop_request.request_data
return (
base64_to_image(request_data["init_images"][0]),
base64_to_mask(request_data["mask"]),
request_data["prompt"],
request_data["negative_prompt"],
int(request_data["width"]),
int(request_data["height"]),
int(request_data["seed"]),
int(request_data["steps"]),
float(request_data["cfg_scale"]),
float(request_data["denoising_strength"]),
int(request_data["mask_blur"]),
float(request_data.get("image_cfg_scale", 0)), # may be missing if txt2img tool calls img2img instead
int(request_data["batch_size"]),
int(request_data["n_iter"]),
)
class OpenOutpainterServingOutputIMG2IMG:
CLASSNAME = "OpenOutpainterServingOutputIMG2IMGV1"
NAME = "OpenOutpainter Serving Output IMG2IMG"
CATEGORY = get_category()
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"oop_request": ("OOP_REQUEST", {"lazy": True}),
"images": ("IMAGE", {"lazy": True}),
"SEEDS": ("INT", {"lazy": True, "forceInput": True}),
},
}
FUNCTION = "out"
RETURN_TYPES = ()
OUTPUT_NODE = True
INPUT_IS_LIST = True
def __init__(self):
self.command_name = POSTPATHS.PATH_IMG2IMG
pass
def check_lazy_status(self, oop_request, images=None, SEEDS=None):
if oop_request is None:
return ["oop_request"]
if oop_request[0] is None:
return ["oop_request"]
if oop_request[0].is_command(self.command_name):
return ["SEEDS", "images", "oop_request"]
return None
def out(self, oop_request, images=None, SEEDS=None):
print(f"{self.NAME} out '{self.command_name}' images: {bool(images is not None)}")
if images is not None and SEEDS is not None and oop_request[0].is_command(self.command_name):
response = {
"images": images_to_base64(images),
"info": json.dumps({"all_seeds": SEEDS}, default=lambda o: None)
}
oop_request[0].finalize(response)
return {}
########################
def register_nodes(node_class_mappings=None, node_display_name_mappings=None):
if node_display_name_mappings is None:
node_display_name_mappings = {}
if node_class_mappings is None:
node_class_mappings = {}
node_classes = [
OpenOutpainterServing,
OpenOutpainterServingInputUpscale,
OpenOutpainterServingOutputUpscale,
OpenOutpainterServingInputInterrogate,
OpenOutpainterServingOutputInterrogate,
OpenOutpainterServingInputTXT2IMG,
OpenOutpainterServingOutputTXT2IMG,
OpenOutpainterServingInputIMG2IMG,
OpenOutpainterServingOutputIMG2IMG,
]
for node_class in node_classes:
node_class_mappings[node_class.CLASSNAME] = node_class
node_display_name_mappings[node_class.CLASSNAME] = node_class.NAME
return node_class_mappings, node_display_name_mappings