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,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']
|
||||
|
||||
Reference in New Issue
Block a user