Files
EricRollei-Local_LLM_Prompt…/text_to_image_node.py
T

1034 lines
40 KiB
Python

"""
Text-to-Image Prompt Enhancer Node
Advanced multi-platform prompt enhancement for image generation
Supports: SDXL, Pony, Illustrious, Flux, Chroma, Qwen-Image, Qwen-Edit, Wan-Image
"""
import torch
import numpy as np
from PIL import Image
import io
import base64
import random
from typing import Tuple, Optional, Dict, List
from .llm_backend import LLMBackend
from .platforms import get_platform_config, get_negative_prompt_for_platform
from .utils import save_prompts_to_file, parse_keywords
class TextToImagePromptEnhancer:
"""
Advanced text-to-image prompt enhancement with platform-specific optimization
Features:
- Multiple platform support with tailored prompting
- Optional image reference input (1-2 images)
- Advanced aesthetic controls (lighting, camera, time of day, etc.)
- Wildcard random options
- Platform-specific formatting and token optimization
"""
def __init__(self):
self.type = "text_to_image_enhancement"
self.output_dir = "output/txt2img_prompts"
# Wildcard options
self.camera_angles = [
"eye level", "low angle", "high angle", "dutch angle", "bird's eye view",
"worm's eye view", "over the shoulder", "point of view", "extreme close-up angle"
]
self.lighting_sources = [
"natural sunlight", "studio lighting", "golden hour sun", "moonlight",
"candlelight", "neon lights", "firelight", "spotlight", "ambient lighting",
"backlight", "rim lighting", "window light", "street lights"
]
self.lighting_quality = [
"soft diffused", "hard dramatic", "even balanced", "high contrast",
"low key", "high key", "chiaroscuro", "volumetric", "atmospheric"
]
self.times_of_day = [
"dawn", "early morning", "mid-morning", "noon", "afternoon",
"golden hour", "dusk", "twilight", "night", "midnight", "blue hour"
]
self.weather_conditions = [
"clear sky", "partly cloudy", "overcast", "misty", "foggy",
"rainy", "stormy", "snowy", "sunny", "hazy"
]
self.composition_styles = [
"rule of thirds", "centered", "symmetrical", "asymmetrical",
"golden ratio", "leading lines", "frame within frame", "negative space",
"balanced", "dynamic diagonal"
]
self.genre_styles = [
"surreal", "cinematic", "dramatic", "action", "humorous",
"indie", "horror", "scifi", "romantic", "artistic",
"documentary", "minimalist", "maximalist", "vintage", "modern",
"fantasy", "noir", "cyberpunk"
]
self.subject_framings = [
"extreme close-up", "close-up", "medium close-up",
"medium shot", "medium wide", "wide shot",
"full body", "cowboy shot", "bust shot",
"head and shoulders", "three-quarter"
]
self.subject_poses = [
"standing", "sitting", "lying down", "kneeling", "crouching",
"action pose", "portrait pose", "dynamic", "static",
"asymmetric", "contrapposto", "relaxed", "tense"
]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
# Core inputs
"text_prompt": ("STRING", {
"multiline": True,
"default": "a beautiful woman in a garden",
"placeholder": "Describe the image you want to generate\n\n"
"Supports:\n"
"- Emphasis: (keyword:1.5) to increase weight\n"
"- De-emphasis: (keyword:0.5) to decrease weight\n"
"- Alternation: {apple|banana|orange} picks one randomly\n"
"- Nested: {red|blue|green} (dress:1.2) works too"
}),
# Platform selection
"target_platform": ([
"flux",
"sd_xl",
"pony",
"illustrious",
"chroma",
"qwen_image",
"qwen_image_edit",
"wan_image"
], {
"default": "flux"
}),
# LLM settings
"llm_backend": ([
"lm_studio",
"ollama"
], {
"default": "lm_studio"
}),
"model_name": ("STRING", {
"default": "llama3",
"multiline": False
}),
"api_endpoint": ("STRING", {
"default": "http://localhost:1234/v1",
"multiline": False
}),
"temperature": ("FLOAT", {
"default": 0.7,
"min": 0.1,
"max": 2.0,
"step": 0.1
}),
# Camera & Composition
"camera_angle": ([
"auto", "random", "none",
"eye level", "low angle", "high angle", "dutch angle",
"bird's eye view", "worm's eye view", "over the shoulder",
"point of view", "extreme close-up angle"
], {
"default": "auto"
}),
"composition": ([
"auto", "random", "none",
"rule of thirds", "centered", "symmetrical", "asymmetrical",
"golden ratio", "leading lines", "frame within frame",
"negative space", "balanced", "dynamic diagonal"
], {
"default": "auto"
}),
# Lighting
"lighting_source": ([
"auto", "random", "none",
"natural sunlight", "studio lighting", "golden hour sun",
"moonlight", "candlelight", "neon lights", "firelight",
"spotlight", "ambient lighting", "backlight", "rim lighting",
"window light", "street lights"
], {
"default": "auto"
}),
"lighting_quality": ([
"auto", "random", "none",
"soft diffused", "hard dramatic", "even balanced",
"high contrast", "low key", "high key", "chiaroscuro",
"volumetric", "atmospheric"
], {
"default": "auto"
}),
# Time & Weather
"time_of_day": ([
"auto", "random", "none",
"dawn", "early morning", "mid-morning", "noon", "afternoon",
"golden hour", "dusk", "twilight", "night", "midnight", "blue hour"
], {
"default": "auto"
}),
"weather": ([
"auto", "random", "none",
"clear sky", "partly cloudy", "overcast", "misty", "foggy",
"rainy", "stormy", "snowy", "sunny", "hazy"
], {
"default": "auto"
}),
# Style & Quality
"art_style": ([
"auto", "none",
"photorealistic", "digital art", "oil painting", "watercolor",
"anime", "manga", "sketch", "pencil drawing", "3D render",
"illustration", "concept art", "impressionist", "abstract",
"pixel art", "low poly", "papercraft", "isometric"
], {
"default": "auto"
}),
"genre_style": ([
"auto", "random", "none",
"surreal", "cinematic", "dramatic", "action", "humorous",
"indie", "horror", "scifi", "romantic", "x-rated", "pg",
"artistic", "documentary", "minimalist", "maximalist",
"vintage", "modern", "fantasy", "noir", "cyberpunk"
], {
"default": "auto"
}),
"color_mood": ([
"auto", "random", "none",
"vibrant", "muted", "monochrome", "warm tones", "cool tones",
"pastel", "high contrast", "desaturated", "neon", "earth tones"
], {
"default": "auto"
}),
"detail_level": ([
"auto",
"standard",
"highly detailed",
"intricate details",
"simplified",
"minimalist"
], {
"default": "auto"
}),
"prompt_length": ([
"auto",
"very_short",
"short",
"medium",
"long",
"very_long"
], {
"default": "auto"
}),
# Subject Controls
"subject_framing": ([
"auto", "random", "none",
"extreme close-up", "close-up", "medium close-up",
"medium shot", "medium wide", "wide shot",
"full body", "cowboy shot", "bust shot",
"head and shoulders", "three-quarter"
], {
"default": "auto"
}),
"subject_pose": ([
"auto", "random", "none",
"standing", "sitting", "lying down", "kneeling", "crouching",
"action pose", "portrait pose", "dynamic", "static",
"asymmetric", "contrapposto", "relaxed", "tense",
"walking", "running", "jumping", "dancing"
], {
"default": "auto"
}),
"quality_emphasis": ("BOOLEAN", {
"default": True
}),
# Keywords
"positive_keywords": ("STRING", {
"default": "",
"multiline": True,
"placeholder": "Additional keywords, LoRA triggers, specific details"
}),
"negative_keywords": ("STRING", {
"default": "",
"multiline": True,
"placeholder": "Things to avoid"
}),
# Output
"save_to_file": ("BOOLEAN", {
"default": False
}),
"filename_base": ("STRING", {
"default": "txt2img_prompt",
"multiline": False
})
},
"optional": {
# Optional image references
"reference_image_1": ("IMAGE",),
"reference_image_2": ("IMAGE",),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("positive_prompt", "negative_prompt", "settings_used", "status")
FUNCTION = "enhance_prompt"
CATEGORY = "Eric Prompt Enhancers"
OUTPUT_NODE = True
def enhance_prompt(
self,
text_prompt: str,
target_platform: str,
llm_backend: str,
model_name: str,
api_endpoint: str,
temperature: float,
camera_angle: str,
composition: str,
lighting_source: str,
lighting_quality: str,
time_of_day: str,
weather: str,
art_style: str,
genre_style: str,
color_mood: str,
detail_level: str,
prompt_length: str,
subject_framing: str,
subject_pose: str,
quality_emphasis: bool,
positive_keywords: str,
negative_keywords: str,
save_to_file: bool,
filename_base: str,
reference_image_1: Optional[torch.Tensor] = None,
reference_image_2: Optional[torch.Tensor] = None
) -> Tuple[str, str, str, str]:
"""Main processing function"""
try:
# STEP 0: Process alternations first (before LLM)
text_prompt = self._process_alternations(text_prompt)
# STEP 0.5: Protect emphasis syntax
text_prompt = self._preserve_emphasis_syntax(text_prompt)
# STEP 1: Process reference images if provided
image_descriptions = []
if reference_image_1 is not None:
desc = self._get_simple_image_description(reference_image_1, "Reference 1")
image_descriptions.append(desc)
if reference_image_2 is not None:
desc = self._get_simple_image_description(reference_image_2, "Reference 2")
image_descriptions.append(desc)
# STEP 2: Resolve random/auto options
resolved_settings = self._resolve_settings(
camera_angle, composition, lighting_source, lighting_quality,
time_of_day, weather, art_style, genre_style, color_mood,
detail_level, prompt_length, subject_framing, subject_pose,
target_platform
)
# STEP 3: Build enhancement prompt
platform_config = get_platform_config(target_platform)
system_prompt = self._build_system_prompt(
platform_config, resolved_settings, quality_emphasis
)
user_prompt = self._build_user_prompt(
text_prompt, image_descriptions, resolved_settings, target_platform
)
# STEP 4: Call LLM
llm = LLMBackend(
backend_type=llm_backend,
endpoint=api_endpoint,
model_name=model_name,
temperature=temperature
)
response = llm.send_prompt(
system_prompt=system_prompt,
user_prompt=user_prompt,
max_tokens=800
)
if not response["success"]:
return (
text_prompt,
"",
str(resolved_settings),
f"❌ LLM Error: {response['error']}"
)
# STEP 5: Parse and format response
enhanced_prompt = self._parse_llm_response(response["response"], target_platform)
# STEP 5.5: Restore emphasis syntax that was protected
enhanced_prompt = self._restore_emphasis_syntax(enhanced_prompt)
# STEP 6: Add custom keywords
pos_kw_list = parse_keywords(positive_keywords)
if pos_kw_list:
enhanced_prompt = self._add_keywords(enhanced_prompt, pos_kw_list, target_platform)
# STEP 7: Add platform-specific required tokens
enhanced_prompt = self._add_platform_requirements(enhanced_prompt, target_platform, quality_emphasis)
# STEP 8: Generate negative prompt
neg_kw_list = parse_keywords(negative_keywords)
negative_prompt = get_negative_prompt_for_platform(target_platform, neg_kw_list)
# STEP 9: Format settings display
settings_display = self._format_settings_display(resolved_settings, platform_config)
# STEP 10: Save if requested
if save_to_file:
metadata = {
"type": "text-to-image",
"platform": target_platform,
"platform_name": platform_config["name"],
"model": model_name,
"original_prompt": text_prompt,
"settings": resolved_settings
}
save_result = save_prompts_to_file(
positive_prompt=enhanced_prompt,
negative_prompt=negative_prompt,
breakdown=settings_display,
metadata=metadata,
filename_base=filename_base,
output_dir=self.output_dir
)
file_status = f"💾 Saved to {save_result['filepath']}" if save_result["success"] else "⚠️ Save failed"
else:
file_status = "Not saved"
status = f"✅ Enhanced for {platform_config['name']} | {file_status}"
return (
enhanced_prompt,
negative_prompt,
settings_display,
status
)
except Exception as e:
import traceback
error_detail = traceback.format_exc()
print(f"ERROR in text-to-image enhancement: {error_detail}")
return (
text_prompt,
"",
"",
f"❌ Error: {str(e)}"
)
def _get_simple_image_description(self, image: torch.Tensor, label: str) -> str:
"""Get description of reference image based on basic analysis"""
try:
# Convert tensor to numpy for analysis
# ComfyUI images are typically [batch, height, width, channels] in range [0, 1]
if isinstance(image, torch.Tensor):
img_np = image.cpu().numpy()
# Get first image if batch
if len(img_np.shape) == 4:
img_np = img_np[0]
# Get dimensions
height, width = img_np.shape[:2]
aspect_ratio = width / height if height > 0 else 1.0
# Analyze colors (basic RGB analysis)
mean_colors = img_np.mean(axis=(0, 1))
brightness = mean_colors.mean()
# Determine general characteristics
orientation = "landscape" if aspect_ratio > 1.3 else "portrait" if aspect_ratio < 0.77 else "square"
if brightness > 0.7:
tone = "bright"
elif brightness > 0.4:
tone = "balanced"
else:
tone = "dark"
# Color dominance (simple heuristic)
if len(mean_colors) >= 3:
r, g, b = mean_colors[0], mean_colors[1], mean_colors[2]
max_channel = max(r, g, b)
min_channel = min(r, g, b)
saturation = max_channel - min_channel
if saturation < 0.1:
color_desc = "monochromatic or low saturation"
elif r > g and r > b:
color_desc = "warm tones, reddish hues"
elif b > r and b > g:
color_desc = "cool tones, bluish hues"
elif g > r and g > b:
color_desc = "greenish hues"
else:
color_desc = "balanced color palette"
else:
color_desc = "grayscale"
desc = f"{label}: {orientation} {width}x{height}, {tone} lighting, {color_desc}"
return desc
except Exception as e:
print(f"Warning: Could not analyze reference image: {e}")
return f"{label}: [Image provided as reference]"
def _process_alternations(self, text: str) -> str:
"""
Process alternation syntax {option1|option2|option3}
Replaces with randomly chosen option
"""
import re
# Pattern to match {option1|option2|option3}
pattern = r'\{([^{}]+)\}'
def replace_alternation(match):
options = match.group(1).split('|')
# Strip whitespace from each option
options = [opt.strip() for opt in options]
return random.choice(options)
# Keep replacing until no more alternations found (handles nested cases)
max_iterations = 10 # Prevent infinite loops
iteration = 0
while '{' in text and '|' in text and iteration < max_iterations:
new_text = re.sub(pattern, replace_alternation, text)
if new_text == text: # No more changes
break
text = new_text
iteration += 1
return text
def _preserve_emphasis_syntax(self, text: str) -> str:
"""
Protect emphasis syntax (keyword:1.5) from being modified
Replaces temporarily with placeholders during LLM processing
"""
import re
# Pattern to match (text:number) emphasis syntax
# This matches things like (dark skin:1.5) or (hair:0.8)
pattern = r'\(([^():]+):(\d+\.?\d*)\)'
# Find all emphasis patterns
emphasis_patterns = re.findall(pattern, text)
# Store original patterns
self._emphasis_store = []
# Replace with placeholders
def replace_emphasis(match):
full_match = match.group(0)
placeholder = f"__EMPHASIS_{len(self._emphasis_store)}__"
self._emphasis_store.append(full_match)
return placeholder
text = re.sub(pattern, replace_emphasis, text)
return text
def _restore_emphasis_syntax(self, text: str) -> str:
"""
Restore emphasis syntax that was protected
"""
if not hasattr(self, '_emphasis_store'):
return text
# Restore placeholders with original emphasis syntax
for i, original in enumerate(self._emphasis_store):
placeholder = f"__EMPHASIS_{i}__"
text = text.replace(placeholder, original)
# Clear the store
self._emphasis_store = []
return text
def _resolve_settings(
self,
camera_angle: str,
composition: str,
lighting_source: str,
lighting_quality: str,
time_of_day: str,
weather: str,
art_style: str,
genre_style: str,
color_mood: str,
detail_level: str,
prompt_length: str,
subject_framing: str,
subject_pose: str,
platform: str
) -> Dict[str, str]:
"""Resolve auto/random options to actual values"""
resolved = {}
# Camera angle
if camera_angle == "random":
resolved["camera_angle"] = random.choice(self.camera_angles)
elif camera_angle not in ["auto", "none"]:
resolved["camera_angle"] = camera_angle
elif camera_angle == "auto":
resolved["camera_angle"] = "auto (LLM decides)"
# Composition
if composition == "random":
resolved["composition"] = random.choice(self.composition_styles)
elif composition not in ["auto", "none"]:
resolved["composition"] = composition
elif composition == "auto":
resolved["composition"] = "auto (LLM decides)"
# Lighting source
if lighting_source == "random":
resolved["lighting_source"] = random.choice(self.lighting_sources)
elif lighting_source not in ["auto", "none"]:
resolved["lighting_source"] = lighting_source
elif lighting_source == "auto":
resolved["lighting_source"] = "auto (LLM decides)"
# Lighting quality
if lighting_quality == "random":
resolved["lighting_quality"] = random.choice(self.lighting_quality)
elif lighting_quality not in ["auto", "none"]:
resolved["lighting_quality"] = lighting_quality
elif lighting_quality == "auto":
resolved["lighting_quality"] = "auto (LLM decides)"
# Time of day
if time_of_day == "random":
resolved["time_of_day"] = random.choice(self.times_of_day)
elif time_of_day not in ["auto", "none"]:
resolved["time_of_day"] = time_of_day
elif time_of_day == "auto":
resolved["time_of_day"] = "auto (LLM decides)"
# Weather
if weather == "random":
resolved["weather"] = random.choice(self.weather_conditions)
elif weather not in ["auto", "none"]:
resolved["weather"] = weather
elif weather == "auto":
resolved["weather"] = "auto (LLM decides)"
# Art style
if art_style not in ["auto", "none"]:
resolved["art_style"] = art_style
elif art_style == "auto":
resolved["art_style"] = "auto (LLM decides)"
# Color mood
if color_mood == "random":
resolved["color_mood"] = random.choice(["vibrant", "muted", "warm tones", "cool tones", "pastel", "high contrast"])
elif color_mood not in ["auto", "none"]:
resolved["color_mood"] = color_mood
elif color_mood == "auto":
resolved["color_mood"] = "auto (LLM decides)"
# Detail level
if detail_level != "auto":
resolved["detail_level"] = detail_level
else:
resolved["detail_level"] = "auto (LLM decides)"
# Genre style
if genre_style == "random":
resolved["genre_style"] = random.choice(self.genre_styles)
elif genre_style not in ["auto", "none"]:
resolved["genre_style"] = genre_style
elif genre_style == "auto":
resolved["genre_style"] = "auto (LLM decides)"
# Prompt length
if prompt_length != "auto":
resolved["prompt_length"] = prompt_length
else:
resolved["prompt_length"] = "auto (based on platform)"
# Subject framing
if subject_framing == "random":
resolved["subject_framing"] = random.choice(self.subject_framings)
elif subject_framing not in ["auto", "none"]:
resolved["subject_framing"] = subject_framing
elif subject_framing == "auto":
resolved["subject_framing"] = "auto (LLM decides)"
# Subject pose
if subject_pose == "random":
resolved["subject_pose"] = random.choice(self.subject_poses)
elif subject_pose not in ["auto", "none"]:
resolved["subject_pose"] = subject_pose
elif subject_pose == "auto":
resolved["subject_pose"] = "auto (LLM decides)"
return resolved
def _build_system_prompt(
self,
platform_config: Dict,
settings: Dict,
quality_emphasis: bool
) -> str:
"""Build LLM system prompt with platform-specific instructions"""
platform_name = platform_config["name"]
platform = platform_config.get("prompt_style", "natural")
prompt = f"""You are an expert prompt engineer for {platform_name} image generation.
CRITICAL OUTPUT RULES:
1. Output ONLY the final prompt text - no labels, explanations, or meta-commentary
2. Do NOT include phrases like "Here is...", "Prompt:", etc.
3. Start directly with the image description
4. Follow the platform-specific format precisely
TARGET PLATFORM: {platform_name}
Description: {platform_config['description']}
Prompting Style: {platform_config['prompt_style']}
Optimal Length: {platform_config['optimal_length']}
"""
# Add platform-specific preferences
if platform_config.get("preferences"):
prompt += "\nPLATFORM REQUIREMENTS:\n"
for pref in platform_config["preferences"]:
prompt += f"- {pref}\n"
# Add quality tokens if enabled
if quality_emphasis and platform_config.get("quality_tokens"):
prompt += f"\nQUALITY TOKENS (use appropriately): {', '.join(platform_config['quality_tokens'][:8])}\n"
# Add required tokens for specific platforms
if platform_config.get("required_positive"):
prompt += f"\nREQUIRED TOKENS (must include): {', '.join(platform_config['required_positive'])}\n"
# Add things to avoid
if platform_config.get("avoid"):
prompt += "\nAVOID:\n"
for avoid in platform_config["avoid"]:
prompt += f"- {avoid}\n"
# Add user-specified settings
prompt += "\n=== USER REQUIREMENTS ===\n"
prompt += "Incorporate these settings into the prompt:\n\n"
# Handle prompt length guidance
prompt_length_guide = settings.get("prompt_length", "auto")
if prompt_length_guide and "auto" not in prompt_length_guide:
length_map = {
"very_short": "20-40 tokens",
"short": "40-80 tokens",
"medium": "80-150 tokens",
"long": "150-250 tokens",
"very_long": "250-400 tokens"
}
target_length = length_map.get(prompt_length_guide, platform_config['optimal_length'])
prompt += f"TARGET LENGTH: {target_length}\n\n"
# Handle genre/style if specified
genre = settings.get("genre_style", "")
if genre and "auto" not in genre.lower() and "none" not in genre.lower():
genre_guidance = {
"surreal": "dreamlike, unexpected juxtapositions, reality-bending elements",
"cinematic": "film-like quality, dramatic lighting, professional composition",
"dramatic": "high contrast, emotional intensity, dynamic tension",
"action": "dynamic motion, energy, movement, intensity",
"humorous": "playful, whimsical, lighthearted, amusing elements",
"indie": "artistic, unconventional, creative freedom, unique perspective",
"horror": "dark atmosphere, ominous mood, eerie elements, tension",
"scifi": "futuristic, technology, otherworldly, advanced elements",
"romantic": "soft, intimate, emotional warmth, tender mood",
"x-rated": "mature themes, adult content, sensual elements",
"pg": "family-friendly, clean, wholesome, appropriate for all ages",
"artistic": "creative interpretation, aesthetic focus, expressive",
"documentary": "realistic, authentic, unposed, natural",
"minimalist": "simple, clean, essential elements only, negative space",
"maximalist": "rich details, complex, layered, ornate",
"vintage": "classic, retro, aged aesthetic, nostalgic feel",
"modern": "contemporary, current, sleek, clean lines",
"fantasy": "magical, mythical, imaginative, enchanted",
"noir": "dark, moody, high contrast shadows, mystery",
"cyberpunk": "neon, futuristic dystopia, tech, gritty urban"
}
style_desc = genre_guidance.get(genre, genre)
prompt += f"STYLE/GENRE: {genre} - Infuse the prompt with {style_desc}\n\n"
for key, value in settings.items():
if key in ["prompt_length", "genre_style"]: # Already handled above
continue
if value and "none" not in value.lower() and "auto" not in value.lower():
label = key.replace("_", " ").title()
prompt += f"- {label}: {value}\n"
# Platform-specific format instructions
if "pony" in platform_name.lower():
prompt += """
PONY-SPECIFIC INSTRUCTIONS:
- START with: score_9, score_8_up, score_7_up
- Use danbooru tag format (underscores, not spaces)
- Tags should be comma-separated
- Quality tags at beginning
- Character/subject descriptions in tag format
- Example: score_9, score_8_up, score_7_up, 1girl, long_hair, blue_eyes, etc.
"""
elif "illustrious" in platform_name.lower():
prompt += """
ILLUSTRIOUS-SPECIFIC INSTRUCTIONS:
- Start with quality tags: masterpiece, best quality
- Use danbooru tag format with underscores
- Detailed character appearance tags
- Clothing and accessory tags
- Background and atmosphere tags
- Example: masterpiece, best quality, 1girl, detailed_face, flowing_dress, etc.
"""
elif "flux" in platform_name.lower():
prompt += """
FLUX-SPECIFIC INSTRUCTIONS:
- Natural language descriptions (75-150 tokens)
- Use artistic terminology
- Can include "in the style of [artist/style]"
- Quality modifiers important
- Photography terms work well
"""
elif "chroma" in platform_name.lower() or "meisson" in platform_name.lower():
prompt += """
CHROMA-SPECIFIC INSTRUCTIONS:
- Natural, detailed language (100-200 tokens)
- Excellent with complex scenes
- Can handle multiple subjects
- Describe spatial relationships clearly
- Compositional details important
"""
elif "wan" in platform_name.lower():
prompt += """
WAN-SPECIFIC INSTRUCTIONS:
- Technical cinematography terms
- Structured format: subject, setting, lighting, composition
- Specific lighting types (soft lighting, edge lighting, etc.)
- Professional photography language
- Medium length (60-120 tokens)
"""
elif "sd_xl" in platform_name.lower() or "sdxl" in platform_name.lower():
prompt += """
SDXL-SPECIFIC INSTRUCTIONS:
- Token limit: 40-75 tokens optimal
- Front-load important concepts
- Quality tokens at start
- Can use natural language or tags
- Keep concise but descriptive
"""
prompt += """
CRITICAL OUTPUT REQUIREMENTS:
- Output ONLY the final image prompt text
- DO NOT include any settings information in your output
- DO NOT append "Settings:" or list the camera/lighting/etc values
- Incorporate settings naturally INTO the description
- Follow platform format exactly
- Use appropriate length for platform
- Start generating NOW (no preamble, no explanations)
Example of CORRECT output:
"masterpiece, best quality, detailed portrait of a woman with flowing hair,
golden hour sunlight, shot from low angle, dramatic composition..."
Example of WRONG output (DO NOT DO THIS):
"...flowing hair in golden hour sunlight | Settings: camera angle: low angle..."
"""
return prompt
def _build_user_prompt(
self,
text_prompt: str,
image_descriptions: List[str],
settings: Dict,
platform: str
) -> str:
"""Build user prompt for LLM"""
parts = [f"Base prompt: {text_prompt}"]
# Emphasize reference images if provided
if image_descriptions:
ref_text = "Reference images provided - Use these characteristics to inform the enhanced prompt:\n"
for desc in image_descriptions:
ref_text += f" - {desc}\n"
ref_text += "Incorporate the visual characteristics (composition, lighting, color tones, mood) from these reference images into your enhancement."
parts.append(ref_text)
# Add explicit settings inline - NOT appended to final output
explicit_settings = []
for key, value in settings.items():
if "auto" not in value.lower() and "none" not in value.lower():
explicit_settings.append(f"{key.replace('_', ' ')}: {value}")
if explicit_settings:
parts.append(f"Required settings to incorporate: {'; '.join(explicit_settings)}")
return "\n\n".join(parts)
def _parse_llm_response(self, response: str, platform: str) -> str:
"""Clean and parse LLM response"""
cleaned = response.strip()
# Remove common artifacts
artifacts = [
"here is the prompt:", "here's the prompt:", "here is:", "here's:",
"prompt:", "final prompt:", "output:", "result:",
"generated prompt:", "enhanced prompt:"
]
cleaned_lower = cleaned.lower()
for artifact in artifacts:
if cleaned_lower.startswith(artifact):
cleaned = cleaned[len(artifact):].strip()
cleaned_lower = cleaned.lower()
# Remove markdown code blocks
if cleaned.startswith("```"):
lines = cleaned.split('\n')
if len(lines) > 2:
cleaned = '\n'.join(lines[1:-1])
cleaned = cleaned.strip()
# Remove quotes if entire prompt is quoted
if cleaned.startswith('"') and cleaned.endswith('"'):
cleaned = cleaned[1:-1]
if cleaned.startswith("'") and cleaned.endswith("'"):
cleaned = cleaned[1:-1]
# Remove any "| Settings:" or similar trailing settings info
import re
# Pattern to match | Settings: ... or | settings: ... at the end
cleaned = re.sub(r'\s*\|\s*[Ss]ettings:.*$', '', cleaned)
# Also catch standalone Settings: at the end
cleaned = re.sub(r'\s*[Ss]ettings:.*$', '', cleaned)
# Remove any trailing pipe symbols
cleaned = cleaned.rstrip('|').strip()
return cleaned.strip()
def _add_keywords(self, prompt: str, keywords: List[str], platform: str) -> str:
"""Add custom keywords in platform-appropriate format"""
if not keywords:
return prompt
# Check which keywords are already present
prompt_lower = prompt.lower()
missing_keywords = [kw for kw in keywords if kw.lower() not in prompt_lower]
if not missing_keywords:
return prompt
# Add missing keywords
platform_config = get_platform_config(platform)
if platform in ["pony", "illustrious"]:
# Tag format with underscores
formatted_kw = [kw.replace(" ", "_") for kw in missing_keywords]
return f"{prompt}, {', '.join(formatted_kw)}"
else:
# Natural language
return f"{prompt}, {', '.join(missing_keywords)}"
def _add_platform_requirements(self, prompt: str, platform: str, quality: bool) -> str:
"""Add platform-specific required tokens if missing"""
platform_config = get_platform_config(platform)
required = platform_config.get("required_positive", [])
if not required:
return prompt
# Check if required tokens are present
prompt_lower = prompt.lower()
missing = [token for token in required if token.lower() not in prompt_lower]
if missing:
# Add at the beginning for these platforms
if platform in ["pony", "illustrious"]:
return f"{', '.join(missing)}, {prompt}"
return prompt
def _format_settings_display(self, settings: Dict, platform_config: Dict) -> str:
"""Format settings for display"""
lines = [
"=" * 60,
"TEXT-TO-IMAGE PROMPT ENHANCEMENT",
"=" * 60,
f"\nTarget Platform: {platform_config['name']}",
f"Prompting Style: {platform_config['prompt_style']}",
f"Optimal Length: {platform_config['optimal_length']}",
"\nSETTINGS APPLIED:"
]
for key, value in settings.items():
label = key.replace("_", " ").title()
lines.append(f" - {label}: {value}")
lines.append("\n" + "=" * 60)
return "\n".join(lines)