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,3 +1,4 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
@@ -27,8 +28,10 @@ logger = logging.getLogger('gaze')
|
||||
|
||||
_job_queue_lock = threading.Lock()
|
||||
_job_queue = deque() # ordered list of job dicts (pending + paused + processing)
|
||||
_llm_queue = deque() # ordered list of LLM task dicts (pending + paused + processing)
|
||||
_job_history = {} # job_id -> job dict (all jobs ever added, for status lookup)
|
||||
_queue_worker_event = threading.Event() # signals worker that a new job is available
|
||||
_llm_worker_event = threading.Event() # signals LLM worker that a new task is available
|
||||
|
||||
# Stored reference to the Flask app, set by init_queue_worker()
|
||||
_app = None
|
||||
@@ -39,6 +42,7 @@ def _enqueue_job(label, workflow, finalize_fn):
|
||||
job = {
|
||||
'id': str(uuid.uuid4()),
|
||||
'label': label,
|
||||
'job_type': 'comfyui',
|
||||
'status': 'pending',
|
||||
'workflow': workflow,
|
||||
'finalize_fn': finalize_fn,
|
||||
@@ -55,6 +59,26 @@ def _enqueue_job(label, workflow, finalize_fn):
|
||||
return job
|
||||
|
||||
|
||||
def _enqueue_task(label, task_fn):
|
||||
"""Add a generic task job (e.g. LLM call) to the LLM queue. Returns the job dict."""
|
||||
job = {
|
||||
'id': str(uuid.uuid4()),
|
||||
'label': label,
|
||||
'job_type': 'llm',
|
||||
'status': 'pending',
|
||||
'task_fn': task_fn,
|
||||
'error': None,
|
||||
'result': None,
|
||||
'created_at': time.time(),
|
||||
}
|
||||
with _job_queue_lock:
|
||||
_llm_queue.append(job)
|
||||
_job_history[job['id']] = job
|
||||
logger.info("LLM task queued: [%s] %s", job['id'][:8], label)
|
||||
_llm_worker_event.set()
|
||||
return job
|
||||
|
||||
|
||||
def _queue_worker():
|
||||
"""Background thread: processes jobs from _job_queue sequentially."""
|
||||
while True:
|
||||
@@ -174,13 +198,14 @@ def _queue_worker():
|
||||
_prune_job_history()
|
||||
|
||||
|
||||
def _make_finalize(category, slug, db_model_class=None, action=None):
|
||||
def _make_finalize(category, slug, db_model_class=None, action=None, metadata=None):
|
||||
"""Return a finalize callback for a standard queue job.
|
||||
|
||||
category — upload sub-directory name (e.g. 'characters', 'outfits')
|
||||
slug — entity slug used for the upload folder name
|
||||
db_model_class — SQLAlchemy model class for cover-image DB update; None = skip
|
||||
action — 'replace' → update DB; None → always update; anything else → skip
|
||||
metadata — optional dict to write as JSON sidecar alongside the image
|
||||
"""
|
||||
def _finalize(comfy_prompt_id, job):
|
||||
logger.debug("=" * 80)
|
||||
@@ -212,6 +237,14 @@ def _make_finalize(category, slug, db_model_class=None, action=None):
|
||||
f.write(image_data)
|
||||
logger.info("Image saved: %s (%d bytes)", full_path, len(image_data))
|
||||
|
||||
# Write JSON sidecar with generation metadata (if provided)
|
||||
if metadata is not None:
|
||||
sidecar_name = filename.rsplit('.', 1)[0] + '.json'
|
||||
sidecar_path = os.path.join(folder, sidecar_name)
|
||||
with open(sidecar_path, 'w') as sf:
|
||||
json.dump(metadata, sf)
|
||||
logger.debug(" Sidecar written: %s", sidecar_path)
|
||||
|
||||
relative_path = f"{category}/{slug}/{filename}"
|
||||
# Include the seed used for this generation
|
||||
used_seed = job['workflow'].get('3', {}).get('inputs', {}).get('seed')
|
||||
@@ -244,6 +277,51 @@ def _make_finalize(category, slug, db_model_class=None, action=None):
|
||||
return _finalize
|
||||
|
||||
|
||||
def _llm_queue_worker():
|
||||
"""Background thread: processes LLM task jobs sequentially."""
|
||||
while True:
|
||||
_llm_worker_event.wait()
|
||||
_llm_worker_event.clear()
|
||||
|
||||
while True:
|
||||
job = None
|
||||
with _job_queue_lock:
|
||||
for j in _llm_queue:
|
||||
if j['status'] == 'pending':
|
||||
job = j
|
||||
break
|
||||
|
||||
if job is None:
|
||||
break
|
||||
|
||||
with _job_queue_lock:
|
||||
job['status'] = 'processing'
|
||||
|
||||
logger.info("LLM task started: [%s] %s", job['id'][:8], job['label'])
|
||||
|
||||
try:
|
||||
with _app.app_context():
|
||||
job['task_fn'](job)
|
||||
|
||||
with _job_queue_lock:
|
||||
job['status'] = 'done'
|
||||
logger.info("LLM task completed: [%s] %s", job['id'][:8], job['label'])
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("LLM task failed: [%s] %s — %s", job['id'][:8], job['label'], e)
|
||||
with _job_queue_lock:
|
||||
job['status'] = 'failed'
|
||||
job['error'] = str(e)
|
||||
|
||||
with _job_queue_lock:
|
||||
try:
|
||||
_llm_queue.remove(job)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
_prune_job_history()
|
||||
|
||||
|
||||
def _prune_job_history(max_age_seconds=3600):
|
||||
"""Remove completed/failed jobs older than max_age_seconds from _job_history."""
|
||||
cutoff = time.time() - max_age_seconds
|
||||
@@ -261,5 +339,5 @@ def init_queue_worker(flask_app):
|
||||
"""
|
||||
global _app
|
||||
_app = flask_app
|
||||
worker = threading.Thread(target=_queue_worker, daemon=True, name='queue-worker')
|
||||
worker.start()
|
||||
threading.Thread(target=_queue_worker, daemon=True, name='comfyui-worker').start()
|
||||
threading.Thread(target=_llm_queue_worker, daemon=True, name='llm-worker').start()
|
||||
|
||||
Reference in New Issue
Block a user