Add extra prompts, endless generation, random character default, and small fixes
- Add extra positive/negative prompt textareas to all 9 detail pages with session persistence - Add Endless generation button to all detail pages (continuous preview generation until stopped) - Default character selector to "Random Character" on all secondary detail pages - Fix queue clear endpoint (remove spurious auth check) - Refactor app.py into routes/ and services/ modules - Update CLAUDE.md with new architecture documentation - Various data file updates and cleanup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
274
services/prompts.py
Normal file
274
services/prompts.py
Normal file
@@ -0,0 +1,274 @@
|
||||
import re
|
||||
from models import db, Character
|
||||
from utils import _IDENTITY_KEYS, _WARDROBE_KEYS, parse_orientation
|
||||
|
||||
|
||||
def _dedup_tags(prompt_str):
|
||||
"""Remove duplicate tags from a comma-separated prompt string, preserving first-occurrence order."""
|
||||
seen = set()
|
||||
result = []
|
||||
for tag in prompt_str.split(','):
|
||||
t = tag.strip()
|
||||
if t and t.lower() not in seen:
|
||||
seen.add(t.lower())
|
||||
result.append(t)
|
||||
return ', '.join(result)
|
||||
|
||||
|
||||
def _cross_dedup_prompts(positive, negative):
|
||||
"""Remove tags shared between positive and negative prompts.
|
||||
|
||||
Repeatedly strips the first occurrence from each side until the tag exists
|
||||
on only one side. Equal counts cancel out completely; any excess on one side
|
||||
retains the remainder, allowing deliberate overrides (e.g. adding a tag twice
|
||||
in the positive while it appears once in the negative leaves one copy positive).
|
||||
"""
|
||||
def parse_tags(s):
|
||||
return [t.strip() for t in s.split(',') if t.strip()]
|
||||
|
||||
pos_tags = parse_tags(positive)
|
||||
neg_tags = parse_tags(negative)
|
||||
|
||||
shared = {t.lower() for t in pos_tags} & {t.lower() for t in neg_tags}
|
||||
for tag_lower in shared:
|
||||
while (
|
||||
any(t.lower() == tag_lower for t in pos_tags) and
|
||||
any(t.lower() == tag_lower for t in neg_tags)
|
||||
):
|
||||
pos_tags.pop(next(i for i, t in enumerate(pos_tags) if t.lower() == tag_lower))
|
||||
neg_tags.pop(next(i for i, t in enumerate(neg_tags) if t.lower() == tag_lower))
|
||||
|
||||
return ', '.join(pos_tags), ', '.join(neg_tags)
|
||||
|
||||
|
||||
def _resolve_character(character_slug):
|
||||
"""Resolve a character_slug string (possibly '__random__') to a Character instance."""
|
||||
if character_slug == '__random__':
|
||||
return Character.query.order_by(db.func.random()).first()
|
||||
if character_slug:
|
||||
return Character.query.filter_by(slug=character_slug).first()
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_character_fields(character, selected_fields, include_wardrobe=True, include_defaults=False):
|
||||
"""Mutate selected_fields in place to include essential character identity/wardrobe/name keys.
|
||||
|
||||
include_wardrobe — also inject active wardrobe keys (default True)
|
||||
include_defaults — also inject defaults::expression and defaults::pose (for outfit/look previews)
|
||||
"""
|
||||
identity = character.data.get('identity', {})
|
||||
for key in _IDENTITY_KEYS:
|
||||
if identity.get(key):
|
||||
field_key = f'identity::{key}'
|
||||
if field_key not in selected_fields:
|
||||
selected_fields.append(field_key)
|
||||
if include_defaults:
|
||||
for key in ['expression', 'pose']:
|
||||
if character.data.get('defaults', {}).get(key):
|
||||
field_key = f'defaults::{key}'
|
||||
if field_key not in selected_fields:
|
||||
selected_fields.append(field_key)
|
||||
if 'special::name' not in selected_fields:
|
||||
selected_fields.append('special::name')
|
||||
if include_wardrobe:
|
||||
wardrobe = character.get_active_wardrobe()
|
||||
for key in _WARDROBE_KEYS:
|
||||
if wardrobe.get(key):
|
||||
field_key = f'wardrobe::{key}'
|
||||
if field_key not in selected_fields:
|
||||
selected_fields.append(field_key)
|
||||
|
||||
|
||||
def _append_background(prompts, character=None):
|
||||
"""Append a (color-prefixed) simple background tag to prompts['main']."""
|
||||
primary_color = character.data.get('styles', {}).get('primary_color', '') if character else ''
|
||||
bg = f"{primary_color} simple background" if primary_color else "simple background"
|
||||
prompts['main'] = f"{prompts['main']}, {bg}"
|
||||
|
||||
|
||||
def build_prompt(data, selected_fields=None, default_fields=None, active_outfit='default'):
|
||||
def is_selected(section, key):
|
||||
# Priority:
|
||||
# 1. Manual selection from form (if list is not empty)
|
||||
# 2. Default fields (saved per character)
|
||||
# 3. Select all (fallback)
|
||||
if selected_fields is not None and len(selected_fields) > 0:
|
||||
return f"{section}::{key}" in selected_fields
|
||||
if default_fields:
|
||||
return f"{section}::{key}" in default_fields
|
||||
return True
|
||||
|
||||
identity = data.get('identity', {})
|
||||
|
||||
# Get wardrobe - handle both new nested format and legacy flat format
|
||||
wardrobe_data = data.get('wardrobe', {})
|
||||
if 'default' in wardrobe_data and isinstance(wardrobe_data.get('default'), dict):
|
||||
# New nested format - get active outfit
|
||||
wardrobe = wardrobe_data.get(active_outfit or 'default', wardrobe_data.get('default', {}))
|
||||
else:
|
||||
# Legacy flat format
|
||||
wardrobe = wardrobe_data
|
||||
|
||||
defaults = data.get('defaults', {})
|
||||
action_data = data.get('action', {})
|
||||
style_data = data.get('style', {})
|
||||
participants = data.get('participants', {})
|
||||
|
||||
# Pre-calculate Hand/Glove priority
|
||||
# Priority: wardrobe gloves > wardrobe hands (outfit) > identity hands (character)
|
||||
hand_val = ""
|
||||
if wardrobe.get('gloves') and is_selected('wardrobe', 'gloves'):
|
||||
hand_val = wardrobe.get('gloves')
|
||||
elif wardrobe.get('hands') and is_selected('wardrobe', 'hands'):
|
||||
hand_val = wardrobe.get('hands')
|
||||
elif identity.get('hands') and is_selected('identity', 'hands'):
|
||||
hand_val = identity.get('hands')
|
||||
|
||||
# 1. Main Prompt
|
||||
parts = []
|
||||
|
||||
# Handle participants logic
|
||||
if participants:
|
||||
if participants.get('solo_focus') == 'true':
|
||||
parts.append('(solo focus:1.2)')
|
||||
|
||||
orientation = participants.get('orientation', '')
|
||||
if orientation:
|
||||
parts.extend(parse_orientation(orientation))
|
||||
else:
|
||||
# Default behavior
|
||||
parts.append("(solo:1.2)")
|
||||
|
||||
# Use character_id (underscores to spaces) for tags compatibility
|
||||
char_tag = data.get('character_id', '').replace('_', ' ')
|
||||
if char_tag and is_selected('special', 'name'):
|
||||
parts.append(char_tag)
|
||||
|
||||
for key in ['base_specs', 'hair', 'eyes', 'extra']:
|
||||
val = identity.get(key)
|
||||
if val and is_selected('identity', key):
|
||||
# Filter out conflicting tags if participants data is present
|
||||
if participants and key == 'base_specs':
|
||||
# Remove 1girl, 1boy, solo, etc.
|
||||
val = re.sub(r'\b(1girl|1boy|solo)\b', '', val).replace(', ,', ',').strip(', ')
|
||||
parts.append(val)
|
||||
|
||||
# Add defaults (expression, pose, scene)
|
||||
for key in ['expression', 'pose', 'scene']:
|
||||
val = defaults.get(key)
|
||||
if val and is_selected('defaults', key):
|
||||
parts.append(val)
|
||||
|
||||
# Add hand priority value to main prompt
|
||||
if hand_val:
|
||||
parts.append(hand_val)
|
||||
|
||||
for key in ['full_body', 'top', 'bottom', 'headwear', 'legwear', 'footwear', 'accessories']:
|
||||
val = wardrobe.get(key)
|
||||
if val and is_selected('wardrobe', key):
|
||||
parts.append(val)
|
||||
|
||||
# Standard character styles
|
||||
char_aesthetic = data.get('styles', {}).get('aesthetic')
|
||||
if char_aesthetic and is_selected('styles', 'aesthetic'):
|
||||
parts.append(f"{char_aesthetic} style")
|
||||
|
||||
# New Styles Gallery logic
|
||||
if style_data.get('artist_name') and is_selected('style', 'artist_name'):
|
||||
parts.append(f"by {style_data['artist_name']}")
|
||||
if style_data.get('artistic_style') and is_selected('style', 'artistic_style'):
|
||||
parts.append(style_data['artistic_style'])
|
||||
|
||||
tags = data.get('tags', [])
|
||||
if tags and is_selected('special', 'tags'):
|
||||
parts.extend(tags)
|
||||
|
||||
lora = data.get('lora', {})
|
||||
if lora.get('lora_triggers') and is_selected('lora', 'lora_triggers'):
|
||||
parts.append(lora.get('lora_triggers'))
|
||||
|
||||
# 2. Face Prompt: Tag, Eyes, Expression, Headwear, Action details
|
||||
face_parts = []
|
||||
if char_tag and is_selected('special', 'name'): face_parts.append(char_tag)
|
||||
if identity.get('eyes') and is_selected('identity', 'eyes'): face_parts.append(identity.get('eyes'))
|
||||
if defaults.get('expression') and is_selected('defaults', 'expression'): face_parts.append(defaults.get('expression'))
|
||||
if wardrobe.get('headwear') and is_selected('wardrobe', 'headwear'): face_parts.append(wardrobe.get('headwear'))
|
||||
|
||||
# Add specific Action expression details if available
|
||||
if action_data.get('head') and is_selected('action', 'head'): face_parts.append(action_data.get('head'))
|
||||
if action_data.get('eyes') and is_selected('action', 'eyes'): face_parts.append(action_data.get('eyes'))
|
||||
|
||||
# 3. Hand Prompt: Hand value (Gloves or Hands), Action details
|
||||
hand_parts = [hand_val] if hand_val else []
|
||||
if action_data.get('arms') and is_selected('action', 'arms'): hand_parts.append(action_data.get('arms'))
|
||||
if action_data.get('hands') and is_selected('action', 'hands'): hand_parts.append(action_data.get('hands'))
|
||||
|
||||
return {
|
||||
"main": _dedup_tags(", ".join(parts)),
|
||||
"face": _dedup_tags(", ".join(face_parts)),
|
||||
"hand": _dedup_tags(", ".join(hand_parts))
|
||||
}
|
||||
|
||||
|
||||
def build_extras_prompt(actions, outfits, scenes, styles, detailers):
|
||||
"""Combine positive prompt text from all selected category items."""
|
||||
parts = []
|
||||
|
||||
for action in actions:
|
||||
data = action.data
|
||||
lora = data.get('lora', {})
|
||||
if lora.get('lora_triggers'):
|
||||
parts.append(lora['lora_triggers'])
|
||||
parts.extend(data.get('tags', []))
|
||||
for key in ['full_body', 'additional']:
|
||||
val = data.get('action', {}).get(key)
|
||||
if val:
|
||||
parts.append(val)
|
||||
|
||||
for outfit in outfits:
|
||||
data = outfit.data
|
||||
wardrobe = data.get('wardrobe', {})
|
||||
for key in ['full_body', 'headwear', 'top', 'bottom', 'legwear', 'footwear', 'hands', 'accessories']:
|
||||
val = wardrobe.get(key)
|
||||
if val:
|
||||
parts.append(val)
|
||||
lora = data.get('lora', {})
|
||||
if lora.get('lora_triggers'):
|
||||
parts.append(lora['lora_triggers'])
|
||||
parts.extend(data.get('tags', []))
|
||||
|
||||
for scene in scenes:
|
||||
data = scene.data
|
||||
scene_fields = data.get('scene', {})
|
||||
for key in ['background', 'foreground', 'lighting']:
|
||||
val = scene_fields.get(key)
|
||||
if val:
|
||||
parts.append(val)
|
||||
lora = data.get('lora', {})
|
||||
if lora.get('lora_triggers'):
|
||||
parts.append(lora['lora_triggers'])
|
||||
parts.extend(data.get('tags', []))
|
||||
|
||||
for style in styles:
|
||||
data = style.data
|
||||
style_fields = data.get('style', {})
|
||||
if style_fields.get('artist_name'):
|
||||
parts.append(f"by {style_fields['artist_name']}")
|
||||
if style_fields.get('artistic_style'):
|
||||
parts.append(style_fields['artistic_style'])
|
||||
lora = data.get('lora', {})
|
||||
if lora.get('lora_triggers'):
|
||||
parts.append(lora['lora_triggers'])
|
||||
|
||||
for detailer in detailers:
|
||||
data = detailer.data
|
||||
prompt = data.get('prompt', '')
|
||||
if isinstance(prompt, list):
|
||||
parts.extend(p for p in prompt if p)
|
||||
elif prompt:
|
||||
parts.append(prompt)
|
||||
lora = data.get('lora', {})
|
||||
if lora.get('lora_triggers'):
|
||||
parts.append(lora['lora_triggers'])
|
||||
|
||||
return ", ".join(p for p in parts if p)
|
||||
Reference in New Issue
Block a user