Skill

ComfyUI (Image Generation)

ComfyUI Image Generation Integration Skill

Overview

This skill provides ready-to-use code for integrating ComfyUI image generation into your applications.

Available Services:

  • Mac Mini ComfyUI - Direct API, faster for single images (Z-Image Turbo)
  • Ubuntu ComfyUI - Job queue, GPU-accelerated, better for batches

Service Comparison

FeatureMac MiniUbuntu
AccessDirect HTTPJob-based API
Response Time8-15 seconds10-20 seconds + queue wait
Best ForSingle images, real-timeBatch processing, production
ModelZ-Image Turbo (BF16)Z-Image Turbo (BF16)
ResolutionUp to 2048x2048Up to 2048x2048
CLIPQwen 3 4B (Arabic support)Qwen 3 4B (Arabic support)
AuthenticationNone (internal)JWT required

Mac Mini Direct API

TypeScript Implementation

interface ComfyUIWorkflow {
  prompt: string;
  negative_prompt?: string;
  width?: number;
  height?: number;
  steps?: number;
  cfg?: number;
  seed?: number;
}

interface ComfyUIResponse {
  prompt_id: string;
  number: number;
  node_errors: Record<string, any>;
}

class ComfyUIClient {
  private baseURL: string;
  private wsURL: string;
  private clientId: string;

  constructor(baseURL: string = 'http://localhost:8188') {
    this.baseURL = baseURL;
    this.wsURL = baseURL.replace('http', 'ws');
    this.clientId = Math.random().toString(36).substring(7);
  }

  // Create Z-Image Turbo workflow
  createZImageWorkflow(options: ComfyUIWorkflow): any {
    const {
      prompt,
      negative_prompt = '',
      width = 1024,
      height = 1024,
      steps = 9,
      cfg = 1.0,
      seed = Math.floor(Math.random() * 1000000),
    } = options;

    return {
      "39": {
        "inputs": {
          "clip_name": "qwen_3_4b.safetensors",
          "type": "lumina2",
          "device": "default"
        },
        "class_type": "CLIPLoader"
      },
      "40": {
        "inputs": {
          "vae_name": "ae.safetensors"
        },
        "class_type": "VAELoader"
      },
      "41": {
        "inputs": {
          "width": width,
          "height": height,
          "batch_size": 1
        },
        "class_type": "EmptySD3LatentImage"
      },
      "45": {
        "inputs": {
          "text": prompt,
          "clip": ["39", 0]
        },
        "class_type": "CLIPTextEncode"
      },
      "42": {
        "inputs": {
          "conditioning": ["45", 0]
        },
        "class_type": "ConditioningZeroOut"
      },
      "46": {
        "inputs": {
          "unet_name": "z_image_turbo_bf16.safetensors",
          "weight_dtype": "default"
        },
        "class_type": "UNETLoader"
      },
      "47": {
        "inputs": {
          "shift": 3,
          "model": ["46", 0]
        },
        "class_type": "ModelSamplingAuraFlow"
      },
      "44": {
        "inputs": {
          "seed": seed,
          "steps": steps,
          "cfg": cfg,
          "sampler_name": "res_multistep",
          "scheduler": "simple",
          "denoise": 1,
          "model": ["47", 0],
          "positive": ["45", 0],
          "negative": ["42", 0],
          "latent_image": ["41", 0]
        },
        "class_type": "KSampler"
      },
      "43": {
        "inputs": {
          "samples": ["44", 0],
          "vae": ["40", 0]
        },
        "class_type": "VAEDecode"
      },
      "48": {
        "inputs": {
          "filename_prefix": "a2zadd",
          "images": ["43", 0]
        },
        "class_type": "SaveImage"
      }
    };
  }

  // Queue a prompt
  async queuePrompt(workflow: any): Promise<ComfyUIResponse> {
    const response = await fetch(`${this.baseURL}/prompt`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        prompt: workflow,
        client_id: this.clientId,
      }),
    });

    if (!response.ok) {
      throw new Error(`ComfyUI queue failed: ${response.statusText}`);
    }

    return response.json();
  }

  // Wait for generation via WebSocket
  async waitForGeneration(promptId: string): Promise<string[]> {
    return new Promise((resolve, reject) => {
      const ws = new WebSocket(`${this.wsURL}/ws?clientId=${this.clientId}`);
      const images: string[] = [];

      ws.onmessage = (event) => {
        const message = JSON.parse(event.data);

        if (message.type === 'executing') {
          const data = message.data;
          if (data.node === null && data.prompt_id === promptId) {
            // Generation complete
            ws.close();
            if (images.length > 0) {
              resolve(images);
            } else {
              reject(new Error('No images generated'));
            }
          }
        }

        if (message.type === 'executed') {
          const output = message.data.output;
          if (output?.images) {
            output.images.forEach((img: any) => {
              images.push(this.getImageUrl(img.filename, img.subfolder, img.type));
            });
          }
        }
      };

      ws.onerror = (error) => {
        reject(new Error('WebSocket error: ' + error));
      };

      setTimeout(() => {
        ws.close();
        reject(new Error('Timeout waiting for generation'));
      }, 60000); // 1 minute timeout
    });
  }

  // Get image URL
  getImageUrl(filename: string, subfolder: string = '', type: string = 'output'): string {
    const params = new URLSearchParams({
      filename,
      subfolder,
      type,
    });
    return `${this.baseURL}/view?${params}`;
  }

  // Complete generation workflow
  async generateImage(options: ComfyUIWorkflow): Promise<string[]> {
    const workflow = this.createZImageWorkflow(options);
    const { prompt_id } = await this.queuePrompt(workflow);
    return this.waitForGeneration(prompt_id);
  }
}

// Usage Example
async function example() {
  const comfy = new ComfyUIClient('http://localhost:8188');

  const imageUrls = await comfy.generateImage({
    prompt: 'A beautiful sunset over the pyramids of Egypt, natural photography',
    negative_prompt: 'cartoon, anime, drawing',
    width: 1024,
    height: 1024,
    steps: 9,
    seed: 42,
  });

  console.log('Generated images:', imageUrls);
  // imageUrls: ['http://localhost:8188/view?filename=a2zadd_00001_.png&...']
}

Ubuntu Job-Based API

TypeScript Implementation

import { JobClient } from './job-management-skill';

interface ImageGenerationParams {
  prompt: string;
  negative_prompt?: string;
  width?: number;
  height?: number;
  steps?: number;
  cfg_scale?: number;
  seed?: number;
}

class UbuntuImageGenerator {
  private jobClient: JobClient;

  constructor(token: string, baseURL: string = 'https://api.proyaro.com') {
    this.jobClient = new JobClient(baseURL, token);
  }

  async generateImage(params: ImageGenerationParams): Promise<any> {
    // Create job
    const job = await this.jobClient.createJob({
      job_type: 'image_generation',
      parameters: {
        prompt: params.prompt,
        negative_prompt: params.negative_prompt || '',
        width: params.width || 1024,
        height: params.height || 1024,
        steps: params.steps || 9,
        cfg_scale: params.cfg_scale || 1.0,
        seed: params.seed,
      },
    });

    console.log(`Image generation job created: ${job.id}`);

    // Wait for completion
    const result = await this.jobClient.waitForJob(job.id);

    return result.result_data;
  }
}

// Usage Example
async function exampleUbuntu() {
  const generator = new UbuntuImageGenerator('your-jwt-token');

  const result = await generator.generateImage({
    prompt: 'A beautiful sunset over the pyramids',
    negative_prompt: 'cartoon, anime',
    width: 1024,
    height: 1024,
    steps: 9,
  });

  console.log('Generated image:', result.images[0].url);
  // Download: https://api.proyaro.com/api/images/ComfyUI_00001_.png
}

React Hook (Mac Mini)

import { useState, useCallback } from 'react';

interface UseComfyUIOptions {
  baseURL?: string;
}

export function useComfyUI(options: UseComfyUIOptions = {}) {
  const [generating, setGenerating] = useState(false);
  const [imageUrls, setImageUrls] = useState<string[]>([]);
  const [error, setError] = useState<string | null>(null);

  const generateImage = useCallback(async (params: ComfyUIWorkflow) => {
    setGenerating(true);
    setError(null);

    try {
      const client = new ComfyUIClient(options.baseURL);
      const urls = await client.generateImage(params);
      setImageUrls(urls);
      return urls;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Generation failed';
      setError(message);
      throw err;
    } finally {
      setGenerating(false);
    }
  }, [options.baseURL]);

  return {
    generateImage,
    generating,
    imageUrls,
    error,
  };
}

// Usage in Component
function ImageGenerator() {
  const { generateImage, generating, imageUrls, error } = useComfyUI();
  const [prompt, setPrompt] = useState('');

  const handleGenerate = async () => {
    await generateImage({
      prompt,
      width: 1024,
      height: 1024,
    });
  };

  return (
    <div>
      <input
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        placeholder="Enter image prompt..."
      />
      <button onClick={handleGenerate} disabled={generating}>
        {generating ? 'Generating...' : 'Generate Image'}
      </button>

      {error && <div className="error">{error}</div>}

      <div className="images">
        {imageUrls.map((url, i) => (
          <img key={i} src={url} alt={`Generated ${i}`} />
        ))}
      </div>
    </div>
  );
}

Python Implementation (Mac Mini)

import requests
import websocket
import json
import time
import random
from typing import List, Dict, Any, Optional


class ComfyUIClient:
    """Client for ComfyUI direct API"""

    def __init__(self, base_url: str = "http://localhost:8188"):
        self.base_url = base_url
        self.client_id = str(random.randint(100000, 999999))

    def create_z_image_workflow(
        self,
        prompt: str,
        negative_prompt: str = "",
        width: int = 1024,
        height: int = 1024,
        steps: int = 9,
        cfg: float = 1.0,
        seed: Optional[int] = None,
    ) -> Dict[str, Any]:
        """Create Z-Image Turbo workflow"""
        if seed is None:
            seed = random.randint(0, 1000000)

        return {
            "39": {
                "inputs": {
                    "clip_name": "qwen_3_4b.safetensors",
                    "type": "lumina2",
                    "device": "default"
                },
                "class_type": "CLIPLoader",
            },
            "40": {
                "inputs": {"vae_name": "ae.safetensors"},
                "class_type": "VAELoader",
            },
            "41": {
                "inputs": {"width": width, "height": height, "batch_size": 1},
                "class_type": "EmptySD3LatentImage",
            },
            "45": {
                "inputs": {"text": prompt, "clip": ["39", 0]},
                "class_type": "CLIPTextEncode",
            },
            "42": {
                "inputs": {"conditioning": ["45", 0]},
                "class_type": "ConditioningZeroOut",
            },
            "46": {
                "inputs": {
                    "unet_name": "z_image_turbo_bf16.safetensors",
                    "weight_dtype": "default"
                },
                "class_type": "UNETLoader",
            },
            "47": {
                "inputs": {"shift": 3, "model": ["46", 0]},
                "class_type": "ModelSamplingAuraFlow",
            },
            "44": {
                "inputs": {
                    "seed": seed,
                    "steps": steps,
                    "cfg": cfg,
                    "sampler_name": "res_multistep",
                    "scheduler": "simple",
                    "denoise": 1,
                    "model": ["47", 0],
                    "positive": ["45", 0],
                    "negative": ["42", 0],
                    "latent_image": ["41", 0],
                },
                "class_type": "KSampler",
            },
            "43": {
                "inputs": {"samples": ["44", 0], "vae": ["40", 0]},
                "class_type": "VAEDecode",
            },
            "48": {
                "inputs": {"filename_prefix": "a2zadd", "images": ["43", 0]},
                "class_type": "SaveImage",
            },
        }

    def queue_prompt(self, workflow: Dict[str, Any]) -> Dict[str, Any]:
        """Queue a workflow for generation"""
        response = requests.post(
            f"{self.base_url}/prompt",
            json={"prompt": workflow, "client_id": self.client_id},
            timeout=30
        )
        response.raise_for_status()
        return response.json()

    def get_image_url(
        self, filename: str, subfolder: str = "", img_type: str = "output"
    ) -> str:
        """Get image URL"""
        params = f"?filename={filename}&subfolder={subfolder}&type={img_type}"
        return f"{self.base_url}/view{params}"

    def wait_for_generation(self, prompt_id: str, timeout: int = 60) -> List[str]:
        """Wait for generation via WebSocket"""
        ws_url = self.base_url.replace("http", "ws")
        ws = websocket.create_connection(
            f"{ws_url}/ws?clientId={self.client_id}",
            timeout=timeout
        )

        images = []
        start_time = time.time()

        try:
            while True:
                if time.time() - start_time > timeout:
                    raise TimeoutError("Generation timeout")

                message = json.loads(ws.recv())

                if message["type"] == "executing":
                    data = message["data"]
                    if data["node"] is None and data["prompt_id"] == prompt_id:
                        # Generation complete
                        break

                if message["type"] == "executed":
                    output = message["data"].get("output", {})
                    if "images" in output:
                        for img in output["images"]:
                            url = self.get_image_url(
                                img["filename"],
                                img.get("subfolder", ""),
                                img.get("type", "output")
                            )
                            images.append(url)

        finally:
            ws.close()

        return images

    def generate_image(
        self,
        prompt: str,
        negative_prompt: str = "",
        width: int = 1024,
        height: int = 1024,
        steps: int = 9,
        seed: Optional[int] = None,
    ) -> List[str]:
        """Complete workflow: queue and wait for generation"""
        workflow = self.create_z_image_workflow(
            prompt=prompt,
            negative_prompt=negative_prompt,
            width=width,
            height=height,
            steps=steps,
            seed=seed,
        )

        result = self.queue_prompt(workflow)
        prompt_id = result["prompt_id"]

        return self.wait_for_generation(prompt_id)


# Usage Example
if __name__ == "__main__":
    client = ComfyUIClient()

    print("Generating image...")
    images = client.generate_image(
        prompt="A beautiful sunset over the pyramids of Egypt",
        negative_prompt="cartoon, anime",
        width=1024,
        height=1024,
        steps=9,
    )

    print(f"Generated {len(images)} image(s):")
    for url in images:
        print(f"  {url}")

Best Practices

Prompt Engineering

// Good prompts for Z-Image Turbo
const goodPrompts = {
  photorealistic: 'natural photography of [subject], high quality, detailed',
  product: '[product], white background, product photography, professional',
  marketing: '[subject], cinematic lighting, vibrant colors, modern style',
  arabic: 'منتج [اسم المنتج]، تصوير احترافي، خلفية بيضاء',
};

// Bad prompts (avoid)
const badPrompts = {
  tooVague: 'image',  // Too generic
  tooLong: '...very long prompt with 200+ words...',  // Too complex
  conflicts: 'dark night bright sunny day',  // Contradictory
};

Resolution Guidelines

const resolutions = {
  square: { width: 1024, height: 1024 },      // Default, fastest
  landscape: { width: 1280, height: 720 },    // 16:9
  portrait: { width: 720, height: 1280 },     // 9:16
  ultraWide: { width: 1920, height: 1080 },   // Slower
};

// Stick to multiples of 64 for best results

Performance Tips

  1. Use Mac Mini for: Real-time single images
  2. Use Ubuntu for: Batch generation, queued processing
  3. Cache prompts: Same prompt + seed = same image
  4. Optimize steps: 9 steps is optimal for Z-Image Turbo
  5. Monitor queue: Check system load before batch jobs

Error Handling

async function safeGenerate(
  client: ComfyUIClient,
  prompt: string
): Promise<string[]> {
  try {
    return await client.generateImage({ prompt });
  } catch (error) {
    if (error.message?.includes('WebSocket')) {
      throw new Error('ComfyUI service not responding. Is it running?');
    }
    if (error.message?.includes('Timeout')) {
      throw new Error('Generation took too long. Try simpler prompt or lower resolution.');
    }
    if (error.message?.includes('CUDA')) {
      throw new Error('GPU out of memory. Reduce resolution or batch size.');
    }
    throw error;
  }
}

Testing

async function testComfyUI() {
  const client = new ComfyUIClient();

  // Health check
  console.log('Testing ComfyUI health...');
  const health = await fetch('http://localhost:8188/system_stats');
  console.assert(health.ok, 'ComfyUI should be healthy');

  // Test generation
  console.log('Testing image generation...');
  const images = await client.generateImage({
    prompt: 'test image',
    width: 512,
    height: 512,
    steps: 4,  // Faster for testing
  });

  console.assert(images.length > 0, 'Should generate image');
  console.log('All tests passed!');
}

Skill Version: 1.0 Last Updated: 2025-01-01

ProYaro AI Infrastructure Documentation • Version 1.2