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:
Aodhan Collins
2026-03-13 02:07:16 +00:00
parent 1b8a798c31
commit 5e4348ebc1
170 changed files with 17367 additions and 9781 deletions

145
models.py
View File

@@ -12,9 +12,27 @@ class Character(db.Model):
default_fields = db.Column(db.JSON, nullable=True)
image_path = db.Column(db.String(255), nullable=True)
active_outfit = db.Column(db.String(100), default='default')
# NEW: Outfit assignment support (Phase 4)
assigned_outfit_ids = db.Column(db.JSON, default=list) # List of outfit_ids from Outfit table
default_outfit_id = db.Column(db.String(100), default='default') # 'default' or specific outfit_id
def get_active_wardrobe(self):
"""Get the currently active wardrobe outfit."""
"""Get the currently active wardrobe outfit.
Priority:
1. If active_outfit is an outfit_id from Outfit table, fetch from Outfit.data['wardrobe']
2. If active_outfit is a key in character's embedded wardrobe, use that
3. Fall back to 'default'
"""
# First check if active_outfit is an outfit_id from assigned outfits
if self.active_outfit and self.active_outfit != 'default':
# Try to get from Outfit table
outfit = Outfit.query.filter_by(outfit_id=self.active_outfit).first()
if outfit and outfit.data:
return outfit.data.get('wardrobe', {})
# Fall back to embedded wardrobe
wardrobe = self.data.get('wardrobe', {})
# Check if wardrobe is nested (new format) or flat (legacy)
if 'default' in wardrobe and isinstance(wardrobe.get('default'), dict):
@@ -25,11 +43,109 @@ class Character(db.Model):
return wardrobe
def get_available_outfits(self):
"""Get list of available outfit names."""
"""Get list of available outfit objects (including embedded and assigned).
Returns list of dicts with keys: outfit_id, name, source ('embedded' or 'assigned')
"""
outfits = [{'outfit_id': 'default', 'name': 'Default', 'source': 'embedded'}]
# Add embedded outfits from character data
wardrobe = self.data.get('wardrobe', {})
if 'default' in wardrobe and isinstance(wardrobe.get('default'), dict):
return list(wardrobe.keys())
return ['default']
for outfit_name in wardrobe.keys():
if outfit_name != 'default':
outfits.append({
'outfit_id': outfit_name,
'name': outfit_name.replace('_', ' ').title(),
'source': 'embedded'
})
# Add assigned outfits from Outfit table
if self.assigned_outfit_ids:
for outfit_id in self.assigned_outfit_ids:
outfit = Outfit.query.filter_by(outfit_id=outfit_id).first()
if outfit:
outfits.append({
'outfit_id': outfit.outfit_id,
'name': outfit.name,
'source': 'assigned'
})
return outfits
def get_outfit_wardrobe(self, outfit_id=None):
"""Get wardrobe data for a specific outfit.
Args:
outfit_id: Outfit ID to get wardrobe for. If None, uses active_outfit.
Returns:
Dict with wardrobe fields, or empty dict if not found.
"""
if outfit_id is None:
outfit_id = self.active_outfit or 'default'
if outfit_id == 'default':
# Return embedded default wardrobe
wardrobe = self.data.get('wardrobe', {})
if 'default' in wardrobe and isinstance(wardrobe.get('default'), dict):
return wardrobe.get('default', {})
return wardrobe
# Try to find in Outfit table
outfit = Outfit.query.filter_by(outfit_id=outfit_id).first()
if outfit and outfit.data:
return outfit.data.get('wardrobe', {})
# Try embedded outfits
wardrobe = self.data.get('wardrobe', {})
if 'default' in wardrobe and isinstance(wardrobe.get('default'), dict):
return wardrobe.get(outfit_id, {})
return {}
def assign_outfit(self, outfit_id):
"""Assign an outfit to this character.
Args:
outfit_id: The outfit_id from the Outfit table to assign.
Returns:
True if assigned, False if already assigned or outfit not found.
"""
current_ids = self.assigned_outfit_ids or []
# Verify outfit exists
outfit = Outfit.query.filter_by(outfit_id=outfit_id).first()
if not outfit:
return False
if outfit_id not in current_ids:
new_ids = list(current_ids)
new_ids.append(outfit_id)
self.assigned_outfit_ids = new_ids
return True
return False
def unassign_outfit(self, outfit_id):
"""Unassign an outfit from this character.
Args:
outfit_id: The outfit_id to unassign.
Returns:
True if unassigned, False if not found in assigned list.
"""
current_ids = self.assigned_outfit_ids or []
if outfit_id in current_ids:
new_ids = list(current_ids)
new_ids.remove(outfit_id)
self.assigned_outfit_ids = new_ids
# Reset active outfit if we just removed it
if self.active_outfit == outfit_id:
self.active_outfit = 'default'
return True
return False
def __repr__(self):
return f'<Character {self.character_id}>'
@@ -40,11 +156,30 @@ class Look(db.Model):
slug = db.Column(db.String(100), unique=True, nullable=False)
filename = db.Column(db.String(255), nullable=True)
name = db.Column(db.String(100), nullable=False)
character_id = db.Column(db.String(100), nullable=True) # linked character
character_id = db.Column(db.String(100), nullable=True) # DEPRECATED: keeping for migration
character_ids = db.Column(db.JSON, default=list) # NEW: List of character_ids
data = db.Column(db.JSON, nullable=False)
default_fields = db.Column(db.JSON, nullable=True)
image_path = db.Column(db.String(255), nullable=True)
def get_linked_characters(self):
"""Get all characters linked to this look."""
if not self.character_ids:
return []
return Character.query.filter(Character.character_id.in_(self.character_ids)).all()
def add_character(self, character_id):
"""Link a character to this look."""
if not self.character_ids:
self.character_ids = []
if character_id not in self.character_ids:
self.character_ids.append(character_id)
def remove_character(self, character_id):
"""Unlink a character from this look."""
if self.character_ids and character_id in self.character_ids:
self.character_ids.remove(character_id)
def __repr__(self):
return f'<Look {self.look_id}>'