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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user