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

@@ -1,10 +1,14 @@
import logging
from services.job_queue import (
_job_queue_lock, _job_queue, _job_history, _queue_worker_event,
_job_queue_lock, _job_queue, _llm_queue, _job_history,
_queue_worker_event, _llm_worker_event,
)
logger = logging.getLogger('gaze')
# Both queues for iteration
_ALL_QUEUES = (_job_queue, _llm_queue)
def register_routes(app):
@@ -12,23 +16,27 @@ def register_routes(app):
def api_queue_list():
"""Return the current queue as JSON."""
with _job_queue_lock:
jobs = [
{
'id': j['id'],
'label': j['label'],
'status': j['status'],
'error': j['error'],
'created_at': j['created_at'],
}
for j in _job_queue
]
jobs = []
for q in _ALL_QUEUES:
for j in q:
jobs.append({
'id': j['id'],
'label': j['label'],
'status': j['status'],
'error': j['error'],
'created_at': j['created_at'],
'job_type': j.get('job_type', 'comfyui'),
})
return {'jobs': jobs, 'count': len(jobs)}
@app.route('/api/queue/count')
def api_queue_count():
"""Return just the count of active (non-done, non-failed) jobs."""
with _job_queue_lock:
count = sum(1 for j in _job_queue if j['status'] in ('pending', 'processing', 'paused'))
count = sum(
1 for q in _ALL_QUEUES for j in q
if j['status'] in ('pending', 'processing', 'paused')
)
return {'count': count}
@app.route('/api/queue/<job_id>/remove', methods=['POST'])
@@ -40,10 +48,12 @@ def register_routes(app):
return {'error': 'Job not found'}, 404
if job['status'] == 'processing':
return {'error': 'Cannot remove a job that is currently processing'}, 400
try:
_job_queue.remove(job)
except ValueError:
pass # Already not in queue
for q in _ALL_QUEUES:
try:
q.remove(job)
break
except ValueError:
continue
job['status'] = 'removed'
return {'status': 'ok'}
@@ -58,24 +68,29 @@ def register_routes(app):
job['status'] = 'paused'
elif job['status'] == 'paused':
job['status'] = 'pending'
_queue_worker_event.set()
# Signal the appropriate worker
if job.get('job_type') == 'llm':
_llm_worker_event.set()
else:
_queue_worker_event.set()
else:
return {'error': f'Cannot pause/resume job with status {job["status"]}'}, 400
return {'status': 'ok', 'new_status': job['status']}
@app.route('/api/queue/clear', methods=['POST'])
def api_queue_clear():
"""Clear all pending jobs from the queue (allows current processing job to finish)."""
"""Clear all pending jobs from the queue (allows current processing jobs to finish)."""
removed_count = 0
with _job_queue_lock:
pending_jobs = [j for j in _job_queue if j['status'] == 'pending']
for job in pending_jobs:
try:
_job_queue.remove(job)
job['status'] = 'removed'
removed_count += 1
except ValueError:
pass
for q in _ALL_QUEUES:
pending_jobs = [j for j in q if j['status'] == 'pending']
for job in pending_jobs:
try:
q.remove(job)
job['status'] = 'removed'
removed_count += 1
except ValueError:
pass
logger.info("Cleared %d pending jobs from queue", removed_count)
return {'status': 'ok', 'removed_count': removed_count}
@@ -91,7 +106,8 @@ def register_routes(app):
'label': job['label'],
'status': job['status'],
'error': job['error'],
'comfy_prompt_id': job['comfy_prompt_id'],
'job_type': job.get('job_type', 'comfyui'),
'comfy_prompt_id': job.get('comfy_prompt_id'),
}
if job.get('result'):
resp['result'] = job['result']