mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-02 02:01:47 +00:00
feat(comfyui): add hardware check + auto-gate local install on verdict
Layers a programmatic hardware-feasibility check on top of the v4 skill
so the agent doesn't silently push users toward a local install they
can't actually run. The official comfy-cli supports --nvidia / --amd /
--m-series / --cpu, but has no guard against "4 GB laptop GPU on SDXL"
or "Intel Mac falling back to CPU" — both route to comfy-cli paths in
the original table and then fail on first workflow.
- scripts/hardware_check.py: detect OS/arch/GPU (NVIDIA nvidia-smi,
AMD rocm-smi, Apple M1+ via arm64+sysctl, Intel Arc via clinfo),
VRAM, system/unified RAM. Emits JSON
{verdict: ok|marginal|cloud, recommended_install_path, comfy_cli_flag}
with practical thresholds: discrete GPU >=6 GB VRAM minimum,
Apple Silicon >=16 GB unified memory minimum, Intel Mac -> cloud,
no accelerator -> cloud. comfy_cli_flag maps directly to
`comfy install` so the agent can stitch the whole flow together.
- scripts/comfyui_setup.sh: runs hardware_check.py first when no
explicit flag is passed. If verdict=cloud, refuses to install
locally, prints Comfy Cloud URL + an override command, exits 2.
Otherwise auto-selects the right --nvidia/--amd/--m-series flag
for `comfy install`. Surfaces marginal-verdict notes to the user.
- SKILL.md Setup & Onboarding: adds mandatory Step 0 "Check If This
Machine Can Run ComfyUI Locally" ahead of the Path A-E selection.
Documents the verdict thresholds inline, ties verdict + comfy_cli_flag
to the install paths, and updates the path-choice table so
"verdict: cloud" is the first row. Quick-Start "Detect Environment"
block extended to include the hardware check. Verification
checklist gains a hardware-check gate.
- Frontmatter setup.help rewritten to point at hardware_check.py
first. Version bumped 4.0.0 -> 4.1.0.
This commit is contained in:
parent
528a13b37a
commit
9d7ece362d
4 changed files with 431 additions and 12 deletions
|
|
@ -2,8 +2,14 @@
|
|||
# ComfyUI Setup — Install, launch, and verify using the official comfy-cli.
|
||||
# Usage: bash scripts/comfyui_setup.sh [--nvidia|--amd|--m-series|--cpu]
|
||||
#
|
||||
# If no flag is passed, runs hardware_check.py to detect the right one
|
||||
# automatically, and refuses to install locally when the verdict is "cloud"
|
||||
# (no usable GPU, too little VRAM, Intel Mac, etc.) — pointing the user
|
||||
# at Comfy Cloud instead.
|
||||
#
|
||||
# Prerequisites: Python 3.10+, pip
|
||||
# What it does:
|
||||
# 0. Hardware check (skipped if a flag was passed explicitly)
|
||||
# 1. Installs comfy-cli (if not present)
|
||||
# 2. Disables analytics tracking
|
||||
# 3. Installs ComfyUI + ComfyUI-Manager
|
||||
|
|
@ -12,7 +18,55 @@
|
|||
|
||||
set -euo pipefail
|
||||
|
||||
GPU_FLAG="${1:---nvidia}" # Default to NVIDIA
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HARDWARE_CHECK="$SCRIPT_DIR/hardware_check.py"
|
||||
|
||||
# Step 0: Hardware check (auto-detect GPU flag when none was provided)
|
||||
if [ $# -ge 1 ]; then
|
||||
GPU_FLAG="$1"
|
||||
echo "==> GPU flag: $GPU_FLAG (user-supplied, skipping hardware check)"
|
||||
else
|
||||
if [ ! -f "$HARDWARE_CHECK" ]; then
|
||||
echo "==> hardware_check.py not found, defaulting to --nvidia"
|
||||
GPU_FLAG="--nvidia"
|
||||
else
|
||||
echo "==> Running hardware check..."
|
||||
set +e
|
||||
HW_JSON="$(python3 "$HARDWARE_CHECK" --json)"
|
||||
HW_EXIT=$?
|
||||
set -e
|
||||
echo "$HW_JSON"
|
||||
echo ""
|
||||
|
||||
VERDICT="$(echo "$HW_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("verdict",""))')"
|
||||
FLAG="$(echo "$HW_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("comfy_cli_flag") or "")')"
|
||||
|
||||
if [ "$VERDICT" = "cloud" ]; then
|
||||
echo ""
|
||||
echo "==> Hardware check: this machine is not suitable for local ComfyUI."
|
||||
echo " Recommended: Comfy Cloud — https://platform.comfy.org"
|
||||
echo ""
|
||||
echo " If you want to override and install anyway, re-run with an"
|
||||
echo " explicit flag: bash $0 --nvidia|--amd|--m-series|--cpu"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ -z "$FLAG" ]; then
|
||||
echo "==> Hardware check couldn't pick a comfy-cli flag. Defaulting to --nvidia."
|
||||
echo " (For Intel Arc or unsupported hardware, use the manual install path.)"
|
||||
GPU_FLAG="--nvidia"
|
||||
else
|
||||
GPU_FLAG="$FLAG"
|
||||
fi
|
||||
|
||||
if [ "$VERDICT" = "marginal" ]; then
|
||||
echo "==> Hardware check: verdict is MARGINAL."
|
||||
echo " SD1.5 should work; SDXL/Flux may be slow or OOM."
|
||||
echo " Consider Comfy Cloud for heavier workflows: https://platform.comfy.org"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "==> ComfyUI Setup"
|
||||
echo " GPU flag: $GPU_FLAG"
|
||||
|
|
|
|||
311
optional-skills/creative/comfyui/scripts/hardware_check.py
Executable file
311
optional-skills/creative/comfyui/scripts/hardware_check.py
Executable file
|
|
@ -0,0 +1,311 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Detect whether this machine can realistically run ComfyUI locally.
|
||||
|
||||
Emits a structured JSON report the agent can read to decide whether to:
|
||||
- help the user install ComfyUI locally, or
|
||||
- steer them to Comfy Cloud instead.
|
||||
|
||||
Usage:
|
||||
python3 hardware_check.py [--json]
|
||||
|
||||
Exit code:
|
||||
0 → "ok" — can run local ComfyUI at reasonable speed
|
||||
1 → "marginal" — technically works but slow / memory-tight
|
||||
2 → "cloud" — local is not viable, recommend Comfy Cloud
|
||||
|
||||
The JSON report always prints to stdout regardless of exit code.
|
||||
|
||||
Output fields the agent should read:
|
||||
verdict: "ok" | "marginal" | "cloud"
|
||||
recommended_install_path: "nvidia" | "amd" | "apple-silicon" | "intel" | "comfy-cloud"
|
||||
comfy_cli_flag: "--nvidia" | "--amd" | "--m-series" | None
|
||||
(pass directly to `comfy install` when verdict != cloud)
|
||||
gpu: detected GPU info or null
|
||||
notes: list of human-readable strings to surface to the user
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
# Rough thresholds. SDXL/Flux need real VRAM; SD1.5 will scrape by on 6GB.
|
||||
# Apple Silicon shares RAM with GPU — unified memory budget is total RAM.
|
||||
MIN_VRAM_GB_USABLE = 6 # below this, most modern models won't load
|
||||
OK_VRAM_GB = 8 # SDXL fits comfortably here
|
||||
GREAT_VRAM_GB = 12 # Flux / video models start being realistic
|
||||
MIN_MAC_RAM_GB = 16 # Apple Silicon unified memory; below = pain
|
||||
OK_MAC_RAM_GB = 32 # smooth for SDXL / most workflows
|
||||
|
||||
|
||||
def _run(cmd: list[str], timeout: int = 5) -> str:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=timeout, check=False
|
||||
)
|
||||
return (out.stdout or "") + (out.stderr or "")
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
||||
return ""
|
||||
|
||||
|
||||
def detect_nvidia() -> dict | None:
|
||||
if not shutil.which("nvidia-smi"):
|
||||
return None
|
||||
out = _run([
|
||||
"nvidia-smi",
|
||||
"--query-gpu=name,memory.total,driver_version",
|
||||
"--format=csv,noheader,nounits",
|
||||
])
|
||||
if not out.strip():
|
||||
return None
|
||||
first = out.strip().splitlines()[0]
|
||||
parts = [p.strip() for p in first.split(",")]
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
name = parts[0]
|
||||
try:
|
||||
vram_mb = int(parts[1])
|
||||
except ValueError:
|
||||
vram_mb = 0
|
||||
driver = parts[2] if len(parts) > 2 else ""
|
||||
return {
|
||||
"vendor": "nvidia",
|
||||
"name": name,
|
||||
"vram_gb": round(vram_mb / 1024, 1),
|
||||
"driver": driver,
|
||||
}
|
||||
|
||||
|
||||
def detect_rocm() -> dict | None:
|
||||
if not shutil.which("rocm-smi"):
|
||||
return None
|
||||
out = _run(["rocm-smi", "--showproductname", "--showmeminfo", "vram"])
|
||||
if not out.strip():
|
||||
return None
|
||||
name_m = re.search(r"Card series:\s*(.+)", out)
|
||||
vram_m = re.search(r"VRAM Total Memory \(B\):\s*(\d+)", out)
|
||||
vram_gb = 0.0
|
||||
if vram_m:
|
||||
vram_gb = round(int(vram_m.group(1)) / (1024**3), 1)
|
||||
return {
|
||||
"vendor": "amd",
|
||||
"name": name_m.group(1).strip() if name_m else "AMD GPU",
|
||||
"vram_gb": vram_gb,
|
||||
"driver": "rocm",
|
||||
}
|
||||
|
||||
|
||||
def detect_apple_silicon() -> dict | None:
|
||||
if platform.system() != "Darwin":
|
||||
return None
|
||||
if platform.machine() != "arm64":
|
||||
return None # Intel Mac — no usable MPS
|
||||
chip = _run(["sysctl", "-n", "machdep.cpu.brand_string"]).strip()
|
||||
# Examples: "Apple M1", "Apple M1 Pro", "Apple M2 Max", "Apple M3 Ultra"
|
||||
m = re.search(r"Apple M(\d+)", chip)
|
||||
generation = int(m.group(1)) if m else 1
|
||||
mem_bytes = 0
|
||||
try:
|
||||
mem_bytes = int(_run(["sysctl", "-n", "hw.memsize"]).strip() or 0)
|
||||
except ValueError:
|
||||
pass
|
||||
ram_gb = round(mem_bytes / (1024**3), 1) if mem_bytes else 0.0
|
||||
return {
|
||||
"vendor": "apple",
|
||||
"name": chip or "Apple Silicon",
|
||||
"generation": generation,
|
||||
"unified_memory_gb": ram_gb,
|
||||
}
|
||||
|
||||
|
||||
def detect_intel_arc() -> dict | None:
|
||||
if platform.system() != "Linux":
|
||||
return None
|
||||
if not shutil.which("clinfo"):
|
||||
return None
|
||||
out = _run(["clinfo", "--list"])
|
||||
if "Intel" in out and ("Arc" in out or "Xe" in out):
|
||||
return {"vendor": "intel", "name": "Intel Arc/Xe", "vram_gb": 0.0}
|
||||
return None
|
||||
|
||||
|
||||
def total_system_ram_gb() -> float:
|
||||
sysname = platform.system()
|
||||
if sysname == "Darwin":
|
||||
try:
|
||||
return round(int(_run(["sysctl", "-n", "hw.memsize"]).strip() or 0) / (1024**3), 1)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
if sysname == "Linux":
|
||||
try:
|
||||
with open("/proc/meminfo", "r") as fh:
|
||||
for line in fh:
|
||||
if line.startswith("MemTotal:"):
|
||||
kb = int(line.split()[1])
|
||||
return round(kb / (1024**2), 1)
|
||||
except OSError:
|
||||
return 0.0
|
||||
if sysname == "Windows":
|
||||
out = _run(["wmic", "ComputerSystem", "get", "TotalPhysicalMemory"])
|
||||
m = re.search(r"(\d{6,})", out)
|
||||
if m:
|
||||
return round(int(m.group(1)) / (1024**3), 1)
|
||||
return 0.0
|
||||
|
||||
|
||||
# Map recommended_install_path → flag the agent can pass to `comfy install`
|
||||
# Set to None when no local install is advised (verdict=cloud).
|
||||
_COMFY_CLI_FLAG = {
|
||||
"nvidia": "--nvidia",
|
||||
"amd": "--amd",
|
||||
"apple-silicon": "--m-series",
|
||||
"intel": None, # comfy-cli has no Intel Arc flag — manual install
|
||||
"comfy-cloud": None,
|
||||
}
|
||||
|
||||
|
||||
def classify(gpu: dict | None, ram_gb: float) -> tuple[str, str, list[str]]:
|
||||
"""Return (verdict, recommended_install_path, notes)."""
|
||||
notes: list[str] = []
|
||||
|
||||
if gpu is None:
|
||||
notes.append(
|
||||
"No supported accelerator found (NVIDIA CUDA / AMD ROCm / Apple Silicon / Intel Arc)."
|
||||
)
|
||||
notes.append(
|
||||
"CPU-only ComfyUI works but is unusably slow for modern models — use Comfy Cloud."
|
||||
)
|
||||
return "cloud", "comfy-cloud", notes
|
||||
|
||||
if gpu["vendor"] == "apple":
|
||||
gen = gpu.get("generation", 1)
|
||||
mem = gpu.get("unified_memory_gb", 0.0)
|
||||
if mem < MIN_MAC_RAM_GB:
|
||||
notes.append(
|
||||
f"Apple Silicon with {mem} GB unified memory — below the {MIN_MAC_RAM_GB} GB practical minimum."
|
||||
)
|
||||
notes.append("SD1.5 may work; SDXL/Flux will swap or OOM. Recommend Comfy Cloud.")
|
||||
return "cloud", "comfy-cloud", notes
|
||||
if mem < OK_MAC_RAM_GB:
|
||||
notes.append(
|
||||
f"Apple Silicon M{gen} with {mem} GB — SDXL works but slow. Flux/video likely too tight."
|
||||
)
|
||||
return "marginal", "apple-silicon", notes
|
||||
notes.append(f"Apple Silicon M{gen} with {mem} GB unified memory — good for SDXL/Flux.")
|
||||
return "ok", "apple-silicon", notes
|
||||
|
||||
# Discrete GPU path (nvidia/amd/intel)
|
||||
vram = gpu.get("vram_gb", 0.0)
|
||||
if gpu["vendor"] == "intel":
|
||||
notes.append("Intel Arc detected — ComfyUI IPEX support is experimental; Comfy Cloud is more reliable.")
|
||||
return "marginal", "intel", notes
|
||||
if vram < MIN_VRAM_GB_USABLE:
|
||||
notes.append(
|
||||
f"{gpu['name']} has only {vram} GB VRAM — below the {MIN_VRAM_GB_USABLE} GB practical minimum."
|
||||
)
|
||||
notes.append("Most modern models won't load. Recommend Comfy Cloud.")
|
||||
return "cloud", "comfy-cloud", notes
|
||||
if vram < OK_VRAM_GB:
|
||||
notes.append(
|
||||
f"{gpu['name']} ({vram} GB VRAM) — SD1.5 works, SDXL tight, Flux/video unlikely."
|
||||
)
|
||||
return "marginal", gpu["vendor"], notes
|
||||
if vram < GREAT_VRAM_GB:
|
||||
notes.append(f"{gpu['name']} ({vram} GB VRAM) — SDXL comfortable, Flux possible with optimizations.")
|
||||
return "ok", gpu["vendor"], notes
|
||||
notes.append(f"{gpu['name']} ({vram} GB VRAM) — can run everything including Flux/video.")
|
||||
return "ok", gpu["vendor"], notes
|
||||
|
||||
|
||||
def build_report() -> dict:
|
||||
sysname = platform.system()
|
||||
arch = platform.machine()
|
||||
ram_gb = total_system_ram_gb()
|
||||
|
||||
gpu = (
|
||||
detect_nvidia()
|
||||
or detect_rocm()
|
||||
or detect_apple_silicon()
|
||||
or detect_intel_arc()
|
||||
)
|
||||
|
||||
# Intel Mac special case — fall out of apple-silicon detection with no GPU
|
||||
if gpu is None and sysname == "Darwin" and platform.machine() != "arm64":
|
||||
notes = [
|
||||
"Intel Mac detected — no MPS backend available.",
|
||||
"ComfyUI will fall back to CPU which is unusably slow. Use Comfy Cloud.",
|
||||
]
|
||||
return {
|
||||
"os": sysname,
|
||||
"arch": arch,
|
||||
"system_ram_gb": ram_gb,
|
||||
"gpu": None,
|
||||
"verdict": "cloud",
|
||||
"recommended_install_path": "comfy-cloud",
|
||||
"comfy_cli_flag": None,
|
||||
"notes": notes,
|
||||
"install_urls": _install_urls(),
|
||||
}
|
||||
|
||||
verdict, install_path, notes = classify(gpu, ram_gb)
|
||||
|
||||
return {
|
||||
"os": sysname,
|
||||
"arch": arch,
|
||||
"system_ram_gb": ram_gb,
|
||||
"gpu": gpu,
|
||||
"verdict": verdict,
|
||||
"recommended_install_path": install_path,
|
||||
"comfy_cli_flag": _COMFY_CLI_FLAG.get(install_path),
|
||||
"notes": notes,
|
||||
"install_urls": _install_urls(),
|
||||
}
|
||||
|
||||
|
||||
def _install_urls() -> dict:
|
||||
return {
|
||||
"desktop": "https://docs.comfy.org/installation/desktop",
|
||||
"manual": "https://docs.comfy.org/installation/manual_install",
|
||||
"comfy_cli": "https://docs.comfy.org/comfy-cli/getting-started",
|
||||
"cloud": "https://platform.comfy.org",
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
report = build_report()
|
||||
json_mode = "--json" in sys.argv
|
||||
|
||||
if json_mode:
|
||||
print(json.dumps(report, indent=2))
|
||||
else:
|
||||
print(f"OS: {report['os']} ({report['arch']})")
|
||||
print(f"RAM: {report['system_ram_gb']} GB")
|
||||
if report["gpu"]:
|
||||
g = report["gpu"]
|
||||
if g["vendor"] == "apple":
|
||||
print(f"GPU: {g['name']} — {g.get('unified_memory_gb', 0)} GB unified memory")
|
||||
else:
|
||||
print(f"GPU: {g['name']} — {g.get('vram_gb', 0)} GB VRAM")
|
||||
else:
|
||||
print("GPU: (none detected)")
|
||||
print(f"Verdict: {report['verdict']} → {report['recommended_install_path']}")
|
||||
if report["comfy_cli_flag"]:
|
||||
print(f" → run: comfy --skip-prompt install {report['comfy_cli_flag']}")
|
||||
for n in report["notes"]:
|
||||
print(f" • {n}")
|
||||
|
||||
if report["verdict"] == "ok":
|
||||
return 0
|
||||
if report["verdict"] == "marginal":
|
||||
return 1
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue