Added style browser.

This commit is contained in:
Aodhan Collins
2026-02-20 21:22:53 +00:00
parent 116941673e
commit 8487b177b4
297 changed files with 5311 additions and 80 deletions

725
app.py
View File

@@ -5,8 +5,9 @@ import re
import requests
import random
from flask import Flask, render_template, request, redirect, url_for, flash, session
from flask_session import Session
from werkzeug.utils import secure_filename
from models import db, Character, Settings, Outfit, Action
from models import db, Character, Settings, Outfit, Action, Style
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
@@ -16,12 +17,19 @@ app.config['SECRET_KEY'] = 'dev-key-123'
app.config['CHARACTERS_DIR'] = 'data/characters'
app.config['CLOTHING_DIR'] = 'data/clothing'
app.config['ACTIONS_DIR'] = 'data/actions'
app.config['STYLES_DIR'] = 'data/styles'
app.config['COMFYUI_URL'] = 'http://127.0.0.1:8188'
app.config['ILLUSTRIOUS_MODELS_DIR'] = '/mnt/alexander/AITools/Image Models/Stable-diffusion/Illustrious/'
app.config['NOOB_MODELS_DIR'] = '/mnt/alexander/AITools/Image Models/Stable-diffusion/Noob/'
app.config['LORA_DIR'] = '/mnt/alexander/AITools/Image Models/lora/Illustrious/Looks/'
# Server-side session configuration to avoid cookie size limits
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_FILE_DIR'] = os.path.join(app.config['UPLOAD_FOLDER'], '../flask_session')
app.config['SESSION_PERMANENT'] = False
db.init_app(app)
Session(app)
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
@@ -54,6 +62,16 @@ def get_available_action_loras():
loras.append(f"Illustrious/Poses/{f}")
return sorted(loras)
def get_available_style_loras():
"""Get LoRAs from the Styles directory for style LoRAs."""
styles_lora_dir = '/mnt/alexander/AITools/Image Models/lora/Illustrious/Styles/'
loras = []
if os.path.exists(styles_lora_dir):
for f in os.listdir(styles_lora_dir):
if f.endswith('.safetensors'):
loras.append(f"Illustrious/Styles/{f}")
return sorted(loras)
def get_available_checkpoints():
checkpoints = []
@@ -99,6 +117,7 @@ def build_prompt(data, selected_fields=None, default_fields=None, active_outfit=
defaults = data.get('defaults', {})
action_data = data.get('action', {})
style_data = data.get('style', {})
# Pre-calculate Hand/Glove priority
# Priority: wardrobe gloves > wardrobe hands (outfit) > identity hands (character)
@@ -137,10 +156,17 @@ def build_prompt(data, selected_fields=None, default_fields=None, active_outfit=
if val and is_selected('wardrobe', key):
parts.append(val)
style = data.get('styles', {}).get('aesthetic')
if style and is_selected('styles', 'aesthetic'):
parts.append(f"{style} style")
# 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)
@@ -365,6 +391,63 @@ def sync_actions():
db.session.commit()
def sync_styles():
if not os.path.exists(app.config['STYLES_DIR']):
return
current_ids = []
for filename in os.listdir(app.config['STYLES_DIR']):
if filename.endswith('.json'):
file_path = os.path.join(app.config['STYLES_DIR'], filename)
try:
with open(file_path, 'r') as f:
data = json.load(f)
style_id = data.get('style_id') or filename.replace('.json', '')
current_ids.append(style_id)
# Generate URL-safe slug
slug = re.sub(r'[^a-zA-Z0-9_]', '', style_id)
# Check if style already exists
style = Style.query.filter_by(style_id=style_id).first()
name = data.get('style_name', style_id.replace('_', ' ').title())
if style:
style.data = data
style.name = name
style.slug = slug
style.filename = filename
# Check if cover image still exists
if style.image_path:
full_img_path = os.path.join(app.config['UPLOAD_FOLDER'], style.image_path)
if not os.path.exists(full_img_path):
print(f"Image missing for {style.name}, clearing path.")
style.image_path = None
flag_modified(style, "data")
else:
new_style = Style(
style_id=style_id,
slug=slug,
filename=filename,
name=name,
data=data
)
db.session.add(new_style)
except Exception as e:
print(f"Error importing style {filename}: {e}")
# Remove styles that are no longer in the folder
all_styles = Style.query.all()
for style in all_styles:
if style.style_id not in current_ids:
db.session.delete(style)
db.session.commit()
def call_llm(prompt, system_prompt="You are a creative assistant."):
settings = Settings.query.first()
if not settings or not settings.openrouter_api_key:
@@ -955,6 +1038,7 @@ def finalize_generation(slug, prompt_id):
# Handle actions - always save as preview
relative_path = f"characters/{slug}/{filename}"
session[f'preview_{slug}'] = relative_path
session.modified = True # Ensure session is saved for JSON response
# If action is 'replace', also update the character's cover image immediately
if action == 'replace':
@@ -982,7 +1066,7 @@ def replace_cover_from_preview(slug):
return redirect(url_for('detail', slug=slug))
def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_negative=None, outfit=None, action=None):
def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_negative=None, outfit=None, action=None, style=None):
# 1. Update prompts using replacement to preserve embeddings
workflow["6"]["inputs"]["text"] = workflow["6"]["inputs"]["text"].replace("{{POSITIVE_PROMPT}}", prompts["main"])
@@ -1007,7 +1091,7 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
if checkpoint:
workflow["4"]["inputs"]["ckpt_name"] = checkpoint
# 3. Handle LoRAs - Node 16 for character, Node 17 for outfit, Node 18 for action
# 3. Handle LoRAs - Node 16 for character, Node 17 for outfit, Node 18 for action, Node 19 for style
# Start with direct checkpoint connections
model_source = ["4", 0]
clip_source = ["4", 1]
@@ -1056,6 +1140,21 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
clip_source = ["18", 1]
print(f"Action LoRA: {action_lora_name} @ {action_lora_data.get('lora_weight', 1.0)}")
# Style LoRA (Node 19) - chains from previous LoRA or checkpoint
style_lora_data = style.data.get('lora', {}) if style else {}
style_lora_name = style_lora_data.get('lora_name')
if style_lora_name and "19" in workflow:
workflow["19"]["inputs"]["lora_name"] = style_lora_name
workflow["19"]["inputs"]["strength_model"] = style_lora_data.get('lora_weight', 1.0)
workflow["19"]["inputs"]["strength_clip"] = style_lora_data.get('lora_weight', 1.0)
# Chain from previous source
workflow["19"]["inputs"]["model"] = model_source
workflow["19"]["inputs"]["clip"] = clip_source
model_source = ["19", 0]
clip_source = ["19", 1]
print(f"Style LoRA: {style_lora_name} @ {style_lora_data.get('lora_weight', 1.0)}")
# Apply connections to all model/clip consumers
workflow["3"]["inputs"]["model"] = model_source
workflow["11"]["inputs"]["model"] = model_source
@@ -1224,6 +1323,32 @@ def save_defaults(slug):
flash('Default prompt selection saved for this character!')
return redirect(url_for('detail', slug=slug))
@app.route('/get_missing_outfits')
def get_missing_outfits():
missing = Outfit.query.filter((Outfit.image_path == None) | (Outfit.image_path == '')).all()
return {'missing': [{'slug': o.slug, 'name': o.name} for o in missing]}
@app.route('/clear_all_outfit_covers', methods=['POST'])
def clear_all_outfit_covers():
outfits = Outfit.query.all()
for outfit in outfits:
outfit.image_path = None
db.session.commit()
return {'success': True}
@app.route('/get_missing_actions')
def get_missing_actions():
missing = Action.query.filter((Action.image_path == None) | (Action.image_path == '')).all()
return {'missing': [{'slug': a.slug, 'name': a.name} for a in missing]}
@app.route('/clear_all_action_covers', methods=['POST'])
def clear_all_action_covers():
actions = Action.query.all()
for action in actions:
action.image_path = None
db.session.commit()
return {'success': True}
# ============ OUTFIT ROUTES ============
@app.route('/outfits')
@@ -1483,6 +1608,7 @@ def finalize_outfit_generation(slug, prompt_id):
# Always save as preview
relative_path = f"outfits/{slug}/{filename}"
session[f'preview_outfit_{slug}'] = relative_path
session.modified = True # Ensure session is saved for JSON response
return {'success': True, 'image_url': url_for('static', filename=f'uploads/{relative_path}')}
@@ -1705,63 +1831,6 @@ def clone_outfit(slug):
flash(f'Outfit cloned as "{new_id}"!')
return redirect(url_for('outfit_detail', slug=new_slug))
def sync_actions():
if not os.path.exists(app.config['ACTIONS_DIR']):
return
current_ids = []
for filename in os.listdir(app.config['ACTIONS_DIR']):
if filename.endswith('.json'):
file_path = os.path.join(app.config['ACTIONS_DIR'], filename)
try:
with open(file_path, 'r') as f:
data = json.load(f)
action_id = data.get('action_id') or filename.replace('.json', '')
current_ids.append(action_id)
# Generate URL-safe slug
slug = re.sub(r'[^a-zA-Z0-9_]', '', action_id)
# Check if action already exists
action = Action.query.filter_by(action_id=action_id).first()
name = data.get('action_name', action_id.replace('_', ' ').title())
if action:
action.data = data
action.name = name
action.slug = slug
action.filename = filename
# Check if cover image still exists
if action.image_path:
full_img_path = os.path.join(app.config['UPLOAD_FOLDER'], action.image_path)
if not os.path.exists(full_img_path):
print(f"Image missing for {action.name}, clearing path.")
action.image_path = None
flag_modified(action, "data")
else:
new_action = Action(
action_id=action_id,
slug=slug,
filename=filename,
name=name,
data=data
)
db.session.add(new_action)
except Exception as e:
print(f"Error importing action {filename}: {e}")
# Remove actions that are no longer in the folder
all_actions = Action.query.all()
for action in all_actions:
if action.action_id not in current_ids:
db.session.delete(action)
db.session.commit()
# ============ ACTION ROUTES ============
@app.route('/actions')
@@ -2070,6 +2139,7 @@ def finalize_action_generation(slug, prompt_id):
# Always save as preview
relative_path = f"actions/{slug}/{filename}"
session[f'preview_action_{slug}'] = relative_path
session.modified = True # Ensure session is saved for JSON response
return {'success': True, 'image_url': url_for('static', filename=f'uploads/{relative_path}')}
@@ -2237,6 +2307,534 @@ def clone_action(slug):
flash(f'Action cloned as "{new_id}"!')
return redirect(url_for('action_detail', slug=new_slug))
# ============ STYLE ROUTES ============
@app.route('/styles')
def styles_index():
styles = Style.query.order_by(Style.name).all()
return render_template('styles/index.html', styles=styles)
@app.route('/styles/rescan', methods=['POST'])
def rescan_styles():
sync_styles()
flash('Database synced with style files.')
return redirect(url_for('styles_index'))
@app.route('/style/<path:slug>')
def style_detail(slug):
style = Style.query.filter_by(slug=slug).first_or_404()
characters = Character.query.order_by(Character.name).all()
# Load state from session
preferences = session.get(f'prefs_style_{slug}')
preview_image = session.get(f'preview_style_{slug}')
selected_character = session.get(f'char_style_{slug}')
return render_template('styles/detail.html', style=style, characters=characters,
preferences=preferences, preview_image=preview_image,
selected_character=selected_character)
@app.route('/style/<path:slug>/edit', methods=['GET', 'POST'])
def edit_style(slug):
style = Style.query.filter_by(slug=slug).first_or_404()
loras = get_available_style_loras()
if request.method == 'POST':
try:
# 1. Update basic fields
style.name = request.form.get('style_name')
# 2. Rebuild the data dictionary
new_data = style.data.copy()
new_data['style_name'] = style.name
# Update style section
if 'style' in new_data:
for key in new_data['style'].keys():
form_key = f"style_{key}"
if form_key in request.form:
new_data['style'][key] = request.form.get(form_key)
# Update lora section
if 'lora' in new_data:
for key in new_data['lora'].keys():
form_key = f"lora_{key}"
if form_key in request.form:
val = request.form.get(form_key)
if key == 'lora_weight':
try: val = float(val)
except: val = 1.0
new_data['lora'][key] = val
style.data = new_data
flag_modified(style, "data")
# 3. Write back to JSON file
style_file = style.filename or f"{re.sub(r'[^a-zA-Z0-9_]', '', style.style_id)}.json"
file_path = os.path.join(app.config['STYLES_DIR'], style_file)
with open(file_path, 'w') as f:
json.dump(new_data, f, indent=2)
db.session.commit()
flash('Style updated successfully!')
return redirect(url_for('style_detail', slug=slug))
except Exception as e:
print(f"Edit error: {e}")
flash(f"Error saving changes: {str(e)}")
return render_template('styles/edit.html', style=style, loras=loras)
@app.route('/style/<path:slug>/upload', methods=['POST'])
def upload_style_image(slug):
style = Style.query.filter_by(slug=slug).first_or_404()
if 'image' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['image']
if file.filename == '':
flash('No selected file')
return redirect(request.url)
if file and allowed_file(file.filename):
# Create style subfolder
style_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"styles/{slug}")
os.makedirs(style_folder, exist_ok=True)
filename = secure_filename(file.filename)
file_path = os.path.join(style_folder, filename)
file.save(file_path)
# Store relative path in DB
style.image_path = f"styles/{slug}/{filename}"
db.session.commit()
flash('Image uploaded successfully!')
return redirect(url_for('style_detail', slug=slug))
def _queue_style_generation(style_obj, character=None, selected_fields=None, client_id=None):
if character:
combined_data = character.data.copy()
combined_data['character_id'] = character.character_id
combined_data['style'] = style_obj.data.get('style', {})
# Merge style lora triggers if present
style_lora = style_obj.data.get('lora', {})
if style_lora.get('lora_triggers'):
if 'lora' not in combined_data: combined_data['lora'] = {}
combined_data['lora']['lora_triggers'] = f"{combined_data['lora'].get('lora_triggers', '')}, {style_lora['lora_triggers']}"
# Merge character identity and wardrobe fields into selected_fields
if selected_fields:
# Add character identity fields to selection if not already present
for key in ['base_specs', 'hair', 'eyes', 'hands', 'arms', 'torso', 'pelvis', 'legs', 'feet', 'extra']:
if character.data.get('identity', {}).get(key):
field_key = f'identity::{key}'
if field_key not in selected_fields:
selected_fields.append(field_key)
# Always include character name
if 'special::name' not in selected_fields:
selected_fields.append('special::name')
# Add active wardrobe fields
wardrobe = character.get_active_wardrobe()
for key in ['full_body', 'headwear', 'top', 'bottom', 'legwear', 'footwear', 'hands', 'gloves', 'accessories']:
if wardrobe.get(key):
field_key = f'wardrobe::{key}'
if field_key not in selected_fields:
selected_fields.append(field_key)
else:
# Auto-include essential character fields
selected_fields = []
for key in ['base_specs', 'hair', 'eyes']:
if character.data.get('identity', {}).get(key):
selected_fields.append(f'identity::{key}')
selected_fields.append('special::name')
# Add active wardrobe
wardrobe = character.get_active_wardrobe()
for key in ['full_body', 'top', 'bottom']:
if wardrobe.get(key):
selected_fields.append(f'wardrobe::{key}')
# Add style fields
selected_fields.extend(['style::artist_name', 'style::artistic_style', 'lora::lora_triggers'])
default_fields = style_obj.default_fields
active_outfit = character.active_outfit
else:
combined_data = {
'character_id': style_obj.style_id,
'style': style_obj.data.get('style', {}),
'lora': style_obj.data.get('lora', {}),
'tags': style_obj.data.get('tags', [])
}
if not selected_fields:
selected_fields = ['style::artist_name', 'style::artistic_style', 'lora::lora_triggers']
default_fields = style_obj.default_fields
active_outfit = 'default'
with open('comfy_workflow.json', 'r') as f:
workflow = json.load(f)
prompts = build_prompt(combined_data, selected_fields, default_fields, active_outfit=active_outfit)
if character:
primary_color = character.data.get('styles', {}).get('primary_color', '')
if primary_color:
prompts["main"] = f"{prompts['main']}, {primary_color} simple background"
else:
prompts["main"] = f"{prompts['main']}, simple background"
else:
prompts["main"] = f"{prompts['main']}, simple background"
workflow = _prepare_workflow(workflow, character, prompts, style=style_obj)
return queue_prompt(workflow, client_id=client_id)
@app.route('/style/<path:slug>/generate', methods=['POST'])
def generate_style_image(slug):
style_obj = Style.query.filter_by(slug=slug).first_or_404()
try:
# Get action type
action = request.form.get('action', 'preview')
client_id = request.form.get('client_id')
# Get selected fields
selected_fields = request.form.getlist('include_field')
# Get selected character (if any)
character_slug = request.form.get('character_slug', '')
character = None
if character_slug == '__random__':
all_characters = Character.query.all()
if all_characters:
character = random.choice(all_characters)
character_slug = character.slug
elif character_slug:
character = Character.query.filter_by(slug=character_slug).first()
# Save preferences
session[f'char_style_{slug}'] = character_slug
session[f'prefs_style_{slug}'] = selected_fields
# Queue generation using helper
prompt_response = _queue_style_generation(style_obj, character, selected_fields, client_id=client_id)
if 'prompt_id' not in prompt_response:
raise Exception(f"ComfyUI failed: {prompt_response.get('error', 'Unknown error')}")
prompt_id = prompt_response['prompt_id']
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return {'status': 'queued', 'prompt_id': prompt_id}
return redirect(url_for('style_detail', slug=slug))
except Exception as e:
print(f"Generation error: {e}")
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return {'error': str(e)}, 500
flash(f"Error during generation: {str(e)}")
return redirect(url_for('style_detail', slug=slug))
@app.route('/style/<path:slug>/finalize_generation/<prompt_id>', methods=['POST'])
def finalize_style_generation(slug, prompt_id):
style_obj = Style.query.filter_by(slug=slug).first_or_404()
action = request.form.get('action', 'preview')
try:
history = get_history(prompt_id)
if prompt_id not in history:
return {'error': 'History not found'}, 404
outputs = history[prompt_id]['outputs']
for node_id in outputs:
if 'images' in outputs[node_id]:
image_info = outputs[node_id]['images'][0]
image_data = get_image(image_info['filename'], image_info['subfolder'], image_info['type'])
style_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"styles/{slug}")
os.makedirs(style_folder, exist_ok=True)
filename = f"gen_{int(time.time())}.png"
file_path = os.path.join(style_folder, filename)
with open(file_path, 'wb') as f:
f.write(image_data)
relative_path = f"styles/{slug}/{filename}"
session[f'preview_style_{slug}'] = relative_path
session.modified = True # Ensure session is saved for JSON response
# If action is 'replace', also update the style's cover image immediately
if action == 'replace':
style_obj.image_path = relative_path
db.session.commit()
return {'success': True, 'image_url': url_for('static', filename=f'uploads/{relative_path}')}
return {'error': 'No image found in output'}, 404
except Exception as e:
print(f"Finalize error: {e}")
return {'error': str(e)}, 500
@app.route('/style/<path:slug>/save_defaults', methods=['POST'])
def save_style_defaults(slug):
style = Style.query.filter_by(slug=slug).first_or_404()
selected_fields = request.form.getlist('include_field')
style.default_fields = selected_fields
db.session.commit()
flash('Default prompt selection saved for this style!')
return redirect(url_for('style_detail', slug=slug))
@app.route('/style/<path:slug>/replace_cover_from_preview', methods=['POST'])
def replace_style_cover_from_preview(slug):
style = Style.query.filter_by(slug=slug).first_or_404()
preview_path = session.get(f'preview_style_{slug}')
if preview_path:
style.image_path = preview_path
db.session.commit()
flash('Cover image updated from preview!')
else:
flash('No preview image available', 'error')
return redirect(url_for('style_detail', slug=slug))
@app.route('/get_missing_styles')
def get_missing_styles():
missing = Style.query.filter((Style.image_path == None) | (Style.image_path == '')).all()
return {'missing': [{'slug': s.slug, 'name': s.name} for s in missing]}
@app.route('/clear_all_style_covers', methods=['POST'])
def clear_all_style_covers():
styles = Style.query.all()
for style in styles:
style.image_path = None
db.session.commit()
return {'success': True}
@app.route('/styles/generate_missing', methods=['POST'])
def generate_missing_styles():
def get_missing_count():
return Style.query.filter((Style.image_path == None) | (Style.image_path == '')).count()
if get_missing_count() == 0:
flash("No styles missing cover images.")
return redirect(url_for('styles_index'))
# Get all characters once to pick from
all_characters = Character.query.all()
if not all_characters:
flash("No characters available to preview styles with.", "error")
return redirect(url_for('styles_index'))
success_count = 0
while get_missing_count() > 0:
style_obj = Style.query.filter((Style.image_path == None) | (Style.image_path == '')).order_by(Style.name).first()
if not style_obj: break
# Pick a random character for each style for variety
character = random.choice(all_characters)
style_slug = style_obj.slug
try:
print(f"Batch generating style: {style_obj.name} with character {character.name}")
prompt_response = _queue_style_generation(style_obj, character=character)
prompt_id = prompt_response['prompt_id']
max_retries = 120
while max_retries > 0:
history = get_history(prompt_id)
if prompt_id in history:
outputs = history[prompt_id]['outputs']
for node_id in outputs:
if 'images' in outputs[node_id]:
image_info = outputs[node_id]['images'][0]
image_data = get_image(image_info['filename'], image_info['subfolder'], image_info['type'])
style_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"styles/{style_slug}")
os.makedirs(style_folder, exist_ok=True)
filename = f"gen_{int(time.time())}.png"
file_path = os.path.join(style_folder, filename)
with open(file_path, 'wb') as f:
f.write(image_data)
style_to_update = Style.query.filter_by(slug=style_slug).first()
if style_to_update:
style_to_update.image_path = f"styles/{style_slug}/{filename}"
db.session.commit()
success_count += 1
break
break
time.sleep(2)
max_retries -= 1
except Exception as e:
print(f"Error generating for style {style_obj.name}: {e}")
db.session.rollback()
flash(f"Batch style generation complete. Generated {success_count} images.")
return redirect(url_for('styles_index'))
@app.route('/styles/bulk_create', methods=['POST'])
def bulk_create_styles_from_loras():
styles_lora_dir = '/mnt/alexander/AITools/Image Models/lora/Illustrious/Styles/'
if not os.path.exists(styles_lora_dir):
flash('Styles LoRA directory not found.', 'error')
return redirect(url_for('styles_index'))
created_count = 0
skipped_count = 0
for filename in os.listdir(styles_lora_dir):
if filename.endswith('.safetensors'):
# Generate style_id and style_name from filename
# Remove extension
name_base = filename.rsplit('.', 1)[0]
# Replace special characters with underscores for ID
style_id = re.sub(r'[^a-zA-Z0-9_]', '_', name_base.lower())
# Format name: replace underscores/dashes with spaces and title case
style_name = re.sub(r'[^a-zA-Z0-9]+', ' ', name_base).title()
# Check if JSON file already exists
json_filename = f"{style_id}.json"
json_path = os.path.join(app.config['STYLES_DIR'], json_filename)
if os.path.exists(json_path):
skipped_count += 1
continue
# Create JSON content
style_data = {
"style_id": style_id,
"style_name": style_name,
"style": {
"artist_name": "",
"artistic_style": ""
},
"lora": {
"lora_name": f"Illustrious/Styles/{filename}",
"lora_weight": 1.0,
"lora_triggers": name_base # Default to filename base as trigger
}
}
try:
with open(json_path, 'w') as f:
json.dump(style_data, f, indent=2)
created_count += 1
except Exception as e:
print(f"Error creating style for {filename}: {e}")
if created_count > 0:
sync_styles()
flash(f'Successfully created {created_count} new styles from LoRAs. (Skipped {skipped_count} existing)')
else:
flash(f'No new styles created. {skipped_count} existing styles found.')
return redirect(url_for('styles_index'))
@app.route('/style/create', methods=['GET', 'POST'])
def create_style():
if request.method == 'POST':
name = request.form.get('name')
slug = request.form.get('filename', '').strip()
if not slug:
slug = re.sub(r'[^a-zA-Z0-9]+', '_', name.lower()).strip('_')
safe_slug = re.sub(r'[^a-zA-Z0-9_]', '', slug)
if not safe_slug:
safe_slug = 'style'
base_slug = safe_slug
counter = 1
while os.path.exists(os.path.join(app.config['STYLES_DIR'], f"{safe_slug}.json")):
safe_slug = f"{base_slug}_{counter}"
counter += 1
style_data = {
"style_id": safe_slug,
"style_name": name,
"style": {
"artist_name": "",
"artistic_style": ""
},
"lora": {
"lora_name": "",
"lora_weight": 1.0,
"lora_triggers": ""
}
}
try:
file_path = os.path.join(app.config['STYLES_DIR'], f"{safe_slug}.json")
with open(file_path, 'w') as f:
json.dump(style_data, f, indent=2)
new_style = Style(
style_id=safe_slug, slug=safe_slug, filename=f"{safe_slug}.json",
name=name, data=style_data
)
db.session.add(new_style)
db.session.commit()
flash('Style created successfully!')
return redirect(url_for('style_detail', slug=safe_slug))
except Exception as e:
print(f"Save error: {e}")
flash(f"Failed to create style: {e}")
return redirect(request.url)
return render_template('styles/create.html')
@app.route('/style/<path:slug>/clone', methods=['POST'])
def clone_style(slug):
style = Style.query.filter_by(slug=slug).first_or_404()
base_id = style.style_id
import re
match = re.match(r'^(.+?)_(\d+)$', base_id)
if match:
base_name = match.group(1)
current_num = int(match.group(2))
else:
base_name = base_id
current_num = 1
next_num = current_num + 1
while True:
new_id = f"{base_name}_{next_num:02d}"
new_filename = f"{new_id}.json"
new_path = os.path.join(app.config['STYLES_DIR'], new_filename)
if not os.path.exists(new_path):
break
next_num += 1
new_data = style.data.copy()
new_data['style_id'] = new_id
new_data['style_name'] = f"{style.name} (Copy)"
with open(new_path, 'w') as f:
json.dump(new_data, f, indent=2)
new_slug = re.sub(r'[^a-zA-Z0-9_]', '', new_id)
new_style = Style(
style_id=new_id, slug=new_slug, filename=new_filename,
name=new_data['style_name'], data=new_data
)
db.session.add(new_style)
db.session.commit()
flash(f'Style cloned as "{new_id}"!')
return redirect(url_for('style_detail', slug=new_slug))
if __name__ == '__main__':
with app.app_context():
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
@@ -2269,4 +2867,5 @@ if __name__ == '__main__':
sync_characters()
sync_outfits()
sync_actions()
sync_styles()
app.run(debug=True, port=5000)