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:
340
routes/transfer.py
Normal file
340
routes/transfer.py
Normal file
@@ -0,0 +1,340 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from flask import render_template, request, redirect, url_for, flash, session, current_app
|
||||
from models import db, Character, Look, Outfit, Action, Style, Scene, Detailer, Settings
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from utils import _LORA_DEFAULTS
|
||||
from services.llm import load_prompt, call_llm
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
_RESOURCE_TRANSFER_MAP = {
|
||||
'looks': {
|
||||
'model_class': Look,
|
||||
'id_field': 'look_id',
|
||||
'target_dir': 'LOOKS_DIR',
|
||||
'index_route': 'looks_index',
|
||||
'detail_route': 'look_detail',
|
||||
'name_key': 'look_name',
|
||||
'id_key': 'look_id',
|
||||
},
|
||||
'outfits': {
|
||||
'model_class': Outfit,
|
||||
'id_field': 'outfit_id',
|
||||
'target_dir': 'CLOTHING_DIR',
|
||||
'index_route': 'outfits_index',
|
||||
'detail_route': 'outfit_detail',
|
||||
'name_key': 'outfit_name',
|
||||
'id_key': 'outfit_id',
|
||||
},
|
||||
'actions': {
|
||||
'model_class': Action,
|
||||
'id_field': 'action_id',
|
||||
'target_dir': 'ACTIONS_DIR',
|
||||
'index_route': 'actions_index',
|
||||
'detail_route': 'action_detail',
|
||||
'name_key': 'action_name',
|
||||
'id_key': 'action_id',
|
||||
},
|
||||
'styles': {
|
||||
'model_class': Style,
|
||||
'id_field': 'style_id',
|
||||
'target_dir': 'STYLES_DIR',
|
||||
'index_route': 'styles_index',
|
||||
'detail_route': 'style_detail',
|
||||
'name_key': 'style_name',
|
||||
'id_key': 'style_id',
|
||||
},
|
||||
'scenes': {
|
||||
'model_class': Scene,
|
||||
'id_field': 'scene_id',
|
||||
'target_dir': 'SCENES_DIR',
|
||||
'index_route': 'scenes_index',
|
||||
'detail_route': 'scene_detail',
|
||||
'name_key': 'scene_name',
|
||||
'id_key': 'scene_id',
|
||||
},
|
||||
'detailers': {
|
||||
'model_class': Detailer,
|
||||
'id_field': 'detailer_id',
|
||||
'target_dir': 'DETAILERS_DIR',
|
||||
'index_route': 'detailers_index',
|
||||
'detail_route': 'detailer_detail',
|
||||
'name_key': 'detailer_name',
|
||||
'id_key': 'detailer_id',
|
||||
},
|
||||
}
|
||||
|
||||
_TRANSFER_TARGET_CATEGORIES = ['looks', 'outfits', 'actions', 'styles', 'scenes', 'detailers']
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
|
||||
def _create_minimal_template(target_category, slug, name, source_data, source_name='resource'):
|
||||
"""Create a minimal template for the target category with basic fields."""
|
||||
templates = {
|
||||
'looks': {
|
||||
'look_id': slug,
|
||||
'look_name': name,
|
||||
'positive': source_data.get('positive', ''),
|
||||
'negative': source_data.get('negative', ''),
|
||||
'lora': source_data.get('lora', {'lora_name': '', 'lora_weight': 0.8, 'lora_triggers': ''}),
|
||||
'tags': source_data.get('tags', []),
|
||||
'description': f"Transferred from {source_name}"
|
||||
},
|
||||
'outfits': {
|
||||
'outfit_id': slug,
|
||||
'outfit_name': name,
|
||||
'wardrobe': source_data.get('wardrobe', {
|
||||
'full_body': '', 'headwear': '', 'top': '', 'bottom': '',
|
||||
'legwear': '', 'footwear': '', 'hands': '', 'accessories': ''
|
||||
}),
|
||||
'lora': source_data.get('lora', {'lora_name': '', 'lora_weight': 0.8, 'lora_triggers': ''}),
|
||||
'tags': source_data.get('tags', []),
|
||||
'description': f"Transferred from {source_name}"
|
||||
},
|
||||
'actions': {
|
||||
'action_id': slug,
|
||||
'action_name': name,
|
||||
'action': source_data.get('action', {
|
||||
'full_body': '', 'head': '', 'eyes': '', 'arms': '', 'hands': '',
|
||||
'torso': '', 'pelvis': '', 'legs': '', 'feet': '', 'additional': ''
|
||||
}),
|
||||
'lora': source_data.get('lora', {'lora_name': '', 'lora_weight': 1.0, 'lora_triggers': ''}),
|
||||
'tags': source_data.get('tags', []),
|
||||
'description': f"Transferred from {source_name}"
|
||||
},
|
||||
'styles': {
|
||||
'style_id': slug,
|
||||
'style_name': name,
|
||||
'style': source_data.get('style', {'artist_name': '', 'artistic_style': ''}),
|
||||
'lora': source_data.get('lora', {'lora_name': '', 'lora_weight': 1.0, 'lora_triggers': ''}),
|
||||
'tags': source_data.get('tags', []),
|
||||
'description': f"Transferred from {source_name}"
|
||||
},
|
||||
'scenes': {
|
||||
'scene_id': slug,
|
||||
'scene_name': name,
|
||||
'scene': source_data.get('scene', {
|
||||
'background': '', 'foreground': '', 'furniture': [],
|
||||
'colors': [], 'lighting': '', 'theme': ''
|
||||
}),
|
||||
'lora': source_data.get('lora', {'lora_name': '', 'lora_weight': 1.0, 'lora_triggers': ''}),
|
||||
'tags': source_data.get('tags', []),
|
||||
'description': f"Transferred from {source_name}"
|
||||
},
|
||||
'detailers': {
|
||||
'detailer_id': slug,
|
||||
'detailer_name': name,
|
||||
'prompt': source_data.get('prompt', source_data.get('positive', '')),
|
||||
'lora': source_data.get('lora', {'lora_name': '', 'lora_weight': 1.0, 'lora_triggers': ''}),
|
||||
'tags': source_data.get('tags', []),
|
||||
'description': f"Transferred from {source_name}"
|
||||
},
|
||||
}
|
||||
return templates.get(target_category, {
|
||||
f'{target_category.rstrip("s")}_id': slug,
|
||||
f'{target_category.rstrip("s")}_name': name,
|
||||
'description': f"Transferred from {source_name}",
|
||||
'tags': source_data.get('tags', []),
|
||||
'lora': source_data.get('lora', {})
|
||||
})
|
||||
|
||||
@app.route('/resource/<category>/<slug>/transfer', methods=['GET', 'POST'])
|
||||
def transfer_resource(category, slug):
|
||||
"""Generic resource transfer route."""
|
||||
if category not in _RESOURCE_TRANSFER_MAP:
|
||||
flash(f'Invalid category: {category}')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
source_config = _RESOURCE_TRANSFER_MAP[category]
|
||||
model_class = source_config['model_class']
|
||||
id_field = source_config['id_field']
|
||||
|
||||
resource = model_class.query.filter_by(slug=slug).first_or_404()
|
||||
resource_name = resource.name
|
||||
resource_data = resource.data
|
||||
|
||||
if request.method == 'POST':
|
||||
target_category = request.form.get('target_category')
|
||||
new_name = request.form.get('new_name', '').strip()
|
||||
new_id = request.form.get('new_id', '').strip()
|
||||
use_llm = request.form.get('use_llm') in ('on', 'true', '1', 'yes') or request.form.get('use_llm') is not None
|
||||
transfer_lora = request.form.get('transfer_lora') == 'on'
|
||||
remove_original = request.form.get('remove_original') == 'on'
|
||||
|
||||
if not new_name:
|
||||
flash('New name is required for transfer')
|
||||
return redirect(url_for('transfer_resource', category=category, slug=slug))
|
||||
|
||||
if len(new_name) > 100:
|
||||
flash('New name must be 100 characters or less')
|
||||
return redirect(url_for('transfer_resource', category=category, slug=slug))
|
||||
|
||||
if not new_id:
|
||||
new_id = re.sub(r'[^a-zA-Z0-9]+', '_', new_name.lower()).strip('_')
|
||||
|
||||
safe_slug = re.sub(r'[^a-zA-Z0-9_]', '', new_id)
|
||||
if not safe_slug:
|
||||
safe_slug = 'transferred'
|
||||
|
||||
if target_category not in _RESOURCE_TRANSFER_MAP:
|
||||
flash('Invalid target category')
|
||||
return redirect(url_for('transfer_resource', category=category, slug=slug))
|
||||
|
||||
if target_category == category:
|
||||
flash('Cannot transfer to the same category')
|
||||
return redirect(url_for('transfer_resource', category=category, slug=slug))
|
||||
|
||||
target_config = _RESOURCE_TRANSFER_MAP[target_category]
|
||||
target_model_class = target_config['model_class']
|
||||
target_id_field = target_config['id_field']
|
||||
target_dir = current_app.config[target_config['target_dir']]
|
||||
target_name_key = target_config['name_key']
|
||||
target_id_key = target_config['id_key']
|
||||
|
||||
base_slug = safe_slug
|
||||
counter = 1
|
||||
while os.path.exists(os.path.join(target_dir, f"{safe_slug}.json")):
|
||||
safe_slug = f"{base_slug}_{counter}"
|
||||
counter += 1
|
||||
|
||||
if use_llm:
|
||||
try:
|
||||
category_system_prompts = {
|
||||
'outfits': 'outfit_system.txt',
|
||||
'actions': 'action_system.txt',
|
||||
'styles': 'style_system.txt',
|
||||
'scenes': 'scene_system.txt',
|
||||
'detailers': 'detailer_system.txt',
|
||||
'looks': 'look_system.txt',
|
||||
}
|
||||
system_prompt_file = category_system_prompts.get(target_category)
|
||||
system_prompt = load_prompt(system_prompt_file) if system_prompt_file else None
|
||||
|
||||
if not system_prompt:
|
||||
system_prompt = load_prompt('transfer_system.txt')
|
||||
|
||||
if not system_prompt:
|
||||
system_prompt = f"""You are an AI assistant that creates {target_category.rstrip('s')} profiles for AI image generation.
|
||||
|
||||
Your task is to create a {target_category.rstrip('s')} profile based on the source resource data provided.
|
||||
|
||||
Target type: {target_category}
|
||||
|
||||
Required JSON Structure:
|
||||
- {target_id_key}: "{safe_slug}"
|
||||
- {target_name_key}: "{new_name}"
|
||||
"""
|
||||
source_summary = json.dumps(resource_data, indent=2)
|
||||
llm_prompt = f"""Create a {target_category.rstrip('s')} profile named "{new_name}" (ID: {safe_slug}) based on this source {category.rstrip('s')} data:
|
||||
|
||||
Source {category.rstrip('s')} name: {resource_name}
|
||||
Source data:
|
||||
{source_summary}
|
||||
|
||||
Generate a complete {target_category.rstrip('s')} profile with all required fields for the {target_category} category."""
|
||||
|
||||
llm_response = call_llm(llm_prompt, system_prompt)
|
||||
clean_json = llm_response.replace('```json', '').replace('```', '').strip()
|
||||
new_data = json.loads(clean_json)
|
||||
new_data[target_id_key] = safe_slug
|
||||
new_data[target_name_key] = new_name
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"LLM transfer error: {e}")
|
||||
flash(f'Failed to generate {target_category.rstrip("s")} with AI: {e}')
|
||||
return redirect(url_for('transfer_resource', category=category, slug=slug))
|
||||
else:
|
||||
new_data = _create_minimal_template(target_category, safe_slug, new_name, resource_data, resource_name)
|
||||
|
||||
try:
|
||||
settings = Settings.query.first()
|
||||
lora_moved = False
|
||||
|
||||
if transfer_lora and 'lora' in resource_data and resource_data['lora'].get('lora_name'):
|
||||
old_lora_path = resource_data['lora']['lora_name']
|
||||
|
||||
lora_dir_map = {
|
||||
'characters': getattr(settings, 'lora_dir_characters', None) or _LORA_DEFAULTS.get('characters', ''),
|
||||
'looks': getattr(settings, 'lora_dir_looks', None) or getattr(settings, 'lora_dir_characters', None) or _LORA_DEFAULTS.get('looks', _LORA_DEFAULTS.get('characters', '')),
|
||||
'outfits': getattr(settings, 'lora_dir_outfits', None) or _LORA_DEFAULTS.get('outfits', ''),
|
||||
'actions': getattr(settings, 'lora_dir_actions', None) or _LORA_DEFAULTS.get('actions', ''),
|
||||
'styles': getattr(settings, 'lora_dir_styles', None) or _LORA_DEFAULTS.get('styles', ''),
|
||||
'scenes': getattr(settings, 'lora_dir_scenes', None) or _LORA_DEFAULTS.get('scenes', ''),
|
||||
'detailers': getattr(settings, 'lora_dir_detailers', None) or _LORA_DEFAULTS.get('detailers', ''),
|
||||
}
|
||||
|
||||
target_lora_dir = lora_dir_map.get(target_category)
|
||||
source_lora_dir = lora_dir_map.get(category)
|
||||
|
||||
if old_lora_path and target_lora_dir and source_lora_dir:
|
||||
lora_filename = os.path.basename(old_lora_path)
|
||||
|
||||
abs_source_path = os.path.join('/ImageModels/lora', old_lora_path)
|
||||
if not os.path.exists(abs_source_path):
|
||||
abs_source_path = os.path.join(source_lora_dir, lora_filename)
|
||||
|
||||
abs_target_path = os.path.join(target_lora_dir, lora_filename)
|
||||
|
||||
if os.path.exists(abs_source_path):
|
||||
try:
|
||||
import shutil
|
||||
os.makedirs(target_lora_dir, exist_ok=True)
|
||||
shutil.move(abs_source_path, abs_target_path)
|
||||
|
||||
target_subfolder = os.path.basename(target_lora_dir.rstrip('/'))
|
||||
new_data['lora']['lora_name'] = f"Illustrious/{target_subfolder}/{lora_filename}"
|
||||
lora_moved = True
|
||||
flash(f'Moved LoRA file to {target_lora_dir}')
|
||||
except Exception as lora_e:
|
||||
logger.exception(f"LoRA move error: {lora_e}")
|
||||
flash(f'Warning: Failed to move LoRA file: {lora_e}', 'warning')
|
||||
else:
|
||||
flash(f'Warning: Source LoRA file not found at {abs_source_path}', 'warning')
|
||||
|
||||
file_path = os.path.join(target_dir, f"{safe_slug}.json")
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
|
||||
new_entity = target_model_class(
|
||||
**{target_id_field: safe_slug},
|
||||
slug=safe_slug,
|
||||
filename=f"{safe_slug}.json",
|
||||
name=new_name,
|
||||
data=new_data
|
||||
)
|
||||
db.session.add(new_entity)
|
||||
|
||||
if remove_original:
|
||||
try:
|
||||
source_dir = current_app.config[source_config['target_dir']]
|
||||
orig_file_path = os.path.join(source_dir, resource.filename or f"{resource.slug}.json")
|
||||
if os.path.exists(orig_file_path):
|
||||
os.remove(orig_file_path)
|
||||
db.session.delete(resource)
|
||||
flash(f'Removed original {category.rstrip("s")}: {resource_name}')
|
||||
except Exception as rm_e:
|
||||
logger.exception(f"Error removing original: {rm_e}")
|
||||
flash(f'Warning: Failed to remove original: {rm_e}', 'warning')
|
||||
|
||||
db.session.commit()
|
||||
flash(f'Successfully transferred to {target_category.rstrip("s")}: {new_name}')
|
||||
return redirect(url_for(target_config['index_route'], highlight=safe_slug))
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Transfer save error: {e}")
|
||||
flash(f'Failed to save transferred {target_category.rstrip("s")}: {e}')
|
||||
return redirect(url_for('transfer_resource', category=category, slug=slug))
|
||||
|
||||
available_targets = [(cat, cat.rstrip('s').replace('_', ' ').title())
|
||||
for cat in _TRANSFER_TARGET_CATEGORIES
|
||||
if cat != category]
|
||||
|
||||
return render_template('transfer_resource.html',
|
||||
category=category,
|
||||
resource=resource,
|
||||
available_targets=available_targets,
|
||||
cancel_route=source_config['detail_route'])
|
||||
Reference in New Issue
Block a user