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>
107 lines
5.6 KiB
HTML
107 lines
5.6 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block content %}
|
|
{% from "partials/library_toolbar.html" import library_toolbar %}
|
|
{{ library_toolbar(
|
|
title="Outfit",
|
|
category="outfits",
|
|
create_url=url_for('create_outfit'),
|
|
create_label="Outfit",
|
|
has_batch_gen=true,
|
|
has_regen_all=true,
|
|
has_lora_create=true,
|
|
bulk_create_url=url_for('bulk_create_outfits_from_loras'),
|
|
has_tags=true,
|
|
regen_tags_category="outfits",
|
|
rescan_url=url_for('rescan_outfits'),
|
|
get_missing_url="/get_missing_outfits",
|
|
clear_covers_url="/clear_all_outfit_covers",
|
|
generate_url_pattern="/outfit/{slug}/generate"
|
|
) }}
|
|
|
|
<!-- Filters -->
|
|
<form method="get" class="mb-3 d-flex gap-3 align-items-center">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="favourite" value="on" id="favFilter" {% if favourite_filter == 'on' %}checked{% endif %} onchange="this.form.submit()">
|
|
<label class="form-check-label small" for="favFilter">★ Favourites</label>
|
|
</div>
|
|
<select name="nsfw" class="form-select form-select-sm" style="width:auto;" onchange="this.form.submit()">
|
|
<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>
|
|
</form>
|
|
|
|
<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 outfit in outfits %}
|
|
<div class="col" id="card-{{ outfit.slug }}">
|
|
<div class="card h-100 character-card {% if request.args.get('highlight') == outfit.slug %}border-success border-3 highlight-card{% endif %}" onclick="window.location.href='/outfit/{{ outfit.slug }}'">
|
|
<div class="img-container">
|
|
{% if outfit.image_path %}
|
|
<img id="img-{{ outfit.slug }}" src="{{ url_for('static', filename='uploads/' + outfit.image_path) }}" alt="{{ outfit.name }}">
|
|
<span id="no-img-{{ outfit.slug }}" class="text-muted d-none">No Image</span>
|
|
{% else %}
|
|
{% set fallback = random_gen_image('outfits', outfit.slug) %}
|
|
{% if fallback %}
|
|
<img id="img-{{ outfit.slug }}" src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ outfit.name }}" class="fallback-cover">
|
|
<span id="no-img-{{ outfit.slug }}" class="text-muted d-none">No Image</span>
|
|
{% else %}
|
|
<img id="img-{{ outfit.slug }}" src="" alt="{{ outfit.name }}" class="d-none">
|
|
<span id="no-img-{{ outfit.slug }}" class="text-muted">No Image</span>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% if outfit.data.lora and outfit.data.lora.lora_name and lora_assignments.get(outfit.data.lora.lora_name, 0) > 0 %}
|
|
<span class="assignment-badge" title="Assigned to {{ lora_assignments.get(outfit.data.lora.lora_name, 0) }} character(s)">{{ lora_assignments.get(outfit.data.lora.lora_name, 0) }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
<h5 class="card-title text-center">{% if outfit.is_favourite %}<span class="text-warning">★</span> {% endif %}{{ outfit.name }}{% if outfit.is_nsfw %} <span class="badge bg-danger" style="font-size:0.6rem;vertical-align:middle;">NSFW</span>{% endif %}</h5>
|
|
<p class="card-text small text-center text-muted">
|
|
{% set ns = namespace(parts=[]) %}
|
|
{% if outfit.data.wardrobe is mapping %}
|
|
{% for v in outfit.data.wardrobe.values() %}
|
|
{% if v %}{% set ns.parts = ns.parts + [v] %}{% endif %}
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% if outfit.data.lora and outfit.data.lora.lora_triggers %}
|
|
{% set ns.parts = ns.parts + [outfit.data.lora.lora_triggers] %}
|
|
{% endif %}
|
|
{{ ns.parts | join(', ') }}
|
|
</p>
|
|
</div>
|
|
<div class="card-footer d-flex justify-content-between align-items-center p-1">
|
|
{% if outfit.data.lora and outfit.data.lora.lora_name %}
|
|
{% set lora_name = outfit.data.lora.lora_name.split('/')[-1].replace('.safetensors', '') %}
|
|
<small class="text-muted text-truncate" title="{{ outfit.data.lora.lora_name }}">{{ lora_name }}</small>
|
|
{% else %}<span></span>{% endif %}
|
|
<button class="btn btn-sm btn-outline-danger py-0 px-1 flex-shrink-0 ms-1 resource-delete-btn" title="Delete"
|
|
data-category="outfits" data-slug="{{ outfit.slug }}" data-name="{{ outfit.name | e }}">🗑</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<style>
|
|
.highlight-card {
|
|
animation: highlight-pulse 2s ease-in-out 3;
|
|
box-shadow: 0 0 20px rgba(25, 135, 84, 0.5) !important;
|
|
}
|
|
@keyframes highlight-pulse {
|
|
0%, 100% { box-shadow: 0 0 20px rgba(25, 135, 84, 0.5); }
|
|
50% { box-shadow: 0 0 30px rgba(25, 135, 84, 0.8); }
|
|
}
|
|
</style>
|
|
<script>
|
|
// Handle highlight parameter
|
|
const highlightSlug = new URLSearchParams(window.location.search).get('highlight');
|
|
if (highlightSlug) {
|
|
const card = document.getElementById(`card-${highlightSlug}`);
|
|
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
</script>
|
|
<script src="{{ url_for('static', filename='js/library-toolbar.js') }}"></script>
|
|
{% endblock %}
|