comfyui

Generate images, video, and audio via diffusion workflows.

  • comfyui
  • image-generation
  • stable-diffusion
  • flux
  • sd3
  • wan-video
  • hunyuan-video
  • creative
  • generative-ai
  • video-generation

Declared platforms: macos · linux · windows

Install
npx skills add 'https://github.com/NousResearch/hermes-agent/tree/main/optional-skills/creative/comfyui'
Download bundle ↓
main · 24fd22bScanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
← Back to SKILL.md
#!/usr/bin/env python3"""health_check.py — One-stop verification that the ComfyUI environment is ready. Runs through the verification checklist:  1. comfy-cli on PATH  2. server reachable (/system_stats)  3. at least one checkpoint installed  4. (optional) a specific workflow's deps are met  5. (optional) actually submit a tiny test workflow and verify round-trip Usage:    python3 health_check.py    python3 health_check.py --host https://cloud.comfy.org    python3 health_check.py --workflow my.json    python3 health_check.py --smoke-test    # actually submit a tiny workflow""" from __future__ import annotations import argparseimport jsonimport shutilimport sysfrom pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent))from _common import (  # noqa: E402    DEFAULT_LOCAL_HOST, ENV_API_KEY, emit_json, http_get, parse_model_list,    resolve_api_key, resolve_url, unwrap_workflow,)  def comfy_cli_status() -> dict:    if shutil.which("comfy"):        return {"available": True, "method": "comfy", "path": shutil.which("comfy")}    if shutil.which("uvx"):        return {"available": True, "method": "uvx",                "hint": "Invoke as `uvx --from comfy-cli comfy ...`"}    return {        "available": False,        "hint": "Install with: pipx install comfy-cli (or `pip install comfy-cli`)",    }  def server_status(host: str, headers: dict) -> dict:    url = resolve_url(host, "/system_stats")    try:        r = http_get(url, headers=headers, retries=2, timeout=10)        if r.status == 200:            try:                stats = r.json() or {}            except Exception:                stats = {}            return {"reachable": True, "url": url, "stats": stats}        return {"reachable": False, "url": url, "http_status": r.status, "body": r.text()[:200]}    except Exception as e:        return {"reachable": False, "url": url, "error": str(e)}  def checkpoint_status(host: str, headers: dict) -> dict:    url = resolve_url(host, "/models/checkpoints")    try:        r = http_get(url, headers=headers, retries=2, timeout=15)    except Exception as e:        return {"queryable": False, "error": str(e)}    if r.status != 200:        return {"queryable": False, "http_status": r.status, "url": url, "body": r.text()[:200]}    try:        models = parse_model_list(r.json())    except Exception:        models = set()    return {"queryable": True, "count": len(models),            "first_few": sorted(models)[:5]}  SMOKE_WORKFLOW = {    # Minimal SD1.5 workflow that doesn't depend on rare nodes.    # 256x256 + 1 step is the smallest config that doesn't trigger SDXL/Flux    # validation errors while still executing fast.    "3": {        "class_type": "KSampler",        "inputs": {            "seed": 1, "steps": 1, "cfg": 7.0,            "sampler_name": "euler", "scheduler": "normal", "denoise": 1.0,            "model": ["4", 0], "positive": ["6", 0], "negative": ["7", 0],            "latent_image": ["5", 0],        },    },    "4": {"class_type": "CheckpointLoaderSimple",          "inputs": {"ckpt_name": "REPLACE_ME"}},    "5": {"class_type": "EmptyLatentImage",          "inputs": {"width": 256, "height": 256, "batch_size": 1}},    "6": {"class_type": "CLIPTextEncode",          "inputs": {"text": "test", "clip": ["4", 1]}},    "7": {"class_type": "CLIPTextEncode",          "inputs": {"text": "", "clip": ["4", 1]}},    "9": {"class_type": "SaveImage",          "inputs": {"filename_prefix": "smoke", "images": ["3", 0]}},}  def smoke_test(host: str, headers: dict, ckpt_name: str | None) -> dict:    """Submit a tiny workflow and verify the server accepts it.     Cancels the job immediately after acceptance so we don't burn GPU    time / cloud minutes on a smoke test.    """    if not ckpt_name:        return {"ran": False, "reason": "no checkpoint available"}    wf = json.loads(json.dumps(SMOKE_WORKFLOW))    wf["4"]["inputs"]["ckpt_name"] = ckpt_name     # Lazy import to avoid circular issues    from run_workflow import ComfyRunner    api_key = headers.get("X-API-Key")    runner = ComfyRunner(host=host, api_key=api_key)    sub = runner.submit(wf)    if "_http_error" in sub:        return {"ran": True, "submitted": False,                "http_status": sub["_http_error"], "body": sub.get("body")}    pid = sub.get("prompt_id")    if not pid:        return {"ran": True, "submitted": False, "response": sub}     # Cancel so we don't actually waste compute on the smoke test.    cancelled = False    try:        cancelled = runner.cancel(pid)    except Exception:        pass     return {        "ran": True, "submitted": True, "prompt_id": pid,        "cancelled_after_submit": cancelled,        "note": "Submission accepted; cancelled to avoid running the full pipeline.",    }  def main(argv: list[str] | None = None) -> int:    p = argparse.ArgumentParser(description="One-stop ComfyUI health check")    p.add_argument("--host", default=DEFAULT_LOCAL_HOST)    p.add_argument("--api-key", help=f"or set ${ENV_API_KEY}")    p.add_argument("--workflow", help="Optional: also run check_deps on this workflow")    p.add_argument("--smoke-test", action="store_true",                   help="Submit a tiny test workflow and verify round-trip")    p.add_argument("--strict", action="store_true",                   help="Exit non-zero on any non-pass condition (including warnings)")    args = p.parse_args(argv)     api_key = resolve_api_key(args.api_key)    headers = {"X-API-Key": api_key} if api_key else {}     cli = comfy_cli_status()    server = server_status(args.host, headers)    ckpts = checkpoint_status(args.host, headers) if server.get("reachable") else None     # ---- workflow check ----    workflow_check: dict | None = None    if args.workflow:        wf_path = Path(args.workflow).expanduser()        if not wf_path.exists():            workflow_check = {"error": "workflow file not found"}        else:            try:                with wf_path.open(encoding="utf-8-sig") as f:                    workflow = unwrap_workflow(json.load(f))                from check_deps import check_deps                workflow_check = check_deps(workflow, host=args.host, api_key=api_key)            except (ValueError, json.JSONDecodeError) as e:                workflow_check = {"error": str(e)}     smoke = None    if args.smoke_test and server.get("reachable"):        first_ckpt = ckpts["first_few"][0] if ckpts and ckpts.get("first_few") else None        smoke = smoke_test(args.host, headers, first_ckpt)     # ---- verdict ----    verdict = "pass"    reasons: list[str] = []    if not server.get("reachable"):        verdict = "fail"        reasons.append("server unreachable")    if ckpts and ckpts.get("queryable") and ckpts.get("count", 0) == 0:        verdict = "warn" if verdict == "pass" else verdict        reasons.append("no checkpoints installed")    if workflow_check and workflow_check.get("error"):        verdict = "fail"        reasons.append(f"workflow check failed: {workflow_check['error']}")    elif workflow_check and not workflow_check.get("is_ready"):        if workflow_check.get("node_check_skipped"):            reasons.append("node check skipped (cloud free tier)")        else:            verdict = "fail"            reasons.append("workflow has missing deps")    if smoke and smoke.get("ran") and not smoke.get("submitted"):        verdict = "fail"        reasons.append("smoke-test submission failed")    if not cli.get("available"):        verdict = "warn" if verdict == "pass" else verdict        reasons.append("comfy-cli not on PATH (lifecycle commands won't work)")     report = {        "verdict": verdict,        "reasons": reasons,        "host": args.host,        "comfy_cli": cli,        "server": server,        "checkpoints": ckpts,        "workflow_check": workflow_check,        "smoke_test": smoke,    }    emit_json(report)     if verdict == "pass":        return 0    if verdict == "warn":        return 1 if args.strict else 0    return 1  if __name__ == "__main__":    sys.exit(main()) 
Referenced from SKILL.md