Add API scripts (upload/download/run/convert) and API-format headswap workflow
Browse files
workflows/api/head-swap-flux-klein.json
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"109": {
|
| 3 |
+
"class_type": "SamplerCustomAdvanced",
|
| 4 |
+
"inputs": {
|
| 5 |
+
"guider": [
|
| 6 |
+
"123",
|
| 7 |
+
0
|
| 8 |
+
],
|
| 9 |
+
"latent_image": [
|
| 10 |
+
"113",
|
| 11 |
+
0
|
| 12 |
+
],
|
| 13 |
+
"noise": [
|
| 14 |
+
"110",
|
| 15 |
+
0
|
| 16 |
+
],
|
| 17 |
+
"sampler": [
|
| 18 |
+
"125",
|
| 19 |
+
0
|
| 20 |
+
],
|
| 21 |
+
"sigmas": [
|
| 22 |
+
"122",
|
| 23 |
+
0
|
| 24 |
+
]
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"110": {
|
| 28 |
+
"class_type": "RandomNoise",
|
| 29 |
+
"inputs": {
|
| 30 |
+
"noise_seed": 432262096973493
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"111": {
|
| 34 |
+
"class_type": "VAELoader",
|
| 35 |
+
"inputs": {
|
| 36 |
+
"vae_name": "flux2-vae.safetensors"
|
| 37 |
+
}
|
| 38 |
+
},
|
| 39 |
+
"112": {
|
| 40 |
+
"class_type": "GetImageSize",
|
| 41 |
+
"inputs": {
|
| 42 |
+
"image": [
|
| 43 |
+
"120",
|
| 44 |
+
0
|
| 45 |
+
]
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"113": {
|
| 49 |
+
"class_type": "EmptyFlux2LatentImage",
|
| 50 |
+
"inputs": {
|
| 51 |
+
"batch_size": 1,
|
| 52 |
+
"height": [
|
| 53 |
+
"112",
|
| 54 |
+
1
|
| 55 |
+
],
|
| 56 |
+
"width": [
|
| 57 |
+
"112",
|
| 58 |
+
0
|
| 59 |
+
]
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
"114:76": {
|
| 63 |
+
"class_type": "ReferenceLatent",
|
| 64 |
+
"inputs": {
|
| 65 |
+
"conditioning": [
|
| 66 |
+
"116:76",
|
| 67 |
+
0
|
| 68 |
+
],
|
| 69 |
+
"latent": [
|
| 70 |
+
"114:78",
|
| 71 |
+
0
|
| 72 |
+
]
|
| 73 |
+
}
|
| 74 |
+
},
|
| 75 |
+
"114:77": {
|
| 76 |
+
"class_type": "ReferenceLatent",
|
| 77 |
+
"inputs": {
|
| 78 |
+
"conditioning": [
|
| 79 |
+
"116:77",
|
| 80 |
+
0
|
| 81 |
+
],
|
| 82 |
+
"latent": [
|
| 83 |
+
"114:78",
|
| 84 |
+
0
|
| 85 |
+
]
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
"114:78": {
|
| 89 |
+
"class_type": "VAEEncode",
|
| 90 |
+
"inputs": {
|
| 91 |
+
"pixels": [
|
| 92 |
+
"121",
|
| 93 |
+
0
|
| 94 |
+
],
|
| 95 |
+
"vae": [
|
| 96 |
+
"111",
|
| 97 |
+
0
|
| 98 |
+
]
|
| 99 |
+
}
|
| 100 |
+
},
|
| 101 |
+
"115": {
|
| 102 |
+
"class_type": "CLIPLoader",
|
| 103 |
+
"inputs": {
|
| 104 |
+
"clip_name": "qwen_3_8b_fp8mixed.safetensors",
|
| 105 |
+
"device": "default",
|
| 106 |
+
"type": "flux2"
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
"116:76": {
|
| 110 |
+
"class_type": "ReferenceLatent",
|
| 111 |
+
"inputs": {
|
| 112 |
+
"conditioning": [
|
| 113 |
+
"119",
|
| 114 |
+
0
|
| 115 |
+
],
|
| 116 |
+
"latent": [
|
| 117 |
+
"116:78",
|
| 118 |
+
0
|
| 119 |
+
]
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
"116:77": {
|
| 123 |
+
"class_type": "ReferenceLatent",
|
| 124 |
+
"inputs": {
|
| 125 |
+
"conditioning": [
|
| 126 |
+
"117",
|
| 127 |
+
0
|
| 128 |
+
],
|
| 129 |
+
"latent": [
|
| 130 |
+
"116:78",
|
| 131 |
+
0
|
| 132 |
+
]
|
| 133 |
+
}
|
| 134 |
+
},
|
| 135 |
+
"116:78": {
|
| 136 |
+
"class_type": "VAEEncode",
|
| 137 |
+
"inputs": {
|
| 138 |
+
"pixels": [
|
| 139 |
+
"120",
|
| 140 |
+
0
|
| 141 |
+
],
|
| 142 |
+
"vae": [
|
| 143 |
+
"111",
|
| 144 |
+
0
|
| 145 |
+
]
|
| 146 |
+
}
|
| 147 |
+
},
|
| 148 |
+
"117": {
|
| 149 |
+
"_meta": {
|
| 150 |
+
"title": "CLIP Text Encode (Positive Prompt)"
|
| 151 |
+
},
|
| 152 |
+
"class_type": "CLIPTextEncode",
|
| 153 |
+
"inputs": {
|
| 154 |
+
"clip": [
|
| 155 |
+
"115",
|
| 156 |
+
0
|
| 157 |
+
],
|
| 158 |
+
"text": "head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. remove the head from Picture 1 completely and replace it with the head from Picture 2, strictly preserving the hair, eye color, nose structure of Picture 2. copy the direction of the eye, head rotation, micro expressions from Picture 1, high quality, sharp details, 4k.\n\nDescribe the expression in Picture 1 and copy it to the new image.\n"
|
| 159 |
+
}
|
| 160 |
+
},
|
| 161 |
+
"118": {
|
| 162 |
+
"class_type": "UNETLoader",
|
| 163 |
+
"inputs": {
|
| 164 |
+
"unet_name": "flux-2-klein-9b.safetensors",
|
| 165 |
+
"weight_dtype": "default"
|
| 166 |
+
}
|
| 167 |
+
},
|
| 168 |
+
"119": {
|
| 169 |
+
"_meta": {
|
| 170 |
+
"title": "CLIP Text Encode ( Negative Prompt)"
|
| 171 |
+
},
|
| 172 |
+
"class_type": "CLIPTextEncode",
|
| 173 |
+
"inputs": {
|
| 174 |
+
"clip": [
|
| 175 |
+
"115",
|
| 176 |
+
0
|
| 177 |
+
],
|
| 178 |
+
"text": "bad quality, noise, blurry, worst quality, low resolution, blur, distortion, unnatural blending, cartoon, illustration, painting"
|
| 179 |
+
}
|
| 180 |
+
},
|
| 181 |
+
"120": {
|
| 182 |
+
"class_type": "ResizeImageMaskNode",
|
| 183 |
+
"inputs": {
|
| 184 |
+
"input": [
|
| 185 |
+
"76",
|
| 186 |
+
0
|
| 187 |
+
],
|
| 188 |
+
"resize_type": "scale total pixels",
|
| 189 |
+
"resize_type.megapixels": 1.5,
|
| 190 |
+
"scale_method": "lanczos"
|
| 191 |
+
}
|
| 192 |
+
},
|
| 193 |
+
"121": {
|
| 194 |
+
"class_type": "ResizeImageMaskNode",
|
| 195 |
+
"inputs": {
|
| 196 |
+
"input": [
|
| 197 |
+
"187",
|
| 198 |
+
0
|
| 199 |
+
],
|
| 200 |
+
"resize_type": "scale total pixels",
|
| 201 |
+
"resize_type.megapixels": 1.5,
|
| 202 |
+
"scale_method": "lanczos"
|
| 203 |
+
}
|
| 204 |
+
},
|
| 205 |
+
"122": {
|
| 206 |
+
"class_type": "Flux2Scheduler",
|
| 207 |
+
"inputs": {
|
| 208 |
+
"height": [
|
| 209 |
+
"112",
|
| 210 |
+
1
|
| 211 |
+
],
|
| 212 |
+
"steps": 8,
|
| 213 |
+
"width": [
|
| 214 |
+
"112",
|
| 215 |
+
0
|
| 216 |
+
]
|
| 217 |
+
}
|
| 218 |
+
},
|
| 219 |
+
"123": {
|
| 220 |
+
"class_type": "CFGGuider",
|
| 221 |
+
"inputs": {
|
| 222 |
+
"cfg": 1,
|
| 223 |
+
"model": [
|
| 224 |
+
"190",
|
| 225 |
+
0
|
| 226 |
+
],
|
| 227 |
+
"negative": [
|
| 228 |
+
"114:76",
|
| 229 |
+
0
|
| 230 |
+
],
|
| 231 |
+
"positive": [
|
| 232 |
+
"114:77",
|
| 233 |
+
0
|
| 234 |
+
]
|
| 235 |
+
}
|
| 236 |
+
},
|
| 237 |
+
"125": {
|
| 238 |
+
"class_type": "KSamplerSelect",
|
| 239 |
+
"inputs": {
|
| 240 |
+
"sampler_name": "lcm"
|
| 241 |
+
}
|
| 242 |
+
},
|
| 243 |
+
"131": {
|
| 244 |
+
"class_type": "VAEDecode",
|
| 245 |
+
"inputs": {
|
| 246 |
+
"samples": [
|
| 247 |
+
"109",
|
| 248 |
+
0
|
| 249 |
+
],
|
| 250 |
+
"vae": [
|
| 251 |
+
"111",
|
| 252 |
+
0
|
| 253 |
+
]
|
| 254 |
+
}
|
| 255 |
+
},
|
| 256 |
+
"133": {
|
| 257 |
+
"class_type": "LoraLoaderModelOnly",
|
| 258 |
+
"inputs": {
|
| 259 |
+
"lora_name": "flux2/klein/bfs_head_v1_flux-klein_9b_step3500_rank128.safetensors",
|
| 260 |
+
"model": [
|
| 261 |
+
"118",
|
| 262 |
+
0
|
| 263 |
+
],
|
| 264 |
+
"strength_model": 1
|
| 265 |
+
}
|
| 266 |
+
},
|
| 267 |
+
"160": {
|
| 268 |
+
"class_type": "ImageCropByMask",
|
| 269 |
+
"inputs": {
|
| 270 |
+
"image": [
|
| 271 |
+
"81",
|
| 272 |
+
0
|
| 273 |
+
],
|
| 274 |
+
"mask": [
|
| 275 |
+
"81",
|
| 276 |
+
1
|
| 277 |
+
]
|
| 278 |
+
}
|
| 279 |
+
},
|
| 280 |
+
"163": {
|
| 281 |
+
"class_type": "SaveImage",
|
| 282 |
+
"inputs": {
|
| 283 |
+
"filename_prefix": "headswap_flux_klein_optimized",
|
| 284 |
+
"images": [
|
| 285 |
+
"131",
|
| 286 |
+
0
|
| 287 |
+
]
|
| 288 |
+
}
|
| 289 |
+
},
|
| 290 |
+
"176": {
|
| 291 |
+
"class_type": "ImageConcanate",
|
| 292 |
+
"inputs": {
|
| 293 |
+
"direction": "down",
|
| 294 |
+
"image1": [
|
| 295 |
+
"120",
|
| 296 |
+
0
|
| 297 |
+
],
|
| 298 |
+
"image2": [
|
| 299 |
+
"121",
|
| 300 |
+
0
|
| 301 |
+
],
|
| 302 |
+
"match_image_size": true
|
| 303 |
+
}
|
| 304 |
+
},
|
| 305 |
+
"177": {
|
| 306 |
+
"class_type": "ImageConcanate",
|
| 307 |
+
"inputs": {
|
| 308 |
+
"direction": "right",
|
| 309 |
+
"image1": [
|
| 310 |
+
"176",
|
| 311 |
+
0
|
| 312 |
+
],
|
| 313 |
+
"image2": [
|
| 314 |
+
"131",
|
| 315 |
+
0
|
| 316 |
+
],
|
| 317 |
+
"match_image_size": true
|
| 318 |
+
}
|
| 319 |
+
},
|
| 320 |
+
"178": {
|
| 321 |
+
"class_type": "PreviewImage",
|
| 322 |
+
"inputs": {
|
| 323 |
+
"images": [
|
| 324 |
+
"177",
|
| 325 |
+
0
|
| 326 |
+
]
|
| 327 |
+
}
|
| 328 |
+
},
|
| 329 |
+
"187": {
|
| 330 |
+
"class_type": "RMBG",
|
| 331 |
+
"inputs": {
|
| 332 |
+
"background": "Alpha",
|
| 333 |
+
"image": [
|
| 334 |
+
"81",
|
| 335 |
+
0
|
| 336 |
+
],
|
| 337 |
+
"invert_output": false,
|
| 338 |
+
"mask_blur": 0,
|
| 339 |
+
"mask_offset": 0,
|
| 340 |
+
"model": "RMBG-2.0",
|
| 341 |
+
"process_res": 1024,
|
| 342 |
+
"refine_foreground": false,
|
| 343 |
+
"sensitivity": 1
|
| 344 |
+
}
|
| 345 |
+
},
|
| 346 |
+
"190": {
|
| 347 |
+
"_meta": {
|
| 348 |
+
"title": "Lissie lora"
|
| 349 |
+
},
|
| 350 |
+
"class_type": "LoraLoaderModelOnly",
|
| 351 |
+
"inputs": {
|
| 352 |
+
"lora_name": "lissie_flux2_klein.safetensors",
|
| 353 |
+
"model": [
|
| 354 |
+
"133",
|
| 355 |
+
0
|
| 356 |
+
],
|
| 357 |
+
"strength_model": 1
|
| 358 |
+
}
|
| 359 |
+
},
|
| 360 |
+
"76": {
|
| 361 |
+
"_meta": {
|
| 362 |
+
"title": "Body Reference"
|
| 363 |
+
},
|
| 364 |
+
"class_type": "LoadImage",
|
| 365 |
+
"inputs": {
|
| 366 |
+
"image": "general_image.jpg"
|
| 367 |
+
}
|
| 368 |
+
},
|
| 369 |
+
"81": {
|
| 370 |
+
"_meta": {
|
| 371 |
+
"title": "Face Reference"
|
| 372 |
+
},
|
| 373 |
+
"class_type": "LoadImage",
|
| 374 |
+
"inputs": {
|
| 375 |
+
"image": "reference_face.JPG"
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
}
|
workflows/scripts/convert_workflow.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Convert a ComfyUI workflow from UI format (Save/Workflow menu) to API format.
|
| 3 |
+
|
| 4 |
+
Equivalent to the frontend's Workflow > Export (API), but headless. Needs a
|
| 5 |
+
running ComfyUI instance because widget-to-input mapping comes from its
|
| 6 |
+
GET /object_info (so the instance must have the same custom nodes installed
|
| 7 |
+
as the workflow uses).
|
| 8 |
+
|
| 9 |
+
Handles: subgraphs (flattened, ids become "<instance>:<inner>"), Reroute
|
| 10 |
+
splicing, bypassed (mode 4) nodes with type-matched pass-through, muted
|
| 11 |
+
(mode 2) nodes, widget-converted-to-input entries, and the phantom
|
| 12 |
+
control_after_generate value that follows every seed widget.
|
| 13 |
+
|
| 14 |
+
Example:
|
| 15 |
+
python convert_workflow.py --url http://127.0.0.1:7865 \
|
| 16 |
+
../head-swap-flux-klein.json -o ../api/head-swap-flux-klein.json
|
| 17 |
+
"""
|
| 18 |
+
import argparse
|
| 19 |
+
import json
|
| 20 |
+
import pathlib
|
| 21 |
+
import sys
|
| 22 |
+
|
| 23 |
+
import requests
|
| 24 |
+
|
| 25 |
+
VIRTUAL_TYPES = {"Reroute", "Note", "MarkdownNote", "PrimitiveNode"}
|
| 26 |
+
WIDGET_SCALAR_TYPES = {"INT", "FLOAT", "STRING", "BOOLEAN", "COMBO"}
|
| 27 |
+
SUBGRAPH_INPUT_ID = -10
|
| 28 |
+
SUBGRAPH_OUTPUT_ID = -20
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Scope:
|
| 32 |
+
"""One graph namespace: either the root workflow or a subgraph instance."""
|
| 33 |
+
|
| 34 |
+
def __init__(self, graph, prefix="", parent=None, instance_id=None):
|
| 35 |
+
self.nodes = {n["id"]: n for n in graph.get("nodes", [])}
|
| 36 |
+
self.links = {}
|
| 37 |
+
for l in graph.get("links", []):
|
| 38 |
+
if isinstance(l, dict): # subgraph defs store links as objects
|
| 39 |
+
self.links[l["id"]] = (l["origin_id"], l["origin_slot"], l["target_id"], l["target_slot"])
|
| 40 |
+
else: # root stores [id, src, src_slot, dst, dst_slot, type]
|
| 41 |
+
self.links[l[0]] = (l[1], l[2], l[3], l[4])
|
| 42 |
+
self.prefix = prefix
|
| 43 |
+
self.parent = parent # (parent Scope, instance node id) or None
|
| 44 |
+
self.instance_id = instance_id
|
| 45 |
+
self.children = {} # instance node id -> child Scope
|
| 46 |
+
|
| 47 |
+
def fid(self, node_id):
|
| 48 |
+
return f"{self.prefix}{node_id}"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class Converter:
|
| 52 |
+
def __init__(self, workflow, object_info):
|
| 53 |
+
self.oi = object_info
|
| 54 |
+
self.subgraph_defs = {
|
| 55 |
+
sg["id"]: sg for sg in workflow.get("definitions", {}).get("subgraphs", [])
|
| 56 |
+
}
|
| 57 |
+
self.root = Scope(workflow)
|
| 58 |
+
self._build_children(self.root)
|
| 59 |
+
|
| 60 |
+
def _build_children(self, scope):
|
| 61 |
+
for nid, node in scope.nodes.items():
|
| 62 |
+
sg = self.subgraph_defs.get(node["type"])
|
| 63 |
+
if sg is not None:
|
| 64 |
+
child = Scope(sg, prefix=f"{scope.fid(nid)}:", parent=(scope, nid), instance_id=nid)
|
| 65 |
+
scope.children[nid] = child
|
| 66 |
+
self._build_children(child)
|
| 67 |
+
|
| 68 |
+
def all_scopes(self):
|
| 69 |
+
stack = [self.root]
|
| 70 |
+
while stack:
|
| 71 |
+
s = stack.pop()
|
| 72 |
+
yield s
|
| 73 |
+
stack.extend(s.children.values())
|
| 74 |
+
|
| 75 |
+
def resolve(self, scope, link_id):
|
| 76 |
+
"""Follow a link upstream to a real executable (node_fid, output_slot), or None."""
|
| 77 |
+
if link_id is None or link_id not in scope.links:
|
| 78 |
+
return None
|
| 79 |
+
origin_id, origin_slot, _, _ = scope.links[link_id]
|
| 80 |
+
|
| 81 |
+
if origin_id == SUBGRAPH_INPUT_ID: # crossed into this subgraph from outside
|
| 82 |
+
parent_scope, instance_id = scope.parent
|
| 83 |
+
instance = parent_scope.nodes[instance_id]
|
| 84 |
+
outer_link = instance["inputs"][origin_slot].get("link")
|
| 85 |
+
return self.resolve(parent_scope, outer_link)
|
| 86 |
+
|
| 87 |
+
node = scope.nodes[origin_id]
|
| 88 |
+
ntype = node["type"]
|
| 89 |
+
|
| 90 |
+
if ntype in self.subgraph_defs: # source is a subgraph instance: descend
|
| 91 |
+
child = scope.children[origin_id]
|
| 92 |
+
for lid, (_, _, tid, tslot) in child.links.items():
|
| 93 |
+
if tid == SUBGRAPH_OUTPUT_ID and tslot == origin_slot:
|
| 94 |
+
return self.resolve(child, lid)
|
| 95 |
+
return None
|
| 96 |
+
|
| 97 |
+
if ntype == "Reroute":
|
| 98 |
+
return self.resolve(scope, node["inputs"][0].get("link"))
|
| 99 |
+
|
| 100 |
+
mode = node.get("mode", 0)
|
| 101 |
+
if mode == 2: # muted: acts as removed
|
| 102 |
+
return None
|
| 103 |
+
if mode == 4: # bypassed: pass through first linked input of matching type
|
| 104 |
+
out_type = node["outputs"][origin_slot].get("type", "*")
|
| 105 |
+
for inp in node.get("inputs", []):
|
| 106 |
+
if inp.get("link") is None:
|
| 107 |
+
continue
|
| 108 |
+
in_type = inp.get("type", "*")
|
| 109 |
+
if in_type == out_type or "*" in (in_type, out_type):
|
| 110 |
+
return self.resolve(scope, inp["link"])
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
return (scope.fid(origin_id), origin_slot)
|
| 114 |
+
|
| 115 |
+
def widget_inputs(self, node):
|
| 116 |
+
"""Map a node's widgets_values onto named inputs using /object_info order."""
|
| 117 |
+
spec = self.oi[node["type"]].get("input", {})
|
| 118 |
+
ordered = list(spec.get("required", {}).items()) + list(spec.get("optional", {}).items())
|
| 119 |
+
values = node.get("widgets_values") or []
|
| 120 |
+
if isinstance(values, dict): # some packs (rgthree) serialize by name
|
| 121 |
+
widget_names = {name for name, s in ordered if self._is_widget(s)}
|
| 122 |
+
return {k: v for k, v in values.items() if k in widget_names}
|
| 123 |
+
result = {}
|
| 124 |
+
self._consume_widgets(ordered, list(values), result)
|
| 125 |
+
return result
|
| 126 |
+
|
| 127 |
+
def _consume_widgets(self, ordered, queue, result, prefix=""):
|
| 128 |
+
for name, s in ordered:
|
| 129 |
+
if not queue:
|
| 130 |
+
return
|
| 131 |
+
opts = s[1] if len(s) > 1 and isinstance(s[1], dict) else {}
|
| 132 |
+
if s[0] == "COMFY_DYNAMICCOMBO_V3":
|
| 133 |
+
# the selection is one widget value; the chosen option pulls in
|
| 134 |
+
# nested inputs addressed with dotted ids ("resize_type.megapixels")
|
| 135 |
+
selected = queue.pop(0)
|
| 136 |
+
result[f"{prefix}{name}"] = selected
|
| 137 |
+
option = next((o for o in opts.get("options", []) if o["key"] == selected), None)
|
| 138 |
+
if option:
|
| 139 |
+
inner = option["inputs"]
|
| 140 |
+
inner_ordered = (list(inner.get("required", {}).items())
|
| 141 |
+
+ list(inner.get("optional", {}).items()))
|
| 142 |
+
self._consume_widgets(inner_ordered, queue, result, prefix=f"{prefix}{name}.")
|
| 143 |
+
elif self._is_widget(s):
|
| 144 |
+
result[f"{prefix}{name}"] = queue.pop(0)
|
| 145 |
+
if opts.get("control_after_generate") and queue:
|
| 146 |
+
queue.pop(0) # discard the UI-only fixed/increment/randomize widget
|
| 147 |
+
|
| 148 |
+
@staticmethod
|
| 149 |
+
def _is_widget(s):
|
| 150 |
+
kind = s[0]
|
| 151 |
+
opts = s[1] if len(s) > 1 and isinstance(s[1], dict) else {}
|
| 152 |
+
if opts.get("forceInput"):
|
| 153 |
+
return False
|
| 154 |
+
return isinstance(kind, list) or kind in WIDGET_SCALAR_TYPES
|
| 155 |
+
|
| 156 |
+
def convert(self):
|
| 157 |
+
prompt = {}
|
| 158 |
+
for scope in self.all_scopes():
|
| 159 |
+
for nid, node in scope.nodes.items():
|
| 160 |
+
ntype = node["type"]
|
| 161 |
+
if (ntype in VIRTUAL_TYPES or ntype in self.subgraph_defs
|
| 162 |
+
or node.get("mode", 0) in (2, 4)):
|
| 163 |
+
continue
|
| 164 |
+
if ntype not in self.oi:
|
| 165 |
+
raise SystemExit(
|
| 166 |
+
f"ERROR: node {scope.fid(nid)} has type {ntype!r} which the ComfyUI "
|
| 167 |
+
"instance does not know -- install its custom nodes first")
|
| 168 |
+
inputs = self.widget_inputs(node)
|
| 169 |
+
for inp in node.get("inputs", []):
|
| 170 |
+
source = self.resolve(scope, inp.get("link"))
|
| 171 |
+
if source is not None:
|
| 172 |
+
inputs[inp["name"]] = list(source)
|
| 173 |
+
elif inp.get("link") is not None and not inp.get("widget"):
|
| 174 |
+
print(f"warning: {scope.fid(nid)} ({ntype}) input {inp['name']!r} "
|
| 175 |
+
"leads to a muted/bypassed dead end; leaving unconnected",
|
| 176 |
+
file=sys.stderr)
|
| 177 |
+
entry = {"class_type": ntype, "inputs": inputs}
|
| 178 |
+
title = node.get("title")
|
| 179 |
+
if title:
|
| 180 |
+
entry["_meta"] = {"title": title}
|
| 181 |
+
prompt[scope.fid(nid)] = entry
|
| 182 |
+
return prompt
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def main():
|
| 186 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 187 |
+
ap.add_argument("--url", required=True, help="ComfyUI base URL (source of /object_info)")
|
| 188 |
+
ap.add_argument("workflow", help="UI-format workflow JSON")
|
| 189 |
+
ap.add_argument("-o", "--output", help="where to write the API JSON (default: stdout)")
|
| 190 |
+
args = ap.parse_args()
|
| 191 |
+
|
| 192 |
+
wf = json.loads(pathlib.Path(args.workflow).read_text())
|
| 193 |
+
if "nodes" not in wf:
|
| 194 |
+
sys.exit("ERROR: input already looks like API format (no 'nodes' array)")
|
| 195 |
+
|
| 196 |
+
object_info = requests.get(f"{args.url.rstrip('/')}/object_info", timeout=120).json()
|
| 197 |
+
prompt = Converter(wf, object_info).convert()
|
| 198 |
+
out = json.dumps(prompt, indent=2, sort_keys=True)
|
| 199 |
+
if args.output:
|
| 200 |
+
pathlib.Path(args.output).write_text(out + "\n")
|
| 201 |
+
print(f"wrote {args.output} ({len(prompt)} nodes)", file=sys.stderr)
|
| 202 |
+
else:
|
| 203 |
+
print(out)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
if __name__ == "__main__":
|
| 207 |
+
main()
|
workflows/scripts/download_files.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Download files from a ComfyUI instance's output (or input/temp) folder.
|
| 3 |
+
|
| 4 |
+
Uses ComfyUI's built-in GET /view endpoint. File names may include a
|
| 5 |
+
subfolder ("qwen_edit/2026-07-07/img_00001_.webp") -- the same strings
|
| 6 |
+
run_workflow.py prints when a job finishes. Prints the local path of each
|
| 7 |
+
downloaded file to stdout.
|
| 8 |
+
|
| 9 |
+
Examples:
|
| 10 |
+
python download_files.py --url https://POD-7865.proxy.runpod.net headswap_00001_.png
|
| 11 |
+
python download_files.py --url http://127.0.0.1:7865 --out results a/x.png b/y.png
|
| 12 |
+
"""
|
| 13 |
+
import argparse
|
| 14 |
+
import pathlib
|
| 15 |
+
import sys
|
| 16 |
+
|
| 17 |
+
import requests
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def download_file(base_url, name, folder_type="output", out_dir=".", timeout=300):
|
| 21 |
+
"""Download one file by its ComfyUI reference name; returns the local path."""
|
| 22 |
+
ref = pathlib.PurePosixPath(name)
|
| 23 |
+
r = requests.get(
|
| 24 |
+
f"{base_url.rstrip('/')}/view",
|
| 25 |
+
params={
|
| 26 |
+
"filename": ref.name,
|
| 27 |
+
"subfolder": str(ref.parent) if str(ref.parent) != "." else "",
|
| 28 |
+
"type": folder_type,
|
| 29 |
+
},
|
| 30 |
+
timeout=timeout,
|
| 31 |
+
)
|
| 32 |
+
r.raise_for_status()
|
| 33 |
+
dest = pathlib.Path(out_dir) / name
|
| 34 |
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
| 35 |
+
dest.write_bytes(r.content)
|
| 36 |
+
return dest
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def main():
|
| 40 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 41 |
+
ap.add_argument("--url", required=True, help="ComfyUI base URL, e.g. https://POD-7865.proxy.runpod.net")
|
| 42 |
+
ap.add_argument("--type", default="output", choices=["output", "input", "temp"],
|
| 43 |
+
help="which ComfyUI folder to read from (default: output)")
|
| 44 |
+
ap.add_argument("--out", default=".", help="local directory to save into (default: cwd)")
|
| 45 |
+
ap.add_argument("names", nargs="+", help='file names, optionally with subfolder ("sub/file.png")')
|
| 46 |
+
args = ap.parse_args()
|
| 47 |
+
|
| 48 |
+
failed = False
|
| 49 |
+
for name in args.names:
|
| 50 |
+
try:
|
| 51 |
+
dest = download_file(args.url, name, args.type, args.out)
|
| 52 |
+
except (OSError, requests.RequestException) as e:
|
| 53 |
+
print(f"ERROR downloading {name}: {e}", file=sys.stderr)
|
| 54 |
+
failed = True
|
| 55 |
+
continue
|
| 56 |
+
print(f"downloaded {args.type}/{name} -> {dest} ({dest.stat().st_size} bytes)", file=sys.stderr)
|
| 57 |
+
print(dest)
|
| 58 |
+
sys.exit(1 if failed else 0)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
if __name__ == "__main__":
|
| 62 |
+
main()
|
workflows/scripts/run_workflow.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run an API-format ComfyUI workflow synchronously and wait for it to finish.
|
| 3 |
+
|
| 4 |
+
Queues the workflow via POST /prompt, polls GET /history/{prompt_id} until the
|
| 5 |
+
job completes, then prints one reference name per produced output file to
|
| 6 |
+
stdout ("file.png" or "subfolder/file.png") -- feed those straight to
|
| 7 |
+
download_files.py. Exits non-zero on validation or execution failure, with
|
| 8 |
+
ComfyUI's error details on stderr.
|
| 9 |
+
|
| 10 |
+
The workflow must be API format (flat {"<id>": {"class_type": ...}} dict).
|
| 11 |
+
Export one from the UI with Workflow > Export (API), or convert a saved UI
|
| 12 |
+
JSON with convert_workflow.py.
|
| 13 |
+
|
| 14 |
+
Runtime patching (so workflow files stay generic):
|
| 15 |
+
--set NODE_ID.INPUT=VALUE override any node input, e.g. --set 76.image=body.jpg
|
| 16 |
+
(VALUE is parsed as JSON when possible, else kept as string)
|
| 17 |
+
--prefix PREFIX set filename_prefix on every node that has one,
|
| 18 |
+
e.g. --prefix "headswap/%year%%month%%day%-%hour%%minute%%second%"
|
| 19 |
+
--randomize-seeds randomize every seed/noise_seed input (on by default;
|
| 20 |
+
disable with --keep-seeds for reproducible reruns)
|
| 21 |
+
|
| 22 |
+
Example:
|
| 23 |
+
python run_workflow.py --url https://POD-7865.proxy.runpod.net \
|
| 24 |
+
../api/head-swap-flux-klein.json \
|
| 25 |
+
--set 76.image=body.jpg --set 81.image=head.jpg \
|
| 26 |
+
--prefix "headswap/%year%%month%%day%-%hour%%minute%%second%"
|
| 27 |
+
"""
|
| 28 |
+
import argparse
|
| 29 |
+
import json
|
| 30 |
+
import pathlib
|
| 31 |
+
import random
|
| 32 |
+
import sys
|
| 33 |
+
import time
|
| 34 |
+
import uuid
|
| 35 |
+
|
| 36 |
+
import requests
|
| 37 |
+
|
| 38 |
+
SEED_INPUT_NAMES = ("seed", "noise_seed")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def parse_set(spec):
|
| 42 |
+
"""'76.image=body.jpg' -> ('76', 'image', 'body.jpg'). Values parse as JSON when possible."""
|
| 43 |
+
try:
|
| 44 |
+
target, raw = spec.split("=", 1)
|
| 45 |
+
node_id, input_name = target.split(".", 1)
|
| 46 |
+
except ValueError:
|
| 47 |
+
raise argparse.ArgumentTypeError(f"--set expects NODE_ID.INPUT=VALUE, got {spec!r}")
|
| 48 |
+
try:
|
| 49 |
+
value = json.loads(raw)
|
| 50 |
+
except json.JSONDecodeError:
|
| 51 |
+
value = raw
|
| 52 |
+
return node_id, input_name, value
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def collect_output_files(history_entry):
|
| 56 |
+
"""Yield (ref_name, folder_type) for every file any output node produced."""
|
| 57 |
+
for node_output in history_entry.get("outputs", {}).values():
|
| 58 |
+
for value in node_output.values():
|
| 59 |
+
if not isinstance(value, list):
|
| 60 |
+
continue
|
| 61 |
+
for item in value:
|
| 62 |
+
if isinstance(item, dict) and "filename" in item:
|
| 63 |
+
name = item["filename"]
|
| 64 |
+
if item.get("subfolder"):
|
| 65 |
+
name = f"{item['subfolder']}/{name}"
|
| 66 |
+
yield name, item.get("type", "output")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def run(base_url, workflow, timeout=900, poll_interval=2.0, quiet=False):
|
| 70 |
+
"""Queue an API-format workflow and block until done. Returns the history entry."""
|
| 71 |
+
base_url = base_url.rstrip("/")
|
| 72 |
+
r = requests.post(
|
| 73 |
+
f"{base_url}/prompt",
|
| 74 |
+
json={"prompt": workflow, "client_id": str(uuid.uuid4())},
|
| 75 |
+
timeout=60,
|
| 76 |
+
)
|
| 77 |
+
if r.status_code != 200:
|
| 78 |
+
try:
|
| 79 |
+
detail = json.dumps(r.json(), indent=2)
|
| 80 |
+
except ValueError:
|
| 81 |
+
detail = r.text
|
| 82 |
+
raise RuntimeError(f"queueing failed (HTTP {r.status_code}):\n{detail}")
|
| 83 |
+
prompt_id = r.json()["prompt_id"]
|
| 84 |
+
if not quiet:
|
| 85 |
+
print(f"queued prompt {prompt_id}", file=sys.stderr)
|
| 86 |
+
|
| 87 |
+
start = time.monotonic()
|
| 88 |
+
while True:
|
| 89 |
+
elapsed = time.monotonic() - start
|
| 90 |
+
if elapsed > timeout:
|
| 91 |
+
raise RuntimeError(f"timed out after {timeout}s waiting for prompt {prompt_id}")
|
| 92 |
+
h = requests.get(f"{base_url}/history/{prompt_id}", timeout=60).json()
|
| 93 |
+
if prompt_id in h:
|
| 94 |
+
entry = h[prompt_id]
|
| 95 |
+
status = entry.get("status", {})
|
| 96 |
+
if status.get("status_str") == "error":
|
| 97 |
+
messages = json.dumps(status.get("messages", []), indent=2)
|
| 98 |
+
raise RuntimeError(f"workflow execution failed:\n{messages}")
|
| 99 |
+
return entry
|
| 100 |
+
if not quiet:
|
| 101 |
+
print(f" waiting... {int(elapsed)}s", file=sys.stderr)
|
| 102 |
+
time.sleep(poll_interval)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def main():
|
| 106 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 107 |
+
ap.add_argument("--url", required=True, help="ComfyUI base URL, e.g. https://POD-7865.proxy.runpod.net")
|
| 108 |
+
ap.add_argument("workflow", help="path to an API-format workflow JSON")
|
| 109 |
+
ap.add_argument("--set", dest="sets", type=parse_set, action="append", default=[],
|
| 110 |
+
metavar="NODE_ID.INPUT=VALUE", help="override a node input (repeatable)")
|
| 111 |
+
ap.add_argument("--prefix", help="set filename_prefix on every node that has one")
|
| 112 |
+
ap.add_argument("--keep-seeds", action="store_true",
|
| 113 |
+
help="keep the seeds saved in the workflow instead of randomizing")
|
| 114 |
+
ap.add_argument("--timeout", type=float, default=900, help="max seconds to wait (default 900)")
|
| 115 |
+
ap.add_argument("--poll", type=float, default=2.0, help="poll interval in seconds (default 2)")
|
| 116 |
+
args = ap.parse_args()
|
| 117 |
+
|
| 118 |
+
workflow = json.loads(pathlib.Path(args.workflow).read_text())
|
| 119 |
+
if "nodes" in workflow and "class_type" not in next(iter(workflow.values()), {}):
|
| 120 |
+
sys.exit("ERROR: this is a UI-format workflow. Convert it first: "
|
| 121 |
+
"python convert_workflow.py --url <URL> <workflow.json>")
|
| 122 |
+
|
| 123 |
+
for node_id, input_name, value in args.sets:
|
| 124 |
+
if node_id not in workflow:
|
| 125 |
+
sys.exit(f"ERROR: --set references unknown node id {node_id!r}")
|
| 126 |
+
workflow[node_id]["inputs"][input_name] = value
|
| 127 |
+
print(f"set {node_id}.{input_name} = {value!r}", file=sys.stderr)
|
| 128 |
+
|
| 129 |
+
if args.prefix:
|
| 130 |
+
for node_id, node in workflow.items():
|
| 131 |
+
if "filename_prefix" in node.get("inputs", {}):
|
| 132 |
+
node["inputs"]["filename_prefix"] = args.prefix
|
| 133 |
+
print(f"set {node_id}.filename_prefix = {args.prefix!r}", file=sys.stderr)
|
| 134 |
+
|
| 135 |
+
if not args.keep_seeds:
|
| 136 |
+
for node_id, node in workflow.items():
|
| 137 |
+
for name in SEED_INPUT_NAMES:
|
| 138 |
+
if isinstance(node.get("inputs", {}).get(name), int):
|
| 139 |
+
node["inputs"][name] = random.randrange(2**48)
|
| 140 |
+
print(f"randomized {node_id}.{name} = {node['inputs'][name]}", file=sys.stderr)
|
| 141 |
+
|
| 142 |
+
try:
|
| 143 |
+
entry = run(args.url, workflow, timeout=args.timeout, poll_interval=args.poll)
|
| 144 |
+
except (requests.RequestException, RuntimeError) as e:
|
| 145 |
+
sys.exit(f"ERROR: {e}")
|
| 146 |
+
|
| 147 |
+
outputs = list(collect_output_files(entry))
|
| 148 |
+
saved = [name for name, ftype in outputs if ftype == "output"]
|
| 149 |
+
temp = [name for name, ftype in outputs if ftype != "output"]
|
| 150 |
+
if temp:
|
| 151 |
+
print(f"({len(temp)} preview/temp file(s) not listed: {', '.join(temp)})", file=sys.stderr)
|
| 152 |
+
if not saved:
|
| 153 |
+
print("workflow finished but produced no saved output files", file=sys.stderr)
|
| 154 |
+
for name in saved:
|
| 155 |
+
print(name)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
if __name__ == "__main__":
|
| 159 |
+
main()
|
workflows/scripts/upload_files.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Upload local files into a ComfyUI instance's input folder.
|
| 3 |
+
|
| 4 |
+
Uses ComfyUI's built-in POST /upload/image endpoint. Prints one reference
|
| 5 |
+
name per uploaded file to stdout ("name.png" or "subfolder/name.png") --
|
| 6 |
+
exactly the string a LoadImage node's `image` input expects.
|
| 7 |
+
|
| 8 |
+
ComfyUI dedups colliding names by renaming to "name (1).png" unless
|
| 9 |
+
--overwrite is given, so ALWAYS use the printed name, not the local one.
|
| 10 |
+
|
| 11 |
+
Examples:
|
| 12 |
+
python upload_files.py --url https://POD-7865.proxy.runpod.net head.jpg body.jpg
|
| 13 |
+
python upload_files.py --url http://127.0.0.1:7865 --subfolder demo --overwrite *.png
|
| 14 |
+
"""
|
| 15 |
+
import argparse
|
| 16 |
+
import pathlib
|
| 17 |
+
import sys
|
| 18 |
+
|
| 19 |
+
import requests
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def upload_file(base_url, path, subfolder="", overwrite=False, timeout=120):
|
| 23 |
+
"""Upload one file; returns the reference name to use in a workflow."""
|
| 24 |
+
path = pathlib.Path(path)
|
| 25 |
+
with path.open("rb") as f:
|
| 26 |
+
r = requests.post(
|
| 27 |
+
f"{base_url.rstrip('/')}/upload/image",
|
| 28 |
+
files={"image": (path.name, f)},
|
| 29 |
+
data={
|
| 30 |
+
"subfolder": subfolder,
|
| 31 |
+
"overwrite": "true" if overwrite else "false",
|
| 32 |
+
"type": "input",
|
| 33 |
+
},
|
| 34 |
+
timeout=timeout,
|
| 35 |
+
)
|
| 36 |
+
r.raise_for_status()
|
| 37 |
+
info = r.json()
|
| 38 |
+
name = info["name"]
|
| 39 |
+
if info.get("subfolder"):
|
| 40 |
+
name = f"{info['subfolder']}/{name}"
|
| 41 |
+
return name
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def main():
|
| 45 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 46 |
+
ap.add_argument("--url", required=True, help="ComfyUI base URL, e.g. https://POD-7865.proxy.runpod.net")
|
| 47 |
+
ap.add_argument("--subfolder", default="", help="subfolder under ComfyUI/input/ to upload into")
|
| 48 |
+
ap.add_argument("--overwrite", action="store_true", help="replace existing files instead of auto-renaming")
|
| 49 |
+
ap.add_argument("files", nargs="+", help="local files to upload")
|
| 50 |
+
args = ap.parse_args()
|
| 51 |
+
|
| 52 |
+
failed = False
|
| 53 |
+
for f in args.files:
|
| 54 |
+
try:
|
| 55 |
+
name = upload_file(args.url, f, args.subfolder, args.overwrite)
|
| 56 |
+
except (OSError, requests.RequestException) as e:
|
| 57 |
+
print(f"ERROR uploading {f}: {e}", file=sys.stderr)
|
| 58 |
+
failed = True
|
| 59 |
+
continue
|
| 60 |
+
print(f"uploaded {f} -> input/{name}", file=sys.stderr)
|
| 61 |
+
print(name)
|
| 62 |
+
sys.exit(1 if failed else 0)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
main()
|