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>
This commit is contained in:
@@ -1,154 +1,135 @@
|
||||
import json
|
||||
import logging
|
||||
from flask import render_template, request, redirect, url_for, flash, session, current_app
|
||||
from models import db, Character, Outfit, Action, Style, Scene, Detailer, Checkpoint
|
||||
from services.prompts import build_prompt, build_extras_prompt
|
||||
from services.workflow import _prepare_workflow, _get_default_checkpoint
|
||||
from services.job_queue import _enqueue_job, _make_finalize
|
||||
|
||||
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', 'POST'])
|
||||
@app.route('/generator', methods=['GET'])
|
||||
def generator():
|
||||
characters = Character.query.order_by(Character.name).all()
|
||||
presets = Preset.query.order_by(Preset.name).all()
|
||||
checkpoints = get_available_checkpoints()
|
||||
actions = Action.query.order_by(Action.name).all()
|
||||
outfits = Outfit.query.order_by(Outfit.name).all()
|
||||
scenes = Scene.query.order_by(Scene.name).all()
|
||||
styles = Style.query.order_by(Style.name).all()
|
||||
detailers = Detailer.query.order_by(Detailer.name).all()
|
||||
|
||||
if not checkpoints:
|
||||
checkpoints = ["Noob/oneObsession_v19Atypical.safetensors"]
|
||||
|
||||
# Default to whatever is currently loaded in ComfyUI, then settings default
|
||||
selected_ckpt = get_loaded_checkpoint()
|
||||
if not selected_ckpt:
|
||||
default_path, _ = _get_default_checkpoint()
|
||||
selected_ckpt = default_path
|
||||
|
||||
if request.method == 'POST':
|
||||
char_slug = request.form.get('character')
|
||||
checkpoint = request.form.get('checkpoint')
|
||||
custom_positive = request.form.get('positive_prompt', '')
|
||||
custom_negative = request.form.get('negative_prompt', '')
|
||||
# Pre-select preset from query param
|
||||
preset_slug = request.args.get('preset', '')
|
||||
|
||||
action_slugs = request.form.getlist('action_slugs')
|
||||
outfit_slugs = request.form.getlist('outfit_slugs')
|
||||
scene_slugs = request.form.getlist('scene_slugs')
|
||||
style_slugs = request.form.getlist('style_slugs')
|
||||
detailer_slugs = request.form.getlist('detailer_slugs')
|
||||
override_prompt = request.form.get('override_prompt', '').strip()
|
||||
width = request.form.get('width') or 1024
|
||||
height = request.form.get('height') or 1024
|
||||
return render_template('generator.html',
|
||||
presets=presets,
|
||||
checkpoints=checkpoints,
|
||||
selected_ckpt=selected_ckpt,
|
||||
preset_slug=preset_slug)
|
||||
|
||||
character = Character.query.filter_by(slug=char_slug).first_or_404()
|
||||
@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
|
||||
|
||||
sel_actions = Action.query.filter(Action.slug.in_(action_slugs)).all() if action_slugs else []
|
||||
sel_outfits = Outfit.query.filter(Outfit.slug.in_(outfit_slugs)).all() if outfit_slugs else []
|
||||
sel_scenes = Scene.query.filter(Scene.slug.in_(scene_slugs)).all() if scene_slugs else []
|
||||
sel_styles = Style.query.filter(Style.slug.in_(style_slugs)).all() if style_slugs else []
|
||||
sel_detailers = Detailer.query.filter(Detailer.slug.in_(detailer_slugs)).all() if detailer_slugs else []
|
||||
preset = Preset.query.filter_by(slug=preset_slug).first()
|
||||
if not preset:
|
||||
return {'error': 'Preset not found'}, 404
|
||||
|
||||
try:
|
||||
with open('comfy_workflow.json', 'r') as f:
|
||||
workflow = json.load(f)
|
||||
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',
|
||||
}
|
||||
|
||||
# Build base prompts from character defaults
|
||||
prompts = build_prompt(character.data, default_fields=character.default_fields)
|
||||
seed_val = request.form.get('seed', '').strip()
|
||||
if seed_val:
|
||||
overrides['seed'] = int(seed_val)
|
||||
|
||||
if override_prompt:
|
||||
prompts["main"] = override_prompt
|
||||
else:
|
||||
extras = build_extras_prompt(sel_actions, sel_outfits, sel_scenes, sel_styles, sel_detailers)
|
||||
combined = prompts["main"]
|
||||
if extras:
|
||||
combined = f"{combined}, {extras}"
|
||||
if custom_positive:
|
||||
combined = f"{custom_positive}, {combined}"
|
||||
prompts["main"] = combined
|
||||
width = request.form.get('width', '').strip()
|
||||
height = request.form.get('height', '').strip()
|
||||
if width and height:
|
||||
overrides['width'] = int(width)
|
||||
overrides['height'] = int(height)
|
||||
|
||||
# Apply face/hand prompt overrides if provided
|
||||
override_face = request.form.get('override_face_prompt', '').strip()
|
||||
override_hand = request.form.get('override_hand_prompt', '').strip()
|
||||
if override_face:
|
||||
prompts["face"] = override_face
|
||||
if override_hand:
|
||||
prompts["hand"] = override_hand
|
||||
job = generate_from_preset(preset, overrides, save_category='generator')
|
||||
|
||||
# Parse optional seed
|
||||
seed_val = request.form.get('seed', '').strip()
|
||||
fixed_seed = int(seed_val) if seed_val else None
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'status': 'queued', 'job_id': job['id']}
|
||||
|
||||
# Prepare workflow - first selected item per category supplies its LoRA slot
|
||||
ckpt_obj = Checkpoint.query.filter_by(checkpoint_path=checkpoint).first() if checkpoint else None
|
||||
workflow = _prepare_workflow(
|
||||
workflow, character, prompts, checkpoint, custom_negative,
|
||||
outfit=sel_outfits[0] if sel_outfits else None,
|
||||
action=sel_actions[0] if sel_actions else None,
|
||||
style=sel_styles[0] if sel_styles else None,
|
||||
detailer=sel_detailers[0] if sel_detailers else None,
|
||||
scene=sel_scenes[0] if sel_scenes else None,
|
||||
width=width,
|
||||
height=height,
|
||||
checkpoint_data=ckpt_obj.data if ckpt_obj else None,
|
||||
fixed_seed=fixed_seed,
|
||||
)
|
||||
flash("Generation queued.")
|
||||
return redirect(url_for('generator', preset=preset_slug))
|
||||
|
||||
print(f"Queueing generator prompt for {character.character_id}")
|
||||
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))
|
||||
|
||||
_finalize = _make_finalize('characters', character.slug)
|
||||
label = f"Generator: {character.name}"
|
||||
job = _enqueue_job(label, workflow, _finalize)
|
||||
@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
|
||||
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'status': 'queued', 'job_id': job['id']}
|
||||
preset = Preset.query.filter_by(slug=slug).first()
|
||||
if not preset:
|
||||
return {'error': 'not found'}, 404
|
||||
|
||||
flash("Generation queued.")
|
||||
except Exception as e:
|
||||
print(f"Generator error: {e}")
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return {'error': str(e)}, 500
|
||||
flash(f"Error: {str(e)}")
|
||||
data = preset.data
|
||||
info = {}
|
||||
|
||||
return render_template('generator.html', characters=characters, checkpoints=checkpoints,
|
||||
actions=actions, outfits=outfits, scenes=scenes,
|
||||
styles=styles, detailers=detailers, selected_ckpt=selected_ckpt)
|
||||
# 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
|
||||
|
||||
@app.route('/generator/preview_prompt', methods=['POST'])
|
||||
def generator_preview_prompt():
|
||||
char_slug = request.form.get('character')
|
||||
if not char_slug:
|
||||
return {'error': 'No character selected'}, 400
|
||||
# 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
|
||||
|
||||
character = Character.query.filter_by(slug=char_slug).first()
|
||||
if not character:
|
||||
return {'error': 'Character not found'}, 404
|
||||
# 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'
|
||||
|
||||
action_slugs = request.form.getlist('action_slugs')
|
||||
outfit_slugs = request.form.getlist('outfit_slugs')
|
||||
scene_slugs = request.form.getlist('scene_slugs')
|
||||
style_slugs = request.form.getlist('style_slugs')
|
||||
detailer_slugs = request.form.getlist('detailer_slugs')
|
||||
custom_positive = request.form.get('positive_prompt', '')
|
||||
# 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'
|
||||
|
||||
sel_actions = Action.query.filter(Action.slug.in_(action_slugs)).all() if action_slugs else []
|
||||
sel_outfits = Outfit.query.filter(Outfit.slug.in_(outfit_slugs)).all() if outfit_slugs else []
|
||||
sel_scenes = Scene.query.filter(Scene.slug.in_(scene_slugs)).all() if scene_slugs else []
|
||||
sel_styles = Style.query.filter(Style.slug.in_(style_slugs)).all() if style_slugs else []
|
||||
sel_detailers = Detailer.query.filter(Detailer.slug.in_(detailer_slugs)).all() if detailer_slugs else []
|
||||
|
||||
prompts = build_prompt(character.data, default_fields=character.default_fields)
|
||||
extras = build_extras_prompt(sel_actions, sel_outfits, sel_scenes, sel_styles, sel_detailers)
|
||||
combined = prompts["main"]
|
||||
if extras:
|
||||
combined = f"{combined}, {extras}"
|
||||
if custom_positive:
|
||||
combined = f"{custom_positive}, {combined}"
|
||||
|
||||
return {'prompt': combined, 'face': prompts['face'], 'hand': prompts['hand']}
|
||||
return info
|
||||
|
||||
Reference in New Issue
Block a user