3713 lines
160 KiB
Python
3713 lines
160 KiB
Python
"""
|
||
Text-to-Image Prompt Enhancer Node
|
||
Advanced multi-platform prompt enhancement for image generation
|
||
Supports: SDXL, Pony, Illustrious, Flux, Chroma, Qwen-Image, Qwen-Edit, Wan-Image
|
||
"""
|
||
|
||
import torch
|
||
import numpy as np
|
||
from PIL import Image
|
||
import io
|
||
import 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)
|