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:
Aodhan Collins
2026-03-21 03:22:09 +00:00
parent 7d79e626a5
commit 32a73b02f5
72 changed files with 3163 additions and 2212 deletions

View File

@@ -14,6 +14,13 @@ from models import (
logger = logging.getLogger('gaze')
def _sync_nsfw_from_tags(entity, data):
"""Sync is_nsfw from data['tags']['nsfw'] if tags is a dict. Never touches is_favourite."""
tags = data.get('tags')
if isinstance(tags, dict):
entity.is_nsfw = bool(tags.get('nsfw', False))
def sync_characters():
if not os.path.exists(current_app.config['CHARACTERS_DIR']):
return
@@ -44,6 +51,7 @@ def sync_characters():
character.name = name
character.slug = slug
character.filename = filename
_sync_nsfw_from_tags(character, data)
# Check if cover image still exists
if character.image_path:
@@ -62,6 +70,7 @@ def sync_characters():
name=name,
data=data
)
_sync_nsfw_from_tags(new_char, data)
db.session.add(new_char)
except Exception as e:
print(f"Error importing {filename}: {e}")
@@ -102,6 +111,7 @@ def sync_outfits():
outfit.name = name
outfit.slug = slug
outfit.filename = filename
_sync_nsfw_from_tags(outfit, data)
# Check if cover image still exists
if outfit.image_path:
@@ -120,6 +130,7 @@ def sync_outfits():
name=name,
data=data
)
_sync_nsfw_from_tags(new_outfit, data)
db.session.add(new_outfit)
except Exception as e:
print(f"Error importing outfit {filename}: {e}")
@@ -243,6 +254,7 @@ def sync_looks():
look.slug = slug
look.filename = filename
look.character_id = character_id
_sync_nsfw_from_tags(look, data)
if look.image_path:
full_img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], look.image_path)
@@ -259,6 +271,7 @@ def sync_looks():
character_id=character_id,
data=data
)
_sync_nsfw_from_tags(new_look, data)
db.session.add(new_look)
except Exception as e:
print(f"Error importing look {filename}: {e}")
@@ -418,6 +431,7 @@ def sync_actions():
action.name = name
action.slug = slug
action.filename = filename
_sync_nsfw_from_tags(action, data)
# Check if cover image still exists
if action.image_path:
@@ -435,6 +449,7 @@ def sync_actions():
name=name,
data=data
)
_sync_nsfw_from_tags(new_action, data)
db.session.add(new_action)
except Exception as e:
print(f"Error importing action {filename}: {e}")
@@ -475,6 +490,7 @@ def sync_styles():
style.name = name
style.slug = slug
style.filename = filename
_sync_nsfw_from_tags(style, data)
# Check if cover image still exists
if style.image_path:
@@ -492,6 +508,7 @@ def sync_styles():
name=name,
data=data
)
_sync_nsfw_from_tags(new_style, data)
db.session.add(new_style)
except Exception as e:
print(f"Error importing style {filename}: {e}")
@@ -532,6 +549,7 @@ def sync_detailers():
detailer.name = name
detailer.slug = slug
detailer.filename = filename
_sync_nsfw_from_tags(detailer, data)
# Check if cover image still exists
if detailer.image_path:
@@ -549,6 +567,7 @@ def sync_detailers():
name=name,
data=data
)
_sync_nsfw_from_tags(new_detailer, data)
db.session.add(new_detailer)
except Exception as e:
print(f"Error importing detailer {filename}: {e}")
@@ -589,6 +608,7 @@ def sync_scenes():
scene.name = name
scene.slug = slug
scene.filename = filename
_sync_nsfw_from_tags(scene, data)
# Check if cover image still exists
if scene.image_path:
@@ -606,6 +626,7 @@ def sync_scenes():
name=name,
data=data
)
_sync_nsfw_from_tags(new_scene, data)
db.session.add(new_scene)
except Exception as e:
print(f"Error importing scene {filename}: {e}")
@@ -679,19 +700,22 @@ def sync_checkpoints():
ckpt.slug = slug
ckpt.checkpoint_path = checkpoint_path
ckpt.data = data
_sync_nsfw_from_tags(ckpt, data)
flag_modified(ckpt, "data")
if ckpt.image_path:
full_img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], ckpt.image_path)
if not os.path.exists(full_img_path):
ckpt.image_path = None
else:
db.session.add(Checkpoint(
new_ckpt = Checkpoint(
checkpoint_id=checkpoint_id,
slug=slug,
name=display_name,
checkpoint_path=checkpoint_path,
data=data,
))
)
_sync_nsfw_from_tags(new_ckpt, data)
db.session.add(new_ckpt)
all_ckpts = Checkpoint.query.all()
for ckpt in all_ckpts: