Skill: halr9000/comfyui-imagegen (version 1.5.0)

--- name: comfyui-imagegen description: Generate images via ComfyUI API (localhost:8188) using Flux2 workflow. Supports structured JSON prompts sent directly as positive prompt parameter, seed/steps customization. Async watcher via sub-agent for low-latency, token-efficient polling (every 5s). --- # ComfyUI ImageGen\n\n## Changelog\n- **[2026-02-11 20:42 EST]**: **v1.5.0** (published) - Standalone `--structured-prompt` fix (no positional arg required); workflow updated to 1920x1080 16:9; production-ready with live tests (JSON direct-to-positive-prompt, async sub-agent delivery).\n- **[2026-02-11 10:10 EST]**: **v1.4.0** - Refactored prompting: agent converts human prompt to structured JSON string; script sends JSON directly as ComfyUI positive prompt (no prose conversion). Added text-in-image quoting rule. ## Changelog - **[2026-02-11 10:10 EST]**: **v1.4.0** - Refactored prompting: agent converts human prompt to structured JSON string; script sends JSON directly as ComfyUI positive prompt (no prose conversion). Added text-in-image quoting rule. - **[2026-02-11 09:52 EST]**: **v1.3.0** - Script now accepts `--structured-prompt` JSON directly (auto-generates prose internally). Removed agent-side prose step. Updated usage/examples. - **[2026-02-11 09:20 EST]**: **v1.2.0** - Added structured prompt parsing: translate human requests into optional JSON schema fields, auto-generate optimized Flux.2 prose prompts. Updated description. - **[2026-02-11 00:15 EST]**: **v1.1.0** - Added `--submit-only` (fast prompt_id return) + `--watch prompt_id` modes. SKILL.md docs async flow: submit → `sessions_spawn` watcher sub-agent (polls every 5s, auto-sends image to Telegram, ~10x token savings vs. main-agent block). - **[2026-02-11 04:05 EST]**: **v1.0.3** - Added Flux.2 [klein] 9B prompting best practices section with guidelines from Black Forest Labs and fal.ai. - **[2026-02-10 23:05]**: **v1.0.2** - Workflow v3: "Image Save with Prompt/Info (WLSH)" (node 85) for improved metadata embedding. Script updated to poll node 85. - **[2026-02-10 23:00]**: Added explicit instruction to **always send generated image** to user post-generation via `message` tool. - **[2026-02-10]**: Updated to new workflow: JPG output with embedded prompt/metadata via "Image Save with Prompt File (WLSH)". Model changed to `darkBeastFeb0826Latest_dbkBlitzV15.safetensors`. Script now polls node 84. ## Usage 1. **Agent converts human request** to mandatory structured JSON schema → compact string. 2. Verify ComfyUI runs on `localhost:8188`. 3. **Async Mode (Recommended)**: ``` 1. exec python skills/comfyui-imagegen/scripts/generate.py --structured-prompt '{"subjects":[{"description":"fluffy tabby cat","position":"center","action":"sitting relaxed"}],"scene":"cozy room interior","lighting":"warm golden sunset rays","mood":"serene and peaceful","camera":{"angle":"low angle"}}' --submit-only → parse prompt_id 2. sessions_spawn task="Set-Location 'C:\\Users\\hal\\.openclaw\\workspace'; python skills/comfyui-imagegen/scripts/generate.py --watch '{prompt_id}' --output ./gen-{prompt_id}.jpg; message action=send channel=telegram target=595846104 media=./gen-{prompt_id}.jpg; Remove-Item ./gen-{prompt_id}.jpg" label="img-{prompt_id}" cleanup=delete runTimeoutSeconds=180 ``` - Watcher polls `/history/{prompt_id}` **every 5s** (optimal: <5s latency, ~12 polls max @60s job, isolated tokens). - Auto-sends JPG to this chat on completion (sub-agent pings back). - Timeout implicitly via spawn runTimeoutSeconds=120. 4. **Sync Mode** (blocks agent): ``` exec python skills/comfyui-imagegen/scripts/generate.py --structured-prompt '{"scene":"your scene"}' [--seed N] [--steps 10] [--output ./my.jpg] [--host localhost:8188] message action=send channel=telegram media=./my.jpg ``` 5. Customize: | Arg | Default | Notes | |-----|---------|-------| | `--seed` | random | Repro | | `--steps` | 5 | 20-50 quality | | `--host` | localhost:8188 | Remote | | `--output` | gen-{seed/pid}.jpg | Full path | ## Structured Prompt Schema (Mandatory Format) **Agent step 1**: Convert human natural language request into this **exact JSON structure** (all fields optional; populate only relevant; subjects array supports multiples). **Rule**: For text in images (signs, logos), surround in double quotes within description/action fields, e.g., `"sign reading \"STOP\""` or `"logo with text \"OpenClaw\""` ```json { "scene": "overall scene description", "subjects": [ { "description": "detailed subject description", "position": "where in frame", "action": "what they're doing" } ], "style": "artistic style", "color_palette": ["#FF0000", "#00AACC"], "lighting": "lighting description", "mood": "emotional tone", "background": "background details", "composition": "framing and layout", "camera": { "angle": "camera angle", "lens": "lens type", "depth_of_field": "focus behavior" } } ``` **Agent step 2**: Stringify JSON (compact, single-line for shell escaping), pass to script `--structured-prompt` (sent directly as ComfyUI positive prompt). Example: **User**: "A cat sitting on a windowsill at sunset" **Structured JSON string** (for `--structured-prompt`): ```bash '{"subjects":[{"description":"fluffy tabby cat","position":"center","action":"sitting relaxed"}],"scene":"cozy room interior","lighting":"warm golden sunset rays","mood":"serene and peaceful","camera":{"angle":"low angle"}}' ``` ## Workflow Details - Polls node **85** ("Image Save with Prompt/Info (WLSH)"). - Model: `darkBeastFeb0826Latest_dbkBlitzV15.safetensors` - Template: `workflows/flux2.json` ## Prompting Best Practices (Flux.2 [klein] 9B) - Prose, not keywords. Subject → Scene → Lighting → Mood. - E.g., "A serene mountain lake at dawn, mist rising, golden light piercing peaks, photorealistic." - Sources: [BFL](https://docs.bfl.ml/guides/prompting_guide_flux2_klein), [fal.ai](https://fal.ai/learn/devs/flux-2-klein-prompt-guide) ## Examples ```bash # Async test (structured JSON string → direct positive prompt) python .../generate.py --structured-prompt '{"subjects":[{"description":"fluffy tabby cat","position":"center","action":"sitting relaxed"}],"scene":"cozy room interior","lighting":"warm golden sunset rays","mood":"serene and peaceful","camera":{"angle":"low angle"}}' --submit-only --steps 10 # → prompt_id=abc123; spawn watcher sub-agent ``` For cron alternative (less optimal): `cron add` one-shot `at=now+10s` payload.systemEvent="Check img job {prompt_id}" but spawn > cron for this.

path: scripts/generate.py
content: #!/usr/bin/env python3
import json
import sys
import time
import requests
import argparse
import random
from pathlib import Path

def poll_and_download(host_url, prompt_id, output=None):
    history_url = f'{host_url}/history/{prompt_id}'
    print(f'Polling {history_url}')
    while True:
        time.sleep(5)
        hist_resp = requests.get(history_url)
        hist_resp.raise_for_status()
        history = hist_resp.json()
        if prompt_id in history and history[prompt_id]['outputs']:
            print('Job complete')
            break
    outputs = history[prompt_id]['outputs']['85']
    if 'images' not in outputs or not outputs['images']:
        raise ValueError('No images in output')
    img_info = outputs['images'][0]
    filename = img_info['filename']
    subfolder_path = img_info.get('subfolder', '')
    print(f'Image: {filename} in {subfolder_path or "root"}')
    view_url = f'{host_url}/view?filename={filename}&type=output&subfolder={subfolder_path}'
    img_resp = requests.get(view_url)
    img_resp.raise_for_status()
    out_path = output or f'./gen-{prompt_id}.jpg'
    Path(out_path).parent.mkdir(parents=True, exist_ok=True)
    with open(out_path, 'wb') as f:
        f.write(img_resp.content)
    print(f'Saved: {out_path}')
    return out_path

def main():
    parser = argparse.ArgumentParser(description='ComfyUI Flux2 Image Generator')
    prompt_group = parser.add_mutually_exclusive_group(required=False)
    prompt_group.add_argument('prompt', nargs='?', help='Image prompt string (default, ignored with --watch)')
    prompt_group.add_argument('--structured-prompt', help='Structured JSON prompt string (sent directly as positive prompt)')
    parser.add_argument('--seed', type=int, default=None, help='Fixed seed (random otherwise)')
    parser.add_argument('--steps', type=int, default=5, help='Sampling steps (1-50)')
    parser.add_argument('--output', default=None, help='Output JPG path')
    parser.add_argument('--host', default='localhost:8188', help='ComfyUI host:port')
    parser.add_argument('--submit-only', action='store_true', help='Submit and print prompt_id only')
    parser.add_argument('--watch', help='Watch prompt_id and download')
    args = parser.parse_args()

    effective_prompt = args.prompt or args.structured_prompt

    host_url = f'http://{args.host}'

    workflow_path = Path(__file__).parent.parent / 'workflows' / 'flux2.json'
    if not workflow_path.exists():
        raise FileNotFoundError(f'Missing workflow: {workflow_path}')

    if args.watch:
        poll_and_download(host_url, args.watch, args.output)
    elif effective_prompt is None:
        parser.error('Prompt required unless --watch')
    else:
        # Load/modify workflow
        with open(workflow_path, 'r') as f:
            workflow = json.load(f)
        workflow['76']['inputs']['value'] = effective_prompt
        seed = args.seed or random.randrange(sys.maxsize)
        workflow['77:73']['inputs']['noise_seed'] = seed
        workflow['77:62']['inputs']['steps'] = args.steps

        # Submit
        url = f'{host_url}/prompt'
        resp = requests.post(url, json={'prompt': workflow})
        resp.raise_for_status()
        data = resp.json()
        prompt_id = data['prompt_id']
        print(f'Submitted: prompt_id={prompt_id} (seed={seed}, steps={args.steps})')

        if args.submit_only:
            return

        # Poll/download
        poll_and_download(host_url, prompt_id, args.output or f'./gen-{seed}.jpg')

if __name__ == '__main__':
    main()

path: workflows/flux2.json
content: {
  "76": {
    "inputs": {
      "value": "A vintage motorcycle parked in front of a retro diner at sunset, warm orange and pink sky, neon signs glowing, 80s vintage photo style, film grain, warm color cast. Sign over diner says \"Outcast Eats\". Sign in window says \"Always Open!\""
    },
    "class_type": "PrimitiveStringMultiline",
    "_meta": {
      "title": "Prompt"
    }
  },
  "85": {
    "inputs": {
      "filename": "%time_%seed",
      "path": "",
      "extension": "jpeg",
      "quality": 95,
      "counter": 0,
      "time_format": "%Y-%m-%d-%H%M%S",
      "images": [
        "77:65",
        0
      ],
      "positive": [
        "76",
        0
      ],
      "modelname": "darkBeastFeb0826Latest_dbkBlitzV15.safetensors"
    },
    "class_type": "Image Save with Prompt/Info (WLSH)",
    "_meta": {
      "title": "Image Save with Prompt/Info (WLSH)"
    }
  },
  "77:64": {
    "inputs": {
      "noise": [
        "77:73",
        0
      ],
      "guider": [
        "77:63",
        0
      ],
      "sampler": [
        "77:61",
        0
      ],
      "sigmas": [
        "77:62",
        0
      ],
      "latent_image": [
        "77:66",
        0
      ]
    },
    "class_type": "SamplerCustomAdvanced",
    "_meta": {
      "title": "SamplerCustomAdvanced"
    }
  },
  "77:65": {
    "inputs": {
      "samples": [
        "77:64",
        0
      ],
      "vae": [
        "77:72",
        0
      ]
    },
    "class_type": "VAEDecode",
    "_meta": {
      "title": "VAE Decode"
    }
  },
  "77:66": {
    "inputs": {
      "width": [
        "77:68",
        0
      ],
      "height": [
        "77:69",
        0
      ],
      "batch_size": 1
    },
    "class_type": "EmptyFlux2LatentImage",
    "_meta": {
      "title": "Empty Flux 2 Latent"
    }
  },
  "77:68": {
    "inputs": {
      "value": 1920
    },
    "class_type": "PrimitiveInt",
    "_meta": {
      "title": "Width"
    }
  },
  "77:69": {
    "inputs": {
      "value": 1080
    },
    "class_type": "PrimitiveInt",
    "_meta": {
      "title": "Height"
    }
  },
  "77:73": {
    "inputs": {
      "noise_seed": 252497838083333
    },
    "class_type": "RandomNoise",
    "_meta": {
      "title": "RandomNoise"
    }
  },
  "77:71": {
    "inputs": {
      "clip_name": "qwen_3_8b_fp8mixed.safetensors",
      "type": "flux2",
      "device": "default"
    },
    "class_type": "CLIPLoader",
    "_meta": {
      "title": "Load CLIP"
    }
  },
  "77:76": {
    "inputs": {
      "conditioning": [
        "77:74",
        0
      ]
    },
    "class_type": "ConditioningZeroOut",
    "_meta": {
      "title": "ConditioningZeroOut"
    }
  },
  "77:74": {
    "inputs": {
      "text": [
        "76",
        0
      ],
      "clip": [
        "77:71",
        0
      ]
    },
    "class_type": "CLIPTextEncode",
    "_meta": {
      "title": "CLIP Text Encode (Positive Prompt)"
    }
  },
  "77:77": {
    "inputs": {
      "ckpt_name": "darkBeastFeb0826Latest_dbkBlitzV15.safetensors"
    },
    "class_type": "CheckpointLoaderSimple",
    "_meta": {
      "title": "Load Checkpoint"
    }
  },
  "77:62": {
    "inputs": {
      "steps": 25,
      "width": [
        "77:68",
        0
      ],
      "height": [
        "77:69",
        0
      ]
    },
    "class_type": "Flux2Scheduler",
    "_meta": {
      "title": "Flux2Scheduler"
    }
  },
  "77:63": {
    "inputs": {
      "cfg": 1,
      "model": [
        "77:77",
        0
      ],
      "positive": [
        "77:74",
        0
      ],
      "negative": [
        "77:76",
        0
      ]
    },
    "class_type": "CFGGuider",
    "_meta": {
      "title": "CFGGuider"
    }
  },
  "77:61": {
    "inputs": {
      "sampler_name": "dpmpp_2m"
    },
    "class_type": "KSamplerSelect",
    "_meta": {
      "title": "KSamplerSelect"
    }
  },
  "77:72": {
    "inputs": {
      "vae_name": "flux2-vae.safetensors"
    },
    "class_type": "VAELoader",
    "_meta": {
      "title": "Load VAE"
    }
  }
}

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)