Fix: flatten comfyui and ltx-video-pipeline to Gitea convention (flat skill dirs). comfyui v5.1.0, ltx-video-pipeline v2.0.0.
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
# comfy-cli Command Reference
|
||||
|
||||
Official CLI from [Comfy-Org/comfy-cli](https://github.com/Comfy-Org/comfy-cli).
|
||||
Docs: https://docs.comfy.org/comfy-cli/getting-started
|
||||
|
||||
## Installation
|
||||
|
||||
Order of preference:
|
||||
|
||||
```bash
|
||||
pipx install comfy-cli # recommended (isolated env)
|
||||
uvx --from comfy-cli comfy --help # zero-install via uv
|
||||
pip install --user comfy-cli # fallback
|
||||
```
|
||||
|
||||
The skill's `comfyui_setup.sh` picks the best available method.
|
||||
|
||||
First run may prompt for analytics. Disable non-interactively:
|
||||
```bash
|
||||
comfy --skip-prompt tracking disable
|
||||
```
|
||||
|
||||
## Global Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--workspace <path>` | Target a specific ComfyUI workspace |
|
||||
| `--recent` | Use most recently used workspace |
|
||||
| `--here` | Use current directory as workspace |
|
||||
| `--skip-prompt` | No interactive prompts (use defaults) |
|
||||
| `-v` / `--version` | Print version |
|
||||
|
||||
Workspace resolution priority:
|
||||
1. `--workspace` (explicit path)
|
||||
2. `--recent` (from config)
|
||||
3. `--here` (cwd)
|
||||
4. `comfy set-default` path
|
||||
5. Most recently used
|
||||
6. `~/comfy/ComfyUI` (Linux) or `~/Documents/comfy/ComfyUI` (macOS/Win)
|
||||
|
||||
## Lifecycle Commands
|
||||
|
||||
### `comfy install`
|
||||
|
||||
Download and install ComfyUI + ComfyUI-Manager.
|
||||
|
||||
```bash
|
||||
comfy install # interactive GPU selection
|
||||
comfy install --nvidia
|
||||
comfy install --amd # ROCm (Linux)
|
||||
comfy install --m-series # Apple Silicon (MPS)
|
||||
comfy install --cpu # CPU only (slow)
|
||||
comfy install --fast-deps # use uv for deps
|
||||
comfy install --skip-manager # skip ComfyUI-Manager
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--nvidia` / `--amd` / `--m-series` / `--cpu` | GPU type |
|
||||
| `--cuda-version` | 11.8, 12.1, 12.4, 12.6, 12.8, 12.9, 13.0 |
|
||||
| `--rocm-version` | 6.1, 6.2, 6.3, 7.0, 7.1 |
|
||||
| `--fast-deps` | uv-based dependency resolution |
|
||||
| `--skip-manager` | Don't install ComfyUI-Manager |
|
||||
| `--skip-torch-or-directml` | Skip PyTorch install |
|
||||
| `--version <ver>` | `0.2.0`, `latest`, `nightly` |
|
||||
| `--commit <hash>` | Install specific commit |
|
||||
| `--pr "#1234"` | Install from a PR |
|
||||
| `--restore` | Restore deps for existing install |
|
||||
|
||||
### `comfy launch`
|
||||
|
||||
```bash
|
||||
comfy launch # foreground :8188
|
||||
comfy launch --background # background daemon
|
||||
comfy launch -- --listen 0.0.0.0 # LAN-accessible
|
||||
comfy launch -- --port 8190 # custom port
|
||||
comfy launch -- --cpu # force CPU mode
|
||||
comfy launch -- --lowvram # 6 GB cards
|
||||
comfy launch --background -- --listen 0.0.0.0 --port 8190
|
||||
```
|
||||
|
||||
Common extra args after `--`: `--listen`, `--port`, `--cpu`, `--lowvram`,
|
||||
`--novram`, `--fp16-vae`, `--force-fp32`, `--disable-cuda-malloc`.
|
||||
|
||||
### `comfy stop`
|
||||
|
||||
```bash
|
||||
comfy stop
|
||||
```
|
||||
|
||||
### `comfy run`
|
||||
|
||||
Submit a raw workflow JSON to a running server. **Limited** — no parameter
|
||||
injection, no structured output download. For agents, use
|
||||
`scripts/run_workflow.py` instead.
|
||||
|
||||
```bash
|
||||
comfy run --workflow workflow_api.json
|
||||
comfy run --workflow workflow_api.json --host 10.0.0.5 --port 8188
|
||||
comfy run --workflow workflow_api.json --timeout 300 --wait
|
||||
```
|
||||
|
||||
### `comfy which`
|
||||
|
||||
```bash
|
||||
comfy which # show targeted workspace
|
||||
comfy --recent which
|
||||
```
|
||||
|
||||
### `comfy set-default`
|
||||
|
||||
```bash
|
||||
comfy set-default /path/to/ComfyUI
|
||||
comfy set-default /path/to/ComfyUI --launch-extras="--listen 0.0.0.0"
|
||||
```
|
||||
|
||||
### `comfy update`
|
||||
|
||||
```bash
|
||||
comfy update # update ComfyUI core
|
||||
comfy node update all # update all custom nodes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `comfy node` — Custom Node Management
|
||||
|
||||
All node operations use ComfyUI-Manager (`cm-cli`) under the hood.
|
||||
|
||||
```bash
|
||||
comfy node show installed # list installed
|
||||
comfy node show enabled # list enabled
|
||||
comfy node show all # all available in registry
|
||||
comfy node simple-show installed # compact list
|
||||
|
||||
comfy node install comfyui-impact-pack
|
||||
comfy node install <name> --uv-compile # ComfyUI-Manager v4.1+ unified resolver
|
||||
comfy node uninstall <name>
|
||||
comfy node update <name> | all
|
||||
comfy node enable <name>
|
||||
comfy node disable <name>
|
||||
comfy node fix <name> # fix broken deps
|
||||
|
||||
comfy node install-deps --workflow=workflow.json
|
||||
comfy node deps-in-workflow --workflow=w.json --output=deps.json
|
||||
|
||||
comfy node save-snapshot
|
||||
comfy node restore-snapshot <file>
|
||||
|
||||
comfy node bisect start # binary-search a culprit node
|
||||
comfy node bisect good
|
||||
comfy node bisect bad
|
||||
comfy node bisect reset
|
||||
```
|
||||
|
||||
### Dependency Resolution Options
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--fast-deps` | comfy-cli built-in uv resolver |
|
||||
| `--uv-compile` | ComfyUI-Manager v4.1+ unified resolver (recommended) |
|
||||
| `--no-deps` | Skip dep installation |
|
||||
|
||||
Make `uv-compile` default: `comfy manager uv-compile-default true`
|
||||
|
||||
---
|
||||
|
||||
## `comfy model` — Model Management
|
||||
|
||||
```bash
|
||||
comfy model list
|
||||
comfy model list --relative-path models/checkpoints
|
||||
|
||||
comfy model download --url <URL>
|
||||
comfy model download --url <URL> --relative-path models/loras
|
||||
comfy model download --url <URL> --filename custom_name.safetensors
|
||||
|
||||
comfy model remove # interactive
|
||||
comfy model remove --relative-path models/checkpoints --model-names "model.safetensors"
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--url` | Download URL (CivitAI, HuggingFace, direct) |
|
||||
| `--relative-path` | Subdirectory under workspace (e.g. `models/checkpoints`) |
|
||||
| `--filename` | Custom save filename |
|
||||
| `--set-civitai-api-token` | Persist CivitAI token |
|
||||
| `--set-hf-api-token` | Persist HuggingFace token |
|
||||
| `--downloader` | `httpx` (default) or `aria2` |
|
||||
|
||||
Standard model directories:
|
||||
```
|
||||
ComfyUI/models/
|
||||
├── checkpoints/ # Full model files
|
||||
├── loras/ # LoRA adapters
|
||||
├── vae/ # VAE models
|
||||
├── controlnet/ # ControlNet models
|
||||
├── clip/ # CLIP / T5 text encoders
|
||||
├── clip_vision/ # CLIP vision encoders
|
||||
├── upscale_models/ # ESRGAN / SwinIR / etc.
|
||||
├── embeddings/ # Textual inversion embeddings
|
||||
├── unet/ # Standalone UNet weights
|
||||
├── diffusion_models/ # Flux / SD3 / Wan diffusion models
|
||||
├── animatediff_models/ # AnimateDiff motion modules
|
||||
├── ipadapter/ # IPAdapter weights
|
||||
└── style_models/ # Style adapters
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `comfy manager` — ComfyUI-Manager Settings
|
||||
|
||||
```bash
|
||||
comfy manager disable # disable Manager completely
|
||||
comfy manager enable-gui # enable new GUI
|
||||
comfy manager disable-gui # API-only
|
||||
comfy manager enable-legacy-gui # legacy GUI
|
||||
comfy manager uv-compile-default true # make --uv-compile the default
|
||||
comfy manager clear # clear startup action
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `comfy pr-cache` — Frontend PR Cache
|
||||
|
||||
```bash
|
||||
comfy pr-cache list
|
||||
comfy pr-cache clean
|
||||
comfy pr-cache clean 456
|
||||
```
|
||||
|
||||
Cache expires after 7 days; max 10 builds.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
| OS | Path |
|
||||
|----|------|
|
||||
| Linux | `~/.config/comfy-cli/config.ini` |
|
||||
| macOS | `~/Library/Application Support/comfy-cli/config.ini` |
|
||||
| Windows | `~/AppData/Local/comfy-cli/config.ini` |
|
||||
|
||||
Stores: default workspace, recent workspace, background server PID, API
|
||||
tokens, manager GUI mode, launch extras.
|
||||
|
||||
## Discovery
|
||||
|
||||
Custom-node registry:
|
||||
- https://registry.comfy.org/
|
||||
|
||||
Model browsers:
|
||||
- https://huggingface.co/models
|
||||
- https://civitai.com (NSFW; requires API token for many)
|
||||
- https://comfyworkflows.com (community workflows)
|
||||
@@ -0,0 +1,222 @@
|
||||
# Remote ComfyUI Server — Connectivity & Diagnostics
|
||||
|
||||
This skill assumes ComfyUI is running locally on `127.0.0.1:8188` by default.
|
||||
When the ComfyUI server lives on another machine (LAN, headless GPU box,
|
||||
Proxmox LXC with passthrough, etc.), follow this guide.
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
1. **ComfyUI running with `--listen 0.0.0.0`** on the remote host
|
||||
2. **Firewall open** for port 8188 (or whatever custom port)
|
||||
3. **Hermes can reach it** via `curl http://REMOTE:8188/system_stats`
|
||||
4. **Use `--host http://REMOTE:PORT`** on every script invocation (or set `$COMFYUI_HOST`)
|
||||
5. If curl fails, **SSH into the remote host to diagnose and restart** (see [SSH-based diagnostics](#ssh-based-remote-diagnostics-and-remediation))
|
||||
|
||||
## SSH-based Remote Diagnostics and Remediation
|
||||
|
||||
When the remote ComfyUI server is unreachable on its HTTP port but you have
|
||||
SSH access to the host, SSH directly in to diagnose and fix. This is
|
||||
common when the user manages headless GPU boxes (Proxmox LXCs, dedicated
|
||||
render servers, etc.) and wants Hermes to handle restarts.
|
||||
|
||||
### Prerequisites in the Hermes container
|
||||
- `sshpass` is needed for non-interactive password auth. If missing:
|
||||
```bash
|
||||
# This Hermes container may not have apt-write access
|
||||
pip install --user sshpass 2>/dev/null || echo "Must install sshpass on the host manually"
|
||||
```
|
||||
- If `sshpass` cannot be installed, use Python `pexpect` instead:
|
||||
```bash
|
||||
pip install pexpect
|
||||
```
|
||||
...or ask the user for the correct username if `root`/`n8n` both fail.
|
||||
|
||||
### One-shot SSH diagnostic (safe for terminal tool — no PTY tricks needed)
|
||||
```bash
|
||||
# Requires sshpass installed
|
||||
sshpass -p 'PASSWORD' ssh -o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
'[email protected]' \
|
||||
'n8n' \
|
||||
'echo ===CONNECTED===; nvidia-smi; echo ===DONE==='
|
||||
```
|
||||
|
||||
**If password is rejected, STOP.** Do not loop. Ask the user:
|
||||
> "SSH password rejected for user `X` on `10.0.0.202`. What is the correct username and password?"
|
||||
|
||||
### What to check/fix once SSH'd in
|
||||
| Command | What it tells you |
|
||||
|---------|-----------------|
|
||||
| `nvidia-smi` | GPU + driver status |
|
||||
| `ps aux \| grep -i comfy` | Is ComfyUI running? |
|
||||
| `curl -s http://127.0.0.1:8188/system_stats` | Is it responding locally? |
|
||||
| `ss -tlnp \| grep 8188` | Is it listening on 0.0.0.0 or just 127.0.0.1? |
|
||||
| `ufw status \| grep 8188` or `iptables -L \| grep 8188` | Is the firewall blocking? |
|
||||
|
||||
### Common fixes
|
||||
```bash
|
||||
# ComfyUI not running → start it with LAN binding
|
||||
comfy launch -- --listen 0.0.0.0 --port 8188 &
|
||||
|
||||
# Or if launched manually, relaunch:
|
||||
cd ~/comfy/ComfyUI # or wherever it's installed
|
||||
python main.py --listen 0.0.0.0 --port 8188 &
|
||||
|
||||
# Firewall blocking → open it
|
||||
ufw allow 8188/tcp
|
||||
# or
|
||||
iptables -A INPUT -p tcp --dport 8188 -j ACCEPT
|
||||
|
||||
# GPU driver issue (e.g. after host kernel update)
|
||||
nvidia-smi # check for driver mismatch
|
||||
# If missing, reinstall driver on the host, NOT in the LXC
|
||||
```
|
||||
|
||||
### Hermes-side SSH config (one-time setup)
|
||||
Store host info so future sessions can SSH without re-configuring:
|
||||
```bash
|
||||
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||
cat > ~/.ssh/config <<EOF
|
||||
Host n8n
|
||||
HostName 10.0.0.202
|
||||
User USERNAME
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
IdentitiesOnly no
|
||||
EOF
|
||||
chmod 600 ~/.ssh/config
|
||||
```
|
||||
|
||||
### Important: Do not PTY-loop (terminal tool)
|
||||
When `sshpass` is unavailable, do NOT write Python PTY scripts that call
|
||||
`os.setsid()` inside the `terminal` tool — this is permission-denied inside
|
||||
unprivileged Proxmox LXCs.
|
||||
|
||||
Instead use **`execute_code` with `pty.fork()`** — the sandbox allows the
|
||||
fork but the `terminal` tool does not. See
|
||||
`references/remote-manual-update.md` for the reusable `ssh_cmd()` recipe.
|
||||
That reference also covers:
|
||||
- Updating a manual `git clone` install (no `comfy` CLI available)
|
||||
- Safely restarting the server when `nohup` is rejected by `terminal`
|
||||
- The model-directory pitfall (`CheckpointLoaderSimple` vs `diffusion_models`)
|
||||
- The version-file staleness pitfall (`comfyui_version.py`)
|
||||
|
||||
If neither `sshpass`/`pexpect` nor `execute_code` + `pty.fork()` works,
|
||||
fallback is asking the user for credentials or to run the command themselves.
|
||||
|
||||
---
|
||||
|
||||
### From Hermes (this container)
|
||||
|
||||
```bash
|
||||
# Test 1: Is the host alive at all?
|
||||
ping -c 1 10.0.0.202
|
||||
|
||||
# Test 2: Is port 8188 open?
|
||||
python3 -c "import socket; s=socket.socket(); s.settimeout(3); s.connect(('10.0.0.202', 8188)); print('open')" 2>/dev/null || echo "closed/unreachable"
|
||||
|
||||
# Test 3: Does it speak HTTP?
|
||||
curl -s --max-time 5 http://10.0.0.202:8188/system_stats | head -c 100
|
||||
|
||||
# Test 4: Wrong port? Scan common ComfyUI ports
|
||||
for port in 8188 8080 3000 5000 7860 9000; do
|
||||
python3 -c "import socket; s=socket.socket(); s.settimeout(1); s.connect(('10.0.0.202', $port)); print('$port open')" 2>/dev/null
|
||||
done
|
||||
```
|
||||
|
||||
### Likely causes & fixes
|
||||
|
||||
| Symptom | Cause | Fix on remote host |
|
||||
|---------|-------|--------------------|
|
||||
| `Connection refused` | ComfyUI listening on `127.0.0.1` only | `comfy launch -- --listen 0.0.0.0 --port 8188` |
|
||||
| `No route to host` | Firewall / network segment blocking | `ufw allow 8188/tcp` or adjust Proxmox firewall rules |
|
||||
| `Connection timed out` | ComfyUI not running at all | `ps aux \| grep -i comfy` then start it |
|
||||
| Wrong port open | ComfyUI launched on a non-default port | Re-launch with `--port 8188` or note the actual port |
|
||||
| Works from remote shell but not Hermes | Proxmox LXC bridge / VLAN isolation | Check CT firewall rules; ensure both CTs share bridge |
|
||||
| Password rejected via sshpass | Wrong username or password, or password-only SSH disabled | Clarify user/password with user; do not loop, ask once |
|
||||
|
||||
## Credential Ambiguity — Pitfall
|
||||
|
||||
When the user says something like:
|
||||
> "SSH n8n - passw0rd"
|
||||
|
||||
They may mean any of:
|
||||
- Host alias = `n8n`, password = `passw0rd`, user = default (`root`)
|
||||
- Host alias = `n8n`, user = `n8n`, password = `passw0rd`
|
||||
- Host IP `10.0.0.202`, user = `n8n`, password = `passw0rd`
|
||||
|
||||
**Best practice:** Immediately write the SSH config with the most likely
|
||||
translation, then run a **minimal connectivity probe** before attempting
|
||||
diagnosis:
|
||||
```bash
|
||||
sshpass -p 'passw0rd' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
n8n 'echo ok' # or 'USER@HOST'
|
||||
```
|
||||
|
||||
If this fails with "Permission denied", **stop and ask the user for the
|
||||
correct username and password** rather than writing elaborate PTY-based
|
||||
workarounds. Time spent writing Python PTY scripts is better spent
|
||||
asking for clarified credentials.
|
||||
|
||||
## Remote Execution Examples
|
||||
|
||||
### Single image generation
|
||||
```bash
|
||||
python3 scripts/run_workflow.py \
|
||||
--workflow workflows/flux_dev_txt2img.json \
|
||||
--args '{"prompt": "cyberpunk city at night", "seed": -1, "steps": 30}' \
|
||||
--host http://10.0.0.202:8188 \
|
||||
--output-dir ./outputs
|
||||
```
|
||||
|
||||
### Batch with parallel sweeps
|
||||
```bash
|
||||
python3 scripts/run_batch.py \
|
||||
--workflow workflows/sdxl.json \
|
||||
--args '{"prompt": "abstract art"}' \
|
||||
--count 8 --randomize-seed --parallel 4 \
|
||||
--host http://10.0.0.202:8188 \
|
||||
--output-dir ./outputs/batch
|
||||
```
|
||||
|
||||
### img2img with local source image uploaded to remote
|
||||
```bash
|
||||
python3 scripts/run_workflow.py \
|
||||
--workflow workflows/sdxl_img2img.json \
|
||||
--input-image image=./photo.png \
|
||||
--args '{"prompt": "oil painting style", "denoise": 0.65}' \
|
||||
--host http://10.0.0.202:8188 \
|
||||
--output-dir ./outputs
|
||||
```
|
||||
|
||||
### Real-time WebSocket progress monitoring
|
||||
```bash
|
||||
python3 scripts/run_workflow.py \
|
||||
--workflow workflows/wan_t2v.json \
|
||||
--args '{"prompt": "a cat dancing", "frames": 81}' \
|
||||
--host http://10.0.0.202:8188 \
|
||||
--ws \
|
||||
--output-dir ./outputs
|
||||
```
|
||||
|
||||
## Environment Variable
|
||||
|
||||
Set once in your shell profile to avoid typing `--host` every time:
|
||||
```bash
|
||||
export COMFYUI_HOST="http://10.0.0.202:8188"
|
||||
```
|
||||
|
||||
All scripts in this skill respect `$COMFYUI_HOST` when `--host` is not
|
||||
explicitly passed.
|
||||
|
||||
## Proxmox LXC Specifics
|
||||
|
||||
If the remote ComfyUI is in an unprivileged Proxmox LXC:
|
||||
- Ensure the LXC has `features: nesting=1` if running comfy-cli inside it
|
||||
- GPU passthrough requires `lxc.cgroup2.devices.allow` entries for the NVIDIA/AMD card
|
||||
- Bridge networking (`net0: bridge=vmbr0`) should make it LAN-reachable from
|
||||
other LXCs on the same bridge
|
||||
- If Hermes is in a different LXC that cannot reach `10.0.0.202`, check:
|
||||
- Proxmox host-level firewall rules
|
||||
- Whether both CTs share the same bridge (`vmbr0` vs `vmbr1`)
|
||||
- The LXC `net0` IP assignment (DHCP vs static)
|
||||
@@ -0,0 +1,192 @@
|
||||
# Remote ComfyUI Management — Non-interactive SSH & Daemon Management
|
||||
|
||||
This reference supplements `references/remote-connectivity.md` with the
|
||||
specific technique used when the Hermes LXC cannot install `sshpass` or
|
||||
`pexpect` (common in unprivileged Proxmox containers).
|
||||
|
||||
## Problem
|
||||
|
||||
- The ComfyUI server lives on a headless GPU box at `10.0.0.202`
|
||||
- Hermes is in an unprivileged LXC without `apt` write access
|
||||
- The `terminal` tool rejects `sshpass` (not on PATH) and forbids `nohup` in foreground mode
|
||||
- The `terminal` tool also rejects `nohup`, `disown`, `setsid`, and `&` backgrounding
|
||||
|
||||
## Solution
|
||||
|
||||
Use `pty.fork()` in a Python script executed via `execute_code`. This creates a
|
||||
pseudo-terminal that SSH is happy with. The Hermes `terminal` tool is not
|
||||
involved, so the PTY is permitted.
|
||||
|
||||
### Reusable Python SSH helper
|
||||
|
||||
Save and run via `execute_code`. Feed the password when the PTY shows the
|
||||
prompt, clean ANSI noise and SSH metadata from the captured output.
|
||||
|
||||
```python
|
||||
import os, pty, select, time, re
|
||||
|
||||
def ssh_cmd(cmd_str, timeout=30):
|
||||
"""Execute cmd via SSH on [email protected] through a PTY. Returns cleaned stdout."""
|
||||
cmd = ['ssh', '-tt', '-o', 'StrictHostKeyChecking=no',
|
||||
'-o', 'UserKnownHostsFile=/dev/null',
|
||||
'[email protected]', cmd_str]
|
||||
pid, master_fd = pty.fork()
|
||||
if pid == 0:
|
||||
os.execvp(cmd[0], cmd)
|
||||
data = b""
|
||||
password_sent = False
|
||||
try:
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
ready, _, _ = select.select([master_fd], [], [], 5)
|
||||
if ready:
|
||||
try:
|
||||
chunk = os.read(master_fd, 4096)
|
||||
except OSError:
|
||||
break
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
if (not password_sent and
|
||||
'password:' in data.decode('utf-8', errors='replace').lower()):
|
||||
os.write(master_fd, b'PASSW0RD\n')
|
||||
password_sent = True
|
||||
else:
|
||||
# Check if child exited
|
||||
try:
|
||||
_, status = os.waitpid(pid, os.WNOHANG)
|
||||
if status != 0:
|
||||
break
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
os.close(master_fd)
|
||||
try:
|
||||
os.waitpid(pid, 0)
|
||||
except:
|
||||
pass
|
||||
|
||||
# --- clean-up ---
|
||||
text = data.decode('utf-8', errors='replace')
|
||||
text = text.replace('\r\n', '\n').replace('\r', '')
|
||||
text = re.sub(r'\x1b\[[0-9;?]*[A-Za-z]', '', text) # ANSI
|
||||
text = re.sub(r'Warning: Permanently added .*?\n?', '', text) # host key noise
|
||||
text = re.sub(r"n8n@10\.0\.0\.202's password: \n", '', text)
|
||||
text = re.sub(r'Connection to 10\.0\.0\.202 closed\.', '', text)
|
||||
return text.strip()
|
||||
```
|
||||
|
||||
### How to use it
|
||||
|
||||
```python
|
||||
# --- diagnostics ---
|
||||
print(ssh_cmd('nvidia-smi --query-gpu=name,memory.total --format=csv,noheader'))
|
||||
print(ssh_cmd('ps aux | grep -i comfy | grep -v grep'))
|
||||
print(ssh_cmd('cd /home/n8n/comfy-ui && git log --oneline -1'))
|
||||
|
||||
# --- manage server ---
|
||||
# Stop
|
||||
ssh_cmd('pkill -f "main.py.*0.0.0.0"')
|
||||
|
||||
# Start (background, nohup — all inside the remote *shell*, not this process)
|
||||
ssh_cmd(
|
||||
'cd /home/n8n/comfy-ui && '
|
||||
'source /home/n8n/comfy-env/bin/activate && '
|
||||
'nohup python main.py --listen 0.0.0.0 --fp16-intermediates '
|
||||
'> /tmp/comfyui.log 2>&1 </dev/null &'
|
||||
)
|
||||
```
|
||||
|
||||
**Why this works:** The `terminal` tool never sees the `&`. It is executed
|
||||
inside the remote shell, which is spawned by our PTY, which is itself
|
||||
spawned inside the `execute_code` sandbox. The sandbox allows `pty.fork()`;
|
||||
the `terminal` tool would reject the same command because of its `&`
|
||||
backgrounding rule.
|
||||
|
||||
## Updating ComfyUI on a Remote Manual Install
|
||||
|
||||
If the remote host was installed via `git clone` (not `comfy-cli`), there is no
|
||||
`comfy` command. Use `git` directly over SSH.
|
||||
|
||||
### Diagnose current state
|
||||
|
||||
| Check | SSH command |
|
||||
|-------|-------------------|
|
||||
| Install location | `ls -d ~/comfy-ui ~/ComfyUI 2>/dev/null` |
|
||||
| Git remote | `cd ~/comfy-ui && git remote -v` |
|
||||
| Current HEAD | `git log --oneline -1` |
|
||||
| Local changes | `git status --short` |
|
||||
| Stash | `git stash list` |
|
||||
|
||||
### Safe update (preserve local changes)
|
||||
|
||||
```python
|
||||
ssh_cmd('cd /home/n8n/comfy-ui && git stash -u')
|
||||
ssh_cmd('cd /home/n8n/comfy-ui && git fetch origin')
|
||||
ssh_cmd('cd /home/n8n/comfy-ui && git checkout v0.18.3')
|
||||
# or branch: git switch master && git pull origin master
|
||||
```
|
||||
|
||||
If `git fetch` hangs on SSH port 22 (GitHub SSH blocked on that host),
|
||||
switch the remote to HTTPS:
|
||||
|
||||
```python
|
||||
ssh_cmd(
|
||||
'cd /home/n8n/comfy-ui && '
|
||||
'git remote set-url origin https://github.com/comfyanonymous/ComfyUI.git && '
|
||||
'git fetch origin'
|
||||
)
|
||||
```
|
||||
|
||||
### Restore stash after verifying
|
||||
|
||||
```python
|
||||
ssh_cmd('cd /home/n8n/comfy-ui && git stash pop')
|
||||
```
|
||||
|
||||
### Re-install dependencies
|
||||
|
||||
If the remote uses a virtual environment (very common for headless clones):
|
||||
|
||||
```python
|
||||
ssh_cmd(
|
||||
'cd /home/n8n/comfy-ui && '
|
||||
'/home/n8n/comfy-env/bin/pip install -r requirements.txt'
|
||||
)
|
||||
```
|
||||
|
||||
### Restart
|
||||
|
||||
See the server-management section above.
|
||||
|
||||
## Pitfalls Learned in the Field
|
||||
|
||||
1. **Always check which branch/tag is currently checked out.** `master` may lag
|
||||
behind `release/v0.18.3` by 38+ commits. A naïve `git pull` on `master`
|
||||
can silently downgrade if the user was previously on a release branch.
|
||||
Use `git log --oneline -3 --all --decorate` to see HEAD, all branches,
|
||||
and all tags before deciding whether to switch or merge.
|
||||
|
||||
2. **`comfyui_version.py` may be stale.** It is auto-generated during build.
|
||||
After a manual `git checkout v0.18.3`, the file can still say `0.18.1` or
|
||||
`0.18.2`. The running server reports the version string from this file,
|
||||
not from git tags. Regenerate it (`python setup.py` or edit it manually)
|
||||
if version-report accuracy matters, or simply verify via `/api/prompt`
|
||||
test run.
|
||||
|
||||
3. **Only one model directory is valid per node type.** LTX models in
|
||||
`models/diffusion_models/` were invisible to `CheckpointLoaderSimple`
|
||||
which walks `models/checkpoints/` only. Either move the file or use a
|
||||
node pack that reads the correct folder. See the table in the SKILL.md
|
||||
"Post-Install: Download Models" section.
|
||||
|
||||
4. **Do not pass `--host` to a stopped server.** Before any workflow run,
|
||||
verify `curl http://10.0.0.202:8188/system_stats` returns valid JSON.
|
||||
If not, SSH in and restart before attempting the run. Checking
|
||||
`/api/object_info` is also a good proxy (returns all registered nodes).
|
||||
|
||||
5. **Avoid `os.setsid()` inside an unprivileged LXC.** The sandbox blocks
|
||||
it. The double-fork + `os.setsid()` pattern common in Python daemon
|
||||
tutorials fails with `PermissionError` here. Instead, background via the
|
||||
remote shell (`nohup … </dev/null &`) or use `systemd`/`cron` on the
|
||||
remote host for persistence.
|
||||
@@ -0,0 +1,312 @@
|
||||
# ComfyUI REST + WebSocket API Reference
|
||||
|
||||
ComfyUI exposes a REST + WebSocket interface for workflow execution and
|
||||
management. **The same surface is used locally and on Comfy Cloud, with
|
||||
auth/path differences.**
|
||||
|
||||
## Connection
|
||||
|
||||
| | Local ComfyUI | Comfy Cloud |
|
||||
|---|---|---|
|
||||
| Base URL | `http://127.0.0.1:8188` | `https://cloud.comfy.org` |
|
||||
| API path prefix | none (`/prompt`, `/view`, …) | `/api/...` (`/api/prompt`, `/api/view`, …) |
|
||||
| Auth | none (or bearer token if configured) | `X-API-Key` header |
|
||||
| WebSocket | `ws://host:port/ws?clientId={uuid}` | `wss://cloud.comfy.org/ws?clientId={uuid}&token={API_KEY}` |
|
||||
| `/api/view` response | direct bytes | 302 redirect → signed URL (use `curl -L`) |
|
||||
|
||||
The skill scripts route URLs automatically via `_common.resolve_url()`.
|
||||
|
||||
## Endpoint differences on Comfy Cloud
|
||||
|
||||
The cloud surface diverges from local ComfyUI in several ways. The skill
|
||||
scripts handle these transparently; document them here so anyone calling
|
||||
`curl` directly knows.
|
||||
|
||||
| Local path | Cloud path | Notes |
|
||||
|------------|-----------|-------|
|
||||
| `/system_stats` | `/api/system_stats` | Cloud version is **public** (no auth required) |
|
||||
| `/object_info` | `/api/object_info` | **Paid tier only** — free returns 403 |
|
||||
| `/queue` | `/api/queue` | Paid tier only |
|
||||
| `/userdata` | `/api/userdata` | Paid tier only |
|
||||
| `/prompt` (POST) | `/api/prompt` (POST) | Paid tier only |
|
||||
| `/upload/image` | `/api/upload/image` | Paid tier only; `subfolder` accepted but ignored |
|
||||
| `/upload/mask` | `/api/upload/mask` | Same as above |
|
||||
| `/view` | `/api/view` | Paid tier only; **returns 302** to signed URL |
|
||||
| `/history` | `/api/history_v2` | **Renamed**; old path returns 404 |
|
||||
| `/history/{id}` | `/api/history_v2/{id}` or `/api/jobs/{id}` | Both work; `/jobs` returns full job |
|
||||
| `/models` | `/api/experiment/models` | **Renamed** |
|
||||
| `/models/{folder}` | `/api/experiment/models/{folder}` | **Renamed**; response shape differs (see below) |
|
||||
|
||||
### Cloud model-list response shape
|
||||
|
||||
- **Local:** `["a.safetensors", "b.safetensors", …]` — flat list of strings.
|
||||
- **Cloud:** `[{"name": "a.safetensors", "pathIndex": 0}, …]` — list of objects.
|
||||
- **Cloud 404 with `code: "folder_not_found"`** — folder is empty or unknown,
|
||||
not an "endpoint missing" error. Distinguish by reading the body.
|
||||
|
||||
The skill helper `_common.parse_model_list()` normalizes both.
|
||||
|
||||
## Workflow Execution
|
||||
|
||||
### Submit Workflow
|
||||
|
||||
```bash
|
||||
# Local
|
||||
curl -X POST "http://127.0.0.1:8188/prompt" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt": '"$(cat workflow_api.json)"', "client_id": "'"$(uuidgen)"'"}'
|
||||
|
||||
# Cloud
|
||||
curl -X POST "https://cloud.comfy.org/api/prompt" \
|
||||
-H "X-API-Key: $COMFY_CLOUD_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt": '"$(cat workflow_api.json)"'}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{"prompt_id": "abc-123-def", "number": 1, "node_errors": {}}
|
||||
```
|
||||
|
||||
If `node_errors` is non-empty, the workflow has validation errors (missing
|
||||
nodes, bad inputs).
|
||||
|
||||
### Check Job Status (Cloud)
|
||||
|
||||
```bash
|
||||
curl -X GET "https://cloud.comfy.org/api/job/{prompt_id}/status" \
|
||||
-H "X-API-Key: $COMFY_CLOUD_API_KEY"
|
||||
```
|
||||
|
||||
| Status | Description |
|
||||
| ------------- | ---------------------------------- |
|
||||
| `pending` | Job is queued and waiting to start |
|
||||
| `in_progress` | Job is currently executing |
|
||||
| `completed` | Job finished successfully |
|
||||
| `failed` | Job encountered an error |
|
||||
| `cancelled` | Job was cancelled by user |
|
||||
|
||||
### Job detail with outputs (Cloud)
|
||||
|
||||
```bash
|
||||
curl -X GET "https://cloud.comfy.org/api/jobs/{prompt_id}" \
|
||||
-H "X-API-Key: $COMFY_CLOUD_API_KEY"
|
||||
```
|
||||
|
||||
Response includes `outputs` keyed by node ID. Cloud uses `video` (singular)
|
||||
in the output structure; local uses `videos` (plural). The skill scripts
|
||||
accept both.
|
||||
|
||||
### Get History (Local)
|
||||
|
||||
```bash
|
||||
curl -s "http://127.0.0.1:8188/history" # all
|
||||
curl -s "http://127.0.0.1:8188/history/{id}" # one prompt_id
|
||||
```
|
||||
|
||||
Local entry shape:
|
||||
```json
|
||||
{
|
||||
"<prompt_id>": {
|
||||
"prompt": [...],
|
||||
"outputs": {"<node_id>": {"images": [...]}},
|
||||
"status": {
|
||||
"status_str": "success" | "error",
|
||||
"completed": true | false,
|
||||
"messages": [["execution_start", {...}], ["execution_error", {...}], …]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** when reading status, check `status_str == "error"` BEFORE
|
||||
checking `completed`, because both can be true for failed runs.
|
||||
|
||||
### Download Output
|
||||
|
||||
```bash
|
||||
# Local (direct bytes)
|
||||
curl -s "http://127.0.0.1:8188/view?filename=ComfyUI_00001_.png&subfolder=&type=output" \
|
||||
-o output.png
|
||||
|
||||
# Cloud (302 → signed URL; -L follows; STRIP X-API-Key for the second hop)
|
||||
curl -L "https://cloud.comfy.org/api/view?filename=...&type=output" \
|
||||
-H "X-API-Key: $COMFY_CLOUD_API_KEY" \
|
||||
-o output.png
|
||||
```
|
||||
|
||||
The skill's `run_workflow.py` strips `X-API-Key` automatically on the
|
||||
cross-host redirect, so the signed URL never sees your auth.
|
||||
|
||||
## WebSocket Monitoring
|
||||
|
||||
Connect for real-time execution events.
|
||||
|
||||
```bash
|
||||
# Local
|
||||
wscat -c "ws://127.0.0.1:8188/ws?clientId=MY-UUID"
|
||||
|
||||
# Cloud
|
||||
wscat -c "wss://cloud.comfy.org/ws?clientId=MY-UUID&token=$COMFY_CLOUD_API_KEY"
|
||||
```
|
||||
|
||||
**Note:** on Cloud the `clientId` is currently ignored — all messages for a
|
||||
user are broadcast to every connection. Filter messages client-side by
|
||||
`data.prompt_id`.
|
||||
|
||||
### JSON Message Types
|
||||
|
||||
| Type | When | Key Fields |
|
||||
|------|------|------------|
|
||||
| `status` | Queue change | `status.exec_info.queue_remaining` |
|
||||
| `notification` | User-friendly status string | `value` |
|
||||
| `execution_start` | Workflow begins | `prompt_id` |
|
||||
| `executing` | Node running (or end-of-run if `node` is null on local) | `node`, `prompt_id` |
|
||||
| `progress` | Sampling steps | `node`, `value`, `max` |
|
||||
| `progress_state` | Extended progress with per-node metadata | `nodes` (dict) |
|
||||
| `executed` | Node output ready | `node`, `output` (with `images`/`video`/etc.) |
|
||||
| `execution_cached` | Nodes skipped because of cache | `nodes` (list of IDs) |
|
||||
| `execution_success` | All done | `prompt_id` |
|
||||
| `execution_error` | Failure | `exception_type`, `exception_message`, `traceback`, `node_id` |
|
||||
| `execution_interrupted` | Cancelled | `prompt_id` |
|
||||
|
||||
### Binary Frames (Preview Images)
|
||||
|
||||
| Type code | Meaning |
|
||||
|-----------|---------|
|
||||
| `0x00000001` | `PREVIEW_IMAGE` — `[type:4][image_type:4][data]` (image_type 1=JPEG, 2=PNG) |
|
||||
| `0x00000003` | `TEXT` — `[type:4][nid_len:4][nid][text]` (UTF-8) |
|
||||
| `0x00000004` | `PREVIEW_IMAGE_WITH_METADATA` — `[type:4][meta_len:4][json][image_data]` |
|
||||
|
||||
`scripts/ws_monitor.py --previews <dir>` saves preview frames to disk.
|
||||
|
||||
## File Upload
|
||||
|
||||
```bash
|
||||
# Image
|
||||
curl -X POST "http://127.0.0.1:8188/upload/image" \
|
||||
-F "[email protected]" -F "type=input" -F "overwrite=true"
|
||||
# Returns: {"name": "photo.png", "subfolder": "", "type": "input"}
|
||||
|
||||
# Mask (linked to a previously uploaded image)
|
||||
curl -X POST "http://127.0.0.1:8188/upload/mask" \
|
||||
-F "[email protected]" -F "type=input" \
|
||||
-F 'original_ref={"filename":"photo.png","subfolder":"","type":"input"}'
|
||||
```
|
||||
|
||||
Cloud equivalent: prepend `https://cloud.comfy.org/api` and add `-H "X-API-Key: $COMFY_CLOUD_API_KEY"`.
|
||||
|
||||
## Node & Model Discovery
|
||||
|
||||
```bash
|
||||
# All node types and their input specs
|
||||
curl -s "http://127.0.0.1:8188/object_info" | python3 -m json.tool
|
||||
|
||||
# Specific node
|
||||
curl -s "http://127.0.0.1:8188/object_info/KSampler"
|
||||
|
||||
# Models per folder (local)
|
||||
curl -s "http://127.0.0.1:8188/models/checkpoints"
|
||||
curl -s "http://127.0.0.1:8188/models/loras"
|
||||
|
||||
# Models per folder (cloud — note the experimental prefix)
|
||||
curl -s "https://cloud.comfy.org/api/experiment/models/checkpoints" \
|
||||
-H "X-API-Key: $COMFY_CLOUD_API_KEY"
|
||||
```
|
||||
|
||||
## Queue Management
|
||||
|
||||
```bash
|
||||
# View queue
|
||||
curl -s "http://127.0.0.1:8188/queue"
|
||||
|
||||
# Clear all pending
|
||||
curl -X POST "http://127.0.0.1:8188/queue" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"clear": true}'
|
||||
|
||||
# Delete specific items
|
||||
curl -X POST "http://127.0.0.1:8188/queue" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"delete": ["prompt_id_1", "prompt_id_2"]}'
|
||||
|
||||
# Cancel currently-running job
|
||||
curl -X POST "http://127.0.0.1:8188/interrupt"
|
||||
```
|
||||
|
||||
## System Management
|
||||
|
||||
```bash
|
||||
# Stats (VRAM, RAM, GPU, ComfyUI version)
|
||||
curl -s "http://127.0.0.1:8188/system_stats"
|
||||
|
||||
# Free GPU memory
|
||||
curl -X POST "http://127.0.0.1:8188/free" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"unload_models": true, "free_memory": true}'
|
||||
```
|
||||
|
||||
## ComfyUI-Manager Endpoints (Optional)
|
||||
|
||||
These require ComfyUI-Manager installed. Useful for installing nodes/models
|
||||
via the API instead of `comfy-cli`.
|
||||
|
||||
```bash
|
||||
# Install a custom node from a git URL
|
||||
curl -X POST "http://127.0.0.1:8188/manager/queue/install" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"git_url": "https://github.com/user/comfyui-node.git"}'
|
||||
|
||||
# Check install queue status
|
||||
curl -s "http://127.0.0.1:8188/manager/queue/status"
|
||||
|
||||
# Install model
|
||||
curl -X POST "http://127.0.0.1:8188/manager/queue/install_model" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url": "https://...", "path": "models/checkpoints", "filename": "model.safetensors"}'
|
||||
```
|
||||
|
||||
## POST /prompt Payload Format
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": {
|
||||
"3": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"seed": 42,
|
||||
"steps": 20,
|
||||
"cfg": 7.5,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "normal",
|
||||
"denoise": 1.0,
|
||||
"model": ["4", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["5", 0]
|
||||
}
|
||||
}
|
||||
},
|
||||
"client_id": "unique-uuid-for-ws-filtering",
|
||||
"extra_data": {
|
||||
"api_key_comfy_org": "optional-PARTNER-NODE-key (NOT the cloud auth key)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `prompt`: workflow graph in API format
|
||||
- `client_id`: UUID — local server uses it to filter WebSocket events; cloud
|
||||
ignores it.
|
||||
- `extra_data.api_key_comfy_org`: ONLY required when the workflow uses
|
||||
partner nodes (Flux Pro, Ideogram, etc.). Don't conflate with `X-API-Key`.
|
||||
|
||||
## Error Categories (cloud `execution_error` `exception_type`)
|
||||
|
||||
| Type | Meaning |
|
||||
|------|---------|
|
||||
| `ValidationError` | Bad workflow / inputs (often nicer to surface from `node_errors`) |
|
||||
| `ModelDownloadError` | Required model not available |
|
||||
| `ImageDownloadError` | Failed to fetch input image from URL |
|
||||
| `OOMError` | Out of GPU memory |
|
||||
| `InsufficientFundsError` | Account balance too low (partner nodes) |
|
||||
| `InactiveSubscriptionError` | Subscription not active |
|
||||
@@ -0,0 +1,243 @@
|
||||
# ComfyUI Workflow-Template Integrity
|
||||
|
||||
> **Authored by [@purzbeats](https://github.com/purzbeats)** — adapted from
|
||||
> [purzbeats/hermes-agent-comfyui-helper](https://github.com/purzbeats/hermes-agent-comfyui-helper).
|
||||
> Use this reference when converting workflows from the official
|
||||
> `comfyui-workflow-templates` package (editor format) into API format for
|
||||
> submission via `/api/prompt`. The conversion has subtle gotchas that cause
|
||||
> hard-to-diagnose validation errors if you don't follow these rules.
|
||||
|
||||
## Background
|
||||
|
||||
The official ComfyUI template package (`comfyui-workflow-templates`, currently
|
||||
v0.9.69) is installed inside the ComfyUI venv at a path like:
|
||||
|
||||
```
|
||||
<comfy-install>/.venv/lib/python3.*/site-packages/comfyui_workflow_templates_*/templates/
|
||||
```
|
||||
|
||||
The exact path depends on how ComfyUI was installed (comfy-cli default,
|
||||
Comfy Desktop, manual venv, etc.). Find it once with:
|
||||
|
||||
```bash
|
||||
comfy --workspace <ws> run-python -c "import comfyui_workflow_templates, pathlib; print(pathlib.Path(comfyui_workflow_templates.__file__).parent / 'templates')"
|
||||
```
|
||||
|
||||
Templates ship in **editor format** — `nodes` / `links` arrays inside
|
||||
`data['definitions']['subgraphs'][0]`. They must be converted to **API
|
||||
format** (a `node_id -> {class_type, inputs}` mapping) before submission.
|
||||
|
||||
---
|
||||
|
||||
## RULE #1: Use templates AS CLOSE TO ORIGINAL AS POSSIBLE
|
||||
|
||||
- **Never strip, simplify, or "minimize" nodes** from a template.
|
||||
- Full template architecture (dual-pass pipelines, LoRA chains, distilled
|
||||
sigmas, conditioning paths) is intentional — removing any part breaks quality.
|
||||
- If an image-dependent path exists but the task is text-to-video, **leave
|
||||
it wired with the bypass toggle enabled** — don't remove the nodes.
|
||||
- Only change: prompt text, seed, and dimensions (when explicitly requested).
|
||||
|
||||
## RULE #2: Server validation errors are the source of truth
|
||||
|
||||
When a workflow submission fails, the server response looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"node_errors": {
|
||||
"238": {
|
||||
"errors": [{
|
||||
"message": "Required input is missing",
|
||||
"details": "width",
|
||||
"extra_info": { "input_name": "resize_type.width" }
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**The `extra_info.input_name` field tells you EXACTLY what JSON key the server
|
||||
wants. Use it literally.** If it says `"values.a"` or `"resize_type.width"`,
|
||||
those are the actual key names in the JSON object. Do not "simplify" them to
|
||||
flat names based on assumptions about what the field "should" be called.
|
||||
|
||||
## RULE #3: Don't rebuild from scratch — patch the failing nodes
|
||||
|
||||
Every regeneration from the template reintroduces the same bugs. Instead:
|
||||
|
||||
1. Submit the workflow once.
|
||||
2. Read the server error details for exact key names.
|
||||
3. Use targeted patch/fix calls against the workflow file on disk.
|
||||
4. Resubmit and check if errors resolved.
|
||||
|
||||
---
|
||||
|
||||
## Reroute nodes: bypass, don't delete
|
||||
|
||||
Most servers (local, Cloud) don't have a `Reroute` node type. When converting
|
||||
a template:
|
||||
|
||||
1. Find what feeds into the Reroute by looking at links where
|
||||
`target_id` = the Reroute node ID.
|
||||
2. Replace all inputs referencing the Reroute with
|
||||
`[source_node_id, source_slot]`.
|
||||
3. Delete the Reroute node from the API mapping.
|
||||
|
||||
**Real example — LTX 2.3 t2v template:**
|
||||
|
||||
- Reroute node 255 receives VAE from `CheckpointLoaderSimple 236` slot 2.
|
||||
- Three nodes reference Reroute 255 for their VAE input:
|
||||
`LTXVImgToVideoInplace` (230), `LTXVLatentUpsampler` (253),
|
||||
`VAEDecodeTiled` (251).
|
||||
- Fix: replace all occurrences of `vae: ["255", 0]` with `vae: ["236", 2]`.
|
||||
- `CheckpointLoaderSimple` slot 2 = VAE (not slot 0 = MODEL).
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| ❌ Wrong | `vae: ["236", 0]` → `MODELV mismatch input_type(VAE)` |
|
||||
| ✅ Correct | `vae: ["236", 2]` |
|
||||
|
||||
---
|
||||
|
||||
## Dynamic template nodes: dotted key names are correct
|
||||
|
||||
### ComfyMathExpression (COMFY_AUTOGROW_V3)
|
||||
|
||||
```json
|
||||
{
|
||||
"class_type": "ComfyMathExpression",
|
||||
"inputs": {
|
||||
"expression": "a/2",
|
||||
"values.a": ["257", 0]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `values` is a `COMFY_AUTOGROW_V3` template.
|
||||
- Input names in links are `values.a`, `values.b`, etc.
|
||||
- **Keep the dotted format as JSON keys.**
|
||||
- Do NOT convert to `{"values": {"a": ...}}` or flatten to just `"a"`.
|
||||
|
||||
### ResizeImageMaskNode (COMFY_DYNAMICCOMBO_V3)
|
||||
|
||||
```json
|
||||
{
|
||||
"class_type": "ResizeImageMaskNode",
|
||||
"inputs": {
|
||||
"input": ["276", 0],
|
||||
"scale_method": "lanczos",
|
||||
"resize_type": "scale dimensions",
|
||||
"resize_type.width": 1920,
|
||||
"resize_type.height": 1088,
|
||||
"resize_type.crop": "center"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `resize_type` is a `COMFY_DYNAMICCOMBO_V3`.
|
||||
- Mode-specific fields: `resize_type.width`, `resize_type.height`, `resize_type.crop`.
|
||||
- `scale_method` options: `"nearest-exact"`, `"bilinear"`, `"area"`, `"bicubic"`, `"lanczos"`.
|
||||
- **Keep the dotted format as JSON keys.**
|
||||
- Do NOT flatten `resize_type.width` to just `"width"`.
|
||||
|
||||
---
|
||||
|
||||
## Conversion recipe
|
||||
|
||||
1. Load template from the installed package path.
|
||||
2. Parse `data['definitions']['subgraphs'][0]`.
|
||||
3. For each node (skip Reroute):
|
||||
- Resolve linked inputs from `sg['links']` dict.
|
||||
- Map `widgets_values` to input field names.
|
||||
- Keep all dotted key names as-is from the template.
|
||||
4. Bypass Reroute: trace source, replace references.
|
||||
5. Change only: prompt text, seed values, and user-requested parameters.
|
||||
6. Add `SaveVideo` terminal node if template uses only `CreateVideo`.
|
||||
7. Submit → read errors → patch specific nodes → resubmit.
|
||||
|
||||
## What to NEVER change in a template
|
||||
|
||||
| Element | Why |
|
||||
|---------|-----|
|
||||
| Node topology | Graph is designed for the specific model |
|
||||
| Sigmas values | Tuned for the model/sampler combination |
|
||||
| LoRA/distilled paths | Required for quality, even if they look unused |
|
||||
| Model parameters (cfg, steps, shifts) | Model-specific |
|
||||
| Conditioning chains (zero-out, crop guides) | Required for correct conditioning |
|
||||
| Pass-through wiring | Don't remove nodes, bypass them |
|
||||
|
||||
---
|
||||
|
||||
## Cloud compatibility (verified May 2025)
|
||||
|
||||
The full LTX 2.3 T2V template (`video_ltx2_3_t2v.json`) runs **without
|
||||
modification** on Comfy Cloud.
|
||||
|
||||
**Confirmed working on Cloud (all custom nodes available):**
|
||||
`ComfyMathExpression`, `ResizeImageMaskNode`, `ResizeImagesByLongerEdge`,
|
||||
`PrimitiveInt`, `PrimitiveStringMultiline`, `PrimitiveBoolean`, `SaveVideo`,
|
||||
`LTXVCropGuides`, `LTXVImgToVideoInplace`, `LTXVConcatAVLatent`,
|
||||
`LTXVSeparateAVLatent`, `LTXVLatentUpsampler`, `LTXVAudioVAELoader`,
|
||||
`LTXVAudioVAEDecode`, `LTXVEmptyLatentAudio`, `LTXVPreprocess`,
|
||||
`LTXVConditioning`, `ManualSigmas`, `LTXAVTextEncoderLoader`, plus all core
|
||||
nodes.
|
||||
|
||||
**Cloud vs Local for LTX 2.3 (768x512):**
|
||||
|
||||
- Cloud: ~39s per video (4x faster).
|
||||
- Local (RTX 5090): ~160s per video.
|
||||
- `example.png` placeholder works on Cloud for bypassed image-dependent paths.
|
||||
- Submission format is **identical** between local and Cloud:
|
||||
`{"prompt": wf, "extra_data": {}}` to `/api/prompt`.
|
||||
- Free tier = 1 concurrent job.
|
||||
|
||||
**Cloud submission pitfalls:**
|
||||
|
||||
- `/api/object_info/<node>` returns 404 on free tier — can't query node
|
||||
schemas remotely, but the workflow runs fine anyway. Always probe
|
||||
`object_info` locally before building workflows.
|
||||
- Cloud is ~4x faster — prefer Cloud for batch runs unless local is needed
|
||||
for debugging.
|
||||
- Cloud `/api/view` returns **302 redirect to signed GCS URL** — use
|
||||
`curl -s -L` to follow and download. Python `urllib` fails with 401
|
||||
(forwards auth headers to GCS CDN).
|
||||
- `COMFY_CLOUD_API_KEY` is only in the terminal/bash env, not in the Python
|
||||
sandbox. Use subprocess or terminal scripts for Cloud API calls.
|
||||
- Cloud free tier processes jobs **sequentially** (1 at a time). Submit all,
|
||||
then poll history.
|
||||
- LTX 2.3 at **1920x1080 OOMs locally** (even RTX 5090) — upscaler pass
|
||||
exceeds VRAM. Prefer Cloud for 1080p; use 1280x720 locally (~90s/video).
|
||||
|
||||
---
|
||||
|
||||
## FFmpeg stitch settings (Discord-compatible)
|
||||
|
||||
Generated ComfyUI videos often use `yuv444p` pixel format which does NOT work
|
||||
on Discord. Re-encode with:
|
||||
|
||||
```bash
|
||||
ffmpeg -y -i input.mp4 \
|
||||
-c:v libx264 -profile:v main -preset medium -crf 13 -pix_fmt yuv420p \
|
||||
-c:a aac -b:a 192k \
|
||||
output_discord.mp4
|
||||
```
|
||||
|
||||
Key settings:
|
||||
|
||||
- `-pix_fmt yuv420p` — **required for Discord**, ComfyUI outputs `yuv444p` by default.
|
||||
- `-crf 13` — high quality without massive file size (default 23 is too lossy).
|
||||
- `-profile:v main` — widely compatible.
|
||||
|
||||
For multi-video crossfade stitching, chain `xfade` (video) and `acrossfade`
|
||||
(audio):
|
||||
|
||||
```bash
|
||||
ffmpeg -y -i a.mp4 -i b.mp4 -i c.mp4 \
|
||||
-filter_complex "[0:v][1:v]xfade=transition=fade:duration=1:offset=3.04[v1];[v1][2:v]xfade=transition=fade:duration=1:offset=6.08[vout];[0:a][1:a]acrossfade=duration=1:c1=tri:c2=tri[a1];[a1][2:a]acrossfade=duration=1:c1=tri:c2=tri[aout]" \
|
||||
-map "[vout]" -map "[aout]" \
|
||||
-c:v libx264 -profile:v main -crf 13 -pix_fmt yuv420p \
|
||||
-c:a aac -b:a 192k \
|
||||
output.mp4
|
||||
```
|
||||
|
||||
Offset for xfade #N = `(N+1) × duration - N × overlap`.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Video Generation Research — Qdrant Quick-Start
|
||||
|
||||
**Canonical data store**: Qdrant `comfyui_decisions` @ `http://10.0.0.22:6333` (64+ points)
|
||||
**Embedding**: `snowflake-arctic-embed2:latest` (1024-dim, Cosine)
|
||||
**Human companion**: `/home/n8n/workspace/comfy/research.md` (search topics + URLs only, no specs)
|
||||
|
||||
---
|
||||
|
||||
## What Lives Where
|
||||
|
||||
| Store | Content | How to Query |
|
||||
|-------|---------|-------------|
|
||||
| **Qdrant** `comfyui_decisions` | All specs, VRAM numbers, node names, workarounds, paper abstracts, trend analysis | Vector search via Ollama embedding + POST to 10.0.0.22:6333 |
|
||||
| **Qdrant** `comfyui_kb` | General knowledge (3884 points, pre-existing) | Same method |
|
||||
| **research.md** | Human-readable search topics, URL quick-reference, comparison tables | grep / raw read |
|
||||
| **This file** | Quick-start pointers to the above | Read once, then query Qdrant |
|
||||
|
||||
---
|
||||
|
||||
## Qdrant Query Pattern
|
||||
|
||||
```python
|
||||
import requests, json
|
||||
|
||||
# 1. Get embedding
|
||||
emb_resp = requests.post("http://localhost:11434/api/embed", json={
|
||||
"model": "snowflake-arctic-embed2:latest",
|
||||
"input": "YOUR SEARCH TOPIC"
|
||||
})
|
||||
vector = emb_resp.json()["embeddings"][0]
|
||||
|
||||
# 2. Search Qdrant
|
||||
search_resp = requests.post(
|
||||
"http://10.0.0.22:6333/collections/comfyui_decisions/points/search",
|
||||
json={"vector": vector, "limit": 5, "with_payload": True}
|
||||
)
|
||||
results = search_resp.json()["result"]["points"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Search Topics
|
||||
|
||||
Use these as `input` strings above (from `research.md`):
|
||||
|
||||
- "LTX 2.3 specs, frames, VRAM, audio latent"
|
||||
- "HunyuanVideo frame limits block swapping FP8"
|
||||
- "Wan 2.2 low VRAM sound-to-video FreeLong"
|
||||
- "Google Veo 3 native audio cloud"
|
||||
- "Kling 3.0 Omni visual identity vocal tone"
|
||||
- "ID-LoRA face identity lock LTX"
|
||||
- "FreeLong spectral blending Wan motion reversal"
|
||||
- "Segment chaining last frame to first frame"
|
||||
- "LivePortrait portrait animation"
|
||||
- "MuseTalk LatentSync lip-sync"
|
||||
- "RIFE frame interpolation"
|
||||
- "MultiTalk NeurIPS multi-person conversation"
|
||||
- "StreamingT2V CVPR long video streaming"
|
||||
|
||||
---
|
||||
|
||||
## Quick Model Comparison (from Qdrant)
|
||||
|
||||
| Tool | Native Audio | Max Frames | VRAM | Best For |
|
||||
|------|-------------|------------|------|----------|
|
||||
| LTX 2.3 | YES | 361 | 12-24 GB | Local, audio-synced |
|
||||
| HunyuanVideo 1.5 | NO | 129 | 20-24 GB | Best open-source quality |
|
||||
| Wan 2.2 | YES (S2V) | 81 / 1025 | 5-16 GB | Low VRAM, sound-driven |
|
||||
| Veo 3 | YES | API | Cloud | Highest fidelity |
|
||||
| Kling 3.0 Omni | YES | API | Cloud | Character + voice |
|
||||
|
||||
---
|
||||
|
||||
## Key URLs (Quick Reference)
|
||||
|
||||
- LTX: https://huggingface.co/Lightricks
|
||||
- Hunyuan: https://github.com/Tencent-Hunyuan/HunyuanVideo
|
||||
- Wan wrapper: https://github.com/kijai/ComfyUI-WanVideoWrapper
|
||||
- Veo: https://deepmind.google/models/veo/
|
||||
- Kling: https://kling.ai/
|
||||
- LivePortrait: https://github.com/kijai/ComfyUI-LivePortraitKJ
|
||||
- VHS: https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite
|
||||
|
||||
---
|
||||
|
||||
*All detailed data is in Qdrant. This file is a navigation aid only.*
|
||||
@@ -0,0 +1,226 @@
|
||||
# ComfyUI Workflow JSON Format
|
||||
|
||||
## Two Formats — Only API Format Is Executable
|
||||
|
||||
**API format** is required for `/api/prompt` and every script in this skill.
|
||||
The web UI also produces an "editor format" used for visual editing, which
|
||||
**cannot** be submitted directly.
|
||||
|
||||
### API Format
|
||||
|
||||
Top-level keys are string node IDs. Each node has `class_type` and `inputs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"3": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"seed": 156680208700286,
|
||||
"steps": 20,
|
||||
"cfg": 8,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "normal",
|
||||
"denoise": 1.0,
|
||||
"model": ["4", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["5", 0]
|
||||
},
|
||||
"_meta": {"title": "KSampler"}
|
||||
},
|
||||
"4": {
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": "v1-5-pruned-emaonly.safetensors"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Detection:** every top-level value has `class_type`. The skill's
|
||||
`_common.is_api_format()` does this check.
|
||||
|
||||
### Editor Format (not directly executable)
|
||||
|
||||
Has `nodes[]` and `links[]` arrays — the visual graph. To convert: open in
|
||||
ComfyUI's web UI and use **Workflow → Export (API)** (newer UI) or the
|
||||
"Save (API Format)" button (older UI).
|
||||
|
||||
**Detection:** top-level has `"nodes"` and `"links"` keys.
|
||||
|
||||
## Inputs: Literals vs Links
|
||||
|
||||
```json
|
||||
"inputs": {
|
||||
"text": "a cat", // literal — modifiable
|
||||
"seed": 42, // literal — modifiable
|
||||
"clip": ["4", 1] // link — wiring; do NOT overwrite
|
||||
}
|
||||
```
|
||||
|
||||
Links are length-2 arrays of `[upstream_node_id, output_slot]`. The skill's
|
||||
parameter injector refuses to overwrite a link with a literal (logs a
|
||||
warning and skips).
|
||||
|
||||
## Common Node Types and Their Controllable Parameters
|
||||
|
||||
The full catalog lives in `scripts/_common.py` (`PARAM_PATTERNS` and
|
||||
`MODEL_LOADERS`). Highlights:
|
||||
|
||||
### Text Prompts
|
||||
|
||||
| Node Class | Key Fields |
|
||||
|------------|------------|
|
||||
| `CLIPTextEncode` | `text` |
|
||||
| `CLIPTextEncodeSDXL` | `text_g`, `text_l`, `width`, `height` |
|
||||
| `CLIPTextEncodeFlux` | `clip_l`, `t5xxl`, `guidance` |
|
||||
|
||||
To distinguish positive from negative the skill traces `KSampler.negative`
|
||||
back through Reroute / Primitive nodes to the source CLIPTextEncode. Falls
|
||||
back to `_meta.title` heuristics ("negative", "neg", "anti").
|
||||
|
||||
### Sampling
|
||||
|
||||
| Node Class | Key Fields |
|
||||
|------------|------------|
|
||||
| `KSampler` | `seed`, `steps`, `cfg`, `sampler_name`, `scheduler`, `denoise` |
|
||||
| `KSamplerAdvanced` | `noise_seed`, `steps`, `cfg`, `start_at_step`, `end_at_step` |
|
||||
| `SamplerCustom` | `noise_seed`, `cfg`, `sampler`, `sigmas` |
|
||||
| `SamplerCustomAdvanced` | `noise_seed` (via RandomNoise input) |
|
||||
| `RandomNoise` | `noise_seed` |
|
||||
| `BasicScheduler` | `steps`, `scheduler`, `denoise` |
|
||||
| `KSamplerSelect` | `sampler_name` |
|
||||
| `BasicGuider` / `CFGGuider` | `cfg` |
|
||||
| `ModelSamplingFlux` | `max_shift`, `base_shift`, `width`, `height` |
|
||||
| `SDTurboScheduler` | `steps`, `denoise` |
|
||||
|
||||
### Latent / Dimensions
|
||||
|
||||
| Node Class | Key Fields |
|
||||
|------------|------------|
|
||||
| `EmptyLatentImage` | `width`, `height`, `batch_size` |
|
||||
| `EmptySD3LatentImage` | `width`, `height`, `batch_size` |
|
||||
| `EmptyHunyuanLatentVideo` | `width`, `height`, `length`, `batch_size` |
|
||||
| `EmptyMochiLatentVideo` | `width`, `height`, `length`, `batch_size` |
|
||||
| `EmptyLTXVLatentVideo` | `width`, `height`, `length`, `batch_size` |
|
||||
|
||||
### Model Loading
|
||||
|
||||
| Node Class | Key Fields | Folder |
|
||||
|------------|------------|--------|
|
||||
| `CheckpointLoaderSimple` | `ckpt_name` | `checkpoints` |
|
||||
| `LoraLoader` | `lora_name`, `strength_model`, `strength_clip` | `loras` |
|
||||
| `LoraLoaderModelOnly` | `lora_name`, `strength_model` | `loras` |
|
||||
| `VAELoader` | `vae_name` | `vae` |
|
||||
| `ControlNetLoader` | `control_net_name` | `controlnet` |
|
||||
| `CLIPLoader` | `clip_name` | `clip` |
|
||||
| `DualCLIPLoader` | `clip_name1`, `clip_name2` | `clip` |
|
||||
| `TripleCLIPLoader` | `clip_name1/2/3` | `clip` |
|
||||
| `UNETLoader` | `unet_name` | `unet` |
|
||||
| `DiffusionModelLoader` | `model_name` | `diffusion_models` |
|
||||
| `UpscaleModelLoader` | `model_name` | `upscale_models` |
|
||||
| `IPAdapterModelLoader` | `ipadapter_file` | `ipadapter` |
|
||||
| `ADE_AnimateDiffLoaderWithContext` | `model_name`, `motion_scale` | `animatediff_models` |
|
||||
|
||||
### Image Input/Output
|
||||
|
||||
| Node Class | Key Fields |
|
||||
|------------|------------|
|
||||
| `LoadImage` | `image` (server-side filename, after upload) |
|
||||
| `LoadImageMask` | `image`, `channel` (`red` / `green` / `blue` / `alpha`) |
|
||||
| `VAEEncode` / `VAEDecode` | (no controllable fields) |
|
||||
| `VAEEncodeForInpaint` | `grow_mask_by` |
|
||||
| `SaveImage` | `filename_prefix` |
|
||||
| `VHS_VideoCombine` | `frame_rate`, `format`, `filename_prefix`, `loop_count`, `pingpong` |
|
||||
|
||||
### ControlNet
|
||||
|
||||
| Node Class | Key Fields |
|
||||
|------------|------------|
|
||||
| `ControlNetApply` | `strength` |
|
||||
| `ControlNetApplyAdvanced` | `strength`, `start_percent`, `end_percent` |
|
||||
|
||||
### IPAdapter (community pack `comfyui_ipadapter_plus`)
|
||||
|
||||
| Node Class | Key Fields |
|
||||
|------------|------------|
|
||||
| `IPAdapterAdvanced` | `weight`, `start_at`, `end_at` |
|
||||
| `IPAdapter` | `weight` |
|
||||
|
||||
### Embeddings (referenced inside prompt strings)
|
||||
|
||||
ComfyUI scans prompt text for `embedding:NAME` syntax. The skill's
|
||||
`_common.iter_embedding_refs()` extracts these as model dependencies.
|
||||
|
||||
```text
|
||||
"a beautiful cat, embedding:goodvibes:1.2, embedding:art-style"
|
||||
```
|
||||
|
||||
`extract_schema.py` and `check_deps.py` surface these in
|
||||
`embedding_dependencies` / `missing_embeddings`.
|
||||
|
||||
## Parameter Injection Pattern
|
||||
|
||||
```python
|
||||
import json, copy
|
||||
|
||||
with open("workflow_api.json") as f:
|
||||
workflow = json.load(f)
|
||||
|
||||
wf = copy.deepcopy(workflow)
|
||||
wf["6"]["inputs"]["text"] = "a beautiful sunset"
|
||||
wf["7"]["inputs"]["text"] = "ugly, blurry"
|
||||
wf["3"]["inputs"]["seed"] = 42
|
||||
wf["3"]["inputs"]["steps"] = 30
|
||||
wf["5"]["inputs"]["width"] = 1024
|
||||
wf["5"]["inputs"]["height"] = 1024
|
||||
```
|
||||
|
||||
`scripts/extract_schema.py` automates discovering which node IDs/fields
|
||||
correspond to which user-facing parameters. It returns a `parameters` dict
|
||||
that `run_workflow.py` reads to inject values from `--args`.
|
||||
|
||||
## Identifying Controllable Parameters (Heuristics)
|
||||
|
||||
For unknown workflows:
|
||||
|
||||
1. **Prompt text** — any `CLIPTextEncode.text`. Use connection tracing back
|
||||
from `KSampler.positive` / `.negative` to disambiguate (don't trust
|
||||
meta-title alone).
|
||||
2. **Seed** — `KSampler.seed` / `KSamplerAdvanced.noise_seed` / `RandomNoise.noise_seed`.
|
||||
3. **Dimensions** — `Empty*LatentImage.width/height` (must be multiples of 8).
|
||||
4. **Steps / CFG** — `KSampler.steps`, `KSampler.cfg`. Steps 20–50 typical.
|
||||
CFG 5–15 typical (Flux uses guidance, not CFG).
|
||||
5. **Model / checkpoint** — `CheckpointLoaderSimple.ckpt_name`. Filename must
|
||||
match an installed file *exactly*.
|
||||
6. **LoRA** — `LoraLoader.lora_name`, `.strength_model`.
|
||||
7. **Images for img2img / inpaint** — `LoadImage.image`. Server-side filename
|
||||
after upload.
|
||||
8. **Denoise** — `KSampler.denoise`. 0.0–1.0; 1.0 = ignore input image,
|
||||
0.0 = pass through. Sweet spot for img2img: 0.4–0.7.
|
||||
|
||||
## Output Nodes
|
||||
|
||||
Output is produced by these node types. The skill's `OUTPUT_NODES` set
|
||||
extends to common community packs.
|
||||
|
||||
| Node | Output Key | Content |
|
||||
|------|-----------|---------|
|
||||
| `SaveImage` | `images` | List of `{filename, subfolder, type}` |
|
||||
| `PreviewImage` | `images` | Temporary preview (not saved) |
|
||||
| `VHS_VideoCombine` | `gifs` (older) or `videos`/`video` (newer cloud) | Video file refs |
|
||||
| `SaveAudio` | `audio` | Audio file refs |
|
||||
| `SaveAnimatedWEBP` / `SaveAnimatedPNG` | `images` | Animated images |
|
||||
| `Save3D` | `3d` | 3D asset refs |
|
||||
|
||||
After execution, fetch outputs from `/history/{prompt_id}` (local) or
|
||||
`/api/jobs/{prompt_id}` (cloud) → `outputs` → `{node_id}` → `{key}`.
|
||||
|
||||
## Wrapper Variants
|
||||
|
||||
Some saved JSON files wrap the workflow under a `"prompt"` key (matching
|
||||
the `/api/prompt` payload shape). The skill's `_common.unwrap_workflow()`
|
||||
handles this — pass any of:
|
||||
|
||||
- raw API format: `{"3": {...}, "4": {...}}`
|
||||
- wrapped: `{"prompt": {"3": {...}}, "client_id": "..."}`
|
||||
|
||||
It rejects editor format with a clear error and a re-export instruction.
|
||||
Reference in New Issue
Block a user