Files
character-browser/templates/search.html
Aodhan Collins 32a73b02f5 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>
2026-03-21 03:22:09 +00:00

127 lines
6.3 KiB
HTML

{% extends "layout.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Search</h2>
</div>
<!-- Search form -->
<form method="get" class="mb-4">
<div class="row g-2 align-items-end">
<div class="col-md-5">
<input type="text" name="q" class="form-control" placeholder="Search resources and images..." value="{{ query }}" autofocus>
</div>
<div class="col-auto">
<select name="category" class="form-select form-select-sm" style="width:auto;">
<option value="all" {% if category == 'all' %}selected{% endif %}>All categories</option>
<option value="characters" {% if category == 'characters' %}selected{% endif %}>Characters</option>
<option value="looks" {% if category == 'looks' %}selected{% endif %}>Looks</option>
<option value="outfits" {% if category == 'outfits' %}selected{% endif %}>Outfits</option>
<option value="actions" {% if category == 'actions' %}selected{% endif %}>Actions</option>
<option value="styles" {% if category == 'styles' %}selected{% endif %}>Styles</option>
<option value="scenes" {% if category == 'scenes' %}selected{% endif %}>Scenes</option>
<option value="detailers" {% if category == 'detailers' %}selected{% endif %}>Detailers</option>
<option value="checkpoints" {% if category == 'checkpoints' %}selected{% endif %}>Checkpoints</option>
</select>
</div>
<div class="col-auto">
<select name="nsfw" class="form-select form-select-sm" style="width:auto;">
<option value="all" {% if nsfw_filter == 'all' %}selected{% endif %}>All ratings</option>
<option value="sfw" {% if nsfw_filter == 'sfw' %}selected{% endif %}>SFW only</option>
<option value="nsfw" {% if nsfw_filter == 'nsfw' %}selected{% endif %}>NSFW only</option>
</select>
</div>
<div class="col-auto">
<select name="type" class="form-select form-select-sm" style="width:auto;">
<option value="all" {% if search_type == 'all' %}selected{% endif %}>Resources & Images</option>
<option value="resources" {% if search_type == 'resources' %}selected{% endif %}>Resources only</option>
<option value="images" {% if search_type == 'images' %}selected{% endif %}>Images only</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary btn-sm">Search</button>
</div>
</div>
</form>
{% if query %}
<p class="text-muted mb-3">Found {{ total_resources }} resource{{ 's' if total_resources != 1 }} and {{ total_images }} image{{ 's' if total_images != 1 }} for "<strong>{{ query }}</strong>"</p>
{% set type_labels = {
'characters': 'Characters', 'looks': 'Looks', 'outfits': 'Outfits',
'actions': 'Actions', 'styles': 'Styles', 'scenes': 'Scenes',
'detailers': 'Detailers', 'checkpoints': 'Checkpoints'
} %}
{% set type_url_prefix = {
'characters': '/character', 'looks': '/look', 'outfits': '/outfit',
'actions': '/action', 'styles': '/style', 'scenes': '/scene',
'detailers': '/detailer', 'checkpoints': '/checkpoint'
} %}
<!-- Resource results -->
{% if grouped_resources %}
{% for cat_name, items in grouped_resources.items() %}
<div class="mb-4">
<h5>{{ type_labels.get(cat_name, cat_name | capitalize) }} <span class="badge bg-secondary">{{ items | length }}</span></h5>
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-3">
{% for item in items %}
<div class="col">
<div class="card h-100 character-card" onclick="window.location.href='{{ type_url_prefix.get(cat_name, '/' + cat_name[:-1]) }}/{{ item.slug }}'">
<div class="img-container">
{% if item.image_path %}
<img src="{{ url_for('static', filename='uploads/' + item.image_path) }}" alt="{{ item.name }}">
{% else %}
<span class="text-muted">No Image</span>
{% endif %}
</div>
<div class="card-body">
<h6 class="card-title text-center mb-1">
{% if item.is_favourite %}<span class="text-warning">&#9733;</span> {% endif %}
{{ item.name }}
{% if item.is_nsfw %}<span class="badge bg-danger" style="font-size:0.55rem;vertical-align:middle;">NSFW</span>{% endif %}
</h6>
<p class="card-text small text-center text-muted text-truncate" title="{{ item.match_context }}">{{ item.match_context }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endif %}
<!-- Image results -->
{% if images %}
<div class="mb-4">
<h5>Gallery Images <span class="badge bg-secondary">{{ images | length }}</span></h5>
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-3">
{% for img in images %}
<div class="col">
<div class="card h-100 character-card" onclick="window.location.href='/gallery?category={{ img.category }}&slug={{ img.slug }}'">
<div class="img-container">
<img src="{{ url_for('static', filename='uploads/' + img.path) }}" alt="{{ img.slug }}">
{% if img.is_favourite %}
<span class="gallery-fav-star active" style="position:absolute;top:4px;right:4px;font-size:1.2rem;color:#ffc107;">&#9733;</span>
{% endif %}
{% if img.is_nsfw %}
<span class="badge bg-danger" style="position:absolute;top:4px;left:4px;font-size:0.55rem;">NSFW</span>
{% endif %}
</div>
<div class="card-body py-1">
<p class="card-text small text-center text-muted text-truncate">{{ img.category }}/{{ img.slug }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if not grouped_resources and not images %}
<p class="text-muted">No results found.</p>
{% endif %}
{% endif %}
{% endblock %}