Major refactor: deduplicate routes, sync, JS, and fix bugs
- Extract 8 common route patterns into factory functions in routes/shared.py (favourite, upload, replace cover, save defaults, clone, save JSON, get missing, clear covers) — removes ~1,100 lines across 9 route files - Extract generic _sync_category() in sync.py — 7 sync functions become one-liner wrappers, removing ~350 lines - Extract shared detail page JS into static/js/detail-common.js — all 9 detail templates now call initDetailPage() with minimal config - Extract layout inline JS into static/js/layout-utils.js (~185 lines) - Extract library toolbar JS into static/js/library-toolbar.js - Fix finalize missing-image bug: raise RuntimeError instead of logging warning so job is marked failed - Fix missing scheduler default in _default_checkpoint_data() - Fix N+1 query in Character.get_available_outfits() with batch IN query - Convert all print() to logger across services and routes - Add missing tags display to styles, scenes, detailers, checkpoints detail - Update delete buttons to use trash.png icon with solid red background - Update CLAUDE.md to reflect new architecture Net reduction: ~1,600 lines Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,24 +16,13 @@ from services.sync import sync_actions
|
||||
from services.file_io import get_available_loras
|
||||
from services.llm import load_prompt, call_llm
|
||||
from utils import allowed_file, _LORA_DEFAULTS
|
||||
from routes.shared import register_common_routes
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
|
||||
@app.route('/get_missing_actions')
|
||||
def get_missing_actions():
|
||||
missing = Action.query.filter((Action.image_path == None) | (Action.image_path == '')).order_by(Action.name).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}
|
||||
register_common_routes(app, 'actions')
|
||||
|
||||
@app.route('/actions')
|
||||
def actions_index():
|
||||
@@ -151,40 +140,11 @@ def register_routes(app):
|
||||
return redirect(url_for('action_detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Edit error: {e}")
|
||||
logger.exception("Edit error: %s", e)
|
||||
flash(f"Error saving changes: {str(e)}")
|
||||
|
||||
return render_template('actions/edit.html', action=action, loras=loras)
|
||||
|
||||
@app.route('/action/<path:slug>/upload', methods=['POST'])
|
||||
def upload_action_image(slug):
|
||||
action = Action.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 action subfolder
|
||||
action_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"actions/{slug}")
|
||||
os.makedirs(action_folder, exist_ok=True)
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
file_path = os.path.join(action_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
# Store relative path in DB
|
||||
action.image_path = f"actions/{slug}/{filename}"
|
||||
db.session.commit()
|
||||
flash('Image uploaded successfully!')
|
||||
|
||||
return redirect(url_for('action_detail', slug=slug))
|
||||
|
||||
@app.route('/action/<path:slug>/generate', methods=['POST'])
|
||||
def generate_action_image(slug):
|
||||
action_obj = Action.query.filter_by(slug=slug).first_or_404()
|
||||
@@ -352,7 +312,7 @@ def register_routes(app):
|
||||
# Append to main prompt
|
||||
if extra_parts:
|
||||
prompts["main"] += ", " + ", ".join(extra_parts)
|
||||
print(f"Added extra character: {extra_char.name}")
|
||||
logger.debug("Added extra character: %s", extra_char.name)
|
||||
|
||||
_append_background(prompts, character)
|
||||
|
||||
@@ -377,33 +337,12 @@ def register_routes(app):
|
||||
return redirect(url_for('action_detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Generation error: {e}")
|
||||
logger.exception("Generation error: %s", 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('action_detail', slug=slug))
|
||||
|
||||
@app.route('/action/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||
def replace_action_cover_from_preview(slug):
|
||||
action = Action.query.filter_by(slug=slug).first_or_404()
|
||||
preview_path = request.form.get('preview_path')
|
||||
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||
action.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('No valid preview image selected.', 'error')
|
||||
return redirect(url_for('action_detail', slug=slug))
|
||||
|
||||
@app.route('/action/<path:slug>/save_defaults', methods=['POST'])
|
||||
def save_action_defaults(slug):
|
||||
action = Action.query.filter_by(slug=slug).first_or_404()
|
||||
selected_fields = request.form.getlist('include_field')
|
||||
action.default_fields = selected_fields
|
||||
db.session.commit()
|
||||
flash('Default prompt selection saved for this action!')
|
||||
return redirect(url_for('action_detail', slug=slug))
|
||||
|
||||
@app.route('/actions/bulk_create', methods=['POST'])
|
||||
def bulk_create_actions_from_loras():
|
||||
_s = Settings.query.first()
|
||||
@@ -556,7 +495,7 @@ def register_routes(app):
|
||||
action_data['action_id'] = safe_slug
|
||||
action_data['action_name'] = name
|
||||
except Exception as e:
|
||||
print(f"LLM error: {e}")
|
||||
logger.exception("LLM error: %s", e)
|
||||
flash(f"Failed to generate action profile: {e}")
|
||||
return render_template('actions/create.html', form_data=form_data)
|
||||
else:
|
||||
@@ -587,74 +526,9 @@ def register_routes(app):
|
||||
flash('Action created successfully!')
|
||||
return redirect(url_for('action_detail', slug=safe_slug))
|
||||
except Exception as e:
|
||||
print(f"Save error: {e}")
|
||||
logger.exception("Save error: %s", e)
|
||||
flash(f"Failed to create action: {e}")
|
||||
return render_template('actions/create.html', form_data=form_data)
|
||||
|
||||
return render_template('actions/create.html', form_data=form_data)
|
||||
|
||||
@app.route('/action/<path:slug>/clone', methods=['POST'])
|
||||
def clone_action(slug):
|
||||
action = Action.query.filter_by(slug=slug).first_or_404()
|
||||
|
||||
# Find the next available number for the clone
|
||||
base_id = action.action_id
|
||||
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['ACTIONS_DIR'], new_filename)
|
||||
if not os.path.exists(new_path):
|
||||
break
|
||||
next_num += 1
|
||||
|
||||
new_data = action.data.copy()
|
||||
new_data['action_id'] = new_id
|
||||
new_data['action_name'] = f"{action.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_action = Action(
|
||||
action_id=new_id, slug=new_slug, filename=new_filename,
|
||||
name=new_data['action_name'], data=new_data
|
||||
)
|
||||
db.session.add(new_action)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Action cloned as "{new_id}"!')
|
||||
return redirect(url_for('action_detail', slug=new_slug))
|
||||
|
||||
@app.route('/action/<path:slug>/save_json', methods=['POST'])
|
||||
def save_action_json(slug):
|
||||
action = Action.query.filter_by(slug=slug).first_or_404()
|
||||
try:
|
||||
new_data = json.loads(request.form.get('json_data', ''))
|
||||
except (ValueError, TypeError) as e:
|
||||
return {'success': False, 'error': f'Invalid JSON: {e}'}, 400
|
||||
action.data = new_data
|
||||
flag_modified(action, 'data')
|
||||
db.session.commit()
|
||||
if action.filename:
|
||||
file_path = os.path.join(app.config['ACTIONS_DIR'], action.filename)
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
return {'success': True}
|
||||
|
||||
@app.route('/action/<path:slug>/favourite', methods=['POST'])
|
||||
def toggle_action_favourite(slug):
|
||||
action = Action.query.filter_by(slug=slug).first_or_404()
|
||||
action.is_favourite = not action.is_favourite
|
||||
db.session.commit()
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'success': True, 'is_favourite': action.is_favourite}
|
||||
return redirect(url_for('action_detail', slug=slug))
|
||||
|
||||
@@ -5,8 +5,6 @@ import re
|
||||
|
||||
from flask import flash, jsonify, redirect, render_template, request, session, url_for
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from models import Action, Character, Detailer, Look, Outfit, Scene, Style, db
|
||||
from services.file_io import get_available_loras
|
||||
from services.job_queue import _enqueue_job, _make_finalize
|
||||
@@ -14,12 +12,13 @@ from services.llm import call_character_mcp_tool, call_llm, load_prompt
|
||||
from services.prompts import build_prompt
|
||||
from services.sync import sync_characters
|
||||
from services.workflow import _get_default_checkpoint, _prepare_workflow
|
||||
from utils import allowed_file
|
||||
from routes.shared import register_common_routes
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
register_common_routes(app, 'characters')
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
@@ -165,7 +164,7 @@ def register_routes(app):
|
||||
new_data[f'{target_type}_name'] = new_name
|
||||
|
||||
except Exception as e:
|
||||
print(f"LLM transfer error: {e}")
|
||||
logger.exception("LLM transfer error: %s", e)
|
||||
flash(f'Failed to generate {target_type} with AI: {e}')
|
||||
return redirect(url_for('transfer_character', slug=slug))
|
||||
else:
|
||||
@@ -212,7 +211,7 @@ def register_routes(app):
|
||||
return redirect(url_for('detailer_detail', slug=safe_slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Transfer save error: {e}")
|
||||
logger.exception("Transfer save error: %s", e)
|
||||
flash(f'Failed to save transferred {target_type}: {e}')
|
||||
return redirect(url_for('transfer_character', slug=slug))
|
||||
|
||||
@@ -356,7 +355,7 @@ Create an outfit JSON with wardrobe fields appropriate for this character."""
|
||||
logger.info(f"Generated outfit: {outfit_name} for character {name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Outfit generation error: {e}")
|
||||
logger.exception("Outfit generation error: %s", e)
|
||||
# Fall back to default
|
||||
default_outfit_id = 'default'
|
||||
|
||||
@@ -404,7 +403,7 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
char_data.pop('wardrobe', None)
|
||||
|
||||
except Exception as e:
|
||||
print(f"LLM error: {e}")
|
||||
logger.exception("LLM error: %s", e)
|
||||
error_msg = f"Failed to generate character profile: {e}"
|
||||
if is_ajax:
|
||||
return jsonify({'error': error_msg}), 500
|
||||
@@ -473,7 +472,7 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
return redirect(url_for('detail', slug=safe_slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Save error: {e}")
|
||||
logger.exception("Save error: %s", e)
|
||||
error_msg = f"Failed to create character: {e}"
|
||||
if is_ajax:
|
||||
return jsonify({'error': error_msg}), 500
|
||||
@@ -565,7 +564,7 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
return redirect(url_for('detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Edit error: {e}")
|
||||
logger.exception("Edit error: %s", e)
|
||||
flash(f"Error saving changes: {str(e)}")
|
||||
|
||||
return render_template('edit.html', character=character, loras=loras, char_looks=char_looks)
|
||||
@@ -757,60 +756,6 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
|
||||
return redirect(url_for('edit_character', slug=slug))
|
||||
|
||||
@app.route('/character/<path:slug>/upload', methods=['POST'])
|
||||
def upload_image(slug):
|
||||
character = Character.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 character subfolder
|
||||
char_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"characters/{slug}")
|
||||
os.makedirs(char_folder, exist_ok=True)
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
file_path = os.path.join(char_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
# Store relative path in DB
|
||||
character.image_path = f"characters/{slug}/{filename}"
|
||||
db.session.commit()
|
||||
flash('Image uploaded successfully!')
|
||||
|
||||
return redirect(url_for('detail', slug=slug))
|
||||
|
||||
@app.route('/character/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||
def replace_cover_from_preview(slug):
|
||||
character = Character.query.filter_by(slug=slug).first_or_404()
|
||||
preview_path = request.form.get('preview_path')
|
||||
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||
character.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('No valid preview image selected.', 'error')
|
||||
return redirect(url_for('detail', slug=slug))
|
||||
|
||||
@app.route('/get_missing_characters')
|
||||
def get_missing_characters():
|
||||
missing = Character.query.filter((Character.image_path == None) | (Character.image_path == '')).order_by(Character.name).all()
|
||||
return {'missing': [{'slug': c.slug, 'name': c.name} for c in missing]}
|
||||
|
||||
@app.route('/clear_all_covers', methods=['POST'])
|
||||
def clear_all_covers():
|
||||
characters = Character.query.all()
|
||||
for char in characters:
|
||||
char.image_path = None
|
||||
db.session.commit()
|
||||
return {'success': True}
|
||||
|
||||
@app.route('/generate_missing', methods=['POST'])
|
||||
def generate_missing():
|
||||
missing = Character.query.filter(
|
||||
@@ -834,7 +779,7 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
_enqueue_job(f"{character.name} – cover", workflow, _make_finalize('characters', _slug, Character))
|
||||
enqueued += 1
|
||||
except Exception as e:
|
||||
print(f"Error queuing cover generation for {character.name}: {e}")
|
||||
logger.exception("Error queuing cover generation for %s: %s", character.name, e)
|
||||
|
||||
flash(f"Queued {enqueued} cover image generation job{'s' if enqueued != 1 else ''}.")
|
||||
return redirect(url_for('index'))
|
||||
@@ -882,26 +827,9 @@ Do NOT include a wardrobe section - the outfit is handled separately."""
|
||||
return redirect(url_for('detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Generation error: {e}")
|
||||
logger.exception("Generation error: %s", 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('detail', slug=slug))
|
||||
|
||||
@app.route('/character/<path:slug>/save_defaults', methods=['POST'])
|
||||
def save_defaults(slug):
|
||||
character = Character.query.filter_by(slug=slug).first_or_404()
|
||||
selected_fields = request.form.getlist('include_field')
|
||||
character.default_fields = selected_fields
|
||||
db.session.commit()
|
||||
flash('Default prompt selection saved for this character!')
|
||||
return redirect(url_for('detail', slug=slug))
|
||||
|
||||
@app.route('/character/<path:slug>/favourite', methods=['POST'])
|
||||
def toggle_character_favourite(slug):
|
||||
character = Character.query.filter_by(slug=slug).first_or_404()
|
||||
character.is_favourite = not character.is_favourite
|
||||
db.session.commit()
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'success': True, 'is_favourite': character.is_favourite}
|
||||
return redirect(url_for('detail', slug=slug))
|
||||
|
||||
@@ -15,11 +15,13 @@ from services.sync import sync_checkpoints, _default_checkpoint_data
|
||||
from services.file_io import get_available_checkpoints
|
||||
from services.llm import load_prompt, call_llm
|
||||
from utils import allowed_file
|
||||
from routes.shared import register_common_routes
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
register_common_routes(app, 'checkpoints')
|
||||
|
||||
def _build_checkpoint_workflow(ckpt_obj, character=None, fixed_seed=None, extra_positive=None, extra_negative=None):
|
||||
"""Build and return a prepared ComfyUI workflow dict for a checkpoint generation."""
|
||||
@@ -95,26 +97,6 @@ def register_routes(app):
|
||||
existing_previews=existing_previews,
|
||||
extra_positive=extra_positive, extra_negative=extra_negative)
|
||||
|
||||
@app.route('/checkpoint/<path:slug>/upload', methods=['POST'])
|
||||
def upload_checkpoint_image(slug):
|
||||
ckpt = Checkpoint.query.filter_by(slug=slug).first_or_404()
|
||||
if 'image' not in request.files:
|
||||
flash('No file part')
|
||||
return redirect(url_for('checkpoint_detail', slug=slug))
|
||||
file = request.files['image']
|
||||
if file.filename == '':
|
||||
flash('No selected file')
|
||||
return redirect(url_for('checkpoint_detail', slug=slug))
|
||||
if file and allowed_file(file.filename):
|
||||
folder = os.path.join(app.config['UPLOAD_FOLDER'], f"checkpoints/{slug}")
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
filename = secure_filename(file.filename)
|
||||
file.save(os.path.join(folder, filename))
|
||||
ckpt.image_path = f"checkpoints/{slug}/{filename}"
|
||||
db.session.commit()
|
||||
flash('Image uploaded successfully!')
|
||||
return redirect(url_for('checkpoint_detail', slug=slug))
|
||||
|
||||
@app.route('/checkpoint/<path:slug>/generate', methods=['POST'])
|
||||
def generate_checkpoint_image(slug):
|
||||
ckpt = Checkpoint.query.filter_by(slug=slug).first_or_404()
|
||||
@@ -145,52 +127,12 @@ def register_routes(app):
|
||||
return {'status': 'queued', 'job_id': job['id']}
|
||||
return redirect(url_for('checkpoint_detail', slug=slug))
|
||||
except Exception as e:
|
||||
print(f"Generation error: {e}")
|
||||
logger.exception("Generation error: %s", 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('checkpoint_detail', slug=slug))
|
||||
|
||||
@app.route('/checkpoint/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||
def replace_checkpoint_cover_from_preview(slug):
|
||||
ckpt = Checkpoint.query.filter_by(slug=slug).first_or_404()
|
||||
preview_path = request.form.get('preview_path')
|
||||
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||
ckpt.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('No valid preview image selected.', 'error')
|
||||
return redirect(url_for('checkpoint_detail', slug=slug))
|
||||
|
||||
@app.route('/checkpoint/<path:slug>/save_json', methods=['POST'])
|
||||
def save_checkpoint_json(slug):
|
||||
ckpt = Checkpoint.query.filter_by(slug=slug).first_or_404()
|
||||
try:
|
||||
new_data = json.loads(request.form.get('json_data', ''))
|
||||
except (ValueError, TypeError) as e:
|
||||
return {'success': False, 'error': f'Invalid JSON: {e}'}, 400
|
||||
ckpt.data = new_data
|
||||
flag_modified(ckpt, 'data')
|
||||
db.session.commit()
|
||||
checkpoints_dir = app.config.get('CHECKPOINTS_DIR', 'data/checkpoints')
|
||||
file_path = os.path.join(checkpoints_dir, f'{ckpt.slug}.json')
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
return {'success': True}
|
||||
|
||||
@app.route('/get_missing_checkpoints')
|
||||
def get_missing_checkpoints():
|
||||
missing = Checkpoint.query.filter((Checkpoint.image_path == None) | (Checkpoint.image_path == '')).order_by(Checkpoint.name).all()
|
||||
return {'missing': [{'slug': c.slug, 'name': c.name} for c in missing]}
|
||||
|
||||
@app.route('/clear_all_checkpoint_covers', methods=['POST'])
|
||||
def clear_all_checkpoint_covers():
|
||||
for ckpt in Checkpoint.query.all():
|
||||
ckpt.image_path = None
|
||||
db.session.commit()
|
||||
return {'success': True}
|
||||
|
||||
@app.route('/checkpoints/bulk_create', methods=['POST'])
|
||||
def bulk_create_checkpoints():
|
||||
checkpoints_dir = app.config.get('CHECKPOINTS_DIR', 'data/checkpoints')
|
||||
@@ -304,11 +246,3 @@ def register_routes(app):
|
||||
flash(f'Queued {len(job_ids)} checkpoint tasks, {written_directly} written directly ({skipped} skipped).')
|
||||
return redirect(url_for('checkpoints_index'))
|
||||
|
||||
@app.route('/checkpoint/<path:slug>/favourite', methods=['POST'])
|
||||
def toggle_checkpoint_favourite(slug):
|
||||
ckpt = Checkpoint.query.filter_by(slug=slug).first_or_404()
|
||||
ckpt.is_favourite = not ckpt.is_favourite
|
||||
db.session.commit()
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'success': True, 'is_favourite': ckpt.is_favourite}
|
||||
return redirect(url_for('checkpoint_detail', slug=slug))
|
||||
|
||||
@@ -3,23 +3,24 @@ import os
|
||||
import re
|
||||
import logging
|
||||
|
||||
from flask import render_template, request, redirect, url_for, flash, session, current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask import render_template, request, redirect, url_for, flash, session
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from models import db, Character, Detailer, Action, Outfit, Style, Scene, Checkpoint, Settings, Look
|
||||
from models import db, Character, Detailer, Action, Settings
|
||||
from services.workflow import _prepare_workflow, _get_default_checkpoint
|
||||
from services.job_queue import _enqueue_job, _make_finalize, _enqueue_task
|
||||
from services.prompts import build_prompt, _resolve_character, _ensure_character_fields, _append_background
|
||||
from services.sync import sync_detailers
|
||||
from services.file_io import get_available_loras
|
||||
from services.llm import load_prompt, call_llm
|
||||
from utils import allowed_file, _LORA_DEFAULTS, _WARDROBE_KEYS
|
||||
from utils import _WARDROBE_KEYS
|
||||
from routes.shared import register_common_routes
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
register_common_routes(app, 'detailers')
|
||||
|
||||
def _queue_detailer_generation(detailer_obj, character=None, selected_fields=None, client_id=None, action=None, extra_positive=None, extra_negative=None, fixed_seed=None):
|
||||
if character:
|
||||
@@ -193,40 +194,11 @@ def register_routes(app):
|
||||
return redirect(url_for('detailer_detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Edit error: {e}")
|
||||
logger.exception("Edit error: %s", e)
|
||||
flash(f"Error saving changes: {str(e)}")
|
||||
|
||||
return render_template('detailers/edit.html', detailer=detailer, loras=loras)
|
||||
|
||||
@app.route('/detailer/<path:slug>/upload', methods=['POST'])
|
||||
def upload_detailer_image(slug):
|
||||
detailer = Detailer.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 detailer subfolder
|
||||
detailer_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"detailers/{slug}")
|
||||
os.makedirs(detailer_folder, exist_ok=True)
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
file_path = os.path.join(detailer_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
# Store relative path in DB
|
||||
detailer.image_path = f"detailers/{slug}/{filename}"
|
||||
db.session.commit()
|
||||
flash('Image uploaded successfully!')
|
||||
|
||||
return redirect(url_for('detailer_detail', slug=slug))
|
||||
|
||||
@app.route('/detailer/<path:slug>/generate', methods=['POST'])
|
||||
def generate_detailer_image(slug):
|
||||
detailer_obj = Detailer.query.filter_by(slug=slug).first_or_404()
|
||||
@@ -277,49 +249,12 @@ def register_routes(app):
|
||||
return redirect(url_for('detailer_detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Generation error: {e}")
|
||||
logger.exception("Generation error: %s", 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('detailer_detail', slug=slug))
|
||||
|
||||
@app.route('/detailer/<path:slug>/save_defaults', methods=['POST'])
|
||||
def save_detailer_defaults(slug):
|
||||
detailer = Detailer.query.filter_by(slug=slug).first_or_404()
|
||||
selected_fields = request.form.getlist('include_field')
|
||||
detailer.default_fields = selected_fields
|
||||
db.session.commit()
|
||||
flash('Default prompt selection saved for this detailer!')
|
||||
return redirect(url_for('detailer_detail', slug=slug))
|
||||
|
||||
@app.route('/detailer/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||
def replace_detailer_cover_from_preview(slug):
|
||||
detailer = Detailer.query.filter_by(slug=slug).first_or_404()
|
||||
preview_path = request.form.get('preview_path')
|
||||
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||
detailer.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('No valid preview image selected.', 'error')
|
||||
return redirect(url_for('detailer_detail', slug=slug))
|
||||
|
||||
@app.route('/detailer/<path:slug>/save_json', methods=['POST'])
|
||||
def save_detailer_json(slug):
|
||||
detailer = Detailer.query.filter_by(slug=slug).first_or_404()
|
||||
try:
|
||||
new_data = json.loads(request.form.get('json_data', ''))
|
||||
except (ValueError, TypeError) as e:
|
||||
return {'success': False, 'error': f'Invalid JSON: {e}'}, 400
|
||||
detailer.data = new_data
|
||||
flag_modified(detailer, 'data')
|
||||
db.session.commit()
|
||||
if detailer.filename:
|
||||
file_path = os.path.join(app.config['DETAILERS_DIR'], detailer.filename)
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
return {'success': True}
|
||||
|
||||
@app.route('/detailers/bulk_create', methods=['POST'])
|
||||
def bulk_create_detailers_from_loras():
|
||||
_s = Settings.query.first()
|
||||
@@ -463,17 +398,9 @@ def register_routes(app):
|
||||
flash('Detailer created successfully!')
|
||||
return redirect(url_for('detailer_detail', slug=safe_slug))
|
||||
except Exception as e:
|
||||
print(f"Save error: {e}")
|
||||
logger.exception("Save error: %s", e)
|
||||
flash(f"Failed to create detailer: {e}")
|
||||
return render_template('detailers/create.html', form_data=form_data)
|
||||
|
||||
return render_template('detailers/create.html', form_data=form_data)
|
||||
|
||||
@app.route('/detailer/<path:slug>/favourite', methods=['POST'])
|
||||
def toggle_detailer_favourite(slug):
|
||||
detailer = Detailer.query.filter_by(slug=slug).first_or_404()
|
||||
detailer.is_favourite = not detailer.is_favourite
|
||||
db.session.commit()
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'success': True, 'is_favourite': detailer.is_favourite}
|
||||
return redirect(url_for('detailer_detail', slug=slug))
|
||||
|
||||
@@ -3,18 +3,17 @@ import os
|
||||
import re
|
||||
import logging
|
||||
|
||||
from flask import render_template, request, redirect, url_for, flash, session, current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask import render_template, request, redirect, url_for, flash, session
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from models import db, Character, Look, Action, Checkpoint, Settings, Outfit
|
||||
from models import db, Character, Look, Settings
|
||||
from services.workflow import _prepare_workflow, _get_default_checkpoint
|
||||
from services.job_queue import _enqueue_job, _make_finalize, _enqueue_task
|
||||
from services.prompts import build_prompt, _resolve_character, _ensure_character_fields, _append_background, _dedup_tags
|
||||
from services.prompts import build_prompt, _resolve_character, _ensure_character_fields, _dedup_tags
|
||||
from services.sync import sync_looks
|
||||
from services.file_io import get_available_loras, _count_look_assignments
|
||||
from services.llm import load_prompt, call_llm
|
||||
from utils import allowed_file
|
||||
from routes.shared import register_common_routes
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
@@ -54,6 +53,7 @@ def _fix_look_lora_data(lora_data):
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
register_common_routes(app, 'looks')
|
||||
|
||||
@app.route('/looks')
|
||||
def looks_index():
|
||||
@@ -173,23 +173,6 @@ def register_routes(app):
|
||||
|
||||
return render_template('looks/edit.html', look=look, characters=characters, loras=loras)
|
||||
|
||||
@app.route('/look/<path:slug>/upload', methods=['POST'])
|
||||
def upload_look_image(slug):
|
||||
look = Look.query.filter_by(slug=slug).first_or_404()
|
||||
if 'image' not in request.files:
|
||||
flash('No file selected')
|
||||
return redirect(url_for('look_detail', slug=slug))
|
||||
file = request.files['image']
|
||||
if file and allowed_file(file.filename):
|
||||
filename = secure_filename(file.filename)
|
||||
look_folder = os.path.join(app.config['UPLOAD_FOLDER'], f'looks/{slug}')
|
||||
os.makedirs(look_folder, exist_ok=True)
|
||||
file_path = os.path.join(look_folder, filename)
|
||||
file.save(file_path)
|
||||
look.image_path = f'looks/{slug}/{filename}'
|
||||
db.session.commit()
|
||||
return redirect(url_for('look_detail', slug=slug))
|
||||
|
||||
@app.route('/look/<path:slug>/generate', methods=['POST'])
|
||||
def generate_look_image(slug):
|
||||
look = Look.query.filter_by(slug=slug).first_or_404()
|
||||
@@ -275,32 +258,12 @@ def register_routes(app):
|
||||
return redirect(url_for('look_detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Generation error: {e}")
|
||||
logger.exception("Generation error: %s", 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('look_detail', slug=slug))
|
||||
|
||||
@app.route('/look/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||
def replace_look_cover_from_preview(slug):
|
||||
look = Look.query.filter_by(slug=slug).first_or_404()
|
||||
preview_path = request.form.get('preview_path')
|
||||
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||
look.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('No valid preview image selected.', 'error')
|
||||
return redirect(url_for('look_detail', slug=slug))
|
||||
|
||||
@app.route('/look/<path:slug>/save_defaults', methods=['POST'])
|
||||
def save_look_defaults(slug):
|
||||
look = Look.query.filter_by(slug=slug).first_or_404()
|
||||
look.default_fields = request.form.getlist('include_field')
|
||||
db.session.commit()
|
||||
flash('Default prompt selection saved!')
|
||||
return redirect(url_for('look_detail', slug=slug))
|
||||
|
||||
@app.route('/look/<path:slug>/generate_character', methods=['POST'])
|
||||
def generate_character_from_look(slug):
|
||||
"""Generate a character JSON using a look as the base."""
|
||||
@@ -426,23 +389,6 @@ Character ID: {character_slug}"""
|
||||
flash(f'Character "{character_name}" created from look!', 'success')
|
||||
return redirect(url_for('detail', slug=character_slug))
|
||||
|
||||
@app.route('/look/<path:slug>/save_json', methods=['POST'])
|
||||
def save_look_json(slug):
|
||||
look = Look.query.filter_by(slug=slug).first_or_404()
|
||||
try:
|
||||
new_data = json.loads(request.form.get('json_data', ''))
|
||||
except (ValueError, TypeError) as e:
|
||||
return {'success': False, 'error': f'Invalid JSON: {e}'}, 400
|
||||
look.data = new_data
|
||||
look.character_id = new_data.get('character_id', look.character_id)
|
||||
flag_modified(look, 'data')
|
||||
db.session.commit()
|
||||
if look.filename:
|
||||
file_path = os.path.join(app.config['LOOKS_DIR'], look.filename)
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
return {'success': True}
|
||||
|
||||
@app.route('/look/create', methods=['GET', 'POST'])
|
||||
def create_look():
|
||||
characters = Character.query.order_by(Character.name).all()
|
||||
@@ -499,25 +445,12 @@ Character ID: {character_slug}"""
|
||||
flash(f'Look "{name}" created!')
|
||||
return redirect(url_for('look_detail', slug=slug))
|
||||
except Exception as e:
|
||||
print(f"Save error: {e}")
|
||||
logger.exception("Save error: %s", e)
|
||||
flash(f"Failed to create look: {e}")
|
||||
return render_template('looks/create.html', characters=characters, loras=loras, form_data=form_data)
|
||||
|
||||
return render_template('looks/create.html', characters=characters, loras=loras, form_data=form_data)
|
||||
|
||||
@app.route('/get_missing_looks')
|
||||
def get_missing_looks():
|
||||
missing = Look.query.filter((Look.image_path == None) | (Look.image_path == '')).order_by(Look.name).all()
|
||||
return {'missing': [{'slug': l.slug, 'name': l.name} for l in missing]}
|
||||
|
||||
@app.route('/clear_all_look_covers', methods=['POST'])
|
||||
def clear_all_look_covers():
|
||||
looks = Look.query.all()
|
||||
for look in looks:
|
||||
look.image_path = None
|
||||
db.session.commit()
|
||||
return {'success': True}
|
||||
|
||||
@app.route('/looks/bulk_create', methods=['POST'])
|
||||
def bulk_create_looks_from_loras():
|
||||
_s = Settings.query.first()
|
||||
@@ -615,11 +548,3 @@ Character ID: {character_slug}"""
|
||||
flash(f'Queued {len(job_ids)} look tasks ({skipped} skipped).')
|
||||
return redirect(url_for('looks_index'))
|
||||
|
||||
@app.route('/look/<path:slug>/favourite', methods=['POST'])
|
||||
def toggle_look_favourite(slug):
|
||||
look = Look.query.filter_by(slug=slug).first_or_404()
|
||||
look.is_favourite = not look.is_favourite
|
||||
db.session.commit()
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'success': True, 'is_favourite': look.is_favourite}
|
||||
return redirect(url_for('look_detail', slug=slug))
|
||||
@@ -15,24 +15,13 @@ from services.sync import sync_outfits
|
||||
from services.file_io import get_available_loras, _count_outfit_lora_assignments
|
||||
from utils import allowed_file, _LORA_DEFAULTS
|
||||
from services.llm import load_prompt, call_llm
|
||||
from routes.shared import register_common_routes
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
|
||||
@app.route('/get_missing_outfits')
|
||||
def get_missing_outfits():
|
||||
missing = Outfit.query.filter((Outfit.image_path == None) | (Outfit.image_path == '')).order_by(Outfit.name).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}
|
||||
register_common_routes(app, 'outfits')
|
||||
|
||||
@app.route('/outfits')
|
||||
def outfits_index():
|
||||
@@ -268,40 +257,11 @@ def register_routes(app):
|
||||
return redirect(url_for('outfit_detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Edit error: {e}")
|
||||
logger.exception("Edit error: %s", e)
|
||||
flash(f"Error saving changes: {str(e)}")
|
||||
|
||||
return render_template('outfits/edit.html', outfit=outfit, loras=loras)
|
||||
|
||||
@app.route('/outfit/<path:slug>/upload', methods=['POST'])
|
||||
def upload_outfit_image(slug):
|
||||
outfit = Outfit.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 outfit subfolder
|
||||
outfit_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"outfits/{slug}")
|
||||
os.makedirs(outfit_folder, exist_ok=True)
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
file_path = os.path.join(outfit_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
# Store relative path in DB
|
||||
outfit.image_path = f"outfits/{slug}/{filename}"
|
||||
db.session.commit()
|
||||
flash('Image uploaded successfully!')
|
||||
|
||||
return redirect(url_for('outfit_detail', slug=slug))
|
||||
|
||||
@app.route('/outfit/<path:slug>/generate', methods=['POST'])
|
||||
def generate_outfit_image(slug):
|
||||
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||
@@ -406,24 +366,12 @@ def register_routes(app):
|
||||
return redirect(url_for('outfit_detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Generation error: {e}")
|
||||
logger.exception("Generation error: %s", 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('outfit_detail', slug=slug))
|
||||
|
||||
@app.route('/outfit/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||
def replace_outfit_cover_from_preview(slug):
|
||||
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||
preview_path = request.form.get('preview_path')
|
||||
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||
outfit.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('No valid preview image selected.', 'error')
|
||||
return redirect(url_for('outfit_detail', slug=slug))
|
||||
|
||||
@app.route('/outfit/create', methods=['GET', 'POST'])
|
||||
def create_outfit():
|
||||
form_data = {}
|
||||
@@ -496,7 +444,7 @@ def register_routes(app):
|
||||
outfit_data['tags'] = []
|
||||
|
||||
except Exception as e:
|
||||
print(f"LLM error: {e}")
|
||||
logger.exception("LLM error: %s", e)
|
||||
flash(f"Failed to generate outfit profile: {e}")
|
||||
return render_template('outfits/create.html', form_data=form_data)
|
||||
else:
|
||||
@@ -542,92 +490,10 @@ def register_routes(app):
|
||||
return redirect(url_for('outfit_detail', slug=safe_slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Save error: {e}")
|
||||
logger.exception("Save error: %s", e)
|
||||
flash(f"Failed to create outfit: {e}")
|
||||
return render_template('outfits/create.html', form_data=form_data)
|
||||
|
||||
return render_template('outfits/create.html', form_data=form_data)
|
||||
|
||||
@app.route('/outfit/<path:slug>/save_defaults', methods=['POST'])
|
||||
def save_outfit_defaults(slug):
|
||||
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||
selected_fields = request.form.getlist('include_field')
|
||||
outfit.default_fields = selected_fields
|
||||
db.session.commit()
|
||||
flash('Default prompt selection saved for this outfit!')
|
||||
return redirect(url_for('outfit_detail', slug=slug))
|
||||
|
||||
@app.route('/outfit/<path:slug>/clone', methods=['POST'])
|
||||
def clone_outfit(slug):
|
||||
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||
|
||||
# Find the next available number for the clone
|
||||
base_id = outfit.outfit_id
|
||||
# Extract base name without number suffix
|
||||
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
|
||||
|
||||
# Find next available number
|
||||
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['CLOTHING_DIR'], new_filename)
|
||||
if not os.path.exists(new_path):
|
||||
break
|
||||
next_num += 1
|
||||
|
||||
# Create new outfit data (copy of original)
|
||||
new_data = outfit.data.copy()
|
||||
new_data['outfit_id'] = new_id
|
||||
new_data['outfit_name'] = f"{outfit.name} (Copy)"
|
||||
|
||||
# Save the new JSON file
|
||||
with open(new_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
|
||||
# Create new outfit in database
|
||||
new_slug = re.sub(r'[^a-zA-Z0-9_]', '', new_id)
|
||||
new_outfit = Outfit(
|
||||
outfit_id=new_id,
|
||||
slug=new_slug,
|
||||
filename=new_filename,
|
||||
name=new_data['outfit_name'],
|
||||
data=new_data
|
||||
)
|
||||
db.session.add(new_outfit)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Outfit cloned as "{new_id}"!')
|
||||
return redirect(url_for('outfit_detail', slug=new_slug))
|
||||
|
||||
@app.route('/outfit/<path:slug>/save_json', methods=['POST'])
|
||||
def save_outfit_json(slug):
|
||||
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||
try:
|
||||
new_data = json.loads(request.form.get('json_data', ''))
|
||||
except (ValueError, TypeError) as e:
|
||||
return {'success': False, 'error': f'Invalid JSON: {e}'}, 400
|
||||
outfit.data = new_data
|
||||
flag_modified(outfit, 'data')
|
||||
db.session.commit()
|
||||
if outfit.filename:
|
||||
file_path = os.path.join(app.config['CLOTHING_DIR'], outfit.filename)
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
return {'success': True}
|
||||
|
||||
@app.route('/outfit/<path:slug>/favourite', methods=['POST'])
|
||||
def toggle_outfit_favourite(slug):
|
||||
outfit = Outfit.query.filter_by(slug=slug).first_or_404()
|
||||
outfit.is_favourite = not outfit.is_favourite
|
||||
db.session.commit()
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'success': True, 'is_favourite': outfit.is_favourite}
|
||||
return redirect(url_for('outfit_detail', slug=slug))
|
||||
|
||||
@@ -2,23 +2,19 @@ import json
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import random
|
||||
from flask import render_template, request, redirect, url_for, flash, session, current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
from models import db, Character, Preset, Outfit, Action, Style, Scene, Detailer, Checkpoint, Look, Settings
|
||||
from models import db, Character, Preset, Outfit, Action, Style, Scene, Detailer, Checkpoint, Look
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from services.prompts import build_prompt, _dedup_tags, _resolve_character, _ensure_character_fields, _append_background
|
||||
from services.workflow import _prepare_workflow, _get_default_checkpoint
|
||||
from services.job_queue import _enqueue_job, _make_finalize
|
||||
from services.sync import sync_presets, _resolve_preset_entity, _resolve_preset_fields, _PRESET_ENTITY_MAP
|
||||
from services.sync import sync_presets
|
||||
from services.generation import generate_from_preset
|
||||
from services.llm import load_prompt, call_llm
|
||||
from utils import allowed_file
|
||||
from routes.shared import register_common_routes
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
register_common_routes(app, 'presets')
|
||||
|
||||
@app.route('/presets')
|
||||
def presets_index():
|
||||
@@ -79,37 +75,6 @@ def register_routes(app):
|
||||
flash(f"Error during generation: {str(e)}")
|
||||
return redirect(url_for('preset_detail', slug=slug))
|
||||
|
||||
@app.route('/preset/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||
def replace_preset_cover_from_preview(slug):
|
||||
preset = Preset.query.filter_by(slug=slug).first_or_404()
|
||||
preview_path = request.form.get('preview_path')
|
||||
if preview_path and os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'], preview_path)):
|
||||
preset.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('No valid preview image selected.', 'error')
|
||||
return redirect(url_for('preset_detail', slug=slug))
|
||||
|
||||
@app.route('/preset/<path:slug>/upload', methods=['POST'])
|
||||
def upload_preset_image(slug):
|
||||
preset = Preset.query.filter_by(slug=slug).first_or_404()
|
||||
if 'image' not in request.files:
|
||||
flash('No file uploaded.')
|
||||
return redirect(url_for('preset_detail', slug=slug))
|
||||
file = request.files['image']
|
||||
if file.filename == '':
|
||||
flash('No file selected.')
|
||||
return redirect(url_for('preset_detail', slug=slug))
|
||||
filename = secure_filename(file.filename)
|
||||
folder = os.path.join(current_app.config['UPLOAD_FOLDER'], f'presets/{slug}')
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
file.save(os.path.join(folder, filename))
|
||||
preset.image_path = f'presets/{slug}/{filename}'
|
||||
db.session.commit()
|
||||
flash('Image uploaded!')
|
||||
return redirect(url_for('preset_detail', slug=slug))
|
||||
|
||||
@app.route('/preset/<path:slug>/edit', methods=['GET', 'POST'])
|
||||
def edit_preset(slug):
|
||||
preset = Preset.query.filter_by(slug=slug).first_or_404()
|
||||
@@ -196,51 +161,6 @@ def register_routes(app):
|
||||
styles=styles, scenes=scenes, detailers=detailers,
|
||||
looks=looks, checkpoints=checkpoints)
|
||||
|
||||
@app.route('/preset/<path:slug>/save_json', methods=['POST'])
|
||||
def save_preset_json(slug):
|
||||
preset = Preset.query.filter_by(slug=slug).first_or_404()
|
||||
try:
|
||||
new_data = json.loads(request.form.get('json_data', ''))
|
||||
preset.data = new_data
|
||||
preset.name = new_data.get('preset_name', preset.name)
|
||||
flag_modified(preset, "data")
|
||||
db.session.commit()
|
||||
if preset.filename:
|
||||
file_path = os.path.join(current_app.config['PRESETS_DIR'], preset.filename)
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
return {'success': True}
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}, 400
|
||||
|
||||
@app.route('/preset/<path:slug>/clone', methods=['POST'])
|
||||
def clone_preset(slug):
|
||||
original = Preset.query.filter_by(slug=slug).first_or_404()
|
||||
new_data = dict(original.data)
|
||||
|
||||
base_id = f"{original.preset_id}_copy"
|
||||
new_id = base_id
|
||||
counter = 1
|
||||
while Preset.query.filter_by(preset_id=new_id).first():
|
||||
new_id = f"{base_id}_{counter}"
|
||||
counter += 1
|
||||
|
||||
new_slug = re.sub(r'[^a-zA-Z0-9_]', '', new_id)
|
||||
new_data['preset_id'] = new_id
|
||||
new_data['preset_name'] = f"{original.name} (Copy)"
|
||||
new_filename = f"{new_id}.json"
|
||||
|
||||
os.makedirs(current_app.config['PRESETS_DIR'], exist_ok=True)
|
||||
with open(os.path.join(current_app.config['PRESETS_DIR'], new_filename), 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
|
||||
new_preset = Preset(preset_id=new_id, slug=new_slug, filename=new_filename,
|
||||
name=new_data['preset_name'], data=new_data)
|
||||
db.session.add(new_preset)
|
||||
db.session.commit()
|
||||
flash(f"Cloned as '{new_data['preset_name']}'")
|
||||
return redirect(url_for('preset_detail', slug=new_slug))
|
||||
|
||||
@app.route('/presets/rescan', methods=['POST'])
|
||||
def rescan_presets():
|
||||
sync_presets()
|
||||
@@ -322,7 +242,3 @@ def register_routes(app):
|
||||
|
||||
return render_template('presets/create.html', form_data=form_data)
|
||||
|
||||
@app.route('/get_missing_presets')
|
||||
def get_missing_presets():
|
||||
missing = Preset.query.filter((Preset.image_path == None) | (Preset.image_path == '')).order_by(Preset.filename).all()
|
||||
return {'missing': [{'slug': p.slug, 'name': p.name} for p in missing]}
|
||||
|
||||
145
routes/scenes.py
145
routes/scenes.py
@@ -3,36 +3,24 @@ import os
|
||||
import re
|
||||
import logging
|
||||
|
||||
from flask import render_template, request, redirect, url_for, flash, session, current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask import render_template, request, redirect, url_for, flash, session
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from models import db, Character, Scene, Outfit, Action, Style, Detailer, Checkpoint, Settings, Look
|
||||
from models import db, Character, Scene, Settings
|
||||
from services.workflow import _prepare_workflow, _get_default_checkpoint
|
||||
from services.job_queue import _enqueue_job, _make_finalize, _enqueue_task
|
||||
from services.prompts import build_prompt, _resolve_character, _ensure_character_fields, _append_background
|
||||
from services.prompts import build_prompt, _resolve_character, _ensure_character_fields
|
||||
from services.sync import sync_scenes
|
||||
from services.file_io import get_available_loras
|
||||
from services.llm import load_prompt, call_llm
|
||||
from utils import allowed_file, _LORA_DEFAULTS, _WARDROBE_KEYS
|
||||
from routes.shared import register_common_routes
|
||||
from utils import _WARDROBE_KEYS
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
|
||||
@app.route('/get_missing_scenes')
|
||||
def get_missing_scenes():
|
||||
missing = Scene.query.filter((Scene.image_path == None) | (Scene.image_path == '')).order_by(Scene.name).all()
|
||||
return {'missing': [{'slug': s.slug, 'name': s.name} for s in missing]}
|
||||
|
||||
@app.route('/clear_all_scene_covers', methods=['POST'])
|
||||
def clear_all_scene_covers():
|
||||
scenes = Scene.query.all()
|
||||
for scene in scenes:
|
||||
scene.image_path = None
|
||||
db.session.commit()
|
||||
return {'success': True}
|
||||
register_common_routes(app, 'scenes')
|
||||
|
||||
@app.route('/scenes')
|
||||
def scenes_index():
|
||||
@@ -147,40 +135,11 @@ def register_routes(app):
|
||||
return redirect(url_for('scene_detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Edit error: {e}")
|
||||
logger.exception("Edit error: %s", e)
|
||||
flash(f"Error saving changes: {str(e)}")
|
||||
|
||||
return render_template('scenes/edit.html', scene=scene, loras=loras)
|
||||
|
||||
@app.route('/scene/<path:slug>/upload', methods=['POST'])
|
||||
def upload_scene_image(slug):
|
||||
scene = Scene.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 scene subfolder
|
||||
scene_folder = os.path.join(app.config['UPLOAD_FOLDER'], f"scenes/{slug}")
|
||||
os.makedirs(scene_folder, exist_ok=True)
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
file_path = os.path.join(scene_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
# Store relative path in DB
|
||||
scene.image_path = f"scenes/{slug}/{filename}"
|
||||
db.session.commit()
|
||||
flash('Image uploaded successfully!')
|
||||
|
||||
return redirect(url_for('scene_detail', slug=slug))
|
||||
|
||||
def _queue_scene_generation(scene_obj, character=None, selected_fields=None, client_id=None, fixed_seed=None, extra_positive=None, extra_negative=None):
|
||||
if character:
|
||||
combined_data = character.data.copy()
|
||||
@@ -306,33 +265,12 @@ def register_routes(app):
|
||||
return redirect(url_for('scene_detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Generation error: {e}")
|
||||
logger.exception("Generation error: %s", 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('scene_detail', slug=slug))
|
||||
|
||||
@app.route('/scene/<path:slug>/save_defaults', methods=['POST'])
|
||||
def save_scene_defaults(slug):
|
||||
scene = Scene.query.filter_by(slug=slug).first_or_404()
|
||||
selected_fields = request.form.getlist('include_field')
|
||||
scene.default_fields = selected_fields
|
||||
db.session.commit()
|
||||
flash('Default prompt selection saved for this scene!')
|
||||
return redirect(url_for('scene_detail', slug=slug))
|
||||
|
||||
@app.route('/scene/<path:slug>/replace_cover_from_preview', methods=['POST'])
|
||||
def replace_scene_cover_from_preview(slug):
|
||||
scene = Scene.query.filter_by(slug=slug).first_or_404()
|
||||
preview_path = request.form.get('preview_path')
|
||||
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||
scene.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('No valid preview image selected.', 'error')
|
||||
return redirect(url_for('scene_detail', slug=slug))
|
||||
|
||||
@app.route('/scenes/bulk_create', methods=['POST'])
|
||||
def bulk_create_scenes_from_loras():
|
||||
_s = Settings.query.first()
|
||||
@@ -483,74 +421,9 @@ def register_routes(app):
|
||||
flash('Scene created successfully!')
|
||||
return redirect(url_for('scene_detail', slug=safe_slug))
|
||||
except Exception as e:
|
||||
print(f"Save error: {e}")
|
||||
logger.exception("Save error: %s", e)
|
||||
flash(f"Failed to create scene: {e}")
|
||||
return render_template('scenes/create.html', form_data=form_data)
|
||||
|
||||
return render_template('scenes/create.html', form_data=form_data)
|
||||
|
||||
@app.route('/scene/<path:slug>/clone', methods=['POST'])
|
||||
def clone_scene(slug):
|
||||
scene = Scene.query.filter_by(slug=slug).first_or_404()
|
||||
|
||||
base_id = scene.scene_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['SCENES_DIR'], new_filename)
|
||||
if not os.path.exists(new_path):
|
||||
break
|
||||
next_num += 1
|
||||
|
||||
new_data = scene.data.copy()
|
||||
new_data['scene_id'] = new_id
|
||||
new_data['scene_name'] = f"{scene.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_scene = Scene(
|
||||
scene_id=new_id, slug=new_slug, filename=new_filename,
|
||||
name=new_data['scene_name'], data=new_data
|
||||
)
|
||||
db.session.add(new_scene)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Scene cloned as "{new_id}"!')
|
||||
return redirect(url_for('scene_detail', slug=new_slug))
|
||||
|
||||
@app.route('/scene/<path:slug>/save_json', methods=['POST'])
|
||||
def save_scene_json(slug):
|
||||
scene = Scene.query.filter_by(slug=slug).first_or_404()
|
||||
try:
|
||||
new_data = json.loads(request.form.get('json_data', ''))
|
||||
except (ValueError, TypeError) as e:
|
||||
return {'success': False, 'error': f'Invalid JSON: {e}'}, 400
|
||||
scene.data = new_data
|
||||
flag_modified(scene, 'data')
|
||||
db.session.commit()
|
||||
if scene.filename:
|
||||
file_path = os.path.join(app.config['SCENES_DIR'], scene.filename)
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
return {'success': True}
|
||||
|
||||
@app.route('/scene/<path:slug>/favourite', methods=['POST'])
|
||||
def toggle_scene_favourite(slug):
|
||||
scene = Scene.query.filter_by(slug=slug).first_or_404()
|
||||
scene.is_favourite = not scene.is_favourite
|
||||
db.session.commit()
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'success': True, 'is_favourite': scene.is_favourite}
|
||||
return redirect(url_for('scene_detail', slug=slug))
|
||||
|
||||
422
routes/shared.py
Normal file
422
routes/shared.py
Normal file
@@ -0,0 +1,422 @@
|
||||
"""
|
||||
Shared route factory functions for common patterns across all resource categories.
|
||||
|
||||
Each factory registers a route on the Flask app using the closure pattern,
|
||||
preserving endpoint names for url_for() compatibility.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from flask import current_app, flash, redirect, request, url_for
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from models import db
|
||||
from utils import allowed_file
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Category configuration registry
|
||||
# ---------------------------------------------------------------------------
|
||||
# Each entry maps a category name to its metadata.
|
||||
# 'url_prefix': the URL segment (e.g. 'outfit' → /outfit/<slug>/...)
|
||||
# 'detail_endpoint': the Flask endpoint name for the detail page
|
||||
# 'config_dir': the app.config key for the JSON data directory
|
||||
# 'category_folder': subfolder under static/uploads/
|
||||
# 'id_field': JSON key for entity ID
|
||||
# 'name_field': JSON key for display name
|
||||
|
||||
CATEGORY_CONFIG = {
|
||||
'characters': {
|
||||
'model': None, # Set at import time to avoid circular imports
|
||||
'url_prefix': 'character',
|
||||
'detail_endpoint': 'detail',
|
||||
'config_dir': 'CHARACTERS_DIR',
|
||||
'category_folder': 'characters',
|
||||
'id_field': 'character_id',
|
||||
'name_field': 'character_name',
|
||||
# Characters use unprefixed endpoint names
|
||||
'endpoint_prefix': '',
|
||||
},
|
||||
'outfits': {
|
||||
'model': None,
|
||||
'url_prefix': 'outfit',
|
||||
'detail_endpoint': 'outfit_detail',
|
||||
'config_dir': 'CLOTHING_DIR',
|
||||
'category_folder': 'outfits',
|
||||
'id_field': 'outfit_id',
|
||||
'name_field': 'outfit_name',
|
||||
'endpoint_prefix': 'outfit_',
|
||||
},
|
||||
'actions': {
|
||||
'model': None,
|
||||
'url_prefix': 'action',
|
||||
'detail_endpoint': 'action_detail',
|
||||
'config_dir': 'ACTIONS_DIR',
|
||||
'category_folder': 'actions',
|
||||
'id_field': 'action_id',
|
||||
'name_field': 'action_name',
|
||||
'endpoint_prefix': 'action_',
|
||||
},
|
||||
'styles': {
|
||||
'model': None,
|
||||
'url_prefix': 'style',
|
||||
'detail_endpoint': 'style_detail',
|
||||
'config_dir': 'STYLES_DIR',
|
||||
'category_folder': 'styles',
|
||||
'id_field': 'style_id',
|
||||
'name_field': 'style_name',
|
||||
'endpoint_prefix': 'style_',
|
||||
},
|
||||
'scenes': {
|
||||
'model': None,
|
||||
'url_prefix': 'scene',
|
||||
'detail_endpoint': 'scene_detail',
|
||||
'config_dir': 'SCENES_DIR',
|
||||
'category_folder': 'scenes',
|
||||
'id_field': 'scene_id',
|
||||
'name_field': 'scene_name',
|
||||
'endpoint_prefix': 'scene_',
|
||||
},
|
||||
'detailers': {
|
||||
'model': None,
|
||||
'url_prefix': 'detailer',
|
||||
'detail_endpoint': 'detailer_detail',
|
||||
'config_dir': 'DETAILERS_DIR',
|
||||
'category_folder': 'detailers',
|
||||
'id_field': 'detailer_id',
|
||||
'name_field': 'detailer_name',
|
||||
'endpoint_prefix': 'detailer_',
|
||||
},
|
||||
'looks': {
|
||||
'model': None,
|
||||
'url_prefix': 'look',
|
||||
'detail_endpoint': 'look_detail',
|
||||
'config_dir': 'LOOKS_DIR',
|
||||
'category_folder': 'looks',
|
||||
'id_field': 'look_id',
|
||||
'name_field': 'look_name',
|
||||
'endpoint_prefix': 'look_',
|
||||
},
|
||||
'checkpoints': {
|
||||
'model': None,
|
||||
'url_prefix': 'checkpoint',
|
||||
'detail_endpoint': 'checkpoint_detail',
|
||||
'config_dir': 'CHECKPOINTS_DIR',
|
||||
'category_folder': 'checkpoints',
|
||||
'id_field': 'checkpoint_path',
|
||||
'name_field': 'checkpoint_name',
|
||||
'endpoint_prefix': 'checkpoint_',
|
||||
},
|
||||
'presets': {
|
||||
'model': None,
|
||||
'url_prefix': 'preset',
|
||||
'detail_endpoint': 'preset_detail',
|
||||
'config_dir': 'PRESETS_DIR',
|
||||
'category_folder': 'presets',
|
||||
'id_field': 'preset_id',
|
||||
'name_field': 'preset_name',
|
||||
'endpoint_prefix': 'preset_',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _init_models():
|
||||
"""Lazily populate model references to avoid circular imports."""
|
||||
from models import (Action, Character, Checkpoint, Detailer, Look,
|
||||
Outfit, Preset, Scene, Style)
|
||||
CATEGORY_CONFIG['characters']['model'] = Character
|
||||
CATEGORY_CONFIG['outfits']['model'] = Outfit
|
||||
CATEGORY_CONFIG['actions']['model'] = Action
|
||||
CATEGORY_CONFIG['styles']['model'] = Style
|
||||
CATEGORY_CONFIG['scenes']['model'] = Scene
|
||||
CATEGORY_CONFIG['detailers']['model'] = Detailer
|
||||
CATEGORY_CONFIG['looks']['model'] = Look
|
||||
CATEGORY_CONFIG['checkpoints']['model'] = Checkpoint
|
||||
CATEGORY_CONFIG['presets']['model'] = Preset
|
||||
|
||||
|
||||
def _get_config(category):
|
||||
"""Get config for a category, initializing models if needed."""
|
||||
cfg = CATEGORY_CONFIG[category]
|
||||
if cfg['model'] is None:
|
||||
_init_models()
|
||||
return cfg
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Factory functions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _register_favourite_route(app, cfg):
|
||||
"""Register POST /<prefix>/<slug>/favourite toggle route."""
|
||||
Model = cfg['model']
|
||||
prefix = cfg['url_prefix']
|
||||
detail_ep = cfg['detail_endpoint']
|
||||
ep_prefix = cfg['endpoint_prefix']
|
||||
|
||||
# Characters use 'toggle_character_favourite', others use 'toggle_{prefix}_favourite'
|
||||
if ep_prefix == '':
|
||||
endpoint_name = 'toggle_character_favourite'
|
||||
else:
|
||||
endpoint_name = f'toggle_{prefix}_favourite'
|
||||
|
||||
@app.route(f'/{prefix}/<path:slug>/favourite', methods=['POST'],
|
||||
endpoint=endpoint_name)
|
||||
def favourite_toggle(slug):
|
||||
entity = Model.query.filter_by(slug=slug).first_or_404()
|
||||
entity.is_favourite = not entity.is_favourite
|
||||
db.session.commit()
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'success': True, 'is_favourite': entity.is_favourite}
|
||||
return redirect(url_for(detail_ep, slug=slug))
|
||||
|
||||
|
||||
def _register_upload_route(app, cfg):
|
||||
"""Register POST /<prefix>/<slug>/upload image route."""
|
||||
Model = cfg['model']
|
||||
prefix = cfg['url_prefix']
|
||||
detail_ep = cfg['detail_endpoint']
|
||||
ep_prefix = cfg['endpoint_prefix']
|
||||
folder = cfg['category_folder']
|
||||
|
||||
if ep_prefix == '':
|
||||
endpoint_name = 'upload_image'
|
||||
else:
|
||||
endpoint_name = f'upload_{prefix}_image'
|
||||
|
||||
@app.route(f'/{prefix}/<path:slug>/upload', methods=['POST'],
|
||||
endpoint=endpoint_name)
|
||||
def upload_image(slug):
|
||||
entity = Model.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):
|
||||
entity_folder = os.path.join(
|
||||
current_app.config['UPLOAD_FOLDER'], f"{folder}/{slug}")
|
||||
os.makedirs(entity_folder, exist_ok=True)
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
file_path = os.path.join(entity_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
entity.image_path = f"{folder}/{slug}/{filename}"
|
||||
db.session.commit()
|
||||
flash('Image uploaded successfully!')
|
||||
|
||||
return redirect(url_for(detail_ep, slug=slug))
|
||||
|
||||
|
||||
def _register_replace_cover_route(app, cfg):
|
||||
"""Register POST /<prefix>/<slug>/replace_cover_from_preview route."""
|
||||
Model = cfg['model']
|
||||
prefix = cfg['url_prefix']
|
||||
detail_ep = cfg['detail_endpoint']
|
||||
ep_prefix = cfg['endpoint_prefix']
|
||||
|
||||
if ep_prefix == '':
|
||||
endpoint_name = 'replace_cover_from_preview'
|
||||
else:
|
||||
endpoint_name = f'replace_{prefix}_cover_from_preview'
|
||||
|
||||
@app.route(f'/{prefix}/<path:slug>/replace_cover_from_preview',
|
||||
methods=['POST'], endpoint=endpoint_name)
|
||||
def replace_cover(slug):
|
||||
entity = Model.query.filter_by(slug=slug).first_or_404()
|
||||
preview_path = request.form.get('preview_path')
|
||||
if preview_path and os.path.exists(
|
||||
os.path.join(current_app.config['UPLOAD_FOLDER'], preview_path)):
|
||||
entity.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('No valid preview image selected.', 'error')
|
||||
return redirect(url_for(detail_ep, slug=slug))
|
||||
|
||||
|
||||
def _register_save_defaults_route(app, cfg):
|
||||
"""Register POST /<prefix>/<slug>/save_defaults route."""
|
||||
Model = cfg['model']
|
||||
prefix = cfg['url_prefix']
|
||||
detail_ep = cfg['detail_endpoint']
|
||||
ep_prefix = cfg['endpoint_prefix']
|
||||
category = cfg['category_folder']
|
||||
|
||||
if ep_prefix == '':
|
||||
endpoint_name = 'save_defaults'
|
||||
else:
|
||||
endpoint_name = f'save_{prefix}_defaults'
|
||||
|
||||
# Display name for the flash message
|
||||
display = category.rstrip('s')
|
||||
|
||||
@app.route(f'/{prefix}/<path:slug>/save_defaults', methods=['POST'],
|
||||
endpoint=endpoint_name)
|
||||
def save_defaults(slug):
|
||||
entity = Model.query.filter_by(slug=slug).first_or_404()
|
||||
selected_fields = request.form.getlist('include_field')
|
||||
entity.default_fields = selected_fields
|
||||
db.session.commit()
|
||||
flash(f'Default prompt selection saved for this {display}!')
|
||||
return redirect(url_for(detail_ep, slug=slug))
|
||||
|
||||
|
||||
def _register_clone_route(app, cfg):
|
||||
"""Register POST /<prefix>/<slug>/clone route."""
|
||||
Model = cfg['model']
|
||||
prefix = cfg['url_prefix']
|
||||
detail_ep = cfg['detail_endpoint']
|
||||
config_dir = cfg['config_dir']
|
||||
id_field = cfg['id_field']
|
||||
name_field = cfg['name_field']
|
||||
|
||||
endpoint_name = f'clone_{prefix}'
|
||||
|
||||
@app.route(f'/{prefix}/<path:slug>/clone', methods=['POST'],
|
||||
endpoint=endpoint_name)
|
||||
def clone_entity(slug):
|
||||
entity = Model.query.filter_by(slug=slug).first_or_404()
|
||||
|
||||
base_id = getattr(entity, id_field, None) or entity.data.get(id_field)
|
||||
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(
|
||||
current_app.config[config_dir], new_filename)
|
||||
if not os.path.exists(new_path):
|
||||
break
|
||||
next_num += 1
|
||||
|
||||
new_data = dict(entity.data)
|
||||
new_data[id_field] = new_id
|
||||
new_data[name_field] = f"{entity.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)
|
||||
kwargs = {
|
||||
id_field: new_id,
|
||||
'slug': new_slug,
|
||||
'filename': new_filename,
|
||||
'name': new_data[name_field],
|
||||
'data': new_data,
|
||||
}
|
||||
new_entity = Model(**kwargs)
|
||||
db.session.add(new_entity)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Cloned as "{new_id}"!')
|
||||
return redirect(url_for(detail_ep, slug=new_slug))
|
||||
|
||||
|
||||
def _register_save_json_route(app, cfg):
|
||||
"""Register POST /<prefix>/<slug>/save_json route."""
|
||||
Model = cfg['model']
|
||||
prefix = cfg['url_prefix']
|
||||
config_dir = cfg['config_dir']
|
||||
ep_prefix = cfg['endpoint_prefix']
|
||||
|
||||
if ep_prefix == '':
|
||||
endpoint_name = 'save_character_json'
|
||||
else:
|
||||
endpoint_name = f'save_{prefix}_json'
|
||||
|
||||
@app.route(f'/{prefix}/<path:slug>/save_json', methods=['POST'],
|
||||
endpoint=endpoint_name)
|
||||
def save_json(slug):
|
||||
entity = Model.query.filter_by(slug=slug).first_or_404()
|
||||
try:
|
||||
new_data = json.loads(request.form.get('json_data', ''))
|
||||
except (ValueError, TypeError) as e:
|
||||
return {'success': False, 'error': f'Invalid JSON: {e}'}, 400
|
||||
entity.data = new_data
|
||||
flag_modified(entity, 'data')
|
||||
db.session.commit()
|
||||
if entity.filename:
|
||||
file_path = os.path.join(
|
||||
current_app.config[config_dir], entity.filename)
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
return {'success': True}
|
||||
|
||||
|
||||
def _register_get_missing_route(app, cfg):
|
||||
"""Register GET /get_missing_<category> route."""
|
||||
Model = cfg['model']
|
||||
category = cfg['category_folder']
|
||||
|
||||
endpoint_name = f'get_missing_{category}'
|
||||
|
||||
@app.route(f'/get_missing_{category}', endpoint=endpoint_name)
|
||||
def get_missing():
|
||||
missing = Model.query.filter(
|
||||
(Model.image_path == None) | (Model.image_path == '')
|
||||
).order_by(Model.name).all()
|
||||
return {'missing': [{'slug': e.slug, 'name': e.name} for e in missing]}
|
||||
|
||||
|
||||
def _register_clear_covers_route(app, cfg):
|
||||
"""Register POST /clear_all_<category>_covers route."""
|
||||
Model = cfg['model']
|
||||
category = cfg['category_folder']
|
||||
ep_prefix = cfg['endpoint_prefix']
|
||||
|
||||
# Characters use 'clear_all_covers', others use 'clear_all_{category}_covers'
|
||||
if ep_prefix == '':
|
||||
endpoint_name = 'clear_all_covers'
|
||||
url = '/clear_all_covers'
|
||||
else:
|
||||
endpoint_name = f'clear_all_{category.rstrip("s")}_covers'
|
||||
url = f'/clear_all_{category.rstrip("s")}_covers'
|
||||
|
||||
@app.route(url, methods=['POST'], endpoint=endpoint_name)
|
||||
def clear_covers():
|
||||
entities = Model.query.all()
|
||||
for entity in entities:
|
||||
entity.image_path = None
|
||||
db.session.commit()
|
||||
return {'success': True}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main registration function
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def register_common_routes(app, category):
|
||||
"""Register all common routes for a category.
|
||||
|
||||
Call this from each route module's register_routes(app) function.
|
||||
"""
|
||||
cfg = _get_config(category)
|
||||
|
||||
_register_favourite_route(app, cfg)
|
||||
_register_upload_route(app, cfg)
|
||||
_register_replace_cover_route(app, cfg)
|
||||
_register_save_defaults_route(app, cfg)
|
||||
_register_clone_route(app, cfg)
|
||||
_register_save_json_route(app, cfg)
|
||||
_register_get_missing_route(app, cfg)
|
||||
_register_clear_covers_route(app, cfg)
|
||||
@@ -263,7 +263,7 @@ def register_routes(app):
|
||||
else:
|
||||
character = None
|
||||
|
||||
print(f"[Strengths] char_slug={char_slug!r} → character={character.slug if character else 'none'}")
|
||||
logger.debug("Strengths: char_slug=%r -> character=%s", char_slug, character.slug if character else 'none')
|
||||
|
||||
action_obj = None
|
||||
extra_positive = ''
|
||||
@@ -274,7 +274,7 @@ def register_routes(app):
|
||||
action_obj = Action.query.filter_by(slug=action_slug).first()
|
||||
extra_positive = session.get(f'extra_pos_detailer_{slug}', '')
|
||||
extra_negative = session.get(f'extra_neg_detailer_{slug}', '')
|
||||
print(f"[Strengths] detailer session — char={char_slug}, action={action_slug}, extra_pos={bool(extra_positive)}, extra_neg={bool(extra_negative)}")
|
||||
logger.debug("Strengths: detailer session -- char=%s, action=%s, extra_pos=%s, extra_neg=%s", char_slug, action_slug, bool(extra_positive), bool(extra_negative))
|
||||
|
||||
prompts = _build_strengths_prompts(category, entity, character,
|
||||
action=action_obj, extra_positive=extra_positive)
|
||||
@@ -321,7 +321,7 @@ def register_routes(app):
|
||||
return {'status': 'queued', 'job_id': job['id']}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Strengths] generate error: {e}")
|
||||
logger.exception("Strengths generate error: %s", e)
|
||||
return {'error': str(e)}, 500
|
||||
|
||||
@app.route('/strengths/<category>/<path:slug>/list')
|
||||
|
||||
157
routes/styles.py
157
routes/styles.py
@@ -4,23 +4,24 @@ import re
|
||||
import random
|
||||
import logging
|
||||
|
||||
from flask import render_template, request, redirect, url_for, flash, session, current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask import render_template, request, redirect, url_for, flash, session
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from models import db, Character, Style, Detailer, Settings
|
||||
from models import db, Character, Style, Settings
|
||||
from services.workflow import _prepare_workflow, _get_default_checkpoint
|
||||
from services.job_queue import _enqueue_job, _make_finalize, _enqueue_task
|
||||
from services.prompts import build_prompt, _resolve_character, _ensure_character_fields, _append_background
|
||||
from services.sync import sync_styles
|
||||
from services.file_io import get_available_loras
|
||||
from services.llm import load_prompt, call_llm
|
||||
from utils import allowed_file, _WARDROBE_KEYS
|
||||
from routes.shared import register_common_routes
|
||||
from utils import _WARDROBE_KEYS
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
register_common_routes(app, 'styles')
|
||||
|
||||
def _build_style_workflow(style_obj, character=None, selected_fields=None, fixed_seed=None, extra_positive=None, extra_negative=None):
|
||||
"""Build and return a prepared ComfyUI workflow dict for a style generation."""
|
||||
@@ -188,40 +189,11 @@ def register_routes(app):
|
||||
return redirect(url_for('style_detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Edit error: {e}")
|
||||
logger.exception("Edit error: %s", 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))
|
||||
|
||||
@app.route('/style/<path:slug>/generate', methods=['POST'])
|
||||
def generate_style_image(slug):
|
||||
style_obj = Style.query.filter_by(slug=slug).first_or_404()
|
||||
@@ -269,59 +241,12 @@ def register_routes(app):
|
||||
return redirect(url_for('style_detail', slug=slug))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Generation error: {e}")
|
||||
logger.exception("Generation error: %s", 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>/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 = request.form.get('preview_path')
|
||||
if preview_path and os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], preview_path)):
|
||||
style.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('No valid preview image selected.', '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 == '')).order_by(Style.name).all()
|
||||
return {'missing': [{'slug': s.slug, 'name': s.name} for s in missing]}
|
||||
|
||||
@app.route('/get_missing_detailers')
|
||||
def get_missing_detailers():
|
||||
missing = Detailer.query.filter((Detailer.image_path == None) | (Detailer.image_path == '')).order_by(Detailer.name).all()
|
||||
return {'missing': [{'slug': d.slug, 'name': d.name} for d in missing]}
|
||||
|
||||
@app.route('/clear_all_detailer_covers', methods=['POST'])
|
||||
def clear_all_detailer_covers():
|
||||
detailers = Detailer.query.all()
|
||||
for detailer in detailers:
|
||||
detailer.image_path = None
|
||||
db.session.commit()
|
||||
return {'success': True}
|
||||
|
||||
@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():
|
||||
missing = Style.query.filter(
|
||||
@@ -347,7 +272,7 @@ def register_routes(app):
|
||||
_make_finalize('styles', style_obj.slug, Style))
|
||||
enqueued += 1
|
||||
except Exception as e:
|
||||
print(f"Error queuing cover generation for style {style_obj.name}: {e}")
|
||||
logger.exception("Error queuing cover generation for style %s: %s", style_obj.name, e)
|
||||
|
||||
flash(f"Queued {enqueued} style cover image generation job{'s' if enqueued != 1 else ''}.")
|
||||
return redirect(url_for('styles_index'))
|
||||
@@ -511,73 +436,9 @@ def register_routes(app):
|
||||
flash('Style created successfully!')
|
||||
return redirect(url_for('style_detail', slug=safe_slug))
|
||||
except Exception as e:
|
||||
print(f"Save error: {e}")
|
||||
logger.exception("Save error: %s", e)
|
||||
flash(f"Failed to create style: {e}")
|
||||
return render_template('styles/create.html', form_data=form_data)
|
||||
|
||||
return render_template('styles/create.html', form_data=form_data)
|
||||
|
||||
@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
|
||||
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))
|
||||
|
||||
@app.route('/style/<path:slug>/save_json', methods=['POST'])
|
||||
def save_style_json(slug):
|
||||
style = Style.query.filter_by(slug=slug).first_or_404()
|
||||
try:
|
||||
new_data = json.loads(request.form.get('json_data', ''))
|
||||
except (ValueError, TypeError) as e:
|
||||
return {'success': False, 'error': f'Invalid JSON: {e}'}, 400
|
||||
style.data = new_data
|
||||
flag_modified(style, 'data')
|
||||
db.session.commit()
|
||||
if style.filename:
|
||||
file_path = os.path.join(app.config['STYLES_DIR'], style.filename)
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
return {'success': True}
|
||||
|
||||
@app.route('/style/<path:slug>/favourite', methods=['POST'])
|
||||
def toggle_style_favourite(slug):
|
||||
style_obj = Style.query.filter_by(slug=slug).first_or_404()
|
||||
style_obj.is_favourite = not style_obj.is_favourite
|
||||
db.session.commit()
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'success': True, 'is_favourite': style_obj.is_favourite}
|
||||
return redirect(url_for('style_detail', slug=slug))
|
||||
|
||||
Reference in New Issue
Block a user