Add natural language formatting for T5-XXL

This commit builds on PR #1 (independence + debug fixes) and adds natural
language formatting to improve T5-XXL prompt quality.

Changes:
- Added strip_weights_for_natural_language() method to remove weights from T5-XXL
- Added _build_natural_language_sentence() helper for consistent templates
- Integrated photo_type into opening sentences (e.g., 'A birds eye view photography of...')
- Converted roles and hairstyles to natural language sentences
- Converted clothing to sentence format ('Dressed in...')
- Converted composition and pose to complete sentences
- Grouped physical features into three coherent sentences:
  - Face, eyes, and expression ('They have...')
  - Skin, age, and ethnicity ('The subject has...')
  - Hair, accessories, and markings ('Notable features include...')
- Added device description at end ('The image was captured using a...')
- Fixed photo_type CLIP_L re-fetch bug by reusing same value from T5-XXL
- Enhanced clean_prompt_string() with better period and spacing handling
- Updated RETURN_TYPES to return 6 outputs: prompt, t5xxl, clip_l, clip_g, seed, debug_info

Benefits:
- T5-XXL receives natural language matching its training
- CLIP_L and CLIP_G retain keyword format with weights
- More consistent results (no RNG re-fetch issues)
- Better readability and debugging
This commit is contained in:
John B
2025-11-19 13:21:07 -08:00
parent f7c4e137c4
commit eef6d5abae
+192 -42
View File
@@ -144,17 +144,61 @@ class PromptGenerator:
text = re.sub(r'\s+(BREAK_CLIP[GL])', r' \1', text)
# Remove spaces after BREAK markers
text = re.sub(r'(BREAK_CLIP[GL])\s+', r'\1 ', text)
# Remove spaces before periods
text = re.sub(r'\s+\.', '.', text)
# Ensure space after period before word (but not multiple spaces)
text = re.sub(r'\.([A-Z])', r'. \1', text)
# Fix double periods
text = re.sub(r'\.\.+', '.', text)
# Replace multiple spaces with a single space
text = re.sub(r'\s+', ' ', text).strip()
# Specific cleanup like 'of a as' -> 'of a' etc.
text = text.replace(" of as ", " of ") # Check variations if needed
text = text.replace(" a as ", " as ") # Example
text = text.replace(" of as ", " of ")
text = text.replace(" a as ", " as ")
text = text.replace(" as as ", " as ") # Fix "working as as a"
text = text.replace(" with with ", " with ") # Fix "with with"
# Remove empty segments like ", ," or leading ", " after splits
text = re.sub(r'^,\s*', '', text)
text = re.sub(r'\s*,\s*$', '', text)
text = re.sub(r',(\s*,)+', ',', text)
return text
def strip_weights_for_natural_language(self, text):
"""Removes parentheses and weights from text for natural language (T5-XXL)."""
if not text: return ""
# Remove weights like (text:1.5) -> text
text = re.sub(r'\(([^:)]+):[0-9.]+\)', r'\1', text)
# Remove simple parentheses (text) -> text
text = re.sub(r'\(([^)]+)\)', r'\1', text)
return text
def _build_natural_language_sentence(self, category, value, context=None):
"""Converts a category value into a natural language fragment."""
if not value:
return ""
templates = {
'role': f"working as {value}",
'hairstyle': f"with {value}",
'clothing': f"dressed in {value}",
'composition': f"The composition follows {value}",
'pose': f"The subject is {value}",
'face_features': f"with {value}",
'eye_color': f"with {value} eyes",
'skin_tone': f"with {value} skin",
'age_group': f"appearing {value}",
'ethnicity': f"of {value} descent",
'accessories': f"wearing {value}",
'expression': f"with a {value} expression",
'tattoos_scars': f"featuring {value}",
'hair_color': f"with {value} hair",
'body_markings': f"displaying {value}",
'facial_hair': f"with {value}",
'makeup': f"styled with {value} makeup"
}
return templates.get(category, value)
def _format_debug_info(self, debug_info):
"""Format debug info as readable string"""
lines = []
@@ -197,6 +241,10 @@ class PromptGenerator:
# Original prompt is T5 content with markers removed
original_content = t5xxl_content.replace('BREAK_CLIPL', '').replace('BREAK_CLIPG', '')
# Strip weights/parentheses from T5-XXL content (natural language doesn't need weights)
original_content = self.strip_weights_for_natural_language(original_content)
t5xxl_content = self.strip_weights_for_natural_language(t5xxl_content)
# Clean all parts
original_clean = self.clean_prompt_string(original_content)
t5xxl_clean = self.clean_prompt_string(t5xxl_content)
@@ -223,36 +271,55 @@ class PromptGenerator:
if custom:
components.append(custom)
# --- 2. Artform / Style Lead-in ---
# --- 2. Artform / Style Lead-in (with photo_type integrated) ---
artform = self._get_choice(kwargs.get("artform", "disabled"), ARTFORM)
is_photographer = (artform.lower() == "photography")
# Get photo_type early to integrate into opening
photo_type = self._get_choice(kwargs.get("photo_type", "random"), PHOTO_TYPE)
if is_photographer:
photo_style = self._get_choice(kwargs.get("photography_styles", "random"), PHOTOGRAPHY_STYLES)
if photo_style:
components.append(photo_style)
else:
# Fallback if random selects nothing or style is disabled
components.append("photography") # Default base style
# Build opening with optional photo_type
opening_parts = []
if photo_type:
opening_parts.append(f"A {photo_type}")
opening_parts.append(photo_style if photo_style else "photography")
components.append(" ".join(opening_parts))
# Add "of" if a subject or default tag will follow
if kwargs.get("subject") or kwargs.get("default_tags", "disabled").lower() != "disabled":
components.append("of") # Add connector word
elif artform and artform.lower() != "disabled":
components.append(artform)
# Build opening with optional photo_type
opening_parts = []
if photo_type:
opening_parts.append(f"A {photo_type}")
opening_parts.append(artform)
components.append(" ".join(opening_parts))
# Add "of" if a subject or default tag will follow and artform isn't inherently descriptive like 'illustration'
if kwargs.get("subject") or kwargs.get("default_tags", "disabled").lower() != "disabled":
# Could refine this list if needed
if artform.lower() not in ["illustration", "painting", "drawing", "sketch"]:
components.append("of")
elif photo_type:
# Standalone photo_type when artform is disabled
components.append(f"A {photo_type}")
# Add "of" if a subject or default tag will follow
if kwargs.get("subject") or kwargs.get("default_tags", "disabled").lower() != "disabled":
components.append("of")
# --- 3. Subject Definition ---
subject = kwargs.get("subject", "")
default_tags_input = kwargs.get("default_tags", "random")
body_type_input = kwargs.get("body_types", "random")
chosen_subject_elements = []
if subject: # User provided subject takes precedence
# User provided subject takes precedence (but not if it's "random" or "disabled")
if subject and subject.lower() not in ["random", "disabled"]:
chosen_body_type = self._get_choice(body_type_input, BODY_TYPES)
if chosen_body_type:
chosen_subject_elements.extend(["a", chosen_body_type]) # e.g., "a muscular"
@@ -282,24 +349,41 @@ class PromptGenerator:
resolved_subject_desc = " ".join(chosen_subject_elements).lower()
# --- 4. Core Details (Roles, Hairstyles, Additional Details) ---
core_details = [
self._get_choice(kwargs.get("roles", "random"), ROLES),
self._get_choice(kwargs.get("hairstyles", "random"), HAIRSTYLES),
self._get_choice(kwargs.get("additional_details", "random"), ADDITIONAL_DETAILS),
]
components.append(smart_join(core_details))
# Build natural language sentences instead of comma-joining
role = self._get_choice(kwargs.get("roles", "random"), ROLES)
hairstyle = self._get_choice(kwargs.get("hairstyles", "random"), HAIRSTYLES)
additional_details = self._get_choice(kwargs.get("additional_details", "random"), ADDITIONAL_DETAILS)
core_parts = []
if role:
core_parts.append(self._build_natural_language_sentence('role', role))
if hairstyle:
core_parts.append(self._build_natural_language_sentence('hairstyle', hairstyle))
if core_parts:
components.append(" ".join(core_parts))
if additional_details:
components.append(f". {additional_details}")
# --- 5. Clothing ---
clothing = self._get_choice(kwargs.get("clothing", "random"), CLOTHING)
if clothing:
components.append(f"dressed in {clothing}")
components.append(f". Dressed in {clothing}")
# --- 6. Composition & Pose ---
comp_pose = [
self._get_choice(kwargs.get("composition", "random"), COMPOSITION),
self._get_choice(kwargs.get("pose", "random"), POSE),
]
components.append(smart_join(comp_pose))
# Build natural language sentences with periods
composition = self._get_choice(kwargs.get("composition", "random"), COMPOSITION)
pose = self._get_choice(kwargs.get("pose", "random"), POSE)
comp_pose_parts = []
if pose:
comp_pose_parts.append(f"The subject is {pose}")
if composition:
comp_pose_parts.append(f"The composition follows {composition}")
if comp_pose_parts:
components.append(". " + ". ".join(comp_pose_parts))
# --- BREAK CLIP G 1 ---
components.append("BREAK_CLIPG")
@@ -328,48 +412,107 @@ class PromptGenerator:
components.append("BREAK_CLIPG")
# --- 9. Physical Features ---
features = [
self._get_choice(kwargs.get("face_features", "random"), FACE_FEATURES),
self._get_choice(kwargs.get("eye_colors", "random"), EYE_COLORS),
self._get_choice(kwargs.get("skin_tone", "random"), SKIN_TONE),
self._get_choice(kwargs.get("age_group", "random"), AGE_GROUP),
self._get_choice(kwargs.get("ethnicity", "random"), ETHNICITY),
self._get_choice(kwargs.get("accessories", "random"), ACCESSORIES),
self._get_choice(kwargs.get("expression", "random"), EXPRESSION),
self._get_choice(kwargs.get("tattoos_scars", "random"), TATTOOS_SCARS),
self._get_choice(kwargs.get("hair_color", "random"), HAIR_COLOR),
self._get_choice(kwargs.get("body_markings", "random"), BODY_MARKINGS),
self._get_choice(kwargs.get("facial_hair", "random"), FACIAL_HAIR),
self._get_choice(kwargs.get("makeup_styles", "random"), MAKEUP_STYLES),
]
# Get all feature values
face_features = self._get_choice(kwargs.get("face_features", "random"), FACE_FEATURES)
eye_color = self._get_choice(kwargs.get("eye_colors", "random"), EYE_COLORS)
skin_tone = self._get_choice(kwargs.get("skin_tone", "random"), SKIN_TONE)
age_group = self._get_choice(kwargs.get("age_group", "random"), AGE_GROUP)
ethnicity = self._get_choice(kwargs.get("ethnicity", "random"), ETHNICITY)
accessories = self._get_choice(kwargs.get("accessories", "random"), ACCESSORIES)
expression = self._get_choice(kwargs.get("expression", "random"), EXPRESSION)
tattoos_scars = self._get_choice(kwargs.get("tattoos_scars", "random"), TATTOOS_SCARS)
hair_color = self._get_choice(kwargs.get("hair_color", "random"), HAIR_COLOR)
body_markings = self._get_choice(kwargs.get("body_markings", "random"), BODY_MARKINGS)
components.append(smart_join(features))
# Facial hair and makeup - now independent of gender for modern/creative contexts
facial_hair = self._get_choice(kwargs.get("facial_hair", "random"), FACIAL_HAIR)
makeup = self._get_choice(kwargs.get("makeup_styles", "random"), MAKEUP_STYLES)
# Group features into coherent sentences
feature_sentences = []
# Sentence 1: Face, eyes, expression
face_parts = []
if face_features:
face_parts.append(face_features)
if eye_color:
face_parts.append(f"{eye_color} eyes")
if expression:
face_parts.append(f"a {expression} expression")
if face_parts:
feature_sentences.append("They have " + " and ".join(face_parts))
# Sentence 2: Skin, age, ethnicity
identity_parts = []
if skin_tone:
identity_parts.append(f"{skin_tone} skin")
if age_group:
# Age groups that are nouns need articles
age_nouns = ["adult", "child", "infant", "preteen", "senior", "teenager", "toddler", "young adult"]
if age_group in age_nouns:
article = "an" if age_group[0].lower() in "aeiou" else "a"
identity_parts.append(f"appearing as {article} {age_group}")
else:
# Adjectives don't need articles
identity_parts.append(f"appearing {age_group}")
if ethnicity:
identity_parts.append(f"of {ethnicity} descent")
if identity_parts:
feature_sentences.append("The subject has " + ", ".join(identity_parts))
# Sentence 3: Hair, accessories, markings, gender-specific (only one of facial_hair OR makeup)
detail_parts = []
if hair_color:
detail_parts.append(f"{hair_color} hair")
if accessories:
detail_parts.append(f"wearing {accessories}")
if tattoos_scars:
detail_parts.append(f"featuring {tattoos_scars}")
if body_markings:
detail_parts.append(f"displaying {body_markings}")
if facial_hair and not makeup: # Only add if makeup isn't present
detail_parts.append(f"{facial_hair}")
elif makeup: # Only add if facial_hair isn't present
detail_parts.append(f"{makeup}")
if detail_parts:
feature_sentences.append("Notable features include " + ", ".join(detail_parts))
if feature_sentences:
components.append(". " + ". ".join(feature_sentences))
# --- 10. Camera/Device (for T5-XXL natural language) ---
device = self._get_choice(kwargs.get("device", "random"), DEVICE)
if device:
components.append(f". The image was captured using a {device}")
# --- BREAK CLIP L 1 ---
components.append("BREAK_CLIPL")
# --- 10. Technical/Artistic Details ---
# --- 11. Technical/Artistic Details (for CLIP_L keywords) ---
# All categories now work independently - users control via their selections
tech_artist_details = []
photo_type = self._get_choice(kwargs.get("photo_type", "random"), PHOTO_TYPE)
# Framing (weighted) - reuse the same value fetched for T5-XXL opening
# This ensures consistency between T5-XXL and CLIP_L
if photo_type:
random_value = round(self.rng.uniform(1.1, 1.5), 1)
tech_artist_details.append(f"({photo_type}:{random_value})")
device = self._get_choice(kwargs.get("device", "random"), DEVICE)
# Device - already fetched above for T5-XXL, reuse the same value
if device:
tech_artist_details.append(f"shot on {device}")
# Digital artform - works independently
digital_artform = self._get_choice(kwargs.get("digital_artform", "random"), DIGITAL_ARTFORM)
if digital_artform:
tech_artist_details.append(digital_artform)
# Photographer - works independently
photographer = self._get_choice(kwargs.get("photographer", "random"), PHOTOGRAPHER)
if photographer:
tech_artist_details.append(f"photo by {photographer}")
# Artist - works independently
artist = self._get_choice(kwargs.get("artist", "random"), ARTIST)
if artist:
tech_artist_details.append(f"by {artist}")
@@ -416,6 +559,13 @@ class PromptGenerator:
debug_info['not_used']['default_tags'] = "ignored (subject takes precedence)"
del debug_info['used']['default_tags']
# photography_styles only used when artform is photography
if kwargs.get('photography_styles', '') and kwargs.get('photography_styles', '').lower() not in ['disabled', '']:
if artform.lower() != "photography":
if 'photography_styles' in debug_info['used']:
debug_info['not_used']['photography_styles'] = f"ignored (artform={artform}, not photography)"
del debug_info['used']['photography_styles']
# Format debug output as readable string
debug_output = self._format_debug_info(debug_info)
@@ -467,7 +617,7 @@ class FluxPromptGenerator:
# Correct RETURN_TYPES and add RETURN_NAMES
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("prompt", "t5xxl", "clip_l", "clip_g", "seed_used", "debug_info")
RETURN_NAMES = ("prompt", "t5xxl", "clip_l", "clip_g", "seed", "debug_info")
FUNCTION = "execute"
CATEGORY = "Prompt"
@@ -477,7 +627,7 @@ class FluxPromptGenerator:
seed = kwargs.get('seed', 0) # Extract seed separately if needed elsewhere
prompt_generator = PromptGenerator(seed)
prompt = prompt_generator.generate_prompt(**kwargs)
# Unpack the 6-tuple and return all outputs including debug_info
# Unpack the 6-tuple and return all 6 outputs including seed_used
original_clean, seed_used, t5xxl_clean, clip_l_clean, clip_g_clean, debug_output = prompt
# Concatenate all sections into a single combined prompt
combined_prompt = ", ".join(filter(None, [t5xxl_clean, clip_l_clean, clip_g_clean]))