SDXL Text-to-Image¶
Complete example of generating images with Stable Diffusion XL.
What you'll learn¶
- How to load an SDXL workflow exported from the GUI.
- How to discover and set inputs by stable index.
- How to submit and wait for completion (blocking path).
- How to map output nodes to generated image filenames and optionally download them.
Prerequisites¶
- InvokeAI server running at http://localhost:9090 (or your own URL).
- An SDXL workflow JSON exported from the GUI, saved locally (e.g., data/workflows/sdxl_text_to_image.json).
- The workflow’s Form includes the parameters you want to control (prompt, steps, width, height, board, etc.). Indices are stable only relative to the current Form structure.
Workflow requirements (GUI)¶
In the InvokeAI GUI, place these fields into the Form before exporting: - Positive Prompt (string) - Negative Prompt (string) - Seed (int) - Steps (int) - CFG Scale (float) - Width (int) - Height (int) - Board (board selector) for the final image node you care about
Note: Labels may vary or be missing. Index is the stable handle.
Input indices at a glance (optional)¶
from invokeai_py_client import InvokeAIClient
from invokeai_py_client.workflow import WorkflowDefinition
client = InvokeAIClient.from_url("http://localhost:9090")
wf = client.workflow_repo.create_workflow(
WorkflowDefinition.from_file("data/workflows/sdxl_text_to_image.json")
)
for inp in wf.list_inputs():
print(f"[{inp.input_index:02d}] {inp.label or inp.field_name} :: {type(inp.field).__name__}")
Quick Start¶
from invokeai_py_client import InvokeAIClient
from invokeai_py_client.workflow import WorkflowDefinition
# 1) Initialize client (URL helper parses host/port/base_path)
client = InvokeAIClient.from_url("http://localhost:9090")
# 2) Load SDXL workflow exported from the GUI
wf = client.workflow_repo.create_workflow(
WorkflowDefinition.from_file("data/workflows/sdxl_text_to_image.json")
)
# 3) Normalize model identifier fields against server inventory
wf.sync_dnn_model(by_name=True, by_base=True)
# 4) Set inputs by index (indices are the stable API)
wf.get_input_value(0).value = "A majestic mountain landscape at sunset, highly detailed, 8k"
wf.get_input_value(1).value = "blurry, low quality, distorted"
wf.get_input_value(2).value = 42 # Seed
wf.get_input_value(3).value = 30 # Steps
wf.get_input_value(4).value = 7.0 # CFG scale
wf.get_input_value(5).value = 1024 # Width
wf.get_input_value(6).value = 1024 # Height
# If your Form exposed a Board selector for the output node, set it too:
# wf.get_input_value(BOARD_INDEX).value = "none" # uncategorized
# 5) Submit and wait (blocking)
submission = wf.submit_sync()
queue_item = wf.wait_for_completion_sync(timeout=180)
print("Final status:", queue_item.get("status"))
# 6) Map outputs (node -> image names)
mappings = wf.map_outputs_to_images(queue_item)
for m in mappings:
print(f"node={m['node_id'][:8]} board={m.get('board_id')} images={m.get('image_names', [])}")
How it works¶
1) The exported workflow JSON is treated as immutable. The client only substitutes values you set on discovered inputs when submitting. 2) Inputs are discovered via a depth‑first traversal of the Form tree. Indices reflect that order and are the only stable reference. 3) submit_sync() enqueues a run; wait_for_completion_sync() polls the server until the job finishes or times out. 4) map_outputs_to_images() correlates output-capable nodes to produced image names, optionally anchored to Form-exposed board fields.
Download results (optional)¶
# Download the first image from the first output mapping (if present)
if mappings and mappings[0].get("image_names"):
first_img = mappings[0]["image_names"][0]
board_id = mappings[0].get("board_id") # may be "none" or a real ID
bh = client.board_repo.get_board_handle(board_id)
data = bh.download_image(first_img, full_resolution=True)
with open(first_img, "wb") as f:
f.write(data)
print("Saved:", first_img)
Common pitfalls¶
- Using labels instead of indices: Labels are not guaranteed unique or present. Always code against indices.
- Forgetting to add fields to the Form in the GUI: Only Form-exposed inputs are discoverable and settable.
- Uploading to uncategorized: When uploading, prefer BoardHandle for uncategorized; uploads omit board_id under the hood (“none” is a read sentinel).
- Passing submission to wait_for_completion_sync(): Not required. Use wait_for_completion_sync(timeout=...) after submit_sync().
Troubleshooting¶
-
Validate before submit to surface per-index issues:
-
Re-discover indices after changing the GUI Form layout:
Complete Implementation¶
Full SDXL Pipeline¶
import json
from pathlib import Path
from typing import Optional, Dict, Any, List
from invokeai_py_client import InvokeAIClient
from invokeai_py_client.workflow import WorkflowDefinition
class SDXLGenerator:
"""SDXL text-to-image generator."""
def __init__(self, client: InvokeAIClient, workflow_path: str):
self.client = client
self.workflow_path = workflow_path
self.wf = None
self.setup_workflow()
def setup_workflow(self):
"""Initialize and configure workflow."""
# Load workflow definition
self.wf = self.client.workflow_repo.create_workflow(
WorkflowDefinition.from_file(self.workflow_path)
)
# Sync models
print("Syncing models...")
changes = self.wf.sync_dnn_model(by_name=True, by_base=True)
if changes:
print(f"Synced {len(changes)} model fields")
# Set default parameters
self.set_defaults()
def set_defaults(self):
"""Set default generation parameters."""
# Find input indices by label
inputs = self.wf.list_inputs()
self.input_map = {inp.label: inp.input_index for inp in inputs}
# Set defaults
if "Positive Prompt" in self.input_map:
self.wf.get_input_value(self.input_map["Positive Prompt"]).value = ""
if "Negative Prompt" in self.input_map:
self.wf.get_input_value(self.input_map["Negative Prompt"]).value = (
"blurry, low quality, distorted, deformed"
)
if "Seed" in self.input_map:
self.wf.get_input_value(self.input_map["Seed"]).value = -1 # Random
if "Steps" in self.input_map:
self.wf.get_input_value(self.input_map["Steps"]).value = 30
if "CFG Scale" in self.input_map:
self.wf.get_input_value(self.input_map["CFG Scale"]).value = 7.0
if "Width" in self.input_map:
self.wf.get_input_value(self.input_map["Width"]).value = 1024
if "Height" in self.input_map:
self.wf.get_input_value(self.input_map["Height"]).value = 1024
if "Board" in self.input_map:
self.wf.get_input_value(self.input_map["Board"]).value = "sdxl_outputs"
def generate(
self,
prompt: str,
negative_prompt: Optional[str] = None,
seed: int = -1,
steps: int = 30,
cfg_scale: float = 7.0,
width: int = 1024,
height: int = 1024,
scheduler: str = "euler",
board: str = "sdxl_outputs"
) -> List[str]:
"""Generate image from text prompt."""
# Set parameters
if "Positive Prompt" in self.input_map:
self.wf.get_input_value(self.input_map["Positive Prompt"]).value = prompt
if negative_prompt and "Negative Prompt" in self.input_map:
self.wf.get_input_value(self.input_map["Negative Prompt"]).value = negative_prompt
if "Seed" in self.input_map:
self.wf.get_input_value(self.input_map["Seed"]).value = seed
if "Steps" in self.input_map:
self.wf.get_input_value(self.input_map["Steps"]).value = steps
if "CFG Scale" in self.input_map:
self.wf.get_input_value(self.input_map["CFG Scale"]).value = cfg_scale
if "Width" in self.input_map:
self.wf.get_input_value(self.input_map["Width"]).value = width
if "Height" in self.input_map:
self.wf.get_input_value(self.input_map["Height"]).value = height
if "Scheduler" in self.input_map:
self.wf.get_input_value(self.input_map["Scheduler"]).value = scheduler
if "Board" in self.input_map:
self.wf.get_input_value(self.input_map["Board"]).value = board
# Submit workflow
print(f"Generating: {prompt[:50]}...")
submission = self.wf.submit_sync()
# Wait for completion
result = self.wf.wait_for_completion_sync(submission, timeout=120)
# Extract images
if result['status'] == 'COMPLETED':
images = self.wf.map_outputs_to_images(result)
print(f"Generated {len(images)} image(s)")
return images
else:
print(f"Generation failed: {result['status']}")
return []
def batch_generate(self, prompts: List[str], **kwargs) -> Dict[str, List[str]]:
"""Generate images for multiple prompts."""
results = {}
for i, prompt in enumerate(prompts):
print(f"\nBatch {i+1}/{len(prompts)}")
images = self.generate(prompt, **kwargs)
results[prompt] = images
return results
# Use the generator
client = InvokeAIClient()
generator = SDXLGenerator(client, "data/workflows/sdxl_text_to_image.json")
# Single generation
images = generator.generate(
prompt="A futuristic city with flying cars, cyberpunk style, neon lights",
negative_prompt="blurry, low quality",
seed=42,
steps=35,
cfg_scale=7.5
)
# Batch generation
prompts = [
"A serene Japanese garden with cherry blossoms",
"A steampunk airship floating above clouds",
"An underwater coral reef teeming with colorful fish"
]
batch_results = generator.batch_generate(
prompts,
steps=30,
cfg_scale=7.0,
width=1024,
height=1024
)
for prompt, images in batch_results.items():
print(f"{prompt[:30]}: {len(images)} images")
Advanced Features¶
With LoRA Support¶
class SDXLWithLoRA(SDXLGenerator):
"""SDXL generator with LoRA support."""
def apply_lora(self, lora_name: str, weight: float = 1.0):
"""Apply LoRA to the workflow."""
# Find LoRA field
for inp in self.wf.list_inputs():
if "lora" in inp.label.lower():
field = self.wf.get_input_value(inp.input_index)
if hasattr(field, 'key'):
field.key = lora_name
field.name = lora_name
if hasattr(field, 'weight'):
field.weight = weight
print(f"Applied LoRA: {lora_name} (weight: {weight})")
return True
print("No LoRA field found in workflow")
return False
def generate_with_lora(
self,
prompt: str,
lora_name: str,
lora_weight: float = 0.8,
**kwargs
) -> List[str]:
"""Generate with LoRA applied."""
self.apply_lora(lora_name, lora_weight)
return self.generate(prompt, **kwargs)
# Use with LoRA
generator = SDXLWithLoRA(client, "data/workflows/sdxl_with_lora.json")
images = generator.generate_with_lora(
prompt="Portrait in anime style",
lora_name="anime-style-xl",
lora_weight=0.7,
negative_prompt="realistic, photographic",
steps=30
)
Style Presets¶
class SDXLStyled(SDXLGenerator):
"""SDXL with style presets."""
STYLES = {
"photorealistic": {
"positive_suffix": ", photorealistic, high detail, sharp focus, 8k, professional photography",
"negative": "cartoon, anime, illustration, painting, drawing",
"cfg_scale": 7.5
},
"anime": {
"positive_suffix": ", anime style, manga, illustration, detailed",
"negative": "photorealistic, 3d render, photograph",
"cfg_scale": 10.0
},
"oil_painting": {
"positive_suffix": ", oil painting, classical art, brush strokes, canvas texture",
"negative": "digital art, photograph, 3d render",
"cfg_scale": 8.0
},
"watercolor": {
"positive_suffix": ", watercolor painting, soft colors, artistic",
"negative": "photograph, digital art, 3d render",
"cfg_scale": 9.0
},
"cyberpunk": {
"positive_suffix": ", cyberpunk style, neon lights, futuristic, high tech",
"negative": "medieval, rustic, natural",
"cfg_scale": 7.0
}
}
def generate_styled(
self,
prompt: str,
style: str = "photorealistic",
**kwargs
) -> List[str]:
"""Generate with style preset."""
if style not in self.STYLES:
print(f"Unknown style: {style}, using photorealistic")
style = "photorealistic"
style_config = self.STYLES[style]
# Apply style
styled_prompt = prompt + style_config["positive_suffix"]
kwargs['negative_prompt'] = kwargs.get('negative_prompt', '') + ", " + style_config["negative"]
kwargs['cfg_scale'] = kwargs.get('cfg_scale', style_config["cfg_scale"])
print(f"Applying style: {style}")
return self.generate(styled_prompt, **kwargs)
# Use styled generator
generator = SDXLStyled(client, "data/workflows/sdxl_text_to_image.json")
# Generate with different styles
for style in ["photorealistic", "anime", "oil_painting"]:
images = generator.generate_styled(
prompt="A majestic dragon",
style=style,
seed=42 # Same seed for comparison
)
print(f"Generated {style}: {images}")
Prompt Engineering¶
Enhanced Prompting¶
class PromptEnhancer:
"""Enhance prompts for better SDXL results."""
def __init__(self):
self.quality_tags = [
"masterpiece", "best quality", "highly detailed",
"ultra-detailed", "8k", "high resolution"
]
self.lighting_tags = [
"perfect lighting", "dramatic lighting", "cinematic lighting",
"natural lighting", "studio lighting"
]
self.composition_tags = [
"perfect composition", "rule of thirds", "golden ratio",
"centered", "symmetrical"
]
def enhance(
self,
prompt: str,
add_quality: bool = True,
add_lighting: bool = True,
add_composition: bool = False,
custom_tags: List[str] = None
) -> str:
"""Enhance prompt with quality tags."""
enhanced = prompt
tags = []
if add_quality:
tags.extend(self.quality_tags[:3])
if add_lighting:
tags.append(self.lighting_tags[0])
if add_composition:
tags.append(self.composition_tags[0])
if custom_tags:
tags.extend(custom_tags)
if tags:
enhanced = f"{prompt}, {', '.join(tags)}"
return enhanced
def build_negative(self, base_negative: str = "") -> str:
"""Build comprehensive negative prompt."""
default_negative = [
"low quality", "worst quality", "blurry", "distorted",
"deformed", "disfigured", "bad anatomy", "wrong anatomy",
"mutation", "mutated", "ugly", "duplicate", "morbid",
"out of frame", "extra limbs", "malformed limbs",
"poorly drawn hands", "poorly drawn face", "jpeg artifacts"
]
if base_negative:
return f"{base_negative}, {', '.join(default_negative[:10])}"
return ", ".join(default_negative[:10])
# Use prompt enhancer
enhancer = PromptEnhancer()
generator = SDXLGenerator(client, "data/workflows/sdxl_text_to_image.json")
enhanced_prompt = enhancer.enhance(
"A magical forest with glowing mushrooms",
add_quality=True,
add_lighting=True,
custom_tags=["fantasy art", "enchanted"]
)
negative_prompt = enhancer.build_negative()
images = generator.generate(
prompt=enhanced_prompt,
negative_prompt=negative_prompt,
steps=35,
cfg_scale=7.5
)
Integration Example¶
Complete Application¶
def main():
"""Complete SDXL application example."""
import argparse
parser = argparse.ArgumentParser(description="SDXL Image Generator")
parser.add_argument("prompt", help="Text prompt for generation")
parser.add_argument("--negative", default="", help="Negative prompt")
parser.add_argument("--seed", type=int, default=-1, help="Random seed")
parser.add_argument("--steps", type=int, default=30, help="Inference steps")
parser.add_argument("--cfg", type=float, default=7.0, help="CFG scale")
parser.add_argument("--width", type=int, default=1024, help="Image width")
parser.add_argument("--height", type=int, default=1024, help="Image height")
parser.add_argument("--style", default="photorealistic", help="Style preset")
parser.add_argument("--output", default="output.png", help="Output filename")
args = parser.parse_args()
# Initialize
client = InvokeAIClient()
generator = SDXLStyled(client, "data/workflows/sdxl_text_to_image.json")
# Generate
print(f"Generating: {args.prompt}")
images = generator.generate_styled(
prompt=args.prompt,
style=args.style,
negative_prompt=args.negative,
seed=args.seed,
steps=args.steps,
cfg_scale=args.cfg,
width=args.width,
height=args.height
)
if images:
# Download result
board = client.board_repo.get_board_handle("sdxl_outputs")
image_data = board.download_image(images[0], full_resolution=True)
# Save to file
with open(args.output, "wb") as f:
f.write(image_data)
print(f"Saved to: {args.output}")
else:
print("Generation failed")
return 1
return 0
if __name__ == "__main__":
import sys
sys.exit(main())
Next Steps¶
- Try FLUX Image-to-Image for image editing
- Explore Multi-Stage Refine for quality enhancement
- See Raw API for custom implementations
- Review Workflow Basics for fundamentals