Spaces:
Running
Running
| """ | |
| End-to-end test: run every output in workflow.json through the real | |
| `WorkflowExecutor` — the same code path the canvas and the REST API use. | |
| python apps/05_workflow1111/test_pipelines.py # everything | |
| python apps/05_workflow1111/test_pipelines.py local # only offline nodes | |
| python apps/05_workflow1111/test_pipelines.py grid image # substring filters | |
| Hits Hugging Face for the `model`/`space` nodes, so it costs quota and takes a | |
| couple of minutes. Rendered outputs are written to ./_test_output for eyeballing. | |
| """ | |
| import base64 | |
| import inspect | |
| import json | |
| import logging | |
| import os | |
| import sys | |
| import time | |
| import types | |
| import urllib.request | |
| import warnings | |
| warnings.filterwarnings("ignore") | |
| logging.disable(logging.CRITICAL) | |
| for _s in (sys.stdout, sys.stderr): | |
| try: | |
| _s.reconfigure(encoding="utf-8", errors="replace") | |
| except (AttributeError, ValueError): | |
| pass | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, HERE) | |
| import gradio.workflow as W # noqa: E402 | |
| from gradio.helpers import special_args # noqa: E402 | |
| from gradio.workflow_api import ( # noqa: E402 | |
| WorkflowExecutor, | |
| WorkflowGraph, | |
| group_free_inputs, | |
| subject_groups, | |
| ) | |
| from huggingface_hub import get_token # noqa: E402 | |
| import nodes as N # noqa: E402 | |
| TOKEN = types.SimpleNamespace(token=get_token() or os.environ.get("HF_TOKEN")) | |
| OUTDIR = os.path.join(HERE, "_test_output") | |
| os.makedirs(OUTDIR, exist_ok=True) | |
| # A real photograph — DETR/ViT/the VLM need actual content to say anything about. | |
| SAMPLE_URL = "https://huggingface.co/datasets/mishig/sample_images/resolve/main/tiger.jpg" | |
| SAMPLE = os.path.join(OUTDIR, "_sample.jpg") | |
| if not os.path.exists(SAMPLE): | |
| urllib.request.urlretrieve(SAMPLE_URL, SAMPLE) # noqa: S310 | |
| # An image that carries embedded generation parameters, for the PNG Info pipeline. | |
| STAMPED = os.path.join(OUTDIR, "_stamped.png") | |
| if not os.path.exists(STAMPED): | |
| _info = N.generation_info("a red fox in snow", "blurry, watermark", 8, 3.5, | |
| 987654, 1024, 768, "black-forest-labs/FLUX.1-schnell") | |
| _uri = N.postprocess(N._emit(N._load_image(SAMPLE)), 1, "Lanczos", 0, 1, 1, 1, | |
| 0, 0, 0, "", _info) | |
| with open(STAMPED, "wb") as f: | |
| f.write(base64.b64decode(_uri.partition(",")[2])) | |
| def call_fn(data, request=None, token=None): | |
| """Mirror of gradio's bound-function server fn (workflow.py `call_fn`).""" | |
| name = data[0] if data else "" | |
| fn = N.BIND.get(name) | |
| if fn is None: | |
| return json.dumps({"error": f"No function '{name}' bound"}) | |
| try: | |
| args = json.loads(data[1] if len(data) > 1 else "[]") | |
| if not isinstance(args, list): | |
| args = [args] | |
| # gradio injects OAuthToken/Request params before calling — mirror that | |
| # so `txt2img` receives its token exactly as it will in the app. | |
| args, *_ = special_args(fn, args, request, None, token=token) | |
| result = fn(*args) | |
| return json.dumps(list(result) if isinstance(result, (list, tuple)) else [result]) | |
| except Exception as e: | |
| return json.dumps({"error": f"{type(e).__name__}: {e}"}) | |
| CALLERS = {"fn": call_fn, "model": W.call_model, | |
| "space": W.call_space, "dataset": W.fetch_dataset} | |
| with open(os.path.join(HERE, "workflow.json"), encoding="utf-8") as f: | |
| GRAPH = WorkflowGraph.from_json(f.read()) | |
| # Values fed to reference nodes. Anything not listed falls back to the node's | |
| # own default (`data.out`), or to the sample image for media ports. | |
| OVERRIDES = { | |
| "ref_interrogate_image": SAMPLE, | |
| "ref_detect_image": SAMPLE, | |
| "ref_extras_image": SAMPLE, | |
| "ref_control_image": SAMPLE, | |
| "ref_init_image": SAMPLE, | |
| "ref_pnginfo_image": STAMPED, | |
| } | |
| # Subjects whose upstream is entirely local — these must pass with no network. | |
| LOCAL_ONLY = {"sub_control", "sub_png_report", "sub_png_fields", | |
| "sub_upscaled", "sub_upscale_report"} | |
| def seed_for(subject_id): | |
| node = GRAPH.node_by_id[subject_id] | |
| inputs = {} | |
| # free_inputs yields {"node", "port", "type", "label"} wrappers, not the | |
| # reference nodes themselves. | |
| for free in group_free_inputs(GRAPH, [node]): | |
| ref = free["node"] | |
| rid = ref["id"] | |
| if rid in OVERRIDES: | |
| inputs[rid] = OVERRIDES[rid] | |
| continue | |
| default = (ref.get("data") or {}).get("out") | |
| if default in (None, "") and free["type"] in ("image", "audio", "video"): | |
| default = SAMPLE | |
| inputs[rid] = default | |
| return inputs | |
| def save(subject_id, value): | |
| """Persist an output so it can actually be looked at; return a summary.""" | |
| if isinstance(value, str) and value.startswith("data:"): | |
| header, _, payload = value.partition(",") | |
| ext = "png" if "png" in header else "jpg" | |
| path = os.path.join(OUTDIR, f"{subject_id}.{ext}") | |
| raw = base64.b64decode(payload) | |
| with open(path, "wb") as f: | |
| f.write(raw) | |
| from PIL import Image | |
| with Image.open(path) as im: | |
| return f"image {im.width}×{im.height} {ext.upper()}, {len(raw) // 1024} KB" | |
| if isinstance(value, str) and os.path.isfile(value): | |
| from PIL import Image | |
| try: | |
| with Image.open(value) as im: | |
| dst = os.path.join(OUTDIR, f"{subject_id}.png") | |
| im.convert("RGBA" if im.mode in ("RGBA", "LA", "P") else "RGB").save(dst) | |
| return f"image {im.width}×{im.height} (file)" | |
| except Exception: | |
| return f"file {os.path.basename(value)}" | |
| if isinstance(value, (dict, list)): | |
| path = os.path.join(OUTDIR, f"{subject_id}.json") | |
| with open(path, "w", encoding="utf-8") as f: | |
| json.dump(value, f, indent=2, ensure_ascii=False) | |
| return f"{type(value).__name__} ({len(value)} entries)" | |
| text = str(value) | |
| path = os.path.join(OUTDIR, f"{subject_id}.txt") | |
| with open(path, "w", encoding="utf-8") as f: | |
| f.write(text) | |
| return f"text ({len(text)} chars): {text.splitlines()[0][:76] if text.strip() else '(empty)'}" | |
| def main(): | |
| args = [a.lower() for a in sys.argv[1:]] | |
| local_only = "local" in args | |
| filters = [a for a in args if a != "local"] | |
| executor = WorkflowExecutor(GRAPH, CALLERS) | |
| targets = [s["id"] for s in GRAPH.subjects] | |
| if local_only: | |
| targets = [t for t in targets if t in LOCAL_ONLY] | |
| if filters: | |
| targets = [t for t in targets | |
| if any(f in t.lower() or f in GRAPH.node_by_id[t]["label"].lower() | |
| for f in filters)] | |
| if not targets: | |
| print("no subjects matched") | |
| return 1 | |
| print(f"\nRunning {len(targets)} output(s) through WorkflowExecutor") | |
| print(f"outputs → {os.path.relpath(OUTDIR, os.getcwd())}\n") | |
| passed, failed = [], [] | |
| for sid in targets: | |
| label = GRAPH.node_by_id[sid]["label"] | |
| t0 = time.time() | |
| try: | |
| value = executor.run(sid, seed_for(sid), request=None, token=TOKEN) | |
| if value is None or value == "": | |
| raise AssertionError("output was empty") | |
| summary = save(sid, value) | |
| except Exception as e: | |
| failed.append((sid, f"{type(e).__name__}: {e}")) | |
| print(f" FAIL {label:28} ({time.time() - t0:6.1f}s) " | |
| f"{type(e).__name__}: {str(e)[:130]}") | |
| else: | |
| passed.append(sid) | |
| print(f" ok {label:28} ({time.time() - t0:6.1f}s) {summary}") | |
| sys.stdout.flush() | |
| print("\n── API surface (one endpoint per subject group) ──") | |
| for group in subject_groups(GRAPH): | |
| names = ", ".join(s["label"] for s in group) | |
| params = ", ".join(f"{f['label']} ({f['type']})" | |
| for f in group_free_inputs(GRAPH, group)) | |
| print(f" • {names}\n inputs: {params or '(none)'}") | |
| print(f"\n{'=' * 66}\n {len(passed)} passed, {len(failed)} failed\n{'=' * 66}") | |
| for sid, err in failed: | |
| print(f" {sid}: {err}") | |
| return 1 if failed else 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |