File size: 6,979 Bytes
cfdb90d | 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 | #!/usr/bin/env python3
"""Run an API-format ComfyUI workflow synchronously and wait for it to finish.
Queues the workflow via POST /prompt, polls GET /history/{prompt_id} until the
job completes, then prints one reference name per produced output file to
stdout ("file.png" or "subfolder/file.png") -- feed those straight to
download_files.py. Exits non-zero on validation or execution failure, with
ComfyUI's error details on stderr.
The workflow must be API format (flat {"<id>": {"class_type": ...}} dict).
Export one from the UI with Workflow > Export (API), or convert a saved UI
JSON with convert_workflow.py.
Runtime patching (so workflow files stay generic):
--set NODE_ID.INPUT=VALUE override any node input, e.g. --set 76.image=body.jpg
(VALUE is parsed as JSON when possible, else kept as string)
--prefix PREFIX set filename_prefix on every node that has one,
e.g. --prefix "headswap/%year%%month%%day%-%hour%%minute%%second%"
--randomize-seeds randomize every seed/noise_seed input (on by default;
disable with --keep-seeds for reproducible reruns)
Example:
python run_workflow.py --url https://POD-7865.proxy.runpod.net \
../api/head-swap-flux-klein.json \
--set 76.image=body.jpg --set 81.image=head.jpg \
--prefix "headswap/%year%%month%%day%-%hour%%minute%%second%"
"""
import argparse
import json
import pathlib
import random
import sys
import time
import uuid
import requests
SEED_INPUT_NAMES = ("seed", "noise_seed")
def parse_set(spec):
"""'76.image=body.jpg' -> ('76', 'image', 'body.jpg'). Values parse as JSON when possible."""
try:
target, raw = spec.split("=", 1)
node_id, input_name = target.split(".", 1)
except ValueError:
raise argparse.ArgumentTypeError(f"--set expects NODE_ID.INPUT=VALUE, got {spec!r}")
try:
value = json.loads(raw)
except json.JSONDecodeError:
value = raw
return node_id, input_name, value
def collect_output_files(history_entry):
"""Yield (ref_name, folder_type) for every file any output node produced."""
for node_output in history_entry.get("outputs", {}).values():
for value in node_output.values():
if not isinstance(value, list):
continue
for item in value:
if isinstance(item, dict) and "filename" in item:
name = item["filename"]
if item.get("subfolder"):
name = f"{item['subfolder']}/{name}"
yield name, item.get("type", "output")
def run(base_url, workflow, timeout=900, poll_interval=2.0, quiet=False):
"""Queue an API-format workflow and block until done. Returns the history entry."""
base_url = base_url.rstrip("/")
r = requests.post(
f"{base_url}/prompt",
json={"prompt": workflow, "client_id": str(uuid.uuid4())},
timeout=60,
)
if r.status_code != 200:
try:
detail = json.dumps(r.json(), indent=2)
except ValueError:
detail = r.text
raise RuntimeError(f"queueing failed (HTTP {r.status_code}):\n{detail}")
prompt_id = r.json()["prompt_id"]
if not quiet:
print(f"queued prompt {prompt_id}", file=sys.stderr)
start = time.monotonic()
while True:
elapsed = time.monotonic() - start
if elapsed > timeout:
raise RuntimeError(f"timed out after {timeout}s waiting for prompt {prompt_id}")
h = requests.get(f"{base_url}/history/{prompt_id}", timeout=60).json()
if prompt_id in h:
entry = h[prompt_id]
status = entry.get("status", {})
if status.get("status_str") == "error":
messages = json.dumps(status.get("messages", []), indent=2)
raise RuntimeError(f"workflow execution failed:\n{messages}")
return entry
if not quiet:
print(f" waiting... {int(elapsed)}s", file=sys.stderr)
time.sleep(poll_interval)
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--url", required=True, help="ComfyUI base URL, e.g. https://POD-7865.proxy.runpod.net")
ap.add_argument("workflow", help="path to an API-format workflow JSON")
ap.add_argument("--set", dest="sets", type=parse_set, action="append", default=[],
metavar="NODE_ID.INPUT=VALUE", help="override a node input (repeatable)")
ap.add_argument("--prefix", help="set filename_prefix on every node that has one")
ap.add_argument("--keep-seeds", action="store_true",
help="keep the seeds saved in the workflow instead of randomizing")
ap.add_argument("--timeout", type=float, default=900, help="max seconds to wait (default 900)")
ap.add_argument("--poll", type=float, default=2.0, help="poll interval in seconds (default 2)")
args = ap.parse_args()
workflow = json.loads(pathlib.Path(args.workflow).read_text())
if "nodes" in workflow and "class_type" not in next(iter(workflow.values()), {}):
sys.exit("ERROR: this is a UI-format workflow. Convert it first: "
"python convert_workflow.py --url <URL> <workflow.json>")
for node_id, input_name, value in args.sets:
if node_id not in workflow:
sys.exit(f"ERROR: --set references unknown node id {node_id!r}")
workflow[node_id]["inputs"][input_name] = value
print(f"set {node_id}.{input_name} = {value!r}", file=sys.stderr)
if args.prefix:
for node_id, node in workflow.items():
if "filename_prefix" in node.get("inputs", {}):
node["inputs"]["filename_prefix"] = args.prefix
print(f"set {node_id}.filename_prefix = {args.prefix!r}", file=sys.stderr)
if not args.keep_seeds:
for node_id, node in workflow.items():
for name in SEED_INPUT_NAMES:
if isinstance(node.get("inputs", {}).get(name), int):
node["inputs"][name] = random.randrange(2**48)
print(f"randomized {node_id}.{name} = {node['inputs'][name]}", file=sys.stderr)
try:
entry = run(args.url, workflow, timeout=args.timeout, poll_interval=args.poll)
except (requests.RequestException, RuntimeError) as e:
sys.exit(f"ERROR: {e}")
outputs = list(collect_output_files(entry))
saved = [name for name, ftype in outputs if ftype == "output"]
temp = [name for name, ftype in outputs if ftype != "output"]
if temp:
print(f"({len(temp)} preview/temp file(s) not listed: {', '.join(temp)})", file=sys.stderr)
if not saved:
print("workflow finished but produced no saved output files", file=sys.stderr)
for name in saved:
print(name)
if __name__ == "__main__":
main()
|