$ cat node-template.py
Image Creation Legacy
// Generates an image from a text prompt. Supports configurable aspect ratio (w/h). The prompt should describe the desired image in detail.
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")18OUTPUT_DIR = "/data/output"1920WORKFLOW = {21 "76": {22 "inputs": {"value": ""},23 "class_type": "PrimitiveStringMultiline",24 "_meta": {"title": "Prompt"},25 },26 "78": {27 "inputs": {"filename_prefix": "Flux2-Klein", "images": ["77:65", 0]},28 "class_type": "SaveImage",29 "_meta": {"title": "Save Image"},30 },31 "84": {32 "inputs": {33 "megapixel": "1.0",34 "aspect_ratio": "9:16 (Slim Vertical)",35 "divisible_by": "64",36 "custom_ratio": True,37 "custom_aspect_ratio": "1:1",38 },39 "class_type": "FluxResolutionNode",40 "_meta": {"title": "Flux Resolution Calc"},41 },42 "77:61": {43 "inputs": {"sampler_name": "euler"},44 "class_type": "KSamplerSelect",45 "_meta": {"title": "KSamplerSelect"},46 },47 "77:64": {48 "inputs": {49 "noise": ["77:73", 0],50 "guider": ["77:63", 0],51 "sampler": ["77:61", 0],52 "sigmas": ["77:62", 0],53 "latent_image": ["77:66", 0],54 },55 "class_type": "SamplerCustomAdvanced",56 "_meta": {"title": "SamplerCustomAdvanced"},57 },58 "77:65": {59 "inputs": {"samples": ["77:64", 0], "vae": ["77:72", 0]},60 "class_type": "VAEDecode",61 "_meta": {"title": "VAE Decode"},62 },63 "77:66": {64 "inputs": {65 "width": ["77:68", 0],66 "height": ["77:69", 0],67 "batch_size": 1,68 },69 "class_type": "EmptyFlux2LatentImage",70 "_meta": {"title": "Empty Flux 2 Latent"},71 },72 "77:68": {73 "inputs": {"value": ["84", 0]},74 "class_type": "PrimitiveInt",75 "_meta": {"title": "Width"},76 },77 "77:69": {78 "inputs": {"value": ["84", 1]},79 "class_type": "PrimitiveInt",80 "_meta": {"title": "Height"},81 },82 "77:73": {83 "inputs": {"noise_seed": 0},84 "class_type": "RandomNoise",85 "_meta": {"title": "RandomNoise"},86 },87 "77:70": {88 "inputs": {89 "unet_name": "flux-2-klein-9b-fp8.safetensors",90 "weight_dtype": "default",91 },92 "class_type": "UNETLoader",93 "_meta": {"title": "Load Diffusion Model"},94 },95 "77:71": {96 "inputs": {97 "clip_name": "qwen_3_8b_fp8mixed.safetensors",98 "type": "flux2",99 "device": "default",100 },101 "class_type": "CLIPLoader",102 "_meta": {"title": "Load CLIP"},103 },104 "77:72": {105 "inputs": {"vae_name": "flux2-vae.safetensors"},106 "class_type": "VAELoader",107 "_meta": {"title": "Load VAE"},108 },109 "77:63": {110 "inputs": {111 "cfg": 1,112 "model": ["77:70", 0],113 "positive": ["77:74", 0],114 "negative": ["77:76", 0],115 },116 "class_type": "CFGGuider",117 "_meta": {"title": "CFGGuider"},118 },119 "77:76": {120 "inputs": {"conditioning": ["77:74", 0]},121 "class_type": "ConditioningZeroOut",122 "_meta": {"title": "ConditioningZeroOut"},123 },124 "77:74": {125 "inputs": {"text": ["76", 0], "clip": ["77:71", 0]},126 "class_type": "CLIPTextEncode",127 "_meta": {"title": "CLIP Text Encode (Positive Prompt)"},128 },129 "77:62": {130 "inputs": {131 "steps": 4,132 "width": ["77:68", 0],133 "height": ["77:69", 0],134 },135 "class_type": "Flux2Scheduler",136 "_meta": {"title": "Flux2Scheduler"},137 },138}139140141def aspect_ratio_to_string(ratio: float) -> str:142 """Convert a float w/h ratio to a W:H string."""143 frac = Fraction(ratio).limit_denominator(64)144 return f"{frac.numerator}:{frac.denominator}"145146147def build_workflow(prompt: str, aspect_ratio: float, megapixel: float = 1.0) -> dict:148 """Inject user inputs into the workflow template."""149 import copy150 wf = copy.deepcopy(WORKFLOW)151152 # Set prompt153 wf["76"]["inputs"]["value"] = prompt154155 # Set aspect ratio156 ratio_str = aspect_ratio_to_string(aspect_ratio)157 wf["84"]["inputs"]["custom_ratio"] = True158 wf["84"]["inputs"]["custom_aspect_ratio"] = ratio_str159160 # Set megapixel161 wf["84"]["inputs"]["megapixel"] = str(megapixel)162163 # Randomize noise seed164 wf["77:73"]["inputs"]["noise_seed"] = random.randint(0, 2**31 - 1)165166 return wf167168169def submit_prompt(workflow: dict) -> str:170 """Submit workflow to ComfyUI and return prompt_id."""171 resp = requests.post(172 f"{COMFYUI_API_URL}/prompt",173 json={"prompt": workflow},174 timeout=30,175 )176 if resp.status_code != 200:177 try:178 error_detail = resp.json()179 except Exception:180 error_detail = resp.text181 raise RuntimeError(182 f"ComfyUI /prompt returned {resp.status_code}: {json.dumps(error_detail, indent=2) if isinstance(error_detail, dict) else error_detail}"183 )184 data = resp.json()185186 # ComfyUI returns 200 even when nodes have validation errors187 node_errors = data.get("node_errors", {})188 if node_errors:189 raise RuntimeError(190 f"ComfyUI workflow has node errors: {json.dumps(node_errors, indent=2)}"191 )192193 return data["prompt_id"]194195196def wait_for_result(prompt_id: str, timeout: int = 600, poll_interval: int = 2) -> dict:197 """Poll ComfyUI history until the prompt completes with outputs."""198 deadline = time.time() + timeout199 empty_complete_retries = 0200 max_empty_retries = 3 # grace period for output serialization lag201202 while time.time() < deadline:203 resp = requests.get(204 f"{COMFYUI_API_URL}/history/{prompt_id}",205 timeout=10,206 )207 resp.raise_for_status()208 history = resp.json()209210 if prompt_id in history:211 prompt_data = history[prompt_id]212 status = prompt_data.get("status", {})213214 if status.get("status_str") == "error":215 messages = status.get("messages", [])216 raise RuntimeError(217 f"ComfyUI prompt failed: {json.dumps(messages, indent=2)}"218 )219220 if status.get("completed", False):221 if prompt_data.get("outputs"):222 return prompt_data223224 # Completed but no outputs — retry briefly for race condition225 empty_complete_retries += 1226 if empty_complete_retries >= max_empty_retries:227 raise RuntimeError(228 f"ComfyUI prompt completed but produced no outputs. "229 f"This usually means a node failed silently (missing custom node or model). "230 f"Status: {json.dumps(status, indent=2)}"231 )232233 time.sleep(poll_interval)234235 raise TimeoutError(f"ComfyUI prompt {prompt_id} did not complete within {timeout}s")236237238def download_output_image(prompt_data: dict, output_dir: str) -> str:239 """Download the generated image from ComfyUI."""240 outputs = prompt_data.get("outputs", {})241 for node_id, node_output in outputs.items():242 images = node_output.get("images", [])243 if images:244 img_info = images[0]245 filename = img_info["filename"]246 subfolder = img_info.get("subfolder", "")247 img_type = img_info.get("type", "output")248249 resp = requests.get(250 f"{COMFYUI_API_URL}/view",251 params={252 "filename": filename,253 "subfolder": subfolder,254 "type": img_type,255 },256 timeout=30,257 )258 resp.raise_for_status()259260 out_filename = f"generated_{filename}"261 out_path = os.path.join(output_dir, out_filename)262 with open(out_path, "wb") as f:263 f.write(resp.content)264265 return out_filename266267 raise RuntimeError(268 f"No output image found in ComfyUI response. Available outputs: {json.dumps(outputs, indent=2)}"269 )270271272def main():273 try:274 input_json = sys.stdin.read()275 execution_input = json.loads(input_json)276 inputs = execution_input.get("inputs", {})277278 prompt = inputs.get("prompt", "")279 aspect_ratio = float(inputs.get("aspect_ratio", 1.0))280 megapixel = float(inputs.get("megapixel", 1.0))281282 if not prompt:283 raise ValueError("Prompt is required")284 if not (0.25 <= aspect_ratio <= 4.0):285 raise ValueError(f"Aspect ratio must be between 0.25 and 4.0, got {aspect_ratio}")286 if not (0.25 <= megapixel <= 4.0):287 raise ValueError(f"Megapixel must be between 0.25 and 4.0, got {megapixel}")288289 os.makedirs(OUTPUT_DIR, exist_ok=True)290291 # Build and submit workflow292 workflow = build_workflow(prompt, aspect_ratio, megapixel)293 prompt_id = submit_prompt(workflow)294295 # Wait for completion and download result296 prompt_data = wait_for_result(prompt_id)297 out_filename = download_output_image(prompt_data, OUTPUT_DIR)298299 # Log metadata to stderr300 print(f"prompt_id={prompt_id}, aspect_ratio={aspect_ratio}, megapixel={megapixel}", file=sys.stderr)301302 # Flat output — keys match OUTPUT_SCHEMA303 output = {304 "image": out_filename,305 }306 print(json.dumps(output, indent=2))307308 except Exception as e:309 error_output = {310 "error": str(e),311 "errorType": type(e).__name__,312 "traceback": traceback.format_exc(),313 }314 print(json.dumps(error_output), file=sys.stderr)315 sys.exit(1)316317318if __name__ == "__main__":319 main()