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
| Feature | Mac Mini | Ubuntu |
|---|---|---|
| Access | Direct HTTP | Job-based API |
| Response Time | 8-15 seconds | 10-20 seconds + queue wait |
| Best For | Single images, real-time | Batch processing, production |
| Model | Z-Image Turbo (BF16) | Z-Image Turbo (BF16) |
| Resolution | Up to 2048x2048 | Up to 2048x2048 |
| CLIP | Qwen 3 4B (Arabic support) | Qwen 3 4B (Arabic support) |
| Authentication | None (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
- Use Mac Mini for: Real-time single images
- Use Ubuntu for: Batch generation, queued processing
- Cache prompts: Same prompt + seed = same image
- Optimize steps: 9 steps is optimal for Z-Image Turbo
- 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