File size: 6,817 Bytes
dcc0102 61c37a8 dcc0102 61c37a8 dcc0102 61c37a8 dcc0102 41a10e6 dcc0102 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | #!/usr/bin/env python3
"""Call the qwen-edit-turbo RunPod serverless endpoint SYNCHRONOUSLY (/runsync).
Standalone: python 3.8+, stdlib only. Blocks until the edit is done (typically
40-65 s warm), saves the returned image(s), prints their paths.
python qwen_edit_sync.py \
--api-key $RUNPOD_API_KEY \
--image person.jpg --ref-image outfit.png \
--prompt "put the outfit from the second image on the person" \
--mode turbo-8 --out ./results
All arguments:
--api-key RunPod API key (or set RUNPOD_API_KEY env var)
--endpoint-id RunPod endpoint id (default: dom5lwr0o5wq6u)
--image main input image (required)
--ref-image optional reference image (outfit/style source)
--prompt edit instruction (required)
--mode turbo-4 | turbo-8 | quality (default turbo-8)
--seed integer seed (default: random; used seed is printed)
--input-max-dim max size of the main image fed to the model (default 2048)
--ref-max-dim max size of the reference image (default 1024)
--output-max-dim upscale target for the result (default 2560)
--lora-skin-fix / --lora-qwen4play enable the optional loras
--lora-skin-fix-strength / --lora-qwen4play-strength (default 1.0)
--workflow which baked workflow to run (default qwen-edit-turbo-v4)
--set NODE.INPUT=VALUE raw graph override, repeatable (advanced)
--workflow-json FILE full API-format graph passthrough (advanced)
--out output directory (default .)
--timeout max seconds to wait (default 600)
"""
import argparse
import base64
import json
import os
import pathlib
import sys
import time
import urllib.error
import urllib.request
DEFAULT_ENDPOINT = "dom5lwr0o5wq6u"
def build_input(args):
images, params = [], {"prompt": args.prompt, "mode": args.mode}
images.append({"name": os.path.basename(args.image),
"image": base64.b64encode(open(args.image, "rb").read()).decode()})
if args.ref_image:
images.append({"name": os.path.basename(args.ref_image),
"image": base64.b64encode(open(args.ref_image, "rb").read()).decode()})
if args.seed is not None:
params["seed"] = args.seed
for cli, param in [("input_max_dim", "input_max_dim"), ("ref_max_dim", "ref_max_dim"),
("output_max_dim", "output_max_dim")]:
v = getattr(args, cli)
if v is not None:
params[param] = v
if args.lora_skin_fix:
params["lora_skin_fix"] = True
params["lora_skin_fix_strength"] = args.lora_skin_fix_strength
if args.lora_qwen4play:
params["lora_qwen4play"] = True
params["lora_qwen4play_strength"] = args.lora_qwen4play_strength
payload = {"images": images, "params": params}
if args.workflow:
payload["workflow"] = args.workflow
if args.workflow_json:
payload["workflow_json"] = json.load(open(args.workflow_json))
overrides = {}
for s in args.set or []:
k, v = s.split("=", 1)
try:
overrides[k] = json.loads(v)
except json.JSONDecodeError:
overrides[k] = v
if overrides:
payload["set"] = overrides
return payload
def save_outputs(output, out_dir):
out_dir = pathlib.Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
saved = []
for i, img in enumerate(output.get("images", [])):
p = out_dir / f"{int(time.time())}-{i}-{img['filename']}"
p.write_bytes(base64.b64decode(img["data"]))
saved.append(str(p))
return saved
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--api-key", default=os.environ.get("RUNPOD_API_KEY"))
ap.add_argument("--endpoint-id", default=DEFAULT_ENDPOINT)
ap.add_argument("--image", required=True)
ap.add_argument("--ref-image")
ap.add_argument("--prompt", required=True)
ap.add_argument("--mode", default="turbo-8", choices=["turbo-4", "turbo-8", "quality"])
ap.add_argument("--seed", type=int)
ap.add_argument("--input-max-dim", type=int)
ap.add_argument("--ref-max-dim", type=int)
ap.add_argument("--output-max-dim", type=int)
ap.add_argument("--lora-skin-fix", action="store_true")
ap.add_argument("--lora-skin-fix-strength", type=float, default=1.0)
ap.add_argument("--lora-qwen4play", action="store_true")
ap.add_argument("--lora-qwen4play-strength", type=float, default=1.0)
ap.add_argument("--workflow")
ap.add_argument("--set", action="append", metavar="NODE.INPUT=VALUE")
ap.add_argument("--workflow-json")
ap.add_argument("--out", default=".")
ap.add_argument("--timeout", type=float, default=600)
args = ap.parse_args()
if not args.api_key:
sys.exit("ERROR: pass --api-key or set RUNPOD_API_KEY")
req = urllib.request.Request(
f"https://api.runpod.ai/v2/{args.endpoint_id}/runsync",
data=json.dumps({"input": build_input(args)}).encode(),
headers={"Content-Type": "application/json",
"Authorization": f"Bearer {args.api_key}"})
t0 = time.monotonic()
try:
with urllib.request.urlopen(req, timeout=args.timeout) as r:
result = json.load(r)
except urllib.error.HTTPError as e:
sys.exit(f"ERROR: HTTP {e.code}: {e.read().decode(errors='replace')[:2000]}")
# /runsync answers early (IN_QUEUE/IN_PROGRESS) when a job outlives the
# sync window (~90 s), e.g. during a cold start — fall back to polling
TERMINAL = ("COMPLETED", "FAILED", "CANCELLED", "TIMED_OUT")
while result.get("status") not in TERMINAL and result.get("id"):
if time.monotonic() - t0 > args.timeout:
sys.exit(f"ERROR: timed out after {args.timeout}s "
f"(job {result['id']} status {result.get('status')})")
time.sleep(3)
poll = urllib.request.Request(
f"https://api.runpod.ai/v2/{args.endpoint_id}/status/{result['id']}",
headers={"Authorization": f"Bearer {args.api_key}"})
with urllib.request.urlopen(poll, timeout=90) as r:
result = json.load(r)
if result.get("status") != "COMPLETED":
sys.exit(f"ERROR: {json.dumps(result, indent=2)[:3000]}")
output = result["output"]
if "error" in output:
sys.exit(f"ERROR from handler: {output['error']}")
for p in save_outputs(output, args.out):
print(p)
print(f"# seed={output.get('seed')} wall={time.monotonic()-t0:.1f}s "
f"delay={result.get('delayTime')}ms exec={result.get('executionTime')}ms",
file=sys.stderr)
if __name__ == "__main__":
main()
|