Files
character-browser/routes/generator.py
Aodhan Collins 32a73b02f5 Add semantic tagging, search, favourite/NSFW filtering, and LLM job queue
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>
2026-03-21 03:22:09 +00:00

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