Replaces old list-format tags (which duplicated prompt content) with structured dict tags per category (origin_series, outfit_type, participants, style_type, scene_type, etc.). Tags are now purely organizational metadata — removed from the prompt pipeline entirely. Adds is_favourite and is_nsfw columns to all 8 resource models. Favourite is DB-only (user preference); NSFW is mirrored in JSON tags for rescan persistence. All library pages get filter controls and favourites-first sorting. Introduces a parallel LLM job queue (_enqueue_task + _llm_queue_worker) for background tag regeneration, with the same status polling UI as ComfyUI jobs. Fixes call_llm() to use has_request_context() fallback for background threads. Adds global search (/search) across resources and gallery images, with navbar search bar. Adds gallery image sidecar JSON for per-image favourite/NSFW metadata. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
4.9 KiB
Python
136 lines
4.9 KiB
Python
import logging
|
|
|
|
from flask import render_template, request, redirect, url_for, flash
|
|
from models import Preset
|
|
from services.generation import generate_from_preset
|
|
from services.file_io import get_available_checkpoints
|
|
from services.comfyui import get_loaded_checkpoint
|
|
from services.workflow import _get_default_checkpoint
|
|
from services.sync import _resolve_preset_entity
|
|
|
|
logger = logging.getLogger('gaze')
|
|
|
|
|
|
def register_routes(app):
|
|
|
|
@app.route('/generator', methods=['GET'])
|
|
def generator():
|
|
presets = Preset.query.order_by(Preset.name).all()
|
|
checkpoints = get_available_checkpoints()
|
|
|
|
selected_ckpt = get_loaded_checkpoint()
|
|
if not selected_ckpt:
|
|
default_path, _ = _get_default_checkpoint()
|
|
selected_ckpt = default_path
|
|
|
|
# Pre-select preset from query param
|
|
preset_slug = request.args.get('preset', '')
|
|
|
|
return render_template('generator.html',
|
|
presets=presets,
|
|
checkpoints=checkpoints,
|
|
selected_ckpt=selected_ckpt,
|
|
preset_slug=preset_slug)
|
|
|
|
@app.route('/generator/generate', methods=['POST'])
|
|
def generator_generate():
|
|
preset_slug = request.form.get('preset_slug', '').strip()
|
|
if not preset_slug:
|
|
return {'error': 'No preset selected'}, 400
|
|
|
|
preset = Preset.query.filter_by(slug=preset_slug).first()
|
|
if not preset:
|
|
return {'error': 'Preset not found'}, 404
|
|
|
|
try:
|
|
overrides = {
|
|
'checkpoint': request.form.get('checkpoint', '').strip() or None,
|
|
'extra_positive': request.form.get('extra_positive', '').strip(),
|
|
'extra_negative': request.form.get('extra_negative', '').strip(),
|
|
'action': 'preview',
|
|
}
|
|
|
|
seed_val = request.form.get('seed', '').strip()
|
|
if seed_val:
|
|
overrides['seed'] = int(seed_val)
|
|
|
|
width = request.form.get('width', '').strip()
|
|
height = request.form.get('height', '').strip()
|
|
if width and height:
|
|
overrides['width'] = int(width)
|
|
overrides['height'] = int(height)
|
|
|
|
job = generate_from_preset(preset, overrides, save_category='generator')
|
|
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
return {'status': 'queued', 'job_id': job['id']}
|
|
|
|
flash("Generation queued.")
|
|
return redirect(url_for('generator', preset=preset_slug))
|
|
|
|
except Exception as e:
|
|
logger.exception("Generator error: %s", e)
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
return {'error': str(e)}, 500
|
|
flash(f"Error: {str(e)}")
|
|
return redirect(url_for('generator', preset=preset_slug))
|
|
|
|
@app.route('/generator/preset_info', methods=['GET'])
|
|
def generator_preset_info():
|
|
"""Return resolved entity names for a preset (for the summary panel)."""
|
|
slug = request.args.get('slug', '')
|
|
if not slug:
|
|
return {'error': 'slug required'}, 400
|
|
|
|
preset = Preset.query.filter_by(slug=slug).first()
|
|
if not preset:
|
|
return {'error': 'not found'}, 404
|
|
|
|
data = preset.data
|
|
info = {}
|
|
|
|
# Character
|
|
char_cfg = data.get('character', {})
|
|
char_id = char_cfg.get('character_id')
|
|
if char_id == 'random':
|
|
info['character'] = 'Random'
|
|
elif char_id:
|
|
obj = _resolve_preset_entity('character', char_id)
|
|
info['character'] = obj.name if obj else char_id
|
|
else:
|
|
info['character'] = None
|
|
|
|
# Secondary entities
|
|
for key, label in [('outfit', 'outfit'), ('action', 'action'), ('style', 'style'),
|
|
('scene', 'scene'), ('detailer', 'detailer'), ('look', 'look')]:
|
|
cfg = data.get(key, {})
|
|
eid = cfg.get(f'{key}_id')
|
|
if eid == 'random':
|
|
info[label] = 'Random'
|
|
elif eid:
|
|
obj = _resolve_preset_entity(key, eid)
|
|
info[label] = obj.name if obj else eid
|
|
else:
|
|
info[label] = None
|
|
|
|
# Checkpoint
|
|
ckpt_cfg = data.get('checkpoint', {})
|
|
ckpt_path = ckpt_cfg.get('checkpoint_path')
|
|
if ckpt_path == 'random':
|
|
info['checkpoint'] = 'Random'
|
|
elif ckpt_path:
|
|
info['checkpoint'] = ckpt_path.split('/')[-1].replace('.safetensors', '')
|
|
else:
|
|
info['checkpoint'] = 'Default'
|
|
|
|
# Resolution
|
|
res_cfg = data.get('resolution', {})
|
|
if res_cfg.get('random'):
|
|
info['resolution'] = 'Random'
|
|
elif res_cfg.get('width') and res_cfg.get('height'):
|
|
info['resolution'] = f"{res_cfg['width']}x{res_cfg['height']}"
|
|
else:
|
|
info['resolution'] = 'Default'
|
|
|
|
return info
|