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, Preset, Outfit, Action, Style, Scene, Detailer, Checkpoint, Look from sqlalchemy.orm.attributes import flag_modified from services.sync import sync_presets from services.generation import generate_from_preset from services.llm import load_prompt, call_llm 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(): presets = Preset.query.order_by(Preset.filename).all() return render_template('presets/index.html', presets=presets) @app.route('/preset/') def preset_detail(slug): preset = Preset.query.filter_by(slug=slug).first_or_404() preview_path = session.get(f'preview_preset_{slug}') extra_positive = session.get(f'extra_pos_preset_{slug}', '') extra_negative = session.get(f'extra_neg_preset_{slug}', '') return render_template('presets/detail.html', preset=preset, preview_path=preview_path, extra_positive=extra_positive, extra_negative=extra_negative) @app.route('/preset//generate', methods=['POST']) def generate_preset_image(slug): preset = Preset.query.filter_by(slug=slug).first_or_404() try: action = request.form.get('action', 'preview') extra_positive = request.form.get('extra_positive', '').strip() extra_negative = request.form.get('extra_negative', '').strip() # Save to session (web UI state) session[f'extra_pos_preset_{slug}'] = extra_positive session[f'extra_neg_preset_{slug}'] = extra_negative session.modified = True # Build overrides from form seed_val = request.form.get('seed', '').strip() form_width = request.form.get('width', '').strip() form_height = request.form.get('height', '').strip() overrides = { 'action': action, 'extra_positive': extra_positive, 'extra_negative': extra_negative, 'checkpoint': request.form.get('checkpoint_override', '').strip(), 'seed': int(seed_val) if seed_val else None, 'width': int(form_width) if form_width else None, 'height': int(form_height) if form_height else None, } job = generate_from_preset(preset, overrides) session[f'preview_preset_{slug}'] = None session.modified = True if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return {'status': 'queued', 'job_id': job['id']} return redirect(url_for('preset_detail', slug=slug)) except Exception as e: logger.exception("Generation error (preset %s): %s", slug, 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('preset_detail', slug=slug)) @app.route('/preset//edit', methods=['GET', 'POST']) def edit_preset(slug): preset = Preset.query.filter_by(slug=slug).first_or_404() if request.method == 'POST': name = request.form.get('preset_name', preset.name) preset.name = name def _tog(val): """Convert form value ('true'/'false'/'random') to JSON toggle value.""" if val == 'random': return 'random' return val == 'true' def _entity_id(val): return val if val else None char_id = _entity_id(request.form.get('char_character_id')) new_data = { 'preset_id': preset.preset_id, 'preset_name': name, 'character': { 'character_id': char_id, 'use_lora': request.form.get('char_use_lora') == 'on', 'fields': { 'identity': {k: _tog(request.form.get(f'id_{k}', 'true')) for k in ['base', 'head', 'upper_body', 'lower_body', 'hands', 'feet', 'additional']}, 'defaults': {k: _tog(request.form.get(f'def_{k}', 'false')) for k in ['expression', 'pose', 'scene']}, 'wardrobe': { 'outfit': request.form.get('wardrobe_outfit', 'default') or 'default', 'fields': {k: _tog(request.form.get(f'wd_{k}', 'true')) for k in ['base', 'head', 'upper_body', 'lower_body', 'hands', 'feet', 'additional']}, }, }, }, 'outfit': {'outfit_id': _entity_id(request.form.get('outfit_id')), 'use_lora': request.form.get('outfit_use_lora') == 'on'}, 'action': {'action_id': _entity_id(request.form.get('action_id')), 'use_lora': request.form.get('action_use_lora') == 'on', 'suppress_wardrobe': {'true': True, 'false': False, 'random': 'random'}.get( request.form.get('act_suppress_wardrobe')), 'fields': {k: _tog(request.form.get(f'act_{k}', 'true')) for k in ['base', 'head', 'upper_body', 'lower_body', 'hands', 'feet', 'additional']}}, 'style': {'style_id': _entity_id(request.form.get('style_id')), 'use_lora': request.form.get('style_use_lora') == 'on'}, 'scene': {'scene_id': _entity_id(request.form.get('scene_id')), 'use_lora': request.form.get('scene_use_lora') == 'on', 'fields': {k: _tog(request.form.get(f'scn_{k}', 'true')) for k in ['background', 'foreground', 'furniture', 'colors', 'lighting', 'theme']}}, 'detailer': {'detailer_id': _entity_id(request.form.get('detailer_id')), 'use_lora': request.form.get('detailer_use_lora') == 'on'}, 'look': {'look_id': _entity_id(request.form.get('look_id'))}, 'checkpoint': {'checkpoint_path': _entity_id(request.form.get('checkpoint_path'))}, 'resolution': { 'width': int(request.form.get('res_width', 1024)), 'height': int(request.form.get('res_height', 1024)), 'random': request.form.get('res_random') == 'on', }, 'tags': [t.strip() for t in request.form.get('tags', '').split(',') if t.strip()], } preset.data = new_data 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) flash('Preset saved!') return redirect(url_for('preset_detail', slug=slug)) characters = Character.query.order_by(Character.name).all() outfits = Outfit.query.order_by(Outfit.name).all() actions = Action.query.order_by(Action.name).all() styles = Style.query.order_by(Style.name).all() scenes = Scene.query.order_by(Scene.name).all() detailers = Detailer.query.order_by(Detailer.name).all() looks = Look.query.order_by(Look.name).all() checkpoints = Checkpoint.query.order_by(Checkpoint.name).all() return render_template('presets/edit.html', preset=preset, characters=characters, outfits=outfits, actions=actions, styles=styles, scenes=scenes, detailers=detailers, looks=looks, checkpoints=checkpoints) @app.route('/presets/rescan', methods=['POST']) def rescan_presets(): sync_presets() flash('Preset library synced.') return redirect(url_for('presets_index')) @app.route('/preset/create', methods=['GET', 'POST']) def create_preset(): form_data = {} if request.method == 'POST': name = request.form.get('name', '').strip() description = request.form.get('description', '').strip() use_llm = request.form.get('use_llm') == 'on' form_data = {'name': name, 'description': description, 'use_llm': use_llm} safe_id = re.sub(r'[^a-zA-Z0-9]+', '_', name.lower()).strip('_') or 'preset' safe_slug = re.sub(r'[^a-zA-Z0-9_]', '', safe_id) base_id = safe_id counter = 1 while os.path.exists(os.path.join(current_app.config['PRESETS_DIR'], f"{safe_id}.json")): safe_id = f"{base_id}_{counter}" safe_slug = re.sub(r'[^a-zA-Z0-9_]', '', safe_id) counter += 1 if use_llm and description: system_prompt = load_prompt('preset_system.txt') if not system_prompt: flash('Preset system prompt file not found.', 'error') return render_template('presets/create.html', form_data=form_data) try: llm_response = call_llm( f"Create a preset profile named '{name}' based on this description: {description}", system_prompt ) clean_json = llm_response.replace('```json', '').replace('```', '').strip() preset_data = json.loads(clean_json) except Exception as e: logger.exception("LLM error creating preset: %s", e) flash(f"AI generation failed: {e}", 'error') return render_template('presets/create.html', form_data=form_data) else: preset_data = { 'character': {'character_id': 'random', 'use_lora': True, 'fields': { 'identity': {k: True for k in ['base', 'head', 'upper_body', 'lower_body', 'hands', 'feet', 'additional']}, 'defaults': {k: False for k in ['expression', 'pose', 'scene']}, 'wardrobe': {'outfit': 'default', 'fields': {k: True for k in ['base', 'head', 'upper_body', 'lower_body', 'hands', 'feet', 'additional']}}, }}, 'outfit': {'outfit_id': None, 'use_lora': True}, 'action': {'action_id': None, 'use_lora': True, 'fields': {k: True for k in ['base', 'head', 'upper_body', 'lower_body', 'hands', 'feet', 'additional']}}, 'style': {'style_id': None, 'use_lora': True}, 'scene': {'scene_id': None, 'use_lora': True, 'fields': {k: True for k in ['background', 'foreground', 'furniture', 'colors', 'lighting', 'theme']}}, 'detailer': {'detailer_id': None, 'use_lora': True}, 'look': {'look_id': None}, 'checkpoint': {'checkpoint_path': None}, 'resolution': {'width': 1024, 'height': 1024, 'random': False}, 'tags': [], } preset_data['preset_id'] = safe_id preset_data['preset_name'] = name os.makedirs(current_app.config['PRESETS_DIR'], exist_ok=True) file_path = os.path.join(current_app.config['PRESETS_DIR'], f"{safe_id}.json") with open(file_path, 'w') as f: json.dump(preset_data, f, indent=2) new_preset = Preset(preset_id=safe_id, slug=safe_slug, filename=f"{safe_id}.json", name=name, data=preset_data) db.session.add(new_preset) db.session.commit() flash(f"Preset '{name}' created!") return redirect(url_for('edit_preset', slug=safe_slug)) return render_template('presets/create.html', form_data=form_data)