aleph65 commited on
Commit
dcc0102
·
verified ·
1 Parent(s): 2784a84

serverless: client scripts (sync/async) + README; timings from live endpoint tests

Browse files
serverless/README.md CHANGED
@@ -27,9 +27,13 @@ serverless/
27
  │ ├── endpoint.json # models + custom nodes + default workflow
28
  │ ├── workflows/<name>.json # API-format workflow(s) baked into the image
29
  │ └── params/<name>.json # friendly-param -> node.input mapping
30
- ── tests/
31
- ├── local_test.py # drive handler against a locally running ComfyUI
32
- └── endpoint_test.py # drive a deployed RunPod endpoint
 
 
 
 
33
  ```
34
 
35
  ## Calling the endpoint
 
27
  │ ├── endpoint.json # models + custom nodes + default workflow
28
  │ ├── workflows/<name>.json # API-format workflow(s) baked into the image
29
  │ └── params/<name>.json # friendly-param -> node.input mapping
30
+ ── tests/
31
+ ├── local_test.py # drive handler against a locally running ComfyUI
32
+ └── endpoint_test.py # drive a deployed RunPod endpoint
33
+ └── client/ # standalone caller scripts (stdlib-only) + usage README
34
+ ├── qwen_edit_sync.py # /runsync: block until done
35
+ ├── qwen_edit_async.py # /run: submit, poll, fire-and-forget, collect, cancel
36
+ └── qwen-edit-turbo-v4.json
37
  ```
38
 
39
  ## Calling the endpoint
serverless/client/README.md ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # qwen-edit-turbo — client scripts
2
+
3
+ Call the deployed RunPod serverless endpoint (`dom5lwr0o5wq6u`, image
4
+ `plx1029/comfyui-serverless:qwen-edit-turbo-v1`) from anywhere. Both scripts
5
+ are standalone (Python 3.8+, stdlib only — no pip installs).
6
+
7
+ | file | what it does |
8
+ |---|---|
9
+ | `qwen_edit_sync.py` | `/runsync` — blocks until done, saves image(s) |
10
+ | `qwen_edit_async.py` | `/run` — submit, poll, or fire-and-forget + collect later |
11
+ | `qwen-edit-turbo-v4.json` | the API-format workflow baked into the endpoint (reference; also usable with `--workflow-json` after editing) |
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ export RUNPOD_API_KEY=... # or pass --api-key
17
+
18
+ # synchronous: person + outfit -> edited image in ./results
19
+ python qwen_edit_sync.py \
20
+ --image person.jpg --ref-image outfit.png \
21
+ --prompt "replace the outfit of the person in the first image with the outfit in the second image" \
22
+ --mode turbo-8 --out ./results
23
+
24
+ # async fire-and-forget
25
+ job=$(python qwen_edit_async.py --image person.jpg --prompt "remove the background people" --no-wait)
26
+ python qwen_edit_async.py --job-id "$job" --out ./results # collect later
27
+ ```
28
+
29
+ Single-image edits: just omit `--ref-image` (the workflow's reference branch
30
+ switches off automatically).
31
+
32
+ ## Arguments (both scripts)
33
+
34
+ | arg | default | meaning |
35
+ |---|---|---|
36
+ | `--api-key` | `$RUNPOD_API_KEY` | RunPod API key |
37
+ | `--endpoint-id` | `dom5lwr0o5wq6u` | endpoint to call |
38
+ | `--image` | required | main input image |
39
+ | `--ref-image` | none | reference image (outfit/style/person source) |
40
+ | `--prompt` | required | edit instruction |
41
+ | `--mode` | `turbo-8` | `turbo-4` (fastest) / `turbo-8` / `quality` (12-step, cfg 3) |
42
+ | `--seed` | random | reproducibility; the used seed is always printed |
43
+ | `--input-max-dim` | 2048 | main image is resized to fit this before editing |
44
+ | `--ref-max-dim` | 1024 | same for the reference image |
45
+ | `--output-max-dim` | 2560 | result upscale target |
46
+ | `--lora-skin-fix [--lora-skin-fix-strength]` | off / 1.0 | optional Skin-Fix lora |
47
+ | `--lora-amateur [--lora-amateur-strength]` | off / 1.0 | optional Amateur-Photo lora |
48
+ | `--workflow` | `qwen-edit-turbo-v4` | which baked workflow to run |
49
+ | `--set NODE.INPUT=VALUE` | — | raw graph override, repeatable (e.g. `--set 43.cfg=1.5`) |
50
+ | `--workflow-json FILE` | — | run a full custom API-format graph instead |
51
+ | `--out` | `.` | where to save result images |
52
+ | `--timeout` | 600 / 1800 | max seconds to wait |
53
+
54
+ `qwen_edit_async.py` extras: `--no-wait` (print job id and exit), `--job-id ID`
55
+ (poll/collect an existing job), `--poll SECONDS`, `--cancel JOB_ID`.
56
+
57
+ ## What to expect (measured 2026-07-23)
58
+
59
+ | situation | delay | execution |
60
+ |---|---|---|
61
+ | warm worker | ~0.1–0.5 s | turbo-4 ≈ 42 s, turbo-8 ≈ 60 s (2048px, 2 input images, 2560px output) |
62
+ | FlashBoot resume (idle worker) | ~0.5 s | same as warm — models stay in VRAM |
63
+ | brand-new host (first ever pull) | ~15 min once | ~78 s (model load from NVMe + inference) |
64
+
65
+ Payload limits: ~10 MB on `/run`, ~20 MB on `/runsync`; base64 adds ~33 % to
66
+ image bytes. Keep the two inputs under ~7 MB combined for sync calls, or use
67
+ async. (URL inputs / S3 outputs are the planned upgrade if this becomes a
68
+ bottleneck.)
69
+
70
+ ## Raw HTTP (no script)
71
+
72
+ ```bash
73
+ curl -s -X POST "https://api.runpod.ai/v2/dom5lwr0o5wq6u/runsync" \
74
+ -H "Authorization: Bearer $RUNPOD_API_KEY" -H "Content-Type: application/json" \
75
+ -d '{"input": {"images": [{"name": "a.jpg", "image": "'"$(base64 -w0 a.jpg)"'"}],
76
+ "params": {"prompt": "make it night time", "mode": "turbo-8"}}}'
77
+ ```
78
+
79
+ Response: `output.images[].data` is a base64 PNG; `output.seed`,
80
+ `output.timings` included. See `../README.md` for the full API schema and how
81
+ the endpoint image is built.
serverless/client/qwen-edit-turbo-v4.json ADDED
@@ -0,0 +1,758 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "1": {
3
+ "class_type": "UNETLoader",
4
+ "inputs": {
5
+ "unet_name": "qwen_image_edit_2511_bf16.safetensors",
6
+ "weight_dtype": "default"
7
+ }
8
+ },
9
+ "13": {
10
+ "class_type": "ModelSamplingAuraFlow",
11
+ "inputs": {
12
+ "model": [
13
+ "53:6",
14
+ 0
15
+ ],
16
+ "shift": 3.1
17
+ }
18
+ },
19
+ "14": {
20
+ "class_type": "CFGNorm",
21
+ "inputs": {
22
+ "model": [
23
+ "13",
24
+ 0
25
+ ],
26
+ "pre_cfg": false,
27
+ "strength": 1
28
+ }
29
+ },
30
+ "15": {
31
+ "_meta": {
32
+ "title": "upscale_input_to_max_size"
33
+ },
34
+ "class_type": "PrimitiveBoolean",
35
+ "inputs": {
36
+ "value": true
37
+ }
38
+ },
39
+ "16": {
40
+ "_meta": {
41
+ "title": "downscale_input_to_max_size"
42
+ },
43
+ "class_type": "PrimitiveBoolean",
44
+ "inputs": {
45
+ "value": false
46
+ }
47
+ },
48
+ "20": {
49
+ "_meta": {
50
+ "title": "Main Image"
51
+ },
52
+ "class_type": "LoadImage",
53
+ "inputs": {
54
+ "image": "733427737_18059858666742129_6215894921112236726_n.jpg"
55
+ }
56
+ },
57
+ "21": {
58
+ "_meta": {
59
+ "title": "Input max size (main)"
60
+ },
61
+ "class_type": "PrimitiveInt",
62
+ "inputs": {
63
+ "value": 2048
64
+ }
65
+ },
66
+ "27": {
67
+ "_meta": {
68
+ "title": "Main image config"
69
+ },
70
+ "class_type": "QwenEditConfigPreparer",
71
+ "inputs": {
72
+ "image": [
73
+ "20",
74
+ 0
75
+ ],
76
+ "ref_crop": "pad",
77
+ "ref_longest_edge": [
78
+ "54:5",
79
+ 0
80
+ ],
81
+ "ref_main_image": true,
82
+ "ref_upscale": "lanczos",
83
+ "to_ref": true,
84
+ "to_vl": true,
85
+ "vl_crop": "center",
86
+ "vl_resize": true,
87
+ "vl_target_size": 384,
88
+ "vl_upscale": "bicubic"
89
+ }
90
+ },
91
+ "3": {
92
+ "class_type": "CLIPLoader",
93
+ "inputs": {
94
+ "clip_name": "qwen_2.5_vl_7b.safetensors",
95
+ "device": "default",
96
+ "type": "qwen_image"
97
+ }
98
+ },
99
+ "30": {
100
+ "_meta": {
101
+ "title": "Reference Image 2 (optional)"
102
+ },
103
+ "class_type": "LoadImage",
104
+ "inputs": {
105
+ "image": "b.png"
106
+ }
107
+ },
108
+ "31": {
109
+ "_meta": {
110
+ "title": "Input max size (ref 2)"
111
+ },
112
+ "class_type": "PrimitiveInt",
113
+ "inputs": {
114
+ "value": 1024
115
+ }
116
+ },
117
+ "37": {
118
+ "_meta": {
119
+ "title": "Reference image 2 config"
120
+ },
121
+ "class_type": "QwenEditConfigPreparer",
122
+ "inputs": {
123
+ "configs": [
124
+ "27",
125
+ 0
126
+ ],
127
+ "image": [
128
+ "30",
129
+ 0
130
+ ],
131
+ "ref_crop": "center",
132
+ "ref_longest_edge": [
133
+ "55:5",
134
+ 0
135
+ ],
136
+ "ref_main_image": false,
137
+ "ref_upscale": "lanczos",
138
+ "to_ref": true,
139
+ "to_vl": true,
140
+ "vl_crop": "center",
141
+ "vl_resize": true,
142
+ "vl_target_size": 384,
143
+ "vl_upscale": "bicubic"
144
+ }
145
+ },
146
+ "4": {
147
+ "class_type": "VAELoader",
148
+ "inputs": {
149
+ "vae_name": "qwen_image_vae.safetensors"
150
+ }
151
+ },
152
+ "40": {
153
+ "_meta": {
154
+ "title": "Qwen Edit Encode (prompt here)"
155
+ },
156
+ "class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
157
+ "inputs": {
158
+ "clip": [
159
+ "3",
160
+ 0
161
+ ],
162
+ "configs": [
163
+ "57",
164
+ 0
165
+ ],
166
+ "instruction": "",
167
+ "prompt": "replace the outfit of the subject in the first image with the outfit in the second image, don't change anything else. do not change the body's anatomy or shape or skin tone. only put the outfit onto the person in the first image. ",
168
+ "return_full_refs_cond": true,
169
+ "vae": [
170
+ "4",
171
+ 0
172
+ ]
173
+ }
174
+ },
175
+ "41": {
176
+ "class_type": "QwenEditOutputExtractor",
177
+ "inputs": {
178
+ "custom_output": [
179
+ "40",
180
+ 2
181
+ ]
182
+ }
183
+ },
184
+ "42": {
185
+ "_meta": {
186
+ "title": "Negative (zeroed)"
187
+ },
188
+ "class_type": "ConditioningZeroOut",
189
+ "inputs": {
190
+ "conditioning": [
191
+ "40",
192
+ 0
193
+ ]
194
+ }
195
+ },
196
+ "43": {
197
+ "class_type": "KSampler",
198
+ "inputs": {
199
+ "cfg": [
200
+ "53:14",
201
+ 0
202
+ ],
203
+ "denoise": 1,
204
+ "latent_image": [
205
+ "40",
206
+ 1
207
+ ],
208
+ "model": [
209
+ "14",
210
+ 0
211
+ ],
212
+ "negative": [
213
+ "42",
214
+ 0
215
+ ],
216
+ "positive": [
217
+ "40",
218
+ 0
219
+ ],
220
+ "sampler_name": "euler",
221
+ "scheduler": "simple",
222
+ "seed": 440984114065874,
223
+ "steps": [
224
+ "53:11",
225
+ 0
226
+ ]
227
+ }
228
+ },
229
+ "44": {
230
+ "class_type": "VAEDecode",
231
+ "inputs": {
232
+ "samples": [
233
+ "43",
234
+ 0
235
+ ],
236
+ "vae": [
237
+ "4",
238
+ 0
239
+ ]
240
+ }
241
+ },
242
+ "45": {
243
+ "_meta": {
244
+ "title": "Remove padding"
245
+ },
246
+ "class_type": "CropWithPadInfo",
247
+ "inputs": {
248
+ "image": [
249
+ "44",
250
+ 0
251
+ ],
252
+ "pad_info": [
253
+ "41",
254
+ 0
255
+ ]
256
+ }
257
+ },
258
+ "46": {
259
+ "_meta": {
260
+ "title": "upscale_output_to_selected_size"
261
+ },
262
+ "class_type": "PrimitiveBoolean",
263
+ "inputs": {
264
+ "value": true
265
+ }
266
+ },
267
+ "47": {
268
+ "_meta": {
269
+ "title": "Selected output size"
270
+ },
271
+ "class_type": "PrimitiveInt",
272
+ "inputs": {
273
+ "value": 2560
274
+ }
275
+ },
276
+ "48": {
277
+ "_meta": {
278
+ "title": "Scale output"
279
+ },
280
+ "class_type": "ImageScaleToMaxDimension",
281
+ "inputs": {
282
+ "image": [
283
+ "45",
284
+ 0
285
+ ],
286
+ "largest_size": [
287
+ "47",
288
+ 0
289
+ ],
290
+ "upscale_method": "lanczos"
291
+ }
292
+ },
293
+ "49": {
294
+ "_meta": {
295
+ "title": "Output size switch"
296
+ },
297
+ "class_type": "ComfySwitchNode",
298
+ "inputs": {
299
+ "on_false": [
300
+ "45",
301
+ 0
302
+ ],
303
+ "on_true": [
304
+ "48",
305
+ 0
306
+ ],
307
+ "switch": [
308
+ "46",
309
+ 0
310
+ ]
311
+ }
312
+ },
313
+ "5": {
314
+ "_meta": {
315
+ "title": "Sampling mode: 1=4-step / 2=8-step / 3=quality"
316
+ },
317
+ "class_type": "PrimitiveInt",
318
+ "inputs": {
319
+ "value": 1
320
+ }
321
+ },
322
+ "50": {
323
+ "class_type": "SaveImage",
324
+ "inputs": {
325
+ "filename_prefix": "qwen-edit-turbo-v4",
326
+ "images": [
327
+ "49",
328
+ 0
329
+ ]
330
+ }
331
+ },
332
+ "52": {
333
+ "_meta": {
334
+ "title": "User LoRAs"
335
+ },
336
+ "class_type": "Power Lora Loader (rgthree)",
337
+ "inputs": {
338
+ "lora_1": {
339
+ "lora": "Qwen_LoRA_Skin_Fix_v2.safetensors",
340
+ "on": false,
341
+ "strength": 1,
342
+ "strengthTwo": null
343
+ },
344
+ "lora_2": {
345
+ "lora": "Qwen_LoRA_Amateur_Photo_v1.safetensors",
346
+ "on": false,
347
+ "strength": 1,
348
+ "strengthTwo": null
349
+ },
350
+ "model": [
351
+ "1",
352
+ 0
353
+ ]
354
+ }
355
+ },
356
+ "53:1": {
357
+ "_meta": {
358
+ "title": "LoRA: Lightning 4 steps"
359
+ },
360
+ "class_type": "LoraLoaderModelOnly",
361
+ "inputs": {
362
+ "lora_name": "Qwen-Image-Edit-2511-Lightning-4steps-V1.0-bf16.safetensors",
363
+ "model": [
364
+ "52",
365
+ 0
366
+ ],
367
+ "strength_model": 1
368
+ }
369
+ },
370
+ "53:10": {
371
+ "_meta": {
372
+ "title": "Steps switch 4/8"
373
+ },
374
+ "class_type": "ComfySwitchNode",
375
+ "inputs": {
376
+ "on_false": [
377
+ "53:7",
378
+ 0
379
+ ],
380
+ "on_true": [
381
+ "53:8",
382
+ 0
383
+ ],
384
+ "switch": [
385
+ "53:3",
386
+ 2
387
+ ]
388
+ }
389
+ },
390
+ "53:11": {
391
+ "_meta": {
392
+ "title": "Steps switch quality"
393
+ },
394
+ "class_type": "ComfySwitchNode",
395
+ "inputs": {
396
+ "on_false": [
397
+ "53:10",
398
+ 0
399
+ ],
400
+ "on_true": [
401
+ "53:9",
402
+ 0
403
+ ],
404
+ "switch": [
405
+ "53:4",
406
+ 2
407
+ ]
408
+ }
409
+ },
410
+ "53:12": {
411
+ "_meta": {
412
+ "title": "CFG (lightning)"
413
+ },
414
+ "class_type": "PrimitiveFloat",
415
+ "inputs": {
416
+ "value": 1.0
417
+ }
418
+ },
419
+ "53:13": {
420
+ "_meta": {
421
+ "title": "CFG (quality)"
422
+ },
423
+ "class_type": "PrimitiveFloat",
424
+ "inputs": {
425
+ "value": 3.0
426
+ }
427
+ },
428
+ "53:14": {
429
+ "_meta": {
430
+ "title": "CFG switch"
431
+ },
432
+ "class_type": "ComfySwitchNode",
433
+ "inputs": {
434
+ "on_false": [
435
+ "53:12",
436
+ 0
437
+ ],
438
+ "on_true": [
439
+ "53:13",
440
+ 0
441
+ ],
442
+ "switch": [
443
+ "53:4",
444
+ 2
445
+ ]
446
+ }
447
+ },
448
+ "53:2": {
449
+ "_meta": {
450
+ "title": "LoRA: Lightning 8 steps"
451
+ },
452
+ "class_type": "LoraLoaderModelOnly",
453
+ "inputs": {
454
+ "lora_name": "Qwen-Image-Edit-2511-Lightning-8steps-V1.0-bf16.safetensors",
455
+ "model": [
456
+ "52",
457
+ 0
458
+ ],
459
+ "strength_model": 1
460
+ }
461
+ },
462
+ "53:3": {
463
+ "_meta": {
464
+ "title": "is 8-step (mode == 2)"
465
+ },
466
+ "class_type": "ComfyMathExpression",
467
+ "inputs": {
468
+ "expression": "a == 2",
469
+ "values.a": [
470
+ "5",
471
+ 0
472
+ ]
473
+ }
474
+ },
475
+ "53:4": {
476
+ "_meta": {
477
+ "title": "is quality (mode == 3)"
478
+ },
479
+ "class_type": "ComfyMathExpression",
480
+ "inputs": {
481
+ "expression": "a == 3",
482
+ "values.a": [
483
+ "5",
484
+ 0
485
+ ]
486
+ }
487
+ },
488
+ "53:5": {
489
+ "_meta": {
490
+ "title": "Model switch 4/8"
491
+ },
492
+ "class_type": "ComfySwitchNode",
493
+ "inputs": {
494
+ "on_false": [
495
+ "53:1",
496
+ 0
497
+ ],
498
+ "on_true": [
499
+ "53:2",
500
+ 0
501
+ ],
502
+ "switch": [
503
+ "53:3",
504
+ 2
505
+ ]
506
+ }
507
+ },
508
+ "53:6": {
509
+ "_meta": {
510
+ "title": "Model switch quality"
511
+ },
512
+ "class_type": "ComfySwitchNode",
513
+ "inputs": {
514
+ "on_false": [
515
+ "53:5",
516
+ 0
517
+ ],
518
+ "on_true": [
519
+ "52",
520
+ 0
521
+ ],
522
+ "switch": [
523
+ "53:4",
524
+ 2
525
+ ]
526
+ }
527
+ },
528
+ "53:7": {
529
+ "_meta": {
530
+ "title": "Steps (4-step)"
531
+ },
532
+ "class_type": "PrimitiveInt",
533
+ "inputs": {
534
+ "value": 4
535
+ }
536
+ },
537
+ "53:8": {
538
+ "_meta": {
539
+ "title": "Steps (8-step)"
540
+ },
541
+ "class_type": "PrimitiveInt",
542
+ "inputs": {
543
+ "value": 8
544
+ }
545
+ },
546
+ "53:9": {
547
+ "_meta": {
548
+ "title": "Steps (quality)"
549
+ },
550
+ "class_type": "PrimitiveInt",
551
+ "inputs": {
552
+ "value": 12
553
+ }
554
+ },
555
+ "54:1": {
556
+ "_meta": {
557
+ "title": "native edge"
558
+ },
559
+ "class_type": "MathExpression|pysssss",
560
+ "inputs": {
561
+ "a": [
562
+ "20",
563
+ 0
564
+ ],
565
+ "expression": "max(a.width, a.height)"
566
+ }
567
+ },
568
+ "54:2": {
569
+ "_meta": {
570
+ "title": "upscaled edge"
571
+ },
572
+ "class_type": "MathExpression|pysssss",
573
+ "inputs": {
574
+ "a": [
575
+ "20",
576
+ 0
577
+ ],
578
+ "c": [
579
+ "21",
580
+ 0
581
+ ],
582
+ "expression": "max(a.width, a.height, c)"
583
+ }
584
+ },
585
+ "54:3": {
586
+ "_meta": {
587
+ "title": "apply upscale"
588
+ },
589
+ "class_type": "ComfySwitchNode",
590
+ "inputs": {
591
+ "on_false": [
592
+ "54:1",
593
+ 0
594
+ ],
595
+ "on_true": [
596
+ "54:2",
597
+ 0
598
+ ],
599
+ "switch": [
600
+ "15",
601
+ 0
602
+ ]
603
+ }
604
+ },
605
+ "54:4": {
606
+ "_meta": {
607
+ "title": "downscaled edge"
608
+ },
609
+ "class_type": "MathExpression|pysssss",
610
+ "inputs": {
611
+ "a": [
612
+ "54:3",
613
+ 0
614
+ ],
615
+ "c": [
616
+ "21",
617
+ 0
618
+ ],
619
+ "expression": "min(a, c)"
620
+ }
621
+ },
622
+ "54:5": {
623
+ "_meta": {
624
+ "title": "apply downscale"
625
+ },
626
+ "class_type": "ComfySwitchNode",
627
+ "inputs": {
628
+ "on_false": [
629
+ "54:3",
630
+ 0
631
+ ],
632
+ "on_true": [
633
+ "54:4",
634
+ 0
635
+ ],
636
+ "switch": [
637
+ "16",
638
+ 0
639
+ ]
640
+ }
641
+ },
642
+ "55:1": {
643
+ "_meta": {
644
+ "title": "native edge"
645
+ },
646
+ "class_type": "MathExpression|pysssss",
647
+ "inputs": {
648
+ "a": [
649
+ "30",
650
+ 0
651
+ ],
652
+ "expression": "max(a.width, a.height)"
653
+ }
654
+ },
655
+ "55:2": {
656
+ "_meta": {
657
+ "title": "upscaled edge"
658
+ },
659
+ "class_type": "MathExpression|pysssss",
660
+ "inputs": {
661
+ "a": [
662
+ "30",
663
+ 0
664
+ ],
665
+ "c": [
666
+ "31",
667
+ 0
668
+ ],
669
+ "expression": "max(a.width, a.height, c)"
670
+ }
671
+ },
672
+ "55:3": {
673
+ "_meta": {
674
+ "title": "apply upscale"
675
+ },
676
+ "class_type": "ComfySwitchNode",
677
+ "inputs": {
678
+ "on_false": [
679
+ "55:1",
680
+ 0
681
+ ],
682
+ "on_true": [
683
+ "55:2",
684
+ 0
685
+ ],
686
+ "switch": [
687
+ "15",
688
+ 0
689
+ ]
690
+ }
691
+ },
692
+ "55:4": {
693
+ "_meta": {
694
+ "title": "downscaled edge"
695
+ },
696
+ "class_type": "MathExpression|pysssss",
697
+ "inputs": {
698
+ "a": [
699
+ "55:3",
700
+ 0
701
+ ],
702
+ "c": [
703
+ "31",
704
+ 0
705
+ ],
706
+ "expression": "min(a, c)"
707
+ }
708
+ },
709
+ "55:5": {
710
+ "_meta": {
711
+ "title": "apply downscale"
712
+ },
713
+ "class_type": "ComfySwitchNode",
714
+ "inputs": {
715
+ "on_false": [
716
+ "55:3",
717
+ 0
718
+ ],
719
+ "on_true": [
720
+ "55:4",
721
+ 0
722
+ ],
723
+ "switch": [
724
+ "16",
725
+ 0
726
+ ]
727
+ }
728
+ },
729
+ "56": {
730
+ "_meta": {
731
+ "title": "Use Reference Image 2"
732
+ },
733
+ "class_type": "PrimitiveBoolean",
734
+ "inputs": {
735
+ "value": true
736
+ }
737
+ },
738
+ "57": {
739
+ "_meta": {
740
+ "title": "Ref 2 on/off switch"
741
+ },
742
+ "class_type": "ComfySwitchNode",
743
+ "inputs": {
744
+ "on_false": [
745
+ "27",
746
+ 0
747
+ ],
748
+ "on_true": [
749
+ "37",
750
+ 0
751
+ ],
752
+ "switch": [
753
+ "56",
754
+ 0
755
+ ]
756
+ }
757
+ }
758
+ }
serverless/client/qwen_edit_async.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Call the qwen-edit-turbo RunPod serverless endpoint ASYNCHRONOUSLY (/run).
3
+
4
+ Standalone: python 3.8+, stdlib only. Submits the job, then either polls until
5
+ done (default) or exits immediately with the job id (--no-wait) so you can
6
+ collect later with --job-id.
7
+
8
+ # submit and wait
9
+ python qwen_edit_async.py --api-key $RUNPOD_API_KEY \
10
+ --image person.jpg --ref-image outfit.png \
11
+ --prompt "put the outfit from the second image on the person"
12
+
13
+ # fire-and-forget, collect later
14
+ python qwen_edit_async.py ... --no-wait # prints JOB_ID
15
+ python qwen_edit_async.py --api-key $RUNPOD_API_KEY --job-id JOB_ID
16
+
17
+ Arguments are identical to qwen_edit_sync.py, plus:
18
+ --no-wait submit only; print the job id and exit
19
+ --job-id ID skip submission; poll/collect an existing job
20
+ --poll SECONDS poll interval (default 3)
21
+ --cancel ID cancel a queued/running job and exit
22
+ """
23
+ import argparse
24
+ import base64
25
+ import json
26
+ import os
27
+ import pathlib
28
+ import sys
29
+ import time
30
+ import urllib.error
31
+ import urllib.request
32
+
33
+ DEFAULT_ENDPOINT = "dom5lwr0o5wq6u"
34
+ TERMINAL = ("COMPLETED", "FAILED", "CANCELLED", "TIMED_OUT")
35
+
36
+
37
+ def api(args, method, path, payload=None, timeout=90):
38
+ req = urllib.request.Request(
39
+ f"https://api.runpod.ai/v2/{args.endpoint_id}/{path}", method=method,
40
+ data=json.dumps(payload).encode() if payload is not None else None,
41
+ headers={"Content-Type": "application/json",
42
+ "Authorization": f"Bearer {args.api_key}"})
43
+ try:
44
+ with urllib.request.urlopen(req, timeout=timeout) as r:
45
+ return json.load(r)
46
+ except urllib.error.HTTPError as e:
47
+ sys.exit(f"ERROR: HTTP {e.code} on /{path}: {e.read().decode(errors='replace')[:2000]}")
48
+
49
+
50
+ def build_input(args):
51
+ images, params = [], {"prompt": args.prompt, "mode": args.mode}
52
+ images.append({"name": os.path.basename(args.image),
53
+ "image": base64.b64encode(open(args.image, "rb").read()).decode()})
54
+ if args.ref_image:
55
+ images.append({"name": os.path.basename(args.ref_image),
56
+ "image": base64.b64encode(open(args.ref_image, "rb").read()).decode()})
57
+ if args.seed is not None:
58
+ params["seed"] = args.seed
59
+ for name in ("input_max_dim", "ref_max_dim", "output_max_dim"):
60
+ v = getattr(args, name)
61
+ if v is not None:
62
+ params[name] = v
63
+ if args.lora_skin_fix:
64
+ params["lora_skin_fix"] = True
65
+ params["lora_skin_fix_strength"] = args.lora_skin_fix_strength
66
+ if args.lora_amateur:
67
+ params["lora_amateur"] = True
68
+ params["lora_amateur_strength"] = args.lora_amateur_strength
69
+
70
+ payload = {"images": images, "params": params}
71
+ if args.workflow:
72
+ payload["workflow"] = args.workflow
73
+ if args.workflow_json:
74
+ payload["workflow_json"] = json.load(open(args.workflow_json))
75
+ overrides = {}
76
+ for s in args.set or []:
77
+ k, v = s.split("=", 1)
78
+ try:
79
+ overrides[k] = json.loads(v)
80
+ except json.JSONDecodeError:
81
+ overrides[k] = v
82
+ if overrides:
83
+ payload["set"] = overrides
84
+ return payload
85
+
86
+
87
+ def save_outputs(output, out_dir):
88
+ out_dir = pathlib.Path(out_dir)
89
+ out_dir.mkdir(parents=True, exist_ok=True)
90
+ saved = []
91
+ for i, img in enumerate(output.get("images", [])):
92
+ p = out_dir / f"{int(time.time())}-{i}-{img['filename']}"
93
+ p.write_bytes(base64.b64decode(img["data"]))
94
+ saved.append(str(p))
95
+ return saved
96
+
97
+
98
+ def main():
99
+ ap = argparse.ArgumentParser(description=__doc__,
100
+ formatter_class=argparse.RawDescriptionHelpFormatter)
101
+ ap.add_argument("--api-key", default=os.environ.get("RUNPOD_API_KEY"))
102
+ ap.add_argument("--endpoint-id", default=DEFAULT_ENDPOINT)
103
+ ap.add_argument("--image")
104
+ ap.add_argument("--ref-image")
105
+ ap.add_argument("--prompt")
106
+ ap.add_argument("--mode", default="turbo-8", choices=["turbo-4", "turbo-8", "quality"])
107
+ ap.add_argument("--seed", type=int)
108
+ ap.add_argument("--input-max-dim", type=int)
109
+ ap.add_argument("--ref-max-dim", type=int)
110
+ ap.add_argument("--output-max-dim", type=int)
111
+ ap.add_argument("--lora-skin-fix", action="store_true")
112
+ ap.add_argument("--lora-skin-fix-strength", type=float, default=1.0)
113
+ ap.add_argument("--lora-amateur", action="store_true")
114
+ ap.add_argument("--lora-amateur-strength", type=float, default=1.0)
115
+ ap.add_argument("--workflow")
116
+ ap.add_argument("--set", action="append", metavar="NODE.INPUT=VALUE")
117
+ ap.add_argument("--workflow-json")
118
+ ap.add_argument("--out", default=".")
119
+ ap.add_argument("--timeout", type=float, default=1800)
120
+ ap.add_argument("--poll", type=float, default=3.0)
121
+ ap.add_argument("--no-wait", action="store_true")
122
+ ap.add_argument("--job-id")
123
+ ap.add_argument("--cancel", metavar="JOB_ID")
124
+ args = ap.parse_args()
125
+ if not args.api_key:
126
+ sys.exit("ERROR: pass --api-key or set RUNPOD_API_KEY")
127
+
128
+ if args.cancel:
129
+ print(json.dumps(api(args, "POST", f"cancel/{args.cancel}"), indent=2))
130
+ return
131
+
132
+ if args.job_id:
133
+ job_id = args.job_id
134
+ else:
135
+ if not args.image or not args.prompt:
136
+ sys.exit("ERROR: --image and --prompt are required to submit a job")
137
+ job = api(args, "POST", "run", {"input": build_input(args)})
138
+ job_id = job["id"]
139
+ print(f"# submitted job {job_id}", file=sys.stderr)
140
+ if args.no_wait:
141
+ print(job_id)
142
+ return
143
+
144
+ t0 = time.monotonic()
145
+ while True:
146
+ result = api(args, "GET", f"status/{job_id}")
147
+ status = result.get("status")
148
+ if status in TERMINAL:
149
+ break
150
+ if time.monotonic() - t0 > args.timeout:
151
+ sys.exit(f"ERROR: timed out after {args.timeout}s (last status {status}); "
152
+ f"job {job_id} is still yours to collect with --job-id")
153
+ print(f"# {status} ... {time.monotonic()-t0:.0f}s", file=sys.stderr)
154
+ time.sleep(args.poll)
155
+
156
+ if status != "COMPLETED":
157
+ sys.exit(f"ERROR: {json.dumps(result, indent=2)[:3000]}")
158
+ output = result["output"]
159
+ if "error" in output:
160
+ sys.exit(f"ERROR from handler: {output['error']}")
161
+ for p in save_outputs(output, args.out):
162
+ print(p)
163
+ print(f"# seed={output.get('seed')} delay={result.get('delayTime')}ms "
164
+ f"exec={result.get('executionTime')}ms", file=sys.stderr)
165
+
166
+
167
+ if __name__ == "__main__":
168
+ main()
serverless/client/qwen_edit_sync.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Call the qwen-edit-turbo RunPod serverless endpoint SYNCHRONOUSLY (/runsync).
3
+
4
+ Standalone: python 3.8+, stdlib only. Blocks until the edit is done (typically
5
+ 40-65 s warm), saves the returned image(s), prints their paths.
6
+
7
+ python qwen_edit_sync.py \
8
+ --api-key $RUNPOD_API_KEY \
9
+ --image person.jpg --ref-image outfit.png \
10
+ --prompt "put the outfit from the second image on the person" \
11
+ --mode turbo-8 --out ./results
12
+
13
+ All arguments:
14
+ --api-key RunPod API key (or set RUNPOD_API_KEY env var)
15
+ --endpoint-id RunPod endpoint id (default: dom5lwr0o5wq6u)
16
+ --image main input image (required)
17
+ --ref-image optional reference image (outfit/style source)
18
+ --prompt edit instruction (required)
19
+ --mode turbo-4 | turbo-8 | quality (default turbo-8)
20
+ --seed integer seed (default: random; used seed is printed)
21
+ --input-max-dim max size of the main image fed to the model (default 2048)
22
+ --ref-max-dim max size of the reference image (default 1024)
23
+ --output-max-dim upscale target for the result (default 2560)
24
+ --lora-skin-fix / --lora-amateur enable the optional loras
25
+ --lora-skin-fix-strength / --lora-amateur-strength (default 1.0)
26
+ --workflow which baked workflow to run (default qwen-edit-turbo-v4)
27
+ --set NODE.INPUT=VALUE raw graph override, repeatable (advanced)
28
+ --workflow-json FILE full API-format graph passthrough (advanced)
29
+ --out output directory (default .)
30
+ --timeout max seconds to wait (default 600)
31
+ """
32
+ import argparse
33
+ import base64
34
+ import json
35
+ import os
36
+ import pathlib
37
+ import sys
38
+ import time
39
+ import urllib.error
40
+ import urllib.request
41
+
42
+ DEFAULT_ENDPOINT = "dom5lwr0o5wq6u"
43
+
44
+
45
+ def build_input(args):
46
+ images, params = [], {"prompt": args.prompt, "mode": args.mode}
47
+ images.append({"name": os.path.basename(args.image),
48
+ "image": base64.b64encode(open(args.image, "rb").read()).decode()})
49
+ if args.ref_image:
50
+ images.append({"name": os.path.basename(args.ref_image),
51
+ "image": base64.b64encode(open(args.ref_image, "rb").read()).decode()})
52
+ if args.seed is not None:
53
+ params["seed"] = args.seed
54
+ for cli, param in [("input_max_dim", "input_max_dim"), ("ref_max_dim", "ref_max_dim"),
55
+ ("output_max_dim", "output_max_dim")]:
56
+ v = getattr(args, cli)
57
+ if v is not None:
58
+ params[param] = v
59
+ if args.lora_skin_fix:
60
+ params["lora_skin_fix"] = True
61
+ params["lora_skin_fix_strength"] = args.lora_skin_fix_strength
62
+ if args.lora_amateur:
63
+ params["lora_amateur"] = True
64
+ params["lora_amateur_strength"] = args.lora_amateur_strength
65
+
66
+ payload = {"images": images, "params": params}
67
+ if args.workflow:
68
+ payload["workflow"] = args.workflow
69
+ if args.workflow_json:
70
+ payload["workflow_json"] = json.load(open(args.workflow_json))
71
+ overrides = {}
72
+ for s in args.set or []:
73
+ k, v = s.split("=", 1)
74
+ try:
75
+ overrides[k] = json.loads(v)
76
+ except json.JSONDecodeError:
77
+ overrides[k] = v
78
+ if overrides:
79
+ payload["set"] = overrides
80
+ return payload
81
+
82
+
83
+ def save_outputs(output, out_dir):
84
+ out_dir = pathlib.Path(out_dir)
85
+ out_dir.mkdir(parents=True, exist_ok=True)
86
+ saved = []
87
+ for i, img in enumerate(output.get("images", [])):
88
+ p = out_dir / f"{int(time.time())}-{i}-{img['filename']}"
89
+ p.write_bytes(base64.b64decode(img["data"]))
90
+ saved.append(str(p))
91
+ return saved
92
+
93
+
94
+ def main():
95
+ ap = argparse.ArgumentParser(description=__doc__,
96
+ formatter_class=argparse.RawDescriptionHelpFormatter)
97
+ ap.add_argument("--api-key", default=os.environ.get("RUNPOD_API_KEY"))
98
+ ap.add_argument("--endpoint-id", default=DEFAULT_ENDPOINT)
99
+ ap.add_argument("--image", required=True)
100
+ ap.add_argument("--ref-image")
101
+ ap.add_argument("--prompt", required=True)
102
+ ap.add_argument("--mode", default="turbo-8", choices=["turbo-4", "turbo-8", "quality"])
103
+ ap.add_argument("--seed", type=int)
104
+ ap.add_argument("--input-max-dim", type=int)
105
+ ap.add_argument("--ref-max-dim", type=int)
106
+ ap.add_argument("--output-max-dim", type=int)
107
+ ap.add_argument("--lora-skin-fix", action="store_true")
108
+ ap.add_argument("--lora-skin-fix-strength", type=float, default=1.0)
109
+ ap.add_argument("--lora-amateur", action="store_true")
110
+ ap.add_argument("--lora-amateur-strength", type=float, default=1.0)
111
+ ap.add_argument("--workflow")
112
+ ap.add_argument("--set", action="append", metavar="NODE.INPUT=VALUE")
113
+ ap.add_argument("--workflow-json")
114
+ ap.add_argument("--out", default=".")
115
+ ap.add_argument("--timeout", type=float, default=600)
116
+ args = ap.parse_args()
117
+ if not args.api_key:
118
+ sys.exit("ERROR: pass --api-key or set RUNPOD_API_KEY")
119
+
120
+ req = urllib.request.Request(
121
+ f"https://api.runpod.ai/v2/{args.endpoint_id}/runsync",
122
+ data=json.dumps({"input": build_input(args)}).encode(),
123
+ headers={"Content-Type": "application/json",
124
+ "Authorization": f"Bearer {args.api_key}"})
125
+ t0 = time.monotonic()
126
+ try:
127
+ with urllib.request.urlopen(req, timeout=args.timeout) as r:
128
+ result = json.load(r)
129
+ except urllib.error.HTTPError as e:
130
+ sys.exit(f"ERROR: HTTP {e.code}: {e.read().decode(errors='replace')[:2000]}")
131
+
132
+ if result.get("status") != "COMPLETED":
133
+ sys.exit(f"ERROR: {json.dumps(result, indent=2)[:3000]}")
134
+ output = result["output"]
135
+ if "error" in output:
136
+ sys.exit(f"ERROR from handler: {output['error']}")
137
+ for p in save_outputs(output, args.out):
138
+ print(p)
139
+ print(f"# seed={output.get('seed')} wall={time.monotonic()-t0:.1f}s "
140
+ f"delay={result.get('delayTime')}ms exec={result.get('executionTime')}ms",
141
+ file=sys.stderr)
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()