$ cat node-template.py
Image Edit Legacy
// Edits images using one or two reference images and a text prompt describing the desired changes. Automatically adapts to single or multi-image input. Outputs the edited image.
Process
Image
template.py
1import os2import sys3import json4import time5import random6import traceback7from fractions import Fraction8from datetime import datetime910try:11 import requests12except ImportError:13 import subprocess14 subprocess.check_call([sys.executable, "-m", "pip", "install", "requests"])15 import requests1617COMFYUI_API_URL = os.getenv("COMFYUI_API_URL", "http://192.168.1.39:8188")18INPUT_DIR = "/data/input"19OUTPUT_DIR = "/data/output"2021# ---------- Single-image workflow (EM_Edit_Image_single_v1) ----------22WORKFLOW_SINGLE = {23 "9": {24 "inputs": {"filename_prefix": "Flux2-Klein", "images": ["75:65", 0]},25 "class_type": "SaveImage",26 "_meta": {"title": "Save Image"},27 },28 "120": {29 "inputs": {30 "megapixel": "1.0",31 "aspect_ratio": "9:16 (Slim Vertical)",32 "divisible_by": "64",33 "custom_ratio": True,34 "custom_aspect_ratio": "1:1",35 },36 "class_type": "FluxResolutionNode",37 "_meta": {"title": "Flux Resolution Calc"},38 },39 "132": {40 "inputs": {"image": "", "custom_width": 0, "custom_height": 0},41 "class_type": "VHS_LoadImagePath",42 "_meta": {"title": "Load Image (Path)"},43 },44 "75:61": {45 "inputs": {"sampler_name": "euler"},46 "class_type": "KSamplerSelect",47 "_meta": {"title": "KSamplerSelect"},48 },49 "75:64": {50 "inputs": {51 "noise": ["75:73", 0],52 "guider": ["75:63", 0],53 "sampler": ["75:61", 0],54 "sigmas": ["75:62", 0],55 "latent_image": ["75:66", 0],56 },57 "class_type": "SamplerCustomAdvanced",58 "_meta": {"title": "SamplerCustomAdvanced"},59 },60 "75:65": {61 "inputs": {"samples": ["75:64", 0], "vae": ["75:72", 0]},62 "class_type": "VAEDecode",63 "_meta": {"title": "VAE Decode"},64 },65 "75:73": {66 "inputs": {"noise_seed": 0},67 "class_type": "RandomNoise",68 "_meta": {"title": "RandomNoise"},69 },70 "75:70": {71 "inputs": {72 "unet_name": "flux-2-klein-9b-fp8.safetensors",73 "weight_dtype": "default",74 },75 "class_type": "UNETLoader",76 "_meta": {"title": "Load Diffusion Model"},77 },78 "75:71": {79 "inputs": {80 "clip_name": "qwen_3_8b_fp8mixed.safetensors",81 "type": "flux2",82 "device": "default",83 },84 "class_type": "CLIPLoader",85 "_meta": {"title": "Load CLIP"},86 },87 "75:72": {88 "inputs": {"vae_name": "flux2-vae.safetensors"},89 "class_type": "VAELoader",90 "_meta": {"title": "Load VAE"},91 },92 "75:63": {93 "inputs": {94 "cfg": 1,95 "model": ["75:70", 0],96 "positive": ["75:79:77", 0],97 "negative": ["75:79:76", 0],98 },99 "class_type": "CFGGuider",100 "_meta": {"title": "CFGGuider"},101 },102 "75:62": {103 "inputs": {104 "steps": 4,105 "width": ["75:81", 0],106 "height": ["75:81", 1],107 },108 "class_type": "Flux2Scheduler",109 "_meta": {"title": "Flux2Scheduler"},110 },111 "75:74": {112 "inputs": {"text": "", "clip": ["75:71", 0]},113 "class_type": "CLIPTextEncode",114 "_meta": {"title": "CLIP Text Encode (Positive Prompt)"},115 },116 "75:82": {117 "inputs": {"conditioning": ["75:74", 0]},118 "class_type": "ConditioningZeroOut",119 "_meta": {"title": "ConditioningZeroOut"},120 },121 "75:80": {122 "inputs": {123 "upscale_method": "nearest-exact",124 "megapixels": 1,125 "resolution_steps": 1,126 "image": ["132", 0],127 },128 "class_type": "ImageScaleToTotalPixels",129 "_meta": {"title": "ImageScaleToTotalPixels"},130 },131 "75:81": {132 "inputs": {"image": ["75:80", 0]},133 "class_type": "GetImageSize",134 "_meta": {"title": "Get Image Size"},135 },136 "75:66": {137 "inputs": {138 "width": ["75:84", 0],139 "height": ["75:83", 0],140 "batch_size": 1,141 },142 "class_type": "EmptyFlux2LatentImage",143 "_meta": {"title": "Empty Flux 2 Latent"},144 },145 "75:79:76": {146 "inputs": {147 "conditioning": ["75:82", 0],148 "latent": ["75:79:78", 0],149 },150 "class_type": "ReferenceLatent",151 "_meta": {"title": "ReferenceLatent"},152 },153 "75:79:78": {154 "inputs": {"pixels": ["75:80", 0], "vae": ["75:72", 0]},155 "class_type": "VAEEncode",156 "_meta": {"title": "VAE Encode"},157 },158 "75:79:77": {159 "inputs": {160 "conditioning": ["75:74", 0],161 "latent": ["75:79:78", 0],162 },163 "class_type": "ReferenceLatent",164 "_meta": {"title": "ReferenceLatent"},165 },166 "75:84": {167 "inputs": {"value": ["120", 0]},168 "class_type": "PrimitiveInt",169 "_meta": {"title": "Width"},170 },171 "75:83": {172 "inputs": {"value": ["120", 1]},173 "class_type": "PrimitiveInt",174 "_meta": {"title": "Height"},175 },176}177178# ---------- Multi-image workflow (EM_Edit_Image_multiple_v1) ----------179WORKFLOW_MULTIPLE = {180 "94": {181 "inputs": {"filename_prefix": "Flux2-Klein", "images": ["123:65", 0]},182 "class_type": "SaveImage",183 "_meta": {"title": "Save Image"},184 },185 "124": {186 "inputs": {187 "megapixel": "1.0",188 "aspect_ratio": "9:16 (Slim Vertical)",189 "divisible_by": "64",190 "custom_ratio": True,191 "custom_aspect_ratio": "1:1",192 },193 "class_type": "FluxResolutionNode",194 "_meta": {"title": "Flux Resolution Calc"},195 },196 "134": {197 "inputs": {"image": "", "custom_width": 0, "custom_height": 0},198 "class_type": "VHS_LoadImagePath",199 "_meta": {"title": "Load Image (Path)"},200 },201 "135": {202 "inputs": {"image": "", "custom_width": 0, "custom_height": 0},203 "class_type": "VHS_LoadImagePath",204 "_meta": {"title": "Load Image (Path) (2)"},205 },206 "123:61": {207 "inputs": {"sampler_name": "euler"},208 "class_type": "KSamplerSelect",209 "_meta": {"title": "KSamplerSelect"},210 },211 "123:62": {212 "inputs": {213 "steps": 4,214 "width": ["123:81", 0],215 "height": ["123:81", 1],216 },217 "class_type": "Flux2Scheduler",218 "_meta": {"title": "Flux2Scheduler"},219 },220 "123:63": {221 "inputs": {222 "cfg": 1,223 "model": ["123:70", 0],224 "positive": ["123:84:77", 0],225 "negative": ["123:84:76", 0],226 },227 "class_type": "CFGGuider",228 "_meta": {"title": "CFGGuider"},229 },230 "123:64": {231 "inputs": {232 "noise": ["123:73", 0],233 "guider": ["123:63", 0],234 "sampler": ["123:61", 0],235 "sigmas": ["123:62", 0],236 "latent_image": ["123:66", 0],237 },238 "class_type": "SamplerCustomAdvanced",239 "_meta": {"title": "SamplerCustomAdvanced"},240 },241 "123:65": {242 "inputs": {"samples": ["123:64", 0], "vae": ["123:72", 0]},243 "class_type": "VAEDecode",244 "_meta": {"title": "VAE Decode"},245 },246 "123:73": {247 "inputs": {"noise_seed": 0},248 "class_type": "RandomNoise",249 "_meta": {"title": "RandomNoise"},250 },251 "123:70": {252 "inputs": {253 "unet_name": "flux-2-klein-9b-fp8.safetensors",254 "weight_dtype": "default",255 },256 "class_type": "UNETLoader",257 "_meta": {"title": "Load Diffusion Model"},258 },259 "123:71": {260 "inputs": {261 "clip_name": "qwen_3_8b_fp8mixed.safetensors",262 "type": "flux2",263 "device": "default",264 },265 "class_type": "CLIPLoader",266 "_meta": {"title": "Load CLIP"},267 },268 "123:74": {269 "inputs": {"text": "", "clip": ["123:71", 0]},270 "class_type": "CLIPTextEncode",271 "_meta": {"title": "CLIP Text Encode (Positive Prompt)"},272 },273 "123:72": {274 "inputs": {"vae_name": "flux2-vae.safetensors"},275 "class_type": "VAELoader",276 "_meta": {"title": "Load VAE"},277 },278 "123:80": {279 "inputs": {280 "upscale_method": "nearest-exact",281 "megapixels": 1,282 "resolution_steps": 1,283 "image": ["134", 0],284 },285 "class_type": "ImageScaleToTotalPixels",286 "_meta": {"title": "ImageScaleToTotalPixels"},287 },288 "123:85": {289 "inputs": {290 "upscale_method": "nearest-exact",291 "megapixels": 1,292 "resolution_steps": 1,293 "image": ["135", 0],294 },295 "class_type": "ImageScaleToTotalPixels",296 "_meta": {"title": "ImageScaleToTotalPixels"},297 },298 "123:79:76": {299 "inputs": {300 "conditioning": ["123:86", 0],301 "latent": ["123:79:78", 0],302 },303 "class_type": "ReferenceLatent",304 "_meta": {"title": "ReferenceLatent"},305 },306 "123:79:78": {307 "inputs": {"pixels": ["123:80", 0], "vae": ["123:72", 0]},308 "class_type": "VAEEncode",309 "_meta": {"title": "VAE Encode"},310 },311 "123:79:77": {312 "inputs": {313 "conditioning": ["123:74", 0],314 "latent": ["123:79:78", 0],315 },316 "class_type": "ReferenceLatent",317 "_meta": {"title": "ReferenceLatent"},318 },319 "123:84:76": {320 "inputs": {321 "conditioning": ["123:79:76", 0],322 "latent": ["123:84:78", 0],323 },324 "class_type": "ReferenceLatent",325 "_meta": {"title": "ReferenceLatent"},326 },327 "123:84:78": {328 "inputs": {"pixels": ["123:85", 0], "vae": ["123:72", 0]},329 "class_type": "VAEEncode",330 "_meta": {"title": "VAE Encode"},331 },332 "123:84:77": {333 "inputs": {334 "conditioning": ["123:79:77", 0],335 "latent": ["123:84:78", 0],336 },337 "class_type": "ReferenceLatent",338 "_meta": {"title": "ReferenceLatent"},339 },340 "123:86": {341 "inputs": {"conditioning": ["123:74", 0]},342 "class_type": "ConditioningZeroOut",343 "_meta": {"title": "ConditioningZeroOut"},344 },345 "123:66": {346 "inputs": {347 "width": ["123:88", 0],348 "height": ["123:87", 0],349 "batch_size": 1,350 },351 "class_type": "EmptyFlux2LatentImage",352 "_meta": {"title": "Empty Flux 2 Latent"},353 },354 "123:81": {355 "inputs": {"image": ["123:80", 0]},356 "class_type": "GetImageSize",357 "_meta": {"title": "Get Image Size"},358 },359 "123:88": {360 "inputs": {"value": ["124", 0]},361 "class_type": "PrimitiveInt",362 "_meta": {"title": "Width"},363 },364 "123:87": {365 "inputs": {"value": ["124", 1]},366 "class_type": "PrimitiveInt",367 "_meta": {"title": "Height"},368 },369}370371372def aspect_ratio_to_string(ratio: float) -> str:373 """Convert a float w/h ratio to a W:H string."""374 frac = Fraction(ratio).limit_denominator(64)375 return f"{frac.numerator}:{frac.denominator}"376377378def upload_image_to_comfyui(local_path: str) -> str:379 """Upload a local image to ComfyUI and return the uploaded filename."""380 with open(local_path, "rb") as f:381 resp = requests.post(382 f"{COMFYUI_API_URL}/upload/image",383 files={"image": (os.path.basename(local_path), f, "image/png")},384 timeout=30,385 )386 resp.raise_for_status()387 data = resp.json()388 return data["name"]389390391def build_single_workflow(prompt: str, aspect_ratio: float, image_name: str) -> dict:392 """Build single-image edit workflow."""393 import copy394 wf = copy.deepcopy(WORKFLOW_SINGLE)395396 # Set prompt397 wf["75:74"]["inputs"]["text"] = prompt398399 # Set aspect ratio400 ratio_str = aspect_ratio_to_string(aspect_ratio)401 wf["120"]["inputs"]["custom_ratio"] = True402 wf["120"]["inputs"]["custom_aspect_ratio"] = ratio_str403404 # Set input image — swap VHS_LoadImagePath to LoadImage405 wf["132"] = {406 "inputs": {"image": image_name},407 "class_type": "LoadImage",408 "_meta": {"title": "Load Image"},409 }410411 # Randomize noise seed412 wf["75:73"]["inputs"]["noise_seed"] = random.randint(0, 2**31 - 1)413414 return wf415416417def build_multiple_workflow(prompt: str, aspect_ratio: float, image_name_1: str, image_name_2: str) -> dict:418 """Build multi-image edit workflow."""419 import copy420 wf = copy.deepcopy(WORKFLOW_MULTIPLE)421422 # Set prompt423 wf["123:74"]["inputs"]["text"] = prompt424425 # Set aspect ratio426 ratio_str = aspect_ratio_to_string(aspect_ratio)427 wf["124"]["inputs"]["custom_ratio"] = True428 wf["124"]["inputs"]["custom_aspect_ratio"] = ratio_str429430 # Set input images — swap VHS_LoadImagePath to LoadImage431 wf["134"] = {432 "inputs": {"image": image_name_1},433 "class_type": "LoadImage",434 "_meta": {"title": "Load Image"},435 }436 wf["135"] = {437 "inputs": {"image": image_name_2},438 "class_type": "LoadImage",439 "_meta": {"title": "Load Image (2)"},440 }441442 # Randomize noise seed443 wf["123:73"]["inputs"]["noise_seed"] = random.randint(0, 2**31 - 1)444445 return wf446447448def submit_prompt(workflow: dict) -> str:449 """Submit workflow to ComfyUI and return prompt_id."""450 resp = requests.post(451 f"{COMFYUI_API_URL}/prompt",452 json={"prompt": workflow},453 timeout=30,454 )455 if resp.status_code != 200:456 try:457 error_detail = resp.json()458 except Exception:459 error_detail = resp.text460 raise RuntimeError(461 f"ComfyUI /prompt returned {resp.status_code}: {json.dumps(error_detail, indent=2) if isinstance(error_detail, dict) else error_detail}"462 )463 data = resp.json()464465 # ComfyUI returns 200 even when nodes have validation errors466 node_errors = data.get("node_errors", {})467 if node_errors:468 raise RuntimeError(469 f"ComfyUI workflow has node errors: {json.dumps(node_errors, indent=2)}"470 )471472 return data["prompt_id"]473474475def wait_for_result(prompt_id: str, timeout: int = 600, poll_interval: int = 2) -> dict:476 """Poll ComfyUI history until the prompt completes with outputs."""477 deadline = time.time() + timeout478 empty_complete_retries = 0479 max_empty_retries = 3 # grace period for output serialization lag480481 while time.time() < deadline:482 resp = requests.get(483 f"{COMFYUI_API_URL}/history/{prompt_id}",484 timeout=10,485 )486 resp.raise_for_status()487 history = resp.json()488489 if prompt_id in history:490 prompt_data = history[prompt_id]491 status = prompt_data.get("status", {})492493 if status.get("status_str") == "error":494 messages = status.get("messages", [])495 raise RuntimeError(496 f"ComfyUI prompt failed: {json.dumps(messages, indent=2)}"497 )498499 if status.get("completed", False):500 if prompt_data.get("outputs"):501 return prompt_data502503 # Completed but no outputs — retry briefly for race condition504 empty_complete_retries += 1505 if empty_complete_retries >= max_empty_retries:506 raise RuntimeError(507 f"ComfyUI prompt completed but produced no outputs. "508 f"This usually means a node failed silently (missing custom node or model). "509 f"Status: {json.dumps(status, indent=2)}"510 )511512 time.sleep(poll_interval)513514 raise TimeoutError(f"ComfyUI prompt {prompt_id} did not complete within {timeout}s")515516517def download_output_image(prompt_data: dict, output_dir: str) -> str:518 """Download the generated image from ComfyUI."""519 outputs = prompt_data.get("outputs", {})520 for node_id, node_output in outputs.items():521 images = node_output.get("images", [])522 if images:523 img_info = images[0]524 filename = img_info["filename"]525 subfolder = img_info.get("subfolder", "")526 img_type = img_info.get("type", "output")527528 resp = requests.get(529 f"{COMFYUI_API_URL}/view",530 params={531 "filename": filename,532 "subfolder": subfolder,533 "type": img_type,534 },535 timeout=30,536 )537 resp.raise_for_status()538539 out_filename = f"edited_{filename}"540 out_path = os.path.join(output_dir, out_filename)541 with open(out_path, "wb") as f:542 f.write(resp.content)543544 return out_filename545546 raise RuntimeError(547 f"No output image found in ComfyUI response. Available outputs: {json.dumps(outputs, indent=2)}"548 )549550551def main():552 try:553 input_json = sys.stdin.read()554 execution_input = json.loads(input_json)555 inputs = execution_input.get("inputs", {})556557 images = inputs.get("images", [])558 prompt = inputs.get("prompt", "")559 aspect_ratio = float(inputs.get("aspect_ratio", 1.0))560561 # Normalize images to a list (single edge gives a string, array edge gives a list)562 if isinstance(images, str):563 images = [images]564565 if not prompt:566 raise ValueError("Prompt is required")567 if not (0.25 <= aspect_ratio <= 4.0):568 raise ValueError(f"Aspect ratio must be between 0.25 and 4.0, got {aspect_ratio}")569 if not images or len(images) == 0:570 raise ValueError("At least one input image is required")571 if len(images) > 2:572 raise ValueError("Maximum of 2 input images supported")573574 os.makedirs(OUTPUT_DIR, exist_ok=True)575576 # Upload input images to ComfyUI577 comfyui_image_names = []578 for img_filename in images:579 local_path = os.path.join(INPUT_DIR, img_filename)580 if not os.path.exists(local_path):581 raise FileNotFoundError(f"Input image not found: {local_path}")582 uploaded_name = upload_image_to_comfyui(local_path)583 comfyui_image_names.append(uploaded_name)584585 # Build workflow based on image count586 if len(comfyui_image_names) == 1:587 workflow = build_single_workflow(prompt, aspect_ratio, comfyui_image_names[0])588 else:589 workflow = build_multiple_workflow(590 prompt, aspect_ratio,591 comfyui_image_names[0], comfyui_image_names[1],592 )593594 # Submit and wait595 prompt_id = submit_prompt(workflow)596 prompt_data = wait_for_result(prompt_id)597 out_filename = download_output_image(prompt_data, OUTPUT_DIR)598599 # Log metadata to stderr600 workflow_type = "single" if len(comfyui_image_names) == 1 else "multiple"601 print(f"prompt_id={prompt_id}, aspect_ratio={aspect_ratio}, images={len(comfyui_image_names)}, workflow={workflow_type}", file=sys.stderr)602603 # Flat output — keys match OUTPUT_SCHEMA604 output = {605 "image": out_filename,606 }607 print(json.dumps(output, indent=2))608609 except Exception as e:610 error_output = {611 "error": str(e),612 "errorType": type(e).__name__,613 "traceback": traceback.format_exc(),614 }615 print(json.dumps(error_output), file=sys.stderr)616 sys.exit(1)617618619if __name__ == "__main__":620 main()