Files

3713 lines
160 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 random
import re
from typing import Tuple, Optional, Dict, List, Any, Union
from .llm_backend import LLMBackend
from .qwen3_vl_backend import caption_with_qwen3_vl
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", "steampunk", "dieselpunk",
"mythic", "gothic", "art deco", "retro futurism"
]
self.historical_periods = [
"prehistoric era", "ancient civilizations", "classical antiquity",
"medieval era", "renaissance", "baroque period", "industrial revolution",
"victorian era", "edwardian era", "roaring twenties", "mid-century modern",
"1960s counterculture", "1980s neon wave", "1990s digital dawn",
"modern day", "near future", "far future", "cyberpunk future",
"post-apocalyptic era", "fantasy realm", "science fiction epoch"
]
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", "establishing shot",
"aerial overview", "profile view"
]
self.subject_poses = [
"standing", "sitting", "lying down", "kneeling", "crouching",
"action pose", "portrait pose", "dynamic", "static",
"asymmetric", "contrapposto", "relaxed", "tense",
"walking", "running", "jumping", "dancing", "floating"
]
self.creative_randomness_modes = {
"off": "Stay close to user wording with minimal embellishment.",
"subtle": "Add gentle flourishes that enhance mood without changing subject focus.",
"moderate": "Introduce fresh context, supporting details, or small narrative beats.",
"bold": "Transform the prompt into a vivid scene with new narrative hooks and world-building.",
"storyteller": "Invent an imaginative mini-story or scenario that surprises while honoring the subject.",
"chaotic": "Push boundaries with experimental, dreamlike, or surreal twists."
}
self.random_story_settings = [
"abandoned futuristic metropolis", "misty forest crossroads",
"luminous underwater research lab", "quiet suburban street at dawn",
"ancient floating temple", "deserted lunar colony", "ornate victorian ballroom",
"hidden speakeasy behind a bookstore", "glitching neon arcade",
"forgotten museum of impossible inventions", "bioluminescent cavern",
"storm-lashed airship deck", "sunset-drenched mountain pass",
"labyrinthine library of living books", "gravity-defying market square"
]
self.random_story_companions = [
"a time-traveling archivist", "an eccentric inventor", "a sentient automaton",
"a mysterious stranger in vintage attire", "a playful cosmic entity",
"a band of skyship pirates", "a chorus of bioluminescent sprites",
"a rival artist seeking inspiration", "a loyal cybernetic fox",
"an undercover interstellar diplomat"
]
self.random_story_conflicts = [
"searching for the last fragment of a lost melody",
"racing against an impending temporal storm",
"unlocking a doorway hidden inside a beam of light",
"negotiating peace between rival realities",
"decoding whispers carried by the rain",
"restoring color to a world frozen in monochrome",
"solving a puzzle etched into the constellations",
"protecting a fragile artifact of collective memories"
]
self.reference_usage_labels = {
"caption": "scene overview",
"style": "artistic style and texture",
"lighting": "lighting qualities and direction",
"genre": "genre or mood cues",
"time_period": "time period context",
"subject": "primary subject appearance",
"objects": "notable secondary objects",
"color": "color palette or grading",
"composition": "framing and spatial layout"
}
self.reference_directive_choices = [
"none",
"auto",
"recreate",
"reinterpret",
"subject only",
"style only",
"lighting only",
"composition",
"genre"
]
self._seed_state: Dict[str, Any] = {
"last_seed": None,
"last_mode": None,
"last_input": None
}
self.reference_directive_configs = {
"none": {
"display": "None",
"user_guidance": "Use this reference for broad inspiration using its full caption.",
"user_summary": "Incorporate helpful elements from the reference caption without enforcing a specific focus.",
"include_categories": ["caption"],
"exclude_categories": [],
"llm_instruction": (
"Absorb the entire reference caption as general inspiration. Keep the meaningful traits but avoid over-emphasizing any single element."
),
"analysis_focus": (
"Summarize the overall subject, setting, lighting, mood, and notable items so the prompt can mirror the scene holistically."
)
},
"auto": {
"display": "Auto",
"user_guidance": "Adapt helpful traits from the reference based on context.",
"user_summary": "Auto-balances useful cues from the reference.",
"include_categories": [
"caption",
"style",
"lighting",
"color",
"genre",
"composition",
"subject",
"objects",
"time_period"
],
"exclude_categories": [],
"llm_instruction": (
"Absorb the full caption from this reference to strengthen the user's brief. "
"Borrow cues that align with the goal and adjust anything that conflicts."
),
"analysis_focus": (
"Identify the most influential subject, style, lighting, composition, and supporting details from the"
" complete caption that will benefit the final prompt."
)
},
"recreate": {
"display": "Recreate",
"user_guidance": "Match the reference closely across subject, palette, lighting, and mood.",
"user_summary": "Recreate the reference look as faithfully as possible.",
"include_categories": "all",
"exclude_categories": [],
"llm_instruction": (
"Reproduce this reference almost verbatim. Maintain subject identity, palette, lighting, composition, "
"and supporting props unless the main prompt explicitly overrides them."
),
"analysis_focus": (
"List the critical traits that must be preserved exactly: subject identity, pose, palette, lighting,"
" composition, and key props or environment cues."
)
},
"reinterpret": {
"display": "Reinterpret",
"user_guidance": "Use the reference as inspiration; preserve core subject or mood, but embrace smart variations.",
"user_summary": "Keep the spirit of the reference while allowing tasteful changes.",
"include_categories": [
"caption",
"subject",
"objects",
"genre",
"style",
"lighting",
"color",
"composition",
"time_period"
],
"exclude_categories": [],
"llm_instruction": (
"Preserve the recognizable subject or atmosphere from this reference using the full caption as context, yet "
"refresh style, palette, or composition when it improves alignment with the user's prompt."
),
"analysis_focus": (
"Summarize the anchor traits (subject identity, mood, palette cues) drawn from the entire caption while"
" noting which aspects feel flexible for reinterpretation."
)
},
"subject_only": {
"display": "Subject Only",
"user_guidance": "Preserve the subject’s identity, pose, and defining traits; let other elements follow the prompt.",
"user_summary": "Keep subject fidelity; redesign other aspects.",
"include_categories": ["caption", "subject", "objects", "composition"],
"exclude_categories": ["style", "lighting", "color", "genre"],
"llm_instruction": (
"Carry over the subject's identity, pose, and silhouette from this reference using the full caption as context. "
"Invent fresh style, lighting, and background details to suit the user's prompt."
),
"analysis_focus": (
"Describe the subject's appearance, pose, silhouette, distinguishing features, and supportive forms as captured"
" in the caption so the identity remains unmistakable."
)
},
"style_only": {
"display": "Style Only",
"user_guidance": "Borrow the artistic style, texture, palette, and brushwork; ignore subject and composition cues.",
"user_summary": "Transfer the reference style while changing subject matter.",
"include_categories": ["caption", "style", "color", "genre", "lighting"],
"exclude_categories": ["subject", "objects", "composition"],
"llm_instruction": (
"Adopt the artistic style, texture, and palette suggested by this reference after studying the complete caption. "
"Replace its subject and layout with the user's requested scene."
),
"analysis_focus": (
"Detail the artistic medium, techniques, palette tendencies, brushwork, texture, and stylistic motifs present"
" in the caption so the style can be transferred accurately."
)
},
"lighting_only": {
"display": "Lighting Only",
"user_guidance": "Replicate lighting qualities like direction, intensity, and warmth; disregard subject or style cues.",
"user_summary": "Reuse lighting mood without copying subject matter.",
"include_categories": ["caption", "lighting", "color"],
"exclude_categories": ["subject", "objects", "style", "genre", "composition"],
"llm_instruction": (
"Match the lighting direction, intensity, and warmth suggested by this reference while treating the full caption as context. "
"Do not replicate its subject, style, or composition."
),
"analysis_focus": (
"Explain the lighting direction, intensity, quality, shadow behavior, and color temperature found in the caption"
" so the mood can be recreated without losing context."
)
},
"composition": {
"display": "Composition",
"user_guidance": "Adopt the camera angle, framing, and spatial layout; let subject and style come from the prompt.",
"user_summary": "Mirror the reference framing while refreshing other traits.",
"include_categories": ["composition", "caption", "objects"],
"exclude_categories": ["style", "lighting", "color", "genre"],
"llm_instruction": (
"Borrow the framing, camera angle, and spatial relationships implied by this reference using the caption as your map. "
"Populate that layout with the subjects and styling requested in the prompt."
),
"analysis_focus": (
"Describe the camera angle, focal length impression, framing, depth relationships, and placement of key elements"
" from the caption so the layout can be preserved."
)
},
"genre": {
"display": "Genre",
"user_guidance": "Match the genre or storytelling conventions; let other specifics follow the prompt unless genre demands otherwise.",
"user_summary": "Carry over the reference genre atmosphere.",
"include_categories": ["caption", "genre", "style", "color", "lighting"],
"exclude_categories": ["subject", "objects", "composition"],
"llm_instruction": (
"Preserve the genre tone and storytelling conventions from this reference by internalizing the whole caption. "
"Let the user's prompt determine subject matter and layout unless genre cues require tweaks."
),
"analysis_focus": (
"Summarize the genre-defining mood, atmosphere, storytelling motifs, and stylistic cues from the full caption"
" so the tone carries over cleanly."
)
}
}
self.default_reference_analysis_method = "sequential_refine"
self.reference_guardrail_text = (
"Blend reference guidance without repeating yourself. Do not mention source image dimensions, aspect ratios, "
"or pixel resolutions under any circumstance."
)
@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"
}),
"reference_directive_1": ([
"none",
"auto",
"recreate",
"reinterpret",
"subject only",
"style only",
"lighting only",
"composition",
"genre"
], {
"default": "none"
}),
"reference_directive_2": ([
"none",
"auto",
"recreate",
"reinterpret",
"subject only",
"style only",
"lighting only",
"composition",
"genre"
], {
"default": "none"
}),
"prompt_context": ([
"none",
"auto",
"expand_short_prompt",
"finish_opening_line",
"prompt_from_item_list",
"modify_reference_image",
"enhance_reference_image"
], {
"default": "none",
"tooltip": (
"none/auto: Standard prompt\n"
"expand_short_prompt: Expand shorthand into full description\n"
"finish_opening_line: Continue from your opening line\n"
"prompt_from_item_list: Transform comma-separated elements into scene\n"
"modify_reference_image: Alter specific parts of reference\n"
"enhance_reference_image: Enrich reference with new elements"
)
}),
"creative_randomness": ([
"auto", "none", "off", "subtle", "moderate", "bold", "storyteller", "chaotic"
], {
"default": "none",
"tooltip": (
"off/none: Minimal embellishment\n"
"subtle: Gentle mood enhancement\n"
"moderate: Add supporting details\n"
"bold: Vivid scene with narrative hooks\n"
"storyteller: Imaginative mini-story\n"
"chaotic: Experimental, surreal twists"
)
}),
# Platform selection
"target_platform": ([
"flux",
"flux_kontex",
"sd_xl",
"sd_1_5",
"pony",
"illustrious",
"chroma",
"pixart_sigma",
"aura_flow",
"noobai",
"kolors",
"qwen_image",
"qwen_image_edit",
"wan_image"
], {
"default": "flux"
}),
# LLM settings
"llm_backend": ([
"lm_studio",
"ollama",
"qwen3_vl"
], {
"default": "lm_studio",
"tooltip": (
"lm_studio: Uses currently loaded model in LM Studio\n"
"ollama: Uses currently loaded model in Ollama\n"
"qwen3_vl: Auto-detects local Qwen3-VL model (no API server needed)"
)
}),
"api_endpoint": ("STRING", {
"default": "http://localhost:1234/v1",
"multiline": False,
"tooltip": (
"lm_studio/ollama: API endpoint URL\n"
"qwen3_vl: Leave default, or specify custom model path like 'A:\\path\\to\\model'"
)
}),
"temperature": ("FLOAT", {
"default": 0.7,
"min": 0.1,
"max": 2.0,
"step": 0.1
}),
"vision_backend": ([
"auto",
"lm_studio",
"ollama",
"qwen3_vl",
"disable"
], {
"default": "auto",
"tooltip": "Vision captioning: auto=inherit from main LLM, qwen3_vl=local Qwen3-VL, lm_studio/ollama=separate vision model, disable=heuristics only"
}),
"vision_api_endpoint": ("STRING", {
"default": "http://localhost:1234/v1",
"multiline": False,
"tooltip": (
"Vision backend API endpoint (lm_studio/ollama) or custom model path (qwen3_vl)\n"
"For qwen3_vl: Leave default for auto-detect, or specify 'A:\\path\\to\\model'"
)
}),
# 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": "none"
}),
"composition": ([
"auto", "random", "none",
"rule of thirds", "centered", "symmetrical", "asymmetrical",
"golden ratio", "leading lines", "frame within frame",
"negative space", "balanced", "dynamic diagonal"
], {
"default": "none"
}),
# 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": "none"
}),
"lighting_quality": ([
"auto", "random", "none",
"soft diffused", "hard dramatic", "even balanced",
"high contrast", "low key", "high key", "chiaroscuro",
"volumetric", "atmospheric"
], {
"default": "none"
}),
# Time & Weather
"time_of_day": ([
"auto", "random", "none",
"dawn", "early morning", "mid-morning", "noon", "afternoon",
"golden hour", "dusk", "twilight", "night", "midnight", "blue hour"
], {
"default": "none"
}),
"historical_period": ([
"auto", "random", "none",
"prehistoric era", "ancient civilizations", "classical antiquity",
"medieval era", "renaissance", "baroque period", "industrial revolution",
"victorian era", "edwardian era", "roaring twenties", "mid-century modern",
"1960s counterculture", "1980s neon wave", "1990s digital dawn",
"modern day", "near future", "far future", "cyberpunk future",
"post-apocalyptic era", "fantasy realm", "science fiction epoch"
], {
"default": "none"
}),
"weather": ([
"auto", "random", "none",
"clear sky", "partly cloudy", "overcast", "misty", "foggy",
"rainy", "stormy", "snowy", "sunny", "hazy"
], {
"default": "none"
}),
# 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": "none"
}),
"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",
"steampunk", "dieselpunk", "mythic", "gothic", "art deco", "retro futurism"
], {
"default": "none"
}),
"color_mood": ([
"auto", "random", "none",
"vibrant", "muted", "monochrome", "warm tones", "cool tones",
"pastel", "high contrast", "desaturated", "neon", "earth tones"
], {
"default": "none"
}),
# 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", "establishing shot",
"aerial overview", "profile view"
], {
"default": "none"
}),
"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", "floating"
], {
"default": "none"
}),
# Keywords
"positive_keywords": ("STRING", {
"default": "",
"multiline": True,
"placeholder": "Additional keywords, LoRA triggers, specific details"
}),
"negative_keywords": ("STRING", {
"default": "ugly, blurry, duplicate, deformed, distorted, lowres, bad anatomy, disfigured, poorly drawn, mutation, mutated, extra limbs, cloned face, disfigured, gross proportions, malformed limbs, missing arms, missing legs, fused fingers, too many fingers, long neck",
"multiline": True,
"placeholder": "Things to avoid"
}),
"seed_mode": ([
"random",
"fixed",
"increment",
"decrement"
], {
"default": "random"
}),
"random_seed": ("INT", {
"default": -1,
"min": -1,
"max": 2_147_483_647,
"step": 1
}),
# 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",),
"reference_caption_override_1": ("STRING", {
"default": "",
"multiline": True,
"placeholder": "Optional external caption for Reference 1"
}),
"reference_caption_override_2": ("STRING", {
"default": "",
"multiline": True,
"placeholder": "Optional external caption for Reference 2"
}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "INT")
RETURN_NAMES = ("positive_prompt", "negative_prompt", "settings_used", "status", "vision_caption", "seed_used")
FUNCTION = "enhance_prompt"
CATEGORY = "Eric Prompt Enhancers"
OUTPUT_NODE = True
@classmethod
def IS_CHANGED(cls, random_seed: int, seed_mode: str, **kwargs):
"""Determine if the node should re-execute based on seed mode.
Returns a unique value for modes that need to re-run each time,
or the seed value for fixed mode (to use ComfyUI's caching).
"""
import time
mode = str(seed_mode).strip().lower()
# For random/increment/decrement, return unique value each time to force re-execution
if mode in {"random", "increment", "decrement"}:
# Return timestamp to guarantee uniqueness
return float(time.time())
# For fixed mode, return the seed value so ComfyUI can cache
try:
seed_value = int(random_seed)
if seed_value < 0:
# Negative seed means random even in fixed mode
return float(time.time())
return seed_value
except Exception:
return float(time.time())
def enhance_prompt(
self,
text_prompt: str,
reference_directive_1: str,
reference_directive_2: str,
prompt_context: str,
target_platform: str,
llm_backend: str,
api_endpoint: str,
temperature: float,
vision_backend: str,
vision_api_endpoint: str,
camera_angle: str,
composition: str,
lighting_source: str,
lighting_quality: str,
time_of_day: str,
historical_period: str,
weather: str,
art_style: str,
genre_style: str,
color_mood: str,
creative_randomness: str,
subject_framing: str,
subject_pose: str,
positive_keywords: str,
negative_keywords: str,
seed_mode: str,
random_seed: int,
save_to_file: bool,
filename_base: str,
reference_image_1: Optional[torch.Tensor] = None,
reference_image_2: Optional[torch.Tensor] = None,
reference_caption_override_1: str = "",
reference_caption_override_2: str = ""
) -> Tuple[str, str, str, str]:
"""Main processing function"""
try:
requested_seed = int(random_seed)
except Exception:
requested_seed = -1
seed_mode_normalized = str(seed_mode).strip().lower()
seed_value, resolved_seed_mode = self._resolve_seed_value(seed_mode_normalized, requested_seed)
python_random_state = random.getstate()
numpy_random_state = np.random.get_state()
random.seed(seed_value)
np.random.seed(seed_value % (2**32))
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
reference_images = []
directive_inputs = []
override_1 = (reference_caption_override_1 or "").strip()
override_2 = (reference_caption_override_2 or "").strip()
if reference_image_1 is not None:
reference_images.append({
"label": "Reference 1",
"tensor": reference_image_1,
"caption_override": override_1,
"override_source": "user" if override_1 else None
})
directive_inputs.append(("Reference 1", reference_directive_1))
elif override_1:
# Allow caption-only references when external descriptor provided
reference_images.append({
"label": "Reference 1",
"tensor": None,
"caption_override": override_1,
"override_source": "user"
})
directive_inputs.append(("Reference 1", reference_directive_1))
if reference_image_2 is not None:
reference_images.append({
"label": "Reference 2",
"tensor": reference_image_2,
"caption_override": override_2,
"override_source": "user" if override_2 else None
})
directive_inputs.append(("Reference 2", reference_directive_2))
elif override_2:
reference_images.append({
"label": "Reference 2",
"tensor": None,
"caption_override": override_2,
"override_source": "user"
})
directive_inputs.append(("Reference 2", reference_directive_2))
reference_plan = self._prepare_reference_plan(directive_inputs)
# Initialize LLM backend (model_name auto-detected)
llm = LLMBackend(
backend_type=llm_backend,
endpoint=api_endpoint,
model_name=None, # Auto-detect for all backends
temperature=temperature
)
vision_backend_selection = (vision_backend or "inherit").strip().lower()
if not vision_backend_selection:
vision_backend_selection = "auto"
# Vision backend now supports custom endpoint via vision_api_endpoint
vision_llm: Optional[LLMBackend] = None
vision_qwen_config: Optional[Dict[str, Any]] = None
vision_backend_mode = "disabled"
vision_caption_enabled = False
vision_model_used = ""
vision_init_warning: Optional[str] = None
if vision_backend_selection in {"", "auto", "inherit"}:
if llm.supports_images():
vision_llm = llm
vision_backend_mode = "inherit"
vision_caption_enabled = True
vision_model_used = getattr(llm, "model_name", "auto-detected")
else:
vision_backend_mode = "disabled"
elif vision_backend_selection == "disable":
vision_backend_mode = "disabled"
elif vision_backend_selection in {"lm_studio", "ollama"}:
# Use vision-specific endpoint
try:
vision_llm = LLMBackend(
backend_type=vision_backend_selection,
endpoint=vision_api_endpoint,
model_name=None, # Auto-detect
temperature=temperature
)
if vision_llm.supports_images():
vision_backend_mode = vision_backend_selection
vision_caption_enabled = True
vision_model_used = getattr(vision_llm, "model_name", "auto-detected")
else:
vision_backend_mode = "disabled"
vision_init_warning = (
f"Vision backend '{vision_backend_selection}' does not advertise image support."
)
except Exception as exc:
vision_backend_mode = "disabled"
vision_init_warning = (
f"Vision backend init failed ({vision_backend_selection}): {exc}"
)
print(f"[Text-to-Image] ⚠️ Vision backend '{vision_backend_selection}' initialization failed: {exc}")
print(f"[Text-to-Image] → Continuing with text expansion only (no vision captions)")
vision_llm = None
elif vision_backend_selection == "qwen3_vl":
# Use vision_api_endpoint for custom model path
vision_qwen_config = {
"model": None, # Auto-detect from vision_api_endpoint
"backend_hint": vision_api_endpoint if vision_api_endpoint != "http://localhost:1234/v1" else None,
}
vision_backend_mode = "qwen3_vl"
vision_caption_enabled = True
vision_model_used = "Qwen3-VL (auto-detected)"
else:
vision_backend_mode = "disabled"
if not vision_model_used:
vision_model_used = getattr(llm, "model_name", "auto-detected")
analysis_backend_label = vision_backend_mode
if analysis_backend_label == "inherit":
analysis_backend_label = f"inherit:{llm.backend_type}"
if not vision_caption_enabled:
vision_model_used = ""
reference_warnings: List[str] = []
if vision_init_warning:
reference_warnings.append(vision_init_warning)
if reference_images and not vision_caption_enabled:
reference_warnings.append(
"Vision captioning disabled; reference traits rely on heuristic estimates."
)
# Wrap vision processing in try/except to prevent vision failures from breaking text expansion
image_analyses = []
try:
for entry in reference_images:
label = entry.get("label", "Reference")
tensor = entry.get("tensor")
override_caption = entry.get("caption_override")
analysis = self._analyze_reference_image(
tensor,
label,
vision_llm=vision_llm if vision_caption_enabled and vision_llm is not None else None,
override_caption=override_caption,
qwen_config=vision_qwen_config if vision_backend_mode == "qwen3_vl" else None,
vision_temperature=temperature,
vision_backend=analysis_backend_label,
vision_model=vision_model_used
)
if override_caption:
existing_warnings = analysis.setdefault("warnings", [])
override_note = f"{label}: caption supplied by override."
if override_note not in existing_warnings:
existing_warnings.append(override_note)
warnings_from_analysis = analysis.get("warnings") or []
for warning in warnings_from_analysis:
if warning not in reference_warnings:
reference_warnings.append(warning)
image_analyses.append(analysis)
except Exception as vision_exc:
# Vision processing failed - log error but continue with text expansion
error_msg = f"Vision processing failed: {str(vision_exc)}"
print(f"[Text-to-Image] ⚠️ {error_msg}")
reference_warnings.append(error_msg)
# Create fallback analyses if we got partial results
if not image_analyses and reference_images:
for entry in reference_images:
label = entry.get("label", "Reference")
fallback = {
"label": label,
"summary": f"{label}: vision analysis failed, continuing with text expansion",
"details": ["Vision model unavailable"],
"warnings": [error_msg]
}
image_analyses.append(fallback)
# STEP 2: Resolve random/auto options
resolved_settings, setting_sources = self._resolve_settings(
camera_angle, composition, lighting_source, lighting_quality,
time_of_day, historical_period, weather, art_style, genre_style, color_mood,
creative_randomness,
subject_framing, subject_pose, prompt_context,
target_platform
)
# STEP 3: Build enhancement prompt
platform_config = get_platform_config(target_platform)
quality_emphasis = bool(platform_config.get("quality_emphasis", True))
resolved_settings["quality_emphasis"] = "enabled" if quality_emphasis else "disabled"
setting_sources["quality_emphasis"] = {"mode": "platform", "value": resolved_settings["quality_emphasis"]}
# Wrap reference guidance processing to prevent failures from blocking text expansion
reference_guidance = ""
reference_notes = []
reference_meta = {}
try:
directive_analyses, directive_meta = self._run_reference_directive_analysis(
image_analyses,
reference_plan,
llm
)
reference_guidance, reference_notes, guidance_meta = self._build_reference_guidance(
directive_analyses,
reference_plan,
llm,
prompt_context
)
reference_meta = self._merge_reference_metadata(reference_plan, directive_meta, guidance_meta)
except Exception as ref_exc:
# Reference guidance processing failed - log but continue
error_msg = f"Reference guidance generation failed: {str(ref_exc)}"
print(f"[Text-to-Image] ⚠️ {error_msg}")
reference_warnings.append(error_msg)
# Initialize empty reference data
reference_meta = {
"analysis_method": "failed",
"reference_count": len(image_analyses),
"llm_queries": 0,
"llm_successes": 0,
"warnings": [error_msg]
}
# Always update reference_meta with vision info (even if guidance failed)
reference_meta.update(
{
"vision_backend_requested": vision_backend_selection,
"vision_backend_resolved": vision_backend_mode,
"vision_caption_enabled": vision_caption_enabled,
"vision_model_used": vision_model_used,
}
)
if vision_qwen_config:
reference_meta["vision_backend_config"] = vision_qwen_config
if reference_warnings:
reference_meta["warnings"] = reference_warnings
backend_set = sorted(
{
str(analysis.get("vision_backend"))
for analysis in image_analyses
if analysis.get("vision_backend")
}
)
model_set = sorted(
{
str(analysis.get("vision_model"))
for analysis in image_analyses
if analysis.get("vision_model")
}
)
source_set = sorted(
{
str(analysis.get("vision_caption_source"))
for analysis in image_analyses
if analysis.get("vision_caption_source")
}
)
if backend_set:
reference_meta["vision_backends_used"] = backend_set
if model_set:
reference_meta["vision_models_used"] = model_set
if source_set:
reference_meta["vision_caption_sources"] = source_set
reference_caption_payload: List[Tuple[str, str]] = []
for analysis, entry in zip(image_analyses, reference_plan):
label = entry.get("label", analysis.get("label", "Reference"))
caption_text = analysis.get("vision_caption") or analysis.get("summary")
if caption_text:
prefix = "(override) " if analysis.get("caption_override") else ""
reference_caption_payload.append((label, f"{prefix}{caption_text}"))
creative_brainstorm = self._generate_creative_brainstorm(
text_prompt,
resolved_settings.get("creative_randomness", "off")
)
system_prompt = self._build_system_prompt(
target_platform,
platform_config,
resolved_settings,
reference_plan,
prompt_context
)
user_prompt = self._build_user_prompt(
text_prompt,
resolved_settings,
target_platform,
prompt_context,
reference_guidance,
creative_brainstorm,
reference_caption_payload
)
# STEP 4: Prepare keyword overrides and call LLM
pos_kw_list = parse_keywords(positive_keywords)
neg_kw_list = parse_keywords(negative_keywords)
max_tokens_requested = self._determine_max_tokens(
platform_config,
resolved_settings.get("creative_randomness")
)
capped_tokens, token_cap_note = self._cap_tokens_for_backend(
llm.backend_type,
getattr(llm, "model_name", None),
max_tokens_requested
)
backend_params = {
"backend_type": llm_backend,
"endpoint": api_endpoint,
"temperature": temperature
}
response, llm_used, llm_attempts = self._call_main_llm_with_retries(
llm,
system_prompt,
user_prompt,
capped_tokens,
backend_params
)
llm = llm_used
raw_llm_output = response.get("response", "")
llm_error_message = response.get("error", "")
main_llm_success = bool(response.get("success")) and bool((raw_llm_output or "").strip())
fallback_meta: Optional[Dict[str, Any]] = None
fallback_used = False
# STEP 5: Parse and format response (fallback on failure)
if main_llm_success:
enhanced_prompt = self._parse_llm_response(raw_llm_output, target_platform)
print(f"[Text-to-Image] LLM successfully enhanced prompt (length: {len(enhanced_prompt)} chars)")
else:
enhanced_prompt, fallback_meta = self._build_deterministic_fallback_prompt(
text_prompt,
target_platform,
platform_config,
resolved_settings,
reference_notes
)
fallback_used = True
print(f"[Text-to-Image] ⚠️ LLM failed or returned empty content - using deterministic fallback")
if llm_error_message:
print(f"[Text-to-Image] Error details: {llm_error_message}")
# STEP 5.5: Restore emphasis syntax that was protected
enhanced_prompt = self._restore_emphasis_syntax(enhanced_prompt)
# STEP 6: Add custom 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)
# STEP 7.5: Boost density if the output is too terse
enhanced_prompt, density_meta = self._ensure_prompt_density(
enhanced_prompt,
target_platform,
platform_config,
reference_notes,
resolved_settings
)
if density_meta and density_meta.get("added_phrases"):
print(
f"[Text-to-Image] ℹ️ Density boost added {density_meta['added_phrases']} phrase(s) "
f"(words {density_meta['before_words']}→{density_meta['after_words']})."
)
else:
density_meta = None
# STEP 8: Generate negative prompt
negative_prompt = get_negative_prompt_for_platform(target_platform, neg_kw_list)
# STEP 8.5: Determine actual selections mentioned by the LLM output
actual_selections = self._infer_setting_mentions(enhanced_prompt)
display_context = {
"setting_sources": setting_sources,
"actual_selections": actual_selections,
"reference_meta": reference_meta,
"reference_image_count": len(reference_images),
"main_llm_success": main_llm_success,
"main_llm_error": llm_error_message,
"llm_attempts": llm_attempts,
"llm_token_cap_note": token_cap_note,
"max_tokens_requested": max_tokens_requested,
"max_tokens_used": capped_tokens,
"fallback_used": fallback_used,
"fallback_meta": fallback_meta,
"density_meta": density_meta,
"quality_emphasis": quality_emphasis,
"reference_guidance_used": bool(reference_guidance.strip()) if reference_guidance else False,
"reference_directives": reference_plan,
"reference_warnings": reference_warnings,
"random_seed": {
"value": seed_value,
"mode": resolved_seed_mode,
"requested": requested_seed,
"requested_mode": seed_mode_normalized
},
"vision": {
"requested_backend": vision_backend_selection,
"resolved_backend": vision_backend_mode,
"resolved_model": vision_model_used,
"caption_enabled": vision_caption_enabled,
}
}
# STEP 9: Format settings display
settings_display = self._format_settings_display(
resolved_settings,
platform_config,
reference_notes,
display_context
)
# STEP 10: Save if requested
if save_to_file:
metadata = {
"type": "text-to-image",
"platform": target_platform,
"platform_name": platform_config["name"],
"model": llm.model_name or "auto-detected",
"backend": llm_backend,
"temperature": temperature,
"original_prompt": text_prompt,
"settings": resolved_settings,
"setting_sources": setting_sources,
"reference_meta": reference_meta,
"vision_captions": reference_meta.get("vision_captions", []),
"vision_caption_errors": reference_meta.get("vision_caption_errors", []),
"vision_backend_requested": vision_backend_selection,
"vision_backend_resolved": vision_backend_mode,
"vision_model_used": vision_model_used,
"vision_caption_enabled": vision_caption_enabled,
"system_prompt": system_prompt,
"user_prompt": user_prompt,
"raw_llm_output": raw_llm_output,
"creative_brainstorm": creative_brainstorm,
"reference_guidance": reference_guidance,
"reference_notes_log": reference_notes,
"reference_llm_logs": reference_meta.get("llm_logs", []),
"main_llm_success": main_llm_success,
"main_llm_error": llm_error_message,
"reference_warnings": reference_warnings,
"max_tokens_requested": max_tokens_requested,
"max_tokens_used": capped_tokens,
"token_cap_note": token_cap_note,
"llm_attempts": llm_attempts,
"fallback_used": fallback_used,
"fallback_meta": fallback_meta,
"density_meta": density_meta,
"random_seed_requested": requested_seed,
"random_seed_used": seed_value,
"random_seed_mode_requested": seed_mode_normalized,
"random_seed_mode_resolved": resolved_seed_mode
}
if vision_qwen_config:
metadata["vision_backend_config"] = vision_qwen_config
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"
llm_status_parts = []
if main_llm_success:
llm_status_parts.append("Main LLM: responded")
else:
error_snippet = (llm_error_message or "unknown error").splitlines()[0][:120]
error_snippet = error_snippet.replace('|', '/')
llm_status_parts.append(f"Main LLM: failed ({error_snippet})")
if token_cap_note:
llm_status_parts.append(
f"Token cap {capped_tokens}/{max_tokens_requested}"
)
if llm_attempts and len(llm_attempts) > 1:
llm_status_parts.append(f"LLM attempts: {len(llm_attempts)}")
if fallback_used:
fallback_phrase_count = fallback_meta.get("combined_total") if fallback_meta else None
if fallback_phrase_count:
llm_status_parts.append(f"Fallback detail +{fallback_phrase_count}")
else:
llm_status_parts.append("Fallback detail applied")
if density_meta:
llm_status_parts.append(
f"Density boost +{density_meta.get('added_phrases', 0)}"
)
ref_count = reference_meta.get("reference_count", 0)
ref_method = reference_meta.get("analysis_method", "none")
if ref_count:
queries = reference_meta.get("llm_queries", 0)
successes = reference_meta.get("llm_successes", 0)
if queries:
llm_status_parts.append(
f"Reference LLM: {successes}/{queries} responses ({ref_method})"
)
else:
llm_status_parts.append(f"Reference LLM: not invoked ({ref_method})")
else:
llm_status_parts.append("Reference LLM: no references")
if ref_count:
llm_status_parts.append(f"References sent: {ref_count}")
directive_labels = reference_meta.get("directive_labels") or []
if directive_labels:
llm_status_parts.append("Directives: " + ", ".join(directive_labels))
else:
llm_status_parts.append("References sent: none")
if reference_warnings:
warning_preview = reference_warnings[0]
if len(reference_warnings) > 1:
warning_preview += f" (+{len(reference_warnings) - 1} more)"
if len(warning_preview) > 160:
warning_preview = warning_preview[:157] + "…"
llm_status_parts.append(f"Reference warnings: {warning_preview}")
llm_status_parts.append(f"Seed: {seed_value} ({resolved_seed_mode})")
status_prefix = "✅" if main_llm_success else "⚠️"
status = (
f"{status_prefix} Enhanced for {platform_config['name']} | "
+ " | ".join(llm_status_parts)
+ f" | {file_status}"
)
# Format vision captions for output
vision_captions_list = reference_meta.get("vision_captions", [])
if vision_captions_list:
vision_caption_output = "\n\n".join(
f"Reference {i+1}: {caption}"
for i, caption in enumerate(vision_captions_list)
)
else:
vision_caption_output = ""
return (
enhanced_prompt,
negative_prompt,
settings_display,
status,
vision_caption_output,
seed_value,
{
"ui": {
"random_seed": int(seed_value),
"seed_mode": resolved_seed_mode
}
}
)
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)}",
"",
-1
)
finally:
random.setstate(python_random_state)
np.random.set_state(numpy_random_state)
def _get_simple_image_description(self, image: torch.Tensor, label: str) -> str:
"""Backward-compatible helper returning the concise summary of an image analysis."""
analysis = self._analyze_reference_image(image, label)
return str(analysis.get("summary", 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,
historical_period: str,
weather: str,
art_style: str,
genre_style: str,
color_mood: str,
creative_randomness: str,
subject_framing: str,
subject_pose: str,
prompt_context: str,
platform: str
) -> Tuple[Dict[str, str], Dict[str, Dict[str, str]]]:
"""Resolve auto/random options to actual values and record their origins."""
resolved: Dict[str, str] = {}
sources: Dict[str, Dict[str, str]] = {}
# Camera angle
if camera_angle == "random":
choice = random.choice(self.camera_angles)
resolved["camera_angle"] = choice
sources["camera_angle"] = {"mode": "random", "value": choice}
elif camera_angle not in ["auto", "none"]:
resolved["camera_angle"] = camera_angle
sources["camera_angle"] = {"mode": "user", "value": camera_angle}
elif camera_angle == "auto":
resolved["camera_angle"] = "auto (LLM chooses camera angle)"
sources["camera_angle"] = {"mode": "llm"}
else:
sources["camera_angle"] = {"mode": "none"}
# Composition
if composition == "random":
choice = random.choice(self.composition_styles)
resolved["composition"] = choice
sources["composition"] = {"mode": "random", "value": choice}
elif composition not in ["auto", "none"]:
resolved["composition"] = composition
sources["composition"] = {"mode": "user", "value": composition}
elif composition == "auto":
resolved["composition"] = "auto (LLM arranges composition)"
sources["composition"] = {"mode": "llm"}
else:
sources["composition"] = {"mode": "none"}
# Lighting source
if lighting_source == "random":
choice = random.choice(self.lighting_sources)
resolved["lighting_source"] = choice
sources["lighting_source"] = {"mode": "random", "value": choice}
elif lighting_source not in ["auto", "none"]:
resolved["lighting_source"] = lighting_source
sources["lighting_source"] = {"mode": "user", "value": lighting_source}
elif lighting_source == "auto":
resolved["lighting_source"] = "auto (LLM selects lighting source)"
sources["lighting_source"] = {"mode": "llm"}
else:
sources["lighting_source"] = {"mode": "none"}
# Lighting quality
if lighting_quality == "random":
choice = random.choice(self.lighting_quality)
resolved["lighting_quality"] = choice
sources["lighting_quality"] = {"mode": "random", "value": choice}
elif lighting_quality not in ["auto", "none"]:
resolved["lighting_quality"] = lighting_quality
sources["lighting_quality"] = {"mode": "user", "value": lighting_quality}
elif lighting_quality == "auto":
resolved["lighting_quality"] = "auto (LLM refines lighting quality)"
sources["lighting_quality"] = {"mode": "llm"}
else:
sources["lighting_quality"] = {"mode": "none"}
# Time of day
if time_of_day == "random":
choice = random.choice(self.times_of_day)
resolved["time_of_day"] = choice
sources["time_of_day"] = {"mode": "random", "value": choice}
elif time_of_day not in ["auto", "none"]:
resolved["time_of_day"] = time_of_day
sources["time_of_day"] = {"mode": "user", "value": time_of_day}
elif time_of_day == "auto":
resolved["time_of_day"] = "auto (LLM selects time of day)"
sources["time_of_day"] = {"mode": "llm"}
else:
sources["time_of_day"] = {"mode": "none"}
# Historical period
if historical_period == "random":
choice = random.choice(self.historical_periods)
resolved["historical_period"] = choice
sources["historical_period"] = {"mode": "random", "value": choice}
elif historical_period not in ["auto", "none"]:
resolved["historical_period"] = historical_period
sources["historical_period"] = {"mode": "user", "value": historical_period}
elif historical_period == "auto":
resolved["historical_period"] = "auto (LLM infers historical context)"
sources["historical_period"] = {"mode": "llm"}
else:
sources["historical_period"] = {"mode": "none"}
# Weather
if weather == "random":
choice = random.choice(self.weather_conditions)
resolved["weather"] = choice
sources["weather"] = {"mode": "random", "value": choice}
elif weather not in ["auto", "none"]:
resolved["weather"] = weather
sources["weather"] = {"mode": "user", "value": weather}
elif weather == "auto":
resolved["weather"] = "auto (LLM selects weather)"
sources["weather"] = {"mode": "llm"}
else:
sources["weather"] = {"mode": "none"}
# Art style
if art_style not in ["auto", "none"]:
resolved["art_style"] = art_style
sources["art_style"] = {"mode": "user", "value": art_style}
elif art_style == "auto":
resolved["art_style"] = "auto (LLM selects art style)"
sources["art_style"] = {"mode": "llm"}
else:
sources["art_style"] = {"mode": "none"}
# Color mood
if color_mood == "random":
choice = random.choice([
"vibrant", "muted", "warm tones", "cool tones", "pastel", "high contrast"
])
resolved["color_mood"] = choice
sources["color_mood"] = {"mode": "random", "value": choice}
elif color_mood not in ["auto", "none"]:
resolved["color_mood"] = color_mood
sources["color_mood"] = {"mode": "user", "value": color_mood}
elif color_mood == "auto":
resolved["color_mood"] = "auto (LLM selects color mood)"
sources["color_mood"] = {"mode": "llm"}
else:
sources["color_mood"] = {"mode": "none"}
# Genre style
if genre_style == "random":
choice = random.choice(self.genre_styles)
resolved["genre_style"] = choice
sources["genre_style"] = {"mode": "random", "value": choice}
elif genre_style not in ["auto", "none"]:
resolved["genre_style"] = genre_style
sources["genre_style"] = {"mode": "user", "value": genre_style}
elif genre_style == "auto":
resolved["genre_style"] = "auto (LLM selects genre/mood)"
sources["genre_style"] = {"mode": "llm"}
else:
sources["genre_style"] = {"mode": "none"}
# Creative randomness level
if creative_randomness == "auto":
resolved_randomness = self._auto_randomness_choice(platform)
sources["creative_randomness"] = {"mode": "auto", "value": resolved_randomness}
elif creative_randomness == "none":
resolved_randomness = "none"
sources["creative_randomness"] = {"mode": "none", "value": "none"}
else:
resolved_randomness = creative_randomness
sources["creative_randomness"] = {"mode": "user", "value": resolved_randomness}
resolved["creative_randomness"] = resolved_randomness
# Subject framing
if subject_framing == "random":
choice = random.choice(self.subject_framings)
resolved["subject_framing"] = choice
sources["subject_framing"] = {"mode": "random", "value": choice}
elif subject_framing not in ["auto", "none"]:
resolved["subject_framing"] = subject_framing
sources["subject_framing"] = {"mode": "user", "value": subject_framing}
elif subject_framing == "auto":
resolved["subject_framing"] = "auto (LLM frames subject)"
sources["subject_framing"] = {"mode": "llm"}
else:
sources["subject_framing"] = {"mode": "none"}
# Subject pose
if subject_pose == "random":
choice = random.choice(self.subject_poses)
resolved["subject_pose"] = choice
sources["subject_pose"] = {"mode": "random", "value": choice}
elif subject_pose not in ["auto", "none"]:
resolved["subject_pose"] = subject_pose
sources["subject_pose"] = {"mode": "user", "value": subject_pose}
elif subject_pose == "auto":
resolved["subject_pose"] = "auto (LLM chooses pose)"
sources["subject_pose"] = {"mode": "llm"}
else:
sources["subject_pose"] = {"mode": "none"}
# Prompt context
if prompt_context == "auto":
resolved["prompt_context"] = "auto (LLM interprets input role)"
sources["prompt_context"] = {"mode": "llm"}
elif prompt_context == "none":
resolved["prompt_context"] = "none"
sources["prompt_context"] = {"mode": "none"}
else:
resolved["prompt_context"] = prompt_context
sources["prompt_context"] = {"mode": "user", "value": prompt_context}
# Attach platform-specific defaults for length/detail expectations
platform_config = get_platform_config(platform)
resolved_length = platform_config.get(
"length_guidance",
"Generate a full-length, highly detailed prompt"
)
resolved_detail = platform_config.get(
"detail_expectation",
"Ultra detailed description"
)
resolved["length_mode"] = resolved_length
resolved["detail_mode"] = resolved_detail
sources["length_mode"] = {"mode": "platform", "value": resolved_length}
sources["detail_mode"] = {"mode": "platform", "value": resolved_detail}
return resolved, sources
def _auto_randomness_choice(self, platform: str) -> str:
"""Choose a creative randomness default based on platform capabilities."""
platform = platform.lower()
if platform in {"flux", "chroma"}:
return "bold"
if platform in {"wan_image", "wan22", "wan"}:
return "moderate"
if platform in {"qwen_image", "qwen_image_edit"}:
return "storyteller"
if platform in {"hunyuan_image"}:
return "subtle"
if platform in {"sd_xl", "pony", "illustrious"}:
return "moderate"
return "subtle"
def _analyze_reference_image(
self,
image: Optional[torch.Tensor],
label: str,
vision_llm: Optional[LLMBackend] = None,
override_caption: Optional[str] = None,
qwen_config: Optional[Dict[str, Any]] = None,
vision_temperature: float = 0.7,
vision_backend: str = "",
vision_model: str = ""
) -> Dict[str, Any]:
"""Generate descriptive statistics and optional vision captions for a reference image."""
llm_logs: List[Dict[str, Any]] = []
warnings: List[str] = []
vision_caption: Optional[str] = None
vision_error: Optional[str] = None
vision_caption_source: Optional[str] = None
try:
if isinstance(image, torch.Tensor):
img_np = image.detach().cpu().numpy()
if img_np.ndim == 4:
img_np = img_np[0]
img_np = np.clip(img_np, 0.0, 1.0)
if img_np.shape[-1] < 3:
rgb = np.repeat(img_np[..., :1], 3, axis=-1)
else:
rgb = img_np[..., :3]
height, width = rgb.shape[:2]
aspect_ratio = width / height if height else 1.0
orientation = "landscape" if aspect_ratio > 1.25 else "portrait" if aspect_ratio < 0.8 else "square"
avg_rgb = rgb.mean(axis=(0, 1))
brightness = float(avg_rgb.mean())
if brightness > 0.7:
tone = "bright"
elif brightness > 0.45:
tone = "balanced"
else:
tone = "dim"
grayscale = rgb.mean(axis=2)
contrast_value = float(grayscale.std())
if contrast_value < 0.08:
contrast_desc = "low contrast"
elif contrast_value < 0.16:
contrast_desc = "moderate contrast"
else:
contrast_desc = "high contrast"
edge_y = np.abs(np.diff(grayscale, axis=0))
edge_x = np.abs(np.diff(grayscale, axis=1))
edge_density = float((edge_x.mean() + edge_y.mean()) / 2.0)
if edge_density < 0.02:
texture_desc = "soft textures"
elif edge_density < 0.05:
texture_desc = "mixed textures"
else:
texture_desc = "detailed textures"
palette_names, palette_hex = self._extract_palette(rgb)
warmth_score = float(avg_rgb[0] - avg_rgb[2])
if warmth_score > 0.05:
temperature_desc = "warm color temperature"
elif warmth_score < -0.05:
temperature_desc = "cool color temperature"
else:
temperature_desc = "neutral color temperature"
genre_hint = self._infer_genre_suggestion(tone, contrast_desc, palette_names)
palette_description = ', '.join(palette_names) if palette_names else "mixed tones"
base_summary = (
f"{label}: {orientation} framing with {tone} exposure, {contrast_desc}, {temperature_desc}, "
f"palette leans {palette_description}"
)
detail_lines = [
f"Orientation: {orientation} framing that feels {texture_desc}.",
f"Lighting: {tone} exposure with {temperature_desc}.",
f"Contrast impression: {contrast_desc} supporting {texture_desc} detail.",
f"Palette impression: {palette_description} hues dominate.",
f"Suggested genre alignment: {genre_hint}"
]
category_notes = {
"caption": base_summary,
"style": f"{contrast_desc} with {texture_desc}; keep the {temperature_desc} mood.",
"lighting": f"{tone} lighting with {temperature_desc}; preserve the same luminance mood.",
"genre": genre_hint,
"time_period": "No explicit era cues detected; align with selected time period.",
"subject": f"Maintain the subject presence implied by the {orientation} framing.",
"objects": "Refer to this image for relative placement of supporting forms.",
"color": f"Palette guidance: {palette_description} tones.",
"composition": f"Keep a {orientation} layout and similar balance between subject and environment."
}
caption_prompt = (
"Describe this reference image in exhaustive detail. Cover subjects, setting, focal points, "
"lighting, mood, palette, and any noteworthy props or actions. Write at least three complete sentences "
"that mention every important element you observe."
)
caption_system_prompt = (
"You are an expert visual analyst. Describe every element in the image clearly and precisely."
)
pil_image: Optional[Image.Image] = None
if override_caption and override_caption.strip():
vision_caption = override_caption.strip()
vision_caption_source = "override"
elif qwen_config:
try:
pil_image = Image.fromarray((rgb * 255).astype(np.uint8))
qwen_result = caption_with_qwen3_vl(
image=pil_image,
prompt=caption_prompt,
system_prompt=caption_system_prompt,
model_spec=qwen_config.get("model"),
backend_hint=qwen_config.get("backend_hint"),
max_new_tokens=768,
temperature=max(0.0, float(vision_temperature)),
)
if qwen_result.get("success") and qwen_result.get("caption"):
vision_caption = qwen_result["caption"].strip()
vision_caption_source = "qwen3_vl"
else:
error_message = qwen_result.get("error") or "Qwen3-VL caption failed"
snippet = str(error_message).splitlines()[0][:200]
warnings.append(f"{label}: Qwen3-VL caption failed ({snippet})")
vision_error = snippet
except Exception as exc:
snippet = str(exc).splitlines()[0][:200]
warnings.append(f"{label}: Qwen3-VL caption failed ({snippet})")
vision_error = snippet
elif vision_llm:
if pil_image is None:
pil_image = Image.fromarray((rgb * 255).astype(np.uint8))
image_buffer = io.BytesIO()
pil_image.save(image_buffer, format="PNG")
image_bytes = image_buffer.getvalue()
image_buffer.close()
caption_result = vision_llm.caption_image(
image_bytes=image_bytes,
label=label,
prompt=caption_prompt,
max_tokens=480
)
log_entry = caption_result.get("log_entry") if isinstance(caption_result, dict) else None
if log_entry:
llm_logs.append(log_entry)
if caption_result.get("success") and caption_result.get("caption"):
vision_caption = caption_result["caption"].strip()
vision_caption_source = vision_llm.backend_type
else:
error_message = caption_result.get("error") if isinstance(caption_result, dict) else None
if error_message:
snippet = str(error_message).splitlines()[0][:160]
warnings.append(f"{label}: vision caption failed ({snippet})")
vision_error = snippet
else:
if not (override_caption and override_caption.strip()):
raise ValueError("Reference image must be provided unless a caption override is supplied")
vision_caption = override_caption.strip()
vision_caption_source = "override"
warnings.append(f"{label}: analysis based solely on supplied caption override.")
height = width = 0
aspect_ratio = 1.0
orientation = "unspecified"
tone = "balanced"
contrast_desc = "moderate contrast"
texture_desc = "mixed textures"
temperature_desc = "neutral color temperature"
palette_names = []
palette_hex = []
palette_description = "mixed tones"
genre_hint = "Flexible genre potential; adapt to project goals."
base_summary = f"{label}: reference caption provided without image analysis."
detail_lines = [vision_caption]
category_notes = {
key: vision_caption for key in self.reference_usage_labels.keys()
}
summary = vision_caption and f"{label}: {vision_caption}" or base_summary
if vision_caption:
detail_lines = [vision_caption] + detail_lines
caption_for_notes = vision_caption
category_notes = {
key: caption_for_notes for key in self.reference_usage_labels.keys()
}
category_notes["lighting"] = (
f"{caption_for_notes} | Lighting heuristics: {tone} with {temperature_desc}."
)
category_notes["composition"] = (
f"{caption_for_notes} | Maintain the {orientation} framing relationships."
)
category_notes["color"] = (
f"{caption_for_notes} | Palette impression: {palette_description} hues."
)
analysis_result: Dict[str, Any] = {
"label": label,
"width": width,
"height": height,
"orientation": orientation,
"aspect_ratio": aspect_ratio,
"tone": tone,
"contrast_description": contrast_desc,
"texture_description": texture_desc,
"temperature_description": temperature_desc,
"palette_names": palette_names,
"palette_hex": palette_hex,
"summary": summary,
"details": detail_lines,
"category_notes": category_notes
}
if llm_logs:
analysis_result["llm_logs"] = llm_logs
if warnings:
analysis_result["warnings"] = warnings
if vision_caption:
analysis_result["vision_caption"] = vision_caption
if override_caption and override_caption.strip():
analysis_result["caption_override"] = True
if vision_caption_source:
analysis_result["vision_caption_source"] = vision_caption_source
if vision_error:
analysis_result["vision_caption_error"] = vision_error
analysis_result["vision_backend"] = vision_backend or "disabled"
analysis_result["vision_model"] = vision_model or ""
return analysis_result
except Exception as exc:
print(f"Warning: Could not analyze reference image ({label}): {exc}")
fallback_summary = f"{label}: reference image provided (analysis unavailable)"
result = {
"label": label,
"summary": fallback_summary,
"details": [fallback_summary],
"category_notes": {
key: fallback_summary for key in self.reference_usage_labels.keys()
},
"palette_names": [],
"palette_hex": []
}
if warnings:
result["warnings"] = warnings
if llm_logs:
result["llm_logs"] = llm_logs
return result
def _extract_palette(self, rgb: np.ndarray, max_colors: int = 3) -> Tuple[List[str], List[str]]:
"""Approximate dominant palette using simple quantization."""
flat = rgb.reshape(-1, 3)
if flat.shape[0] > 5000:
indices = np.random.choice(flat.shape[0], 5000, replace=False)
flat = flat[indices]
quantized = (flat * 255).astype(int)
quantized = (quantized // 32) * 32 + 16
color_counts: Dict[Tuple[int, int, int], int] = {}
for r, g, b in quantized:
key = (int(r), int(g), int(b))
color_counts[key] = color_counts.get(key, 0) + 1
if not color_counts:
color_counts = {(128, 128, 128): 1}
top_colors = sorted(color_counts.items(), key=lambda item: item[1], reverse=True)[:max_colors]
palette_rgb = [np.array(color) for color, _ in top_colors]
palette_names = [self._nearest_color_name(tuple(color)) for color in palette_rgb]
palette_hex = [self._rgb_to_hex(tuple(color)) for color in palette_rgb]
return palette_names, palette_hex
def _nearest_color_name(self, rgb: Tuple[int, int, int]) -> str:
"""Map RGB values to a basic color name."""
reference_colors = {
"black": (0, 0, 0),
"white": (255, 255, 255),
"gray": (128, 128, 128),
"red": (210, 50, 40),
"orange": (230, 140, 45),
"yellow": (235, 220, 70),
"green": (70, 150, 70),
"teal": (50, 150, 150),
"blue": (60, 100, 220),
"purple": (130, 80, 200),
"magenta": (200, 70, 180),
"brown": (120, 80, 50),
"pink": (240, 170, 200)
}
r, g, b = rgb
best_name = "color"
best_distance = float("inf")
for name, ref_rgb in reference_colors.items():
distance = ((r - ref_rgb[0]) ** 2 + (g - ref_rgb[1]) ** 2 + (b - ref_rgb[2]) ** 2) ** 0.5
if distance < best_distance:
best_distance = distance
best_name = name
return best_name
def _rgb_to_hex(self, rgb: Tuple[int, int, int]) -> str:
return "#" + "".join(f"{max(0, min(255, channel)):02x}" for channel in rgb)
def _infer_genre_suggestion(self, tone: str, contrast_desc: str, palette_names: List[str]) -> str:
"""Generate a lightweight genre recommendation from tonal clues."""
if tone == "dim" and "high" in contrast_desc:
return "Could lean into noir, horror, or dramatic lighting."
if tone == "bright" and "high" in contrast_desc:
return "Supports energetic action or bold cinematic styles."
if tone == "bright" and "low" in contrast_desc:
return "Suited for airy documentary, minimalist, or romantic moods."
if "blue" in palette_names or "teal" in palette_names:
return "Great candidate for sci-fi or cyberpunk palettes."
if "brown" in palette_names or "orange" in palette_names:
return "Pairs well with vintage, steampunk, or historical vibes."
return "Flexible genre potential; adapt to project goals."
def _resolve_reference_directive_key(self, choice: Optional[str]) -> str:
"""Map user-facing directive text to an internal key."""
if not choice:
return "auto"
normalized = " ".join(choice.strip().lower().replace('-', ' ').split())
alias_map = {
"subject only": "subject_only",
"subject_only": "subject_only",
"style only": "style_only",
"style_only": "style_only",
"lighting only": "lighting_only",
"lighting_only": "lighting_only"
}
if normalized in alias_map:
key = alias_map[normalized]
else:
key = normalized.replace(" ", "_")
if key not in self.reference_directive_configs:
return "auto"
return key
def _build_reference_plan_entry(self, label: str, choice: Optional[str]) -> Dict[str, Any]:
"""Create a normalized directive entry for a reference image."""
directive_key = self._resolve_reference_directive_key(choice)
config = self.reference_directive_configs.get(directive_key, self.reference_directive_configs["auto"])
display = config.get("display") or directive_key.replace('_', ' ').title()
return {
"label": label,
"user_choice": choice,
"directive_key": directive_key,
"display": display,
"config": config
}
def _prepare_reference_plan(self, directive_inputs: List[Tuple[str, str]]) -> List[Dict[str, Any]]:
"""Normalize user selections into directive plan entries."""
plan: List[Dict[str, Any]] = []
for label, choice in directive_inputs:
plan.append(self._build_reference_plan_entry(label, choice))
return plan
def _align_reference_plan(self, analyses: List[Dict[str, Any]], plan: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Align directive plan entries with available analyses, defaulting to auto when missing."""
resolved_plan: List[Dict[str, Any]] = []
for idx, analysis in enumerate(analyses):
if idx < len(plan):
resolved_plan.append(plan[idx])
else:
label = analysis.get("label", f"Reference {idx + 1}")
resolved_plan.append(self._build_reference_plan_entry(label, "none"))
return resolved_plan
def _resolve_category_list(self, categories: Union[str, List[str], None]) -> List[str]:
"""Resolve category hints into a concrete ordered list."""
if categories is None:
return []
if isinstance(categories, str):
cleaned = categories.strip().lower()
if cleaned == "all":
return list(self.reference_usage_labels.keys())
return [cleaned]
resolved: List[str] = []
seen: set = set()
for category in categories:
key = str(category).strip().lower()
if not key:
continue
if key == "all":
return list(self.reference_usage_labels.keys())
if key not in seen:
seen.add(key)
resolved.append(key)
return resolved
def _collect_directive_traits(
self,
analysis: Dict[str, Any],
include_hint: Union[str, List[str], None]
) -> List[str]:
"""Gather trait notes that align with the selected directive categories."""
categories = self._resolve_category_list(include_hint)
notes = analysis.get("category_notes", {}) or {}
traits: List[str] = []
for category in categories:
note = notes.get(category)
if not note:
continue
label = self.reference_usage_labels.get(category, category.replace('_', ' '))
traits.append(f"{label}: {note}")
return traits
def _normalize_focus_lines(self, focus_note: Optional[str]) -> List[str]:
"""Break directive focus text into clean bullet-friendly lines."""
if not focus_note:
return []
sanitized = focus_note.replace("•", "\n").replace(" - ", "\n").replace("- ", "\n")
fragments = [frag.strip(" \t•-") for frag in sanitized.splitlines() if frag.strip(" \t•-")]
if not fragments:
cleaned = focus_note.strip()
return [cleaned] if cleaned else []
if len(fragments) == 1:
sentences = [seg.strip() for seg in re.split(r'(?<=[.!?])\s+', fragments[0]) if seg.strip()]
return sentences or fragments
return fragments
def _clone_analysis_payload(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
"""Copy analysis dictionary while ensuring mutable members are duplicated."""
cloned = dict(analysis)
cloned.pop("llm_logs", None)
details = analysis.get("details")
if isinstance(details, list):
cloned["details"] = list(details)
elif details is None:
cloned["details"] = []
else:
cloned["details"] = [str(details)]
category_notes = analysis.get("category_notes")
cloned["category_notes"] = dict(category_notes) if isinstance(category_notes, dict) else {}
return cloned
def _append_analysis_detail(self, analysis: Dict[str, Any], detail: str) -> None:
"""Attach a detail string to the analysis record if it is new."""
if not detail:
return
details = analysis.get("details")
if not isinstance(details, list):
details = [str(details)] if details else []
if detail not in details:
details.append(detail)
analysis["details"] = details
def _fallback_directive_analysis(self, analysis: Dict[str, Any], focus_text: str) -> str:
"""Provide a deterministic fallback summary when LLM analysis fails."""
summary = analysis.get("summary") or "Reference image supplied."
focus = focus_text.strip() if focus_text else ""
if focus and focus.lower() not in summary.lower():
return f"{summary} Focus: {focus}."
return summary
def _conduct_directive_analysis_llm(
self,
llm: Optional[LLMBackend],
analysis: Dict[str, Any],
entry: Dict[str, Any]
) -> Tuple[Dict[str, Any], Dict[str, Any], bool]:
"""Call the LLM to produce directive-focused reference notes."""
cloned = self._clone_analysis_payload(analysis)
config = entry.get("config", {}) or {}
directive_text = config.get("llm_instruction") or config.get("user_guidance") or "Follow the directive."
focus_text = config.get("analysis_focus") or "Highlight the most useful traits from this reference."
label = entry.get("label") or cloned.get("label", "Reference")
display = entry.get("display") or entry.get("directive_key", "Directive")
category_notes = cloned.get("category_notes") or {}
note_lines: List[str] = []
for category, note in category_notes.items():
if not note:
continue
label_text = self.reference_usage_labels.get(category, category.replace('_', ' '))
note_lines.append(f"{label_text}: {note}")
details = cloned.get("details")
if not isinstance(details, list):
details = [str(details)] if details else []
caption_text = cloned.get("vision_caption")
if caption_text:
normalized_caption = caption_text.strip()
details = [
str(detail)
for detail in details
if str(detail).strip() and str(detail).strip() != normalized_caption
]
system_prompt = (
"You analyze detailed reference captions for a prompt engineer. Provide up to two concise sentences that"
" explain how to use the full caption while honoring the directive focus. Avoid mentioning pixel dimensions,"
" aspect ratios, or describing yourself. Do not use bullet points or numbering. Keep all critical subjects intact."
)
lines: List[str] = [
f"Reference label: {label}",
f"Directive choice: {display}",
f"Directive intent: {directive_text}",
"",
"Baseline summary:",
cloned.get("summary", "")
]
if caption_text:
lines.append("")
lines.append("Full reference caption (canonical):")
lines.append(caption_text)
if details:
lines.append("")
lines.append("Additional analysis notes:")
lines.extend(f"- {detail}" for detail in details)
if note_lines:
lines.append("")
lines.append("Category cues:")
lines.extend(f"- {note}" for note in note_lines)
lines.append("")
lines.append("Focus request:")
lines.append(f"- {focus_text}")
lines.append("")
lines.append("Compose at most two sentences that satisfy the focus. Return plain text without headers.")
user_prompt = "\n".join(lines)
log_entry = {
"label": label,
"directive": display,
"mode": "directive_analysis",
"system_prompt": system_prompt,
"user_prompt": user_prompt,
"raw_response": None,
"success": False
}
attempted = bool(llm)
if not attempted:
fallback = self._fallback_directive_analysis(cloned, focus_text)
cloned["directive_analysis"] = fallback
self._append_analysis_detail(cloned, fallback)
return cloned, log_entry, attempted
try:
response = llm.send_prompt(system_prompt=system_prompt, user_prompt=user_prompt, max_tokens=220)
raw = response.get("response", "")
if response.get("success") and raw:
cleaned = raw.strip()
log_entry["raw_response"] = cleaned
log_entry["success"] = True
cloned["directive_analysis"] = cleaned
self._append_analysis_detail(cloned, cleaned)
return cloned, log_entry, attempted
if raw:
log_entry["raw_response"] = raw.strip()
except Exception as exc:
print(f"Directive analysis failed: {exc}")
fallback = self._fallback_directive_analysis(cloned, focus_text)
cloned["directive_analysis"] = fallback
self._append_analysis_detail(cloned, fallback)
return cloned, log_entry, attempted
def _run_reference_directive_analysis(
self,
analyses: List[Dict[str, Any]],
plan: List[Dict[str, Any]],
llm: Optional[LLMBackend]
) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
"""Generate directive-driven summaries for each reference before prompt construction."""
if not analyses:
empty_meta = {
"phase": "directive_analysis",
"reference_count": 0,
"llm_queries": 0,
"llm_successes": 0,
"llm_logs": []
}
return [], empty_meta
resolved_plan = self._align_reference_plan(analyses, plan)
processed: List[Dict[str, Any]] = []
llm_logs: List[Dict[str, Any]] = []
llm_queries = 0
llm_successes = 0
for analysis, entry in zip(analyses, resolved_plan):
analysis_logs = analysis.get("llm_logs") or []
for pre_log in analysis_logs:
llm_logs.append(pre_log)
attempted_flag = pre_log.get("attempted")
if attempted_flag not in (False, None):
llm_queries += 1
if pre_log.get("success"):
llm_successes += 1
if entry.get("directive_key") == "none":
cloned = self._clone_analysis_payload(analysis)
cloned.pop("llm_logs", None)
caption_text = analysis.get("vision_caption") or analysis.get("summary") or "Reference image provided."
cloned["directive_analysis"] = caption_text
self._append_analysis_detail(cloned, caption_text)
processed.append(cloned)
continue
enriched, log_entry, attempted = self._conduct_directive_analysis_llm(llm, analysis, entry)
processed.append(enriched)
if log_entry:
llm_logs.append(log_entry)
if attempted:
llm_queries += 1
if log_entry.get("success"):
llm_successes += 1
meta = {
"phase": "directive_analysis",
"reference_count": len(processed),
"llm_queries": llm_queries,
"llm_successes": llm_successes,
"llm_logs": llm_logs
}
return processed, meta
def _build_reference_guidance(
self,
analyses: List[Dict[str, Any]],
plan: List[Dict[str, Any]],
llm: LLMBackend,
prompt_context: str
) -> Tuple[str, List[str], Dict[str, Any]]:
"""Generate textual guidance for using reference images and capture LLM usage stats."""
if not analyses:
return "", [], {
"analysis_method": "none",
"reference_count": 0,
"llm_queries": 0,
"llm_successes": 0,
"guardrail": self.reference_guardrail_text,
"directive_labels": [],
"directives": [],
"llm_logs": []
}
resolved_plan = self._align_reference_plan(analyses, plan)
method = self.default_reference_analysis_method or "sequential_refine"
if method not in {"single_pass", "sequential_refine"}:
method = "sequential_refine"
if not llm:
method = "single_pass"
guidance_lines: List[str] = [
"Reference guardrails:",
f"- {self.reference_guardrail_text}",
"- Integrate each directive once; avoid repeating identical phrasing.",
"- Preserve every major subject and environmental cue from the provided captions."
]
guidance_lines.append("")
reference_notes: List[str] = []
reference_logs: List[Dict[str, Any]] = []
previous_guidance = ""
llm_queries = 0
llm_successes = 0
vision_captions: List[str] = []
vision_caption_errors: List[str] = []
for analysis, entry in zip(analyses, resolved_plan):
note_label = entry.get('label', analysis.get('label', 'Reference'))
note_display = entry.get('display', 'Auto')
focus_lines = self._normalize_focus_lines(analysis.get("directive_analysis"))
focus_block: List[str] = []
if focus_lines:
focus_header = f"{note_label} directive focus:"
focus_block.append(focus_header)
focus_block.extend(f"- {line}" for line in focus_lines)
if method == "sequential_refine":
refined, success, log_entry = self._refine_reference_with_llm(llm, analysis, entry, previous_guidance)
llm_queries += 1
if success:
llm_successes += 1
else:
refined = self._compose_reference_line(analysis, entry)
success = False
log_entry = {
"label": entry.get("label", analysis.get("label", "Reference")),
"directive": entry.get("display"),
"mode": "single_pass",
"system_prompt": None,
"user_prompt": None,
"raw_response": None,
"success": False
}
block_lines: List[str] = []
if focus_block:
guidance_lines.extend(focus_block)
block_lines.extend(focus_block)
guidance_lines.append(refined)
block_lines.append(refined)
config = entry.get('config', {})
summary_note = config.get('user_summary') or config.get('user_guidance') or ""
focus_note = focus_lines[0] if focus_lines else ""
note_parts: List[str] = []
if summary_note:
note_parts.append(summary_note)
if focus_note:
note_parts.append(f"Focus: {focus_note}")
caption_text = analysis.get("vision_caption")
if caption_text:
vision_captions.append(caption_text)
snippet = caption_text if len(caption_text) <= 220 else caption_text[:217] + "…"
note_parts.append(f"Caption: {snippet}")
elif analysis.get("vision_caption_error"):
error_entry = analysis.get("vision_caption_error")
if error_entry:
vision_caption_errors.append(str(error_entry))
if note_parts:
reference_notes.append(f"{note_label} ({note_display}): " + " | ".join(note_parts))
else:
reference_notes.append(f"{note_label} ({note_display})")
previous_guidance += "\n".join(block_lines) + "\n"
reference_logs.append(log_entry)
if prompt_context == "reference_modification":
guidance_lines.append(
"Focus on adjusting existing reference traits rather than inventing wholly new scenes."
)
return "\n".join(guidance_lines).strip(), reference_notes, {
"analysis_method": method,
"reference_count": len(analyses),
"llm_queries": llm_queries,
"llm_successes": llm_successes,
"guardrail": self.reference_guardrail_text,
"directive_labels": [entry.get("display") for entry in resolved_plan],
"directives": [entry.get("directive_key") for entry in resolved_plan],
"llm_logs": reference_logs,
"vision_captions": vision_captions,
"vision_caption_errors": vision_caption_errors
}
def _merge_reference_metadata(
self,
plan: List[Dict[str, Any]],
directive_meta: Dict[str, Any],
guidance_meta: Dict[str, Any]
) -> Dict[str, Any]:
"""Combine directive analysis and guidance metadata for downstream reporting."""
directives = [entry.get("directive_key") for entry in plan]
directive_labels = [entry.get("display") for entry in plan]
directive_queries = directive_meta.get("llm_queries", 0)
guidance_queries = guidance_meta.get("llm_queries", 0)
directive_success = directive_meta.get("llm_successes", 0)
guidance_success = guidance_meta.get("llm_successes", 0)
reference_count = max(
directive_meta.get("reference_count", 0),
guidance_meta.get("reference_count", 0),
len(plan)
)
combined_logs = (directive_meta.get("llm_logs") or []) + (guidance_meta.get("llm_logs") or [])
analysis_method = guidance_meta.get("analysis_method") or (
self.default_reference_analysis_method or "sequential_refine"
)
if reference_count == 0:
analysis_method = "none"
return {
"analysis_method": analysis_method,
"reference_count": reference_count,
"llm_queries": directive_queries + guidance_queries,
"llm_successes": directive_success + guidance_success,
"guardrail": self.reference_guardrail_text,
"directive_labels": directive_labels,
"directives": directives,
"llm_logs": combined_logs,
"analysis_phase": directive_meta,
"guidance_phase": guidance_meta,
"vision_captions": guidance_meta.get("vision_captions", []),
"vision_caption_errors": guidance_meta.get("vision_caption_errors", [])
}
def _resolve_seed_value(self, mode: str, requested_seed: int) -> Tuple[int, str]:
"""Resolve the actual seed value based on the selected seed mode."""
normalized_mode = (mode or "random").strip().lower()
valid_modes = {"random", "fixed", "increment", "decrement"}
if normalized_mode not in valid_modes:
normalized_mode = "random"
max_seed = 2_147_483_647
system_rng = random.SystemRandom()
state = self._seed_state
last_mode = state.get("last_mode")
last_seed = state.get("last_seed")
last_input = state.get("last_input")
if requested_seed is None:
requested_seed = -1
else:
requested_seed = int(requested_seed)
seed_specified = requested_seed >= 0
def fresh_seed() -> int:
if seed_specified:
return requested_seed
return system_rng.randint(0, max_seed)
if normalized_mode == "fixed":
if seed_specified:
seed_value = requested_seed
elif last_mode == "fixed" and last_input == requested_seed and last_seed is not None:
seed_value = int(last_seed)
else:
seed_value = fresh_seed()
elif normalized_mode == "increment":
if last_mode == "increment" and last_input == requested_seed and last_seed is not None:
seed_value = int(last_seed) + 1
if seed_value > max_seed:
seed_value = 0
else:
seed_value = fresh_seed()
elif normalized_mode == "decrement":
if last_mode == "decrement" and last_input == requested_seed and last_seed is not None:
seed_value = int(last_seed) - 1
if seed_value < 0:
seed_value = max_seed
else:
seed_value = fresh_seed()
else: # random or fallback
seed_value = system_rng.randint(0, max_seed)
normalized_mode = "random"
state["last_seed"] = int(seed_value)
state["last_mode"] = normalized_mode
state["last_input"] = requested_seed
return int(seed_value), normalized_mode
def _compose_reference_line(self, analysis: Dict[str, Any], entry: Dict[str, Any]) -> str:
"""Compose a descriptive line for a reference image using directive presets."""
label = analysis.get("label", "Reference")
summary = analysis.get("summary", f"{label}: reference image provided")
config = entry.get("config", {})
include_hint = config.get("include_categories")
exclude_categories = config.get("exclude_categories", []) or []
extra_note = config.get("extra_notes")
lines = [f"- {summary}"]
directive_text = config.get("user_guidance")
if directive_text:
lines.append(f" • Directive: {directive_text}")
traits = self._collect_directive_traits(analysis, include_hint)
for trait in traits:
lines.append(f" • {trait}")
focus_lines = self._normalize_focus_lines(analysis.get("directive_analysis"))
for focus in focus_lines:
lines.append(f" • Focus note: {focus}")
for category in exclude_categories:
label_text = self.reference_usage_labels.get(category, category.replace('_', ' '))
lines.append(f" • Avoid mirroring {label_text}; generate a fresh interpretation.")
if extra_note:
lines.append(f" • {extra_note}")
return "\n".join(lines)
def _refine_reference_with_llm(
self,
llm: LLMBackend,
analysis: Dict[str, Any],
entry: Dict[str, Any],
previous_guidance: str
) -> Tuple[str, bool, Dict[str, Any]]:
"""Request a compact reference summary from the LLM when sequential refinement is desired."""
config = entry.get("config", {})
include_hint = config.get("include_categories")
exclude_categories = config.get("exclude_categories", []) or []
directive_text = config.get("llm_instruction") or config.get("user_guidance", "Follow the directive.")
emphasis_lines = self._collect_directive_traits(analysis, include_hint)
avoid_lines = []
for category in exclude_categories:
label_text = self.reference_usage_labels.get(category, category.replace('_', ' '))
avoid_lines.append(f"Avoid copying {label_text}; invent something new.")
detail_entries = analysis.get("details", [])
if not isinstance(detail_entries, (list, tuple)):
detail_entries = [str(detail_entries)] if detail_entries else []
system_prompt = (
"You help transform reference analysis into concise prompt guidance. Provide at most two sentences. "
"Do not mention pixel dimensions, resolutions, or aspect ratios. Avoid repeating phrases verbatim."
)
lines: List[str] = [
f"Reference label: {analysis.get('label', 'Reference')}",
f"Directive: {directive_text}",
f"Summary: {analysis.get('summary', '')}"
]
if emphasis_lines:
lines.append("Focus cues:")
lines.extend(f"- {cue}" for cue in emphasis_lines)
if avoid_lines:
lines.append("Avoid copying:")
lines.extend(f"- {cue}" for cue in avoid_lines)
if detail_entries:
lines.append("Additional observations:")
lines.extend(f"- {str(detail)}" for detail in detail_entries)
if previous_guidance:
lines.append("Guidance already covered for earlier references:")
lines.append(previous_guidance.strip())
lines.append("Write guidance that honors the directive without repeating earlier sentences.")
user_prompt = "\n".join(lines)
log_entry = {
"label": analysis.get("label", "Reference"),
"directive": entry.get("display"),
"mode": "sequential_refine",
"system_prompt": system_prompt,
"user_prompt": user_prompt,
"raw_response": None,
"success": False
}
try:
response = llm.send_prompt(system_prompt=system_prompt, user_prompt=user_prompt, max_tokens=240)
raw_payload = response.get("response")
if response.get("success") and raw_payload:
raw = raw_payload.strip()
log_entry["raw_response"] = raw
log_entry["success"] = True
return "- " + raw, True, log_entry
if raw_payload:
log_entry["raw_response"] = raw_payload.strip()
except Exception as exc:
print(f"Reference refinement failed: {exc}")
fallback_text = self._compose_reference_line(analysis, entry)
return fallback_text, False, log_entry
def _generate_creative_brainstorm(self, text_prompt: str, randomness_mode: str) -> Optional[str]:
"""Produce creative guidance snippets based on the selected randomness level."""
randomness_mode = (randomness_mode or "off").lower()
if randomness_mode in {"off", "none"}:
return None
base_subject = text_prompt.split(',')[0].strip() if text_prompt else "the subject"
if randomness_mode == "subtle":
return (
"Creative flourish: add gentle ambient storytelling cues (background sounds, faint weather, or "
"minor props) that enrich the scene without changing the main subject."
)
if randomness_mode == "moderate":
suggestion = random.choice(self.random_story_settings)
companion = random.choice(self.random_story_companions)
return (
"Creative prompt idea: place {subject} within {setting} accompanied by {companion}. "
"Retain the original tone but add cohesive environmental storytelling."
).format(subject=base_subject, setting=suggestion, companion=companion)
if randomness_mode in {"bold", "storyteller", "chaotic"}:
setting = random.choice(self.random_story_settings)
companion = random.choice(self.random_story_companions)
conflict = random.choice(self.random_story_conflicts)
tone = "Embrace surreal twists" if randomness_mode == "chaotic" else "Craft a vivid narrative"
return (
f"{tone}: imagine {base_subject} within {setting}, joined by {companion}, {conflict}. "
"Blend this storyline into the enhanced prompt while preserving user intent."
)
return None
def _determine_max_tokens(
self,
platform_config: Dict,
creative_randomness: Optional[str]
) -> int:
"""Adjust LLM max tokens according to platform and desired verbosity."""
max_tokens_hint = platform_config.get("max_tokens")
max_words_hint = platform_config.get("max_words")
if max_tokens_hint:
base_tokens = int(max_tokens_hint)
elif max_words_hint:
base_tokens = int(max_words_hint * 1.45)
else:
base_tokens = 1000
if creative_randomness in {"bold", "storyteller", "chaotic"}:
base_tokens += 120
# Provide additional headroom for extremely long prompts
return min(base_tokens + 200, 1800)
def _cap_tokens_for_backend(
self,
backend: str,
model_name: Optional[str],
requested_tokens: int
) -> Tuple[int, Optional[str]]:
"""Apply backend-aware caps to avoid overloading smaller local models."""
backend_lower = (backend or "").lower()
model_lower = (model_name or "").lower()
adjusted = int(requested_tokens)
cap_reason: Optional[str] = None
def apply_cap(limit: int, reason: str) -> None:
nonlocal adjusted, cap_reason
if adjusted > limit:
adjusted = limit
if cap_reason is None:
cap_reason = reason
if backend_lower == "qwen3_vl":
apply_cap(512, "Capped max tokens to 512 for local Qwen3-VL stability.")
elif backend_lower in {"lm_studio", "ollama"}:
small_signals = ["1.5b", "2b", "3b", "4b", "tiny", "mini", "phi-2", "phi-3", "phi3", "smol"]
medium_signals = ["5b", "6b", "7b", "8b"]
if any(sig in model_lower for sig in small_signals):
apply_cap(512, f"Capped max tokens for small model '{model_name}'.")
elif any(sig in model_lower for sig in medium_signals):
apply_cap(768, f"Capped max tokens for mid-size model '{model_name}'.")
apply_cap(1024, f"Capped max tokens to 1024 for backend '{backend}'.")
else:
apply_cap(1024, "Capped max tokens to 1024 for stability.")
return adjusted, cap_reason
def _call_main_llm_with_retries(
self,
llm: LLMBackend,
system_prompt: str,
user_prompt: str,
max_tokens: int,
backend_params: Dict[str, Any]
) -> Tuple[Dict[str, Any], LLMBackend, List[Dict[str, Any]]]:
"""Send prompt with limited retries and adaptive token ceilings."""
attempt_tokens: List[int] = [int(max_tokens)]
fallback_candidates = [max(240, min(max_tokens, 640)), 320]
for candidate in fallback_candidates:
if candidate < attempt_tokens[0]:
attempt_tokens.append(int(candidate))
# Preserve order while removing duplicates
seen: set = set()
ordered_tokens: List[int] = []
for value in attempt_tokens:
if value not in seen:
ordered_tokens.append(value)
seen.add(value)
attempts_log: List[Dict[str, Any]] = []
current_llm = llm
last_response: Dict[str, Any] = {"success": False, "response": "", "error": "No attempt"}
for attempt_index, tokens in enumerate(ordered_tokens):
response = current_llm.send_prompt(
system_prompt=system_prompt,
user_prompt=user_prompt,
max_tokens=tokens
)
last_response = response
attempt_info = {
"attempt": attempt_index + 1,
"backend": current_llm.backend_type,
"model": getattr(current_llm, "model_name", None),
"max_tokens": tokens,
"success": bool(response.get("success")),
"error": response.get("error"),
"empty_response": not bool((response.get("response") or "").strip())
}
attempts_log.append(attempt_info)
if response.get("success") and (response.get("response") or "").strip():
attempt_info["used"] = True
return response, current_llm, attempts_log
if attempt_index < len(ordered_tokens) - 1:
try:
current_llm = LLMBackend(
backend_type=backend_params["backend_type"],
endpoint=backend_params["endpoint"],
model_name=None,
temperature=backend_params["temperature"]
)
except Exception as exc:
attempts_log.append({
"attempt": attempt_index + 1,
"backend": backend_params.get("backend_type"),
"max_tokens": tokens,
"success": False,
"error": f"Backend reinit failed: {exc}",
"reinit_failed": True
})
# Continue with existing llm instance if reinit fails
return last_response, current_llm, attempts_log
def _settings_to_phrases(self, settings: Dict[str, str]) -> List[str]:
"""Convert resolved settings into descriptive phrases for fallbacks."""
phrases: List[str] = []
mapping = {
"camera_angle": "shot from {value}",
"composition": "composition guided by {value}",
"lighting_source": "lit by {value}",
"lighting_quality": "lighting quality is {value}",
"time_of_day": "set during {value}",
"historical_period": "evokes the {value}",
"weather": "{value} conditions",
"color_mood": "{value} color palette",
"genre_style": "{value} tone",
"subject_framing": "{value} framing",
"subject_pose": "subject posed {value}"
}
for key, template in mapping.items():
value = settings.get(key)
if not value:
continue
lowered = value.lower()
if "auto" in lowered or "none" in lowered:
continue
phrase = template.format(value=value)
if phrase not in phrases:
phrases.append(phrase)
return phrases
def _build_deterministic_fallback_prompt(
self,
base_prompt: str,
platform: str,
platform_config: Dict[str, Any],
settings: Dict[str, str],
reference_notes: List[str]
) -> Tuple[str, Dict[str, Any]]:
"""Synthesize a deterministic enhancement when the main LLM fails."""
base = base_prompt.strip() or "Describe the requested scene vividly"
phrases = self._settings_to_phrases(settings)
reference_phrases: List[str] = []
for note in reference_notes:
if not note:
continue
cleaned = note.split(":", 1)[-1].strip() if ":" in note else note.strip()
if cleaned and cleaned not in reference_phrases:
reference_phrases.append(cleaned)
combined = phrases + reference_phrases
meta = {
"setting_phrases": len(phrases),
"reference_phrases": len(reference_phrases),
"combined_total": len(combined)
}
if not combined:
return base, meta
addition = ", ".join(combined[:8])
if platform == "pony":
normalized = base
prefix_tokens = ["score_9", "score_8_up", "score_7_up"]
normalized_lower = normalized.lower()
missing_prefix = [token for token in prefix_tokens if token not in normalized_lower]
if missing_prefix:
normalized = ", ".join(missing_prefix + [normalized])
if not normalized.endswith(","):
normalized = normalized.rstrip() + ","
fallback_prompt = f"{normalized} {addition}".strip()
else:
cleaned_base = base.rstrip("., ")
fallback_prompt = f"{cleaned_base}. {addition}."
return fallback_prompt, meta
def _ensure_prompt_density(
self,
prompt: str,
platform: str,
platform_config: Dict[str, Any],
reference_notes: List[str],
settings: Dict[str, str]
) -> Tuple[str, Optional[Dict[str, Any]]]:
"""Pad prompts that land well below the target word count."""
target_words = platform_config.get("max_words")
if not target_words or target_words < 60:
return prompt, None
min_words = max(80, int(target_words * 0.7))
words_before = len(prompt.split())
if words_before >= min_words:
return prompt, None
additions: List[str] = []
for note in reference_notes:
if not note:
continue
cleaned = note.split(":", 1)[-1].strip() if ":" in note else note.strip()
if cleaned and cleaned not in additions:
additions.append(cleaned)
if words_before + sum(len(entry.split()) for entry in additions) >= min_words:
break
if words_before + sum(len(entry.split()) for entry in additions) < min_words:
for phrase in self._settings_to_phrases(settings):
if phrase and phrase not in additions:
additions.append(phrase)
if words_before + sum(len(entry.split()) for entry in additions) >= min_words:
break
if not additions:
return prompt, None
if platform == "pony":
updated = prompt.rstrip()
if not updated.endswith(","):
updated = updated.rstrip("., ") + ","
updated = f"{updated} {', '.join(additions)}"
else:
cleaned = prompt.rstrip(".")
extra = "; ".join(additions)
updated = f"{cleaned}. {extra}."
density_meta = {
"min_words": min_words,
"before_words": words_before,
"after_words": len(updated.split()),
"added_phrases": len(additions)
}
return updated, density_meta
def _build_system_prompt(
self,
platform_key: str,
platform_config: Dict,
settings: Dict,
reference_plan: List[Dict[str, Any]],
prompt_context: str
) -> str:
"""Build LLM system prompt with platform-specific instructions"""
quality_emphasis = bool(platform_config.get("quality_emphasis", True))
platform_name = platform_config["name"]
platform = platform_config.get("prompt_style", "natural")
target_words = platform_config.get("max_words")
min_word_goal: Optional[int] = None
if isinstance(target_words, (int, float)) and target_words >= 60:
min_word_goal = max(80, int(target_words * 0.75))
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
5. Never mention source image dimensions, aspect ratios, or pixel counts
6. Keep the user's base prompt concept central; additions must support rather than replace it
7. Treat every reference directive and focus note from the user message as mandatory content; weave them into the final prompt exactly once.
TARGET PLATFORM: {platform_name}
Description: {platform_config['description']}
Prompting Style: {platform_config['prompt_style']}
Optimal Length: {platform_config['optimal_length']}
"""
prompt += "\nOUTPUT INTENSITY GUIDANCE:\n"
if min_word_goal:
prompt += f"- Minimum acceptable length: {min_word_goal} words. Falling short counts as a failure.\n"
else:
prompt += "- Deliver a multi-sentence, richly layered description (no terse summaries).\n"
if reference_plan:
prompt += "- Dedicate vivid language to every reference directive so each image influences the result.\n"
if len(reference_plan) > 1:
prompt += "- Keep reference-derived cues distinct; do not merge them into a single generic sentence.\n"
if platform_key == "pony":
prompt += (
"- Start with the score tags exactly once, then shift into flowing natural-language prose.\n"
"- After the score tags, produce an expansive narrative covering subject, wardrobe, environment, lighting, and atmosphere. Sparse checklists are unacceptable.\n"
)
if reference_plan:
prompt += "- When references are provided, weave their traits into luxuriant supporting clauses for extra detail.\n"
prompt += "\n"
# 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"
# Length and detail expectations (always max detail)
length_guidance = platform_config.get("length_guidance")
if length_guidance:
prompt += f"TARGET LENGTH: {length_guidance}\n"
detail_expectation = platform_config.get("detail_expectation")
if detail_expectation:
prompt += f"DETAIL EXPECTATION: {detail_expectation}\n"
max_words = platform_config.get("max_words")
if max_words:
floor_words = int(max_words * 0.6)
if min_word_goal and min_word_goal > floor_words:
floor_words = min_word_goal
prompt += f"ABSOLUTE MINIMUM DETAIL: deliver no fewer than {floor_words} words.\n\n"
else:
prompt += "Ensure the description is long-form and exhaustive.\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",
"steampunk": "retro-futuristic contraptions, brass machinery, victorian flair",
"dieselpunk": "industrial grit, roaring engines, interwar aesthetics",
"mythic": "legendary scale, archetypal symbolism, timeless myth",
"gothic": "dramatic architecture, chiaroscuro mood, romantic darkness",
"art deco": "sleek geometry, metallic sheen, roaring twenties glamour",
"retro futurism": "optimistic vintage sci-fi, bold colors, nostalgic futurism"
}
style_desc = genre_guidance.get(genre, genre)
prompt += f"STYLE/GENRE: {genre} - Infuse the prompt with {style_desc}\n\n"
# Prompt context guidance
context_map = {
"expand_short_prompt": "User provided a shorthand idea. Expand it into a full, production-ready description.",
"finish_opening_line": "Treat input as the opening line of the prompt. Continue and embellish it coherently.",
"prompt_from_item_list": "User supplied a checklist. Transform it into a cohesive narrative scene.",
"modify_reference_image": "Focus on altering specific parts of the reference imagery as requested.",
"enhance_reference_image": "Use reference images as a base and enrich them with new imaginative elements.",
# Legacy mappings for backward compatibility
"abbreviated_prompt": "User provided a shorthand idea. Expand it into a full, production-ready description.",
"prompt_seed": "Treat input as the opening line of the prompt. Continue and embellish it coherently.",
"item_list": "User supplied a checklist. Transform it into a cohesive narrative scene.",
"reference_modification": "Focus on altering specific parts of the reference imagery as requested.",
"reference_expansion": "Use reference images as a base and enrich them with new imaginative elements."
}
if prompt_context in context_map:
prompt += f"PROMPT CONTEXT: {context_map[prompt_context]}\n\n"
# Historical period guidance
historical = settings.get("historical_period")
if historical and "auto" not in historical.lower() and "none" not in historical.lower():
prompt += f"TIME PERIOD: Align visuals with the {historical}.\n\n"
# Creative randomness guidance
creativity = settings.get("creative_randomness", "off")
if creativity in self.creative_randomness_modes and creativity not in ["off", "none"]:
prompt += f"CREATIVE RANDOMNESS ({creativity.upper()}): {self.creative_randomness_modes[creativity]}\n"
prompt += "Blend surprise elements while keeping the requested subject recognizable.\n\n"
# Base prompt reinforcement
prompt += "\nBASE PROMPT PRIORITY:\n"
prompt += "- The user's text prompt is the authoritative subject. Preserve its characters, actions, and tone.\n"
prompt += "- If creative randomness or references introduce new ideas, they must enhance (not replace) the base concept.\n"
prompt += "- Reference directives override conflicting improvisations; missing them counts as failing the task.\n"
# Reference usage guidance
if reference_plan:
prompt += "REFERENCE IMAGE GUIDELINES:\n"
prompt += f"- {self.reference_guardrail_text}\n"
prompt += "- Integrate guidance once within the final prompt; do not repeat phrases verbatim.\n"
for entry in reference_plan:
config = entry.get("config", {})
display = entry.get("display", "Reference")
instruction = config.get("llm_instruction") or config.get("user_guidance")
label = entry.get("label", "Reference")
if instruction:
prompt += f"- {label} ({display}): {instruction}\n"
prompt += "\n"
for key, value in settings.items():
if key in [
"genre_style",
"creative_randomness",
"prompt_context",
"historical_period",
"length_mode",
"detail_mode"
]:
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 DIFFUSION FORMAT (CRITICAL):
- MUST START with exactly: score_9, score_8_up, score_7_up
- After score tags, use NATURAL LANGUAGE descriptions (NO underscores)
- Comma-separated detailed phrases
- DO NOT use danbooru tag format (no long_hair, brown_eyes, etc.)
- Use readable text: "long flowing hair", "brown eyes", "red dress"
- Aim for 150-200 words of rich descriptive detail
- Example: score_9, score_8_up, score_7_up, a young woman with long flowing hair, intense gaze, wearing elegant red dress, soft studio lighting, professional portrait
"""
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,
settings: Dict,
platform: str,
prompt_context: str,
reference_guidance: str,
creative_brainstorm: Optional[str],
reference_captions: Optional[List[Tuple[str, str]]] = None
) -> str:
"""Build user prompt for LLM"""
context_labels = {
"expand_short_prompt": "Compressed idea to expand",
"finish_opening_line": "Prompt opening line",
"prompt_from_item_list": "Checklist of required elements",
"modify_reference_image": "Modify references",
"enhance_reference_image": "Expand upon references",
# Legacy mappings for backward compatibility
"abbreviated_prompt": "Compressed idea to expand",
"prompt_seed": "Prompt opening line",
"item_list": "Checklist of required elements",
"reference_modification": "Modify references",
"reference_expansion": "Expand upon references"
}
header = context_labels.get(prompt_context, "Base prompt")
parts = [f"{header}: {text_prompt}"]
parts.append(
f'MANDATORY SUBJECT ANCHOR: The final description must clearly feature "{text_prompt}" without omission or substitution.'
)
if reference_guidance:
parts.append(
"REFERENCE DIRECTIVES (MANDATORY):\n"
f"{reference_guidance}\n\n"
"MANDATORY ACTION: integrate each reference block and focus note above into the final prompt exactly once,"
" preserving every major subject and environmental clue from the captions."
)
if reference_captions:
caption_lines = [
"REFERENCE CAPTIONS (VERBATIM):"
]
for label, caption in reference_captions:
caption_lines.append(f"{label}: {caption}")
caption_lines.append(
"MANDATORY: Maintain the core subjects, actions, objects, and environmental details expressed in these captions."
)
parts.append("\n".join(caption_lines))
# Add explicit settings inline - NOT appended to final output
explicit_settings = []
for key, value in settings.items():
if not isinstance(value, str):
continue
lowered = value.lower()
if "auto" in lowered or "none" in lowered:
continue
if key in {"prompt_context", "length_mode", "detail_mode"}:
continue
display_key = key.replace('_', ' ')
explicit_settings.append(f"{display_key}: {value}")
if explicit_settings:
parts.append("Settings to weave into the prompt: " + "; ".join(explicit_settings))
if creative_brainstorm:
parts.append(creative_brainstorm)
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 ["illustrious"]:
# Illustrious uses tag format with underscores
formatted_kw = [kw.replace(" ", "_") for kw in missing_keywords]
return f"{prompt}, {', '.join(formatted_kw)}"
else:
# Natural language (including Pony after score tags)
return f"{prompt}, {', '.join(missing_keywords)}"
def _add_platform_requirements(self, prompt: str, platform: str) -> 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_choice_label(self, text: Optional[str]) -> str:
"""Human-friendly capitalization for option labels."""
if not text:
return ""
cleaned = text.replace('_', ' ').strip()
if not cleaned:
return ""
words = []
for word in cleaned.split():
upper = word.upper()
if upper in {"POV", "OTS", "HDR", "SDXL"}:
words.append(upper)
elif any(char.isdigit() for char in word):
words.append(word.upper())
else:
words.append(word.capitalize())
result = " ".join(words)
result = result.replace("'S", "'s").replace("’S", "’s")
result = result.replace(" - ", "-")
return result
def _format_setting_value(
self,
key: str,
settings: Dict[str, str],
sources: Dict[str, Dict[str, str]],
actuals: Dict[str, str]
) -> Optional[str]:
"""Produce a descriptive string for a setting using origin metadata."""
source = sources.get(key, {})
mode = source.get("mode")
stored_value = source.get("value") or settings.get(key)
actual_value = actuals.get(key)
if mode == "llm":
if actual_value:
return f"LLM chose {self._format_choice_label(actual_value)}"
return "LLM will decide during prompt generation"
if mode == "random":
label = self._format_choice_label(stored_value)
return f"Randomized → {label}" if label else "Randomized"
if mode == "auto":
label = self._format_choice_label(stored_value)
return f"Auto default → {label}" if label else "Auto-resolved"
if mode == "platform":
return f"{stored_value} (platform default)"
if mode == "none":
return "Not specified"
if stored_value:
return self._format_choice_label(stored_value)
if actual_value:
return self._format_choice_label(actual_value)
return None
def _infer_setting_mentions(self, prompt: str) -> Dict[str, str]:
"""Scan the enhanced prompt to detect explicit mentions of key settings."""
normalized = prompt.lower().replace("’", "'")
normalized_alt = normalized.replace('-', ' ')
search_texts = [normalized, normalized_alt]
results: Dict[str, str] = {}
search_map: Dict[str, List[str]] = {
"camera_angle": self.camera_angles,
"composition": self.composition_styles,
"lighting_source": self.lighting_sources,
"lighting_quality": self.lighting_quality,
"time_of_day": self.times_of_day,
"weather": self.weather_conditions,
"art_style": [style for style in [
"photorealistic", "digital art", "oil painting", "watercolor",
"anime", "manga", "sketch", "pencil drawing", "3d render",
"illustration", "concept art", "impressionist", "abstract",
"pixel art", "low poly", "papercraft", "isometric"
]],
"genre_style": self.genre_styles,
"color_mood": [
"vibrant", "muted", "monochrome", "warm tones", "cool tones",
"pastel", "high contrast", "desaturated", "neon", "earth tones"
],
"subject_framing": self.subject_framings,
"subject_pose": self.subject_poses
}
alias_map: Dict[Tuple[str, str], List[str]] = {
("camera_angle", "point of view"): ["pov", "pov shot", "point-of-view"],
("camera_angle", "over the shoulder"): ["ots", "over-the-shoulder"],
("camera_angle", "bird's eye view"): ["birds eye view", "birdseye view", "bird-eye view"],
("camera_angle", "worm's eye view"): ["worms eye view", "worms-eye view"],
("subject_framing", "extreme close-up"): ["extreme closeup"],
("subject_framing", "medium close-up"): ["medium closeup"],
("subject_pose", "lying down"): ["lying-down"],
("color_mood", "warm tones"): ["warm-toned", "warm lighting"],
("color_mood", "cool tones"): ["cool-toned", "cool lighting"],
("color_mood", "high contrast"): ["high-contrast"],
("art_style", "3d render"): ["3d-render", "3d rendering"]
}
for key, options in search_map.items():
if key in results:
continue
for option in options:
option_lower = option.lower()
if any(option_lower in text for text in search_texts):
results[key] = option
break
alias_key = (key, option_lower)
aliases = alias_map.get(alias_key, [])
if any(alias.lower() in text for text in search_texts for alias in aliases):
results[key] = option
break
return results
def _format_settings_display(
self,
settings: Dict[str, str],
platform_config: Dict[str, Any],
reference_notes: List[str],
context: Dict[str, Any]
) -> str:
"""Format settings for display with origin tracking and detected selections."""
sources: Dict[str, Dict[str, str]] = context.get("setting_sources", {}) or {}
actuals: Dict[str, str] = context.get("actual_selections", {}) or {}
ref_meta: Dict[str, Any] = context.get("reference_meta", {}) or {}
main_llm_success: bool = context.get("main_llm_success", True)
main_llm_error: str = context.get("main_llm_error", "")
ref_count: int = context.get("reference_image_count", 0)
quality_emphasis_active: bool = bool(context.get("quality_emphasis", False))
ref_warnings: List[str] = context.get("reference_warnings", []) or []
vision_context: Dict[str, Any] = context.get("vision", {}) or {}
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']}"
]
lines.append("\nLLM CALL SUMMARY:")
if main_llm_success:
lines.append(" - Main prompt LLM: responded")
else:
snippet = (main_llm_error or "unknown error").splitlines()[0][:120]
snippet = snippet.replace('|', '/').strip()
lines.append(f" - Main prompt LLM: FAILED ({snippet or 'no details'})")
analysis_method = ref_meta.get("analysis_method", "none")
ref_queries = ref_meta.get("llm_queries", 0)
ref_successes = ref_meta.get("llm_successes", 0)
if ref_count:
if ref_queries:
lines.append(
f" - Reference LLM: {ref_successes}/{ref_queries} responses ({analysis_method})"
)
else:
lines.append(f" - Reference LLM: not invoked ({analysis_method})")
else:
lines.append(" - Reference LLM: no reference images")
lines.append(f" - References sent: {ref_count}")
if ref_count:
lines.append(f" - Reference guidance applied: {'yes' if context.get('reference_guidance_used') else 'no'}")
directive_labels = ref_meta.get("directive_labels") or []
if directive_labels:
lines.append(f" - Reference directives: {', '.join(directive_labels)}")
lines.append(f" - Quality emphasis: {'enabled' if quality_emphasis_active else 'disabled'}")
if vision_context:
requested_backend = (vision_context.get("requested_backend") or "auto").strip() or "auto"
resolved_backend = (vision_context.get("resolved_backend") or "disabled").strip() or "disabled"
resolved_model = vision_context.get("resolved_model") or "auto"
caption_enabled = bool(vision_context.get("caption_enabled"))
if resolved_backend.startswith("inherit:"):
inherit_source = resolved_backend.split(":", 1)[1]
resolved_display = f"inherit ({inherit_source})"
else:
resolved_display = resolved_backend
status_note = "enabled" if caption_enabled else "disabled"
lines.append(
f" - Vision captioning: {status_note} (requested: {requested_backend}, resolved: {resolved_display})"
)
if caption_enabled and resolved_model:
lines.append(f" • Vision model: {resolved_model}")
lines.append("\nSETTINGS APPLIED:")
context_display = {
"expand_short_prompt": "Abbreviated prompt (expand fully)",
"finish_opening_line": "Prompt seed (continue logically)",
"prompt_from_item_list": "Item list (merge into scene)",
"modify_reference_image": "Modify reference traits",
"enhance_reference_image": "Expand reference storytelling",
# Legacy mappings for backward compatibility
"abbreviated_prompt": "Abbreviated prompt (expand fully)",
"prompt_seed": "Prompt seed (continue logically)",
"item_list": "Item list (merge into scene)",
"reference_modification": "Modify reference traits",
"reference_expansion": "Expand reference storytelling"
}
ordered_keys = [
"camera_angle", "composition", "lighting_source", "lighting_quality",
"time_of_day", "historical_period", "weather",
"art_style", "genre_style", "color_mood",
"creative_randomness", "subject_framing", "subject_pose",
"prompt_context", "length_mode", "detail_mode", "quality_emphasis"
]
remaining_keys = [key for key in settings.keys() if key not in ordered_keys]
display_keys = ordered_keys + remaining_keys
for key in display_keys:
if key not in settings:
continue
label = key.replace("_", " ").title()
desc: Optional[str] = None
if key == "prompt_context":
mode = sources.get(key, {}).get("mode")
if mode == "llm":
desc = "LLM interprets input role dynamically"
else:
raw_value = settings.get(key, "")
desc = context_display.get(raw_value, self._format_choice_label(raw_value))
elif key == "creative_randomness":
desc = self._format_setting_value(key, settings, sources, actuals)
raw_value = (settings.get(key, "") or "").lower()
explanation = self.creative_randomness_modes.get(raw_value)
if explanation:
pretty = self._format_choice_label(raw_value)
if desc:
desc = f"{desc} — {explanation}"
else:
desc = f"{pretty or raw_value} — {explanation}"
elif key in {"length_mode", "detail_mode"}:
desc = self._format_setting_value(key, settings, sources, actuals)
elif key == "quality_emphasis":
mode = sources.get(key, {}).get("mode")
if mode == "platform":
origin = "platform default"
elif mode:
origin = mode
else:
origin = settings.get(key, "")
desc = f"{'Enabled' if quality_emphasis_active else 'Disabled'} ({origin})"
else:
desc = self._format_setting_value(key, settings, sources, actuals)
if not desc:
desc = settings.get(key)
if desc:
lines.append(f" - {label}: {desc}")
if ref_count:
lines.append("\nREFERENCE IMAGE NOTES:")
guardrail = ref_meta.get("guardrail")
if guardrail:
lines.append(f" • Guardrail: {guardrail}")
for note in reference_notes:
lines.append(f" • {note}")
for warning in ref_warnings:
lines.append(f" • Warning: {warning}")
caption_sources = ref_meta.get("vision_caption_sources") or []
backends_used = ref_meta.get("vision_backends_used") or []
models_used = ref_meta.get("vision_models_used") or []
if backends_used:
lines.append(f" • Vision backends used: {', '.join(backends_used)}")
if models_used:
lines.append(f" • Vision models used: {', '.join(model for model in models_used if model) or 'n/a'}")
if caption_sources:
lines.append(f" • Vision caption sources: {', '.join(caption_sources)}")
elif ref_warnings:
lines.append("\nREFERENCE WARNINGS:")
for warning in ref_warnings:
lines.append(f" • {warning}")
seed_info = context.get("random_seed") or {}
seed_value = seed_info.get("value")
if seed_value is not None:
mode_label = seed_info.get("mode", "manual")
lines.append(f"\nRandom Seed Used: {seed_value} ({mode_label})")
lines.append("\n" + "=" * 60)
return "\n".join(lines)