$ cat node-template.py
Image Upscale Legacy
// Upscales an image to higher resolution with AI-enhanced detail generation. Accepts an image and a configurable upscale factor (2-4x). Outputs the upscaled image.
Process
Image
template.py
1import os2import sys3import json4import time5import random6import traceback78try:9 import requests10except ImportError:11 import subprocess12 subprocess.check_call([sys.executable, "-m", "pip", "install", "requests"])13 import requests1415COMFYUI_API_URL = os.getenv("COMFYUI_API_URL", "http://192.168.1.39:8188")16INPUT_DIR = "/data/input"17OUTPUT_DIR = "/data/output"1819WORKFLOW = {20 "10": {21 "inputs": {"vae_name": "ae.safetensors"},22 "class_type": "VAELoader",23 "_meta": {"title": "Load VAE"},24 },25 "11": {26 "inputs": {27 "clip_name1": "ViT-L-14-TEXT-detail-improved-hiT-GmP-TE-only-HF.safetensors",28 "clip_name2": "t5xxl_fp16.safetensors",29 "type": "flux",30 "device": "default",31 },32 "class_type": "DualCLIPLoader",33 "_meta": {"title": "DualCLIPLoader"},34 },35 "12": {36 "inputs": {37 "unet_name": "flux1-dev.safetensors",38 "weight_dtype": "default",39 },40 "class_type": "UNETLoader",41 "_meta": {"title": "Load Diffusion Model"},42 },43 "22": {44 "inputs": {45 "model": ["12", 0],46 "conditioning": ["40", 0],47 },48 "class_type": "BasicGuider",49 "_meta": {"title": "BasicGuider"},50 },51 "26": {52 "inputs": {"model_name": "4x_NMKD-Siax_200k.pth"},53 "class_type": "UpscaleModelLoader",54 "_meta": {"title": "Load Upscale Model"},55 },56 "29": {57 "inputs": {58 "filename_prefix": "image/emblema-upscale",59 "images": ["39", 0],60 },61 "class_type": "SaveImage",62 "_meta": {"title": "Save Image"},63 },64 "37": {65 "inputs": {66 "text": "blurry",67 "clip": ["11", 0],68 },69 "class_type": "CLIPTextEncode",70 "_meta": {"title": "CLIP Text Encode (Prompt)"},71 },72 "39": {73 "inputs": {74 "upscale_by": ["57", 0],75 "seed": 0,76 "steps": 1,77 "cfg": 2,78 "sampler_name": "euler",79 "scheduler": "normal",80 "denoise": 0.2,81 "mode_type": "Linear",82 "tile_width": ["52", 0],83 "tile_height": ["53", 0],84 "mask_blur": 8,85 "tile_padding": 32,86 "seam_fix_mode": "None",87 "seam_fix_denoise": 1,88 "seam_fix_width": 64,89 "seam_fix_mask_blur": 8,90 "seam_fix_padding": 16,91 "force_uniform_tiles": True,92 "tiled_decode": False,93 "batch_size": 1,94 "image": ["61", 0],95 "model": ["12", 0],96 "positive": ["40", 0],97 "negative": ["37", 0],98 "vae": ["10", 0],99 "upscale_model": ["26", 0],100 },101 "class_type": "UltimateSDUpscale",102 "_meta": {"title": "Ultimate SD Upscale"},103 },104 "40": {105 "inputs": {106 "clip_l": "",107 "t5xxl": "",108 "guidance": 3.5,109 "clip": ["11", 0],110 },111 "class_type": "CLIPTextEncodeFlux",112 "_meta": {"title": "CLIPTextEncodeFlux"},113 },114 "47": {115 "inputs": {"image": ["61", 0]},116 "class_type": "GetImageSize+",117 "_meta": {"title": "Get Image Size"},118 },119 "48": {120 "inputs": {121 "Value_A": ["49", 0],122 "Value_B": ["57", 0],123 },124 "class_type": "DF_Multiply",125 "_meta": {"title": "Multiply"},126 },127 "49": {128 "inputs": {129 "output_type": "float",130 "*": ["47", 0],131 },132 "class_type": "easy convertAnything",133 "_meta": {"title": "Convert Any"},134 },135 "50": {136 "inputs": {137 "output_type": "float",138 "*": ["47", 1],139 },140 "class_type": "easy convertAnything",141 "_meta": {"title": "Convert Any"},142 },143 "51": {144 "inputs": {145 "Value_A": ["50", 0],146 "Value_B": ["57", 0],147 },148 "class_type": "DF_Multiply",149 "_meta": {"title": "Multiply"},150 },151 "52": {152 "inputs": {153 "output_type": "int",154 "*": ["48", 0],155 },156 "class_type": "easy convertAnything",157 "_meta": {"title": "Convert Any"},158 },159 "53": {160 "inputs": {161 "output_type": "int",162 "*": ["51", 0],163 },164 "class_type": "easy convertAnything",165 "_meta": {"title": "Convert Any"},166 },167 "54": {168 "inputs": {169 "number_type": "integer",170 "number": 3.0,171 },172 "class_type": "Constant Number",173 "_meta": {"title": "Constant Number"},174 },175 "57": {176 "inputs": {177 "output_type": "float",178 "*": ["54", 0],179 },180 "class_type": "easy convertAnything",181 "_meta": {"title": "Convert Any"},182 },183 "61": {184 "inputs": {185 "image": "",186 "custom_width": 0,187 "custom_height": 0,188 },189 "class_type": "VHS_LoadImagePath",190 "_meta": {"title": "Load Image (Path)"},191 },192}193194195def upload_image_to_comfyui(local_path: str) -> str:196 """Upload a local image to ComfyUI and return the uploaded filename."""197 with open(local_path, "rb") as f:198 resp = requests.post(199 f"{COMFYUI_API_URL}/upload/image",200 files={"image": (os.path.basename(local_path), f, "image/png")},201 timeout=30,202 )203 resp.raise_for_status()204 data = resp.json()205 return data["name"]206207208def build_workflow(image_name: str, upscale_factor: float) -> dict:209 """Inject user inputs into the workflow template."""210 import copy211 wf = copy.deepcopy(WORKFLOW)212213 # Set input image — swap VHS_LoadImagePath to LoadImage214 wf["61"] = {215 "inputs": {"image": image_name},216 "class_type": "LoadImage",217 "_meta": {"title": "Load Image"},218 }219220 # Set upscale factor221 wf["54"]["inputs"]["number"] = float(upscale_factor)222223 # Randomize seed224 wf["39"]["inputs"]["seed"] = random.randint(0, 2**31 - 1)225226 # Set output prefix227 wf["29"]["inputs"]["filename_prefix"] = "image/emblema-upscale"228229 return wf230231232def submit_prompt(workflow: dict) -> str:233 """Submit workflow to ComfyUI and return prompt_id."""234 resp = requests.post(235 f"{COMFYUI_API_URL}/prompt",236 json={"prompt": workflow},237 timeout=30,238 )239 if resp.status_code != 200:240 try:241 error_detail = resp.json()242 except Exception:243 error_detail = resp.text244 raise RuntimeError(245 f"ComfyUI /prompt returned {resp.status_code}: {json.dumps(error_detail, indent=2) if isinstance(error_detail, dict) else error_detail}"246 )247 data = resp.json()248249 # ComfyUI returns 200 even when nodes have validation errors250 node_errors = data.get("node_errors", {})251 if node_errors:252 raise RuntimeError(253 f"ComfyUI workflow has node errors: {json.dumps(node_errors, indent=2)}"254 )255256 return data["prompt_id"]257258259def wait_for_result(prompt_id: str, timeout: int = 600, poll_interval: int = 2) -> dict:260 """Poll ComfyUI history until the prompt completes with outputs."""261 deadline = time.time() + timeout262 empty_complete_retries = 0263 max_empty_retries = 3 # grace period for output serialization lag264265 while time.time() < deadline:266 resp = requests.get(267 f"{COMFYUI_API_URL}/history/{prompt_id}",268 timeout=10,269 )270 resp.raise_for_status()271 history = resp.json()272273 if prompt_id in history:274 prompt_data = history[prompt_id]275 status = prompt_data.get("status", {})276277 if status.get("status_str") == "error":278 messages = status.get("messages", [])279 raise RuntimeError(280 f"ComfyUI prompt failed: {json.dumps(messages, indent=2)}"281 )282283 if status.get("completed", False):284 if prompt_data.get("outputs"):285 return prompt_data286287 # Completed but no outputs — retry briefly for race condition288 empty_complete_retries += 1289 if empty_complete_retries >= max_empty_retries:290 raise RuntimeError(291 f"ComfyUI prompt completed but produced no outputs. "292 f"This usually means a node failed silently (missing custom node or model). "293 f"Status: {json.dumps(status, indent=2)}"294 )295296 time.sleep(poll_interval)297298 raise TimeoutError(f"ComfyUI prompt {prompt_id} did not complete within {timeout}s")299300301def download_output_image(prompt_data: dict, output_dir: str) -> str:302 """Download the generated image from ComfyUI."""303 outputs = prompt_data.get("outputs", {})304 for node_id, node_output in outputs.items():305 images = node_output.get("images", [])306 if images:307 img_info = images[0]308 filename = img_info["filename"]309 subfolder = img_info.get("subfolder", "")310 img_type = img_info.get("type", "output")311312 resp = requests.get(313 f"{COMFYUI_API_URL}/view",314 params={315 "filename": filename,316 "subfolder": subfolder,317 "type": img_type,318 },319 timeout=30,320 )321 resp.raise_for_status()322323 out_filename = f"upscaled_{filename}"324 out_path = os.path.join(output_dir, out_filename)325 with open(out_path, "wb") as f:326 f.write(resp.content)327328 return out_filename329330 raise RuntimeError(331 f"No output image found in ComfyUI response. Available outputs: {json.dumps(outputs, indent=2)}"332 )333334335def main():336 try:337 input_json = sys.stdin.read()338 execution_input = json.loads(input_json)339 inputs = execution_input.get("inputs", {})340341 image = inputs.get("image", "")342 upscale_factor = float(inputs.get("upscale_factor", 3))343344 if not image:345 raise ValueError("Image is required")346 if not (2 <= upscale_factor <= 4):347 raise ValueError(f"Upscale factor must be between 2 and 4, got {upscale_factor}")348349 os.makedirs(OUTPUT_DIR, exist_ok=True)350351 # Upload input image to ComfyUI352 local_path = os.path.join(INPUT_DIR, image)353 if not os.path.exists(local_path):354 raise FileNotFoundError(f"Input image not found: {local_path}")355 image_name = upload_image_to_comfyui(local_path)356357 # Build and submit workflow358 workflow = build_workflow(image_name, upscale_factor)359 prompt_id = submit_prompt(workflow)360361 # Wait for completion and download result362 prompt_data = wait_for_result(prompt_id)363 out_filename = download_output_image(prompt_data, OUTPUT_DIR)364365 # Log metadata to stderr366 print(f"prompt_id={prompt_id}, upscale_factor={upscale_factor}", file=sys.stderr)367368 # Flat output — keys match OUTPUT_SCHEMA369 output = {370 "image": out_filename,371 }372 print(json.dumps(output, indent=2))373374 except Exception as e:375 error_output = {376 "error": str(e),377 "errorType": type(e).__name__,378 "traceback": traceback.format_exc(),379 }380 print(json.dumps(error_output), file=sys.stderr)381 sys.exit(1)382383384if __name__ == "__main__":385 main()