feat(suggestions): auto-accept threshold + retroactive blocklist sweep
Two capabilities that work together to turn the suggestion system from
a 'review every noisy chip' UX into a 'curated tags only, configurable
auto-apply' UX for a solo-user library.
Auto-accept threshold
Service (tag_suggestions.py):
- New default auto_accept_general_threshold = 0.95 in _DEFAULTS
- get_suggestions splits general-category hits at/above the threshold
into a separate auto_accept_candidates list; the core 'character'/
'copyright'/'general' keys stay the same shape for existing callers
- get_bulk_suggestions ignores the new key (no side effects in bulk)
Route (main.py GET /image/<id>/suggestions):
- For each candidate: find-or-create Tag(kind='user', name=name), attach
to image, log SuggestionFeedback(source='wd14'|etc, decision='accepted')
- Response adds 'auto_accepted: [{id, name, display_name, kind,
confidence, source}, ...]' so the modal can review + undo
Config endpoints (main.py):
GET /api/suggestions/config/auto-accept-threshold
POST /api/suggestions/config/auto-accept-threshold {threshold}
Values > 1.0 effectively disable the feature
UI (view-modal.js):
- New renderAutoAccepted block at top of suggestions section with
green-tinted chips carrying ✕ (undo for this image, logs rejection)
and ⊘ (blocklist + remove from all images)
- loadSuggestions refreshes the tag pill list when auto_accepted has
items so the user sees them in the Tags section too
Settings:
- Maintenance tab gains a number input + save button backed by
/api/suggestions/config/auto-accept-threshold
Retroactive blocklist sweep
New Celery task app.tasks.maintenance.sweep_blocklisted_tag_from_images
on the maintenance queue. Enqueued automatically when a name is added
via POST /api/suggestions/blocklist or appears new in the bulk replace.
Task body: finds kind='user' Tag matching the name, deletes its
image_tags rows, deletes the Tag itself. Scope limited to kind='user'
so deliberate character/fandom/artist tags sharing a blocklisted name
aren't silently destroyed.
Verification (local dev):
- Threshold GET/POST round-trip correct (default 0.95, persisted in
tag_suggestion_config)
- Image 633 at threshold=0.9: 4 general tags auto-applied, each with
a SuggestionFeedback row, returned in auto_accepted
- Blocklist add of 'sweeptestonly' with an attached test tag: Redis
maintenance queue depth = 1, task body removes 1 image_tags row +
the Tag row
- get_bulk_suggestions still works (auto_accept_candidates key skipped)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -458,6 +458,23 @@
|
||||
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h3>Auto-accept threshold (general tags)</h3>
|
||||
<p class="settings-hint">
|
||||
When the modal loads suggestions for an image, any general-category
|
||||
tag with confidence at or above this threshold is auto-applied and
|
||||
shown in a reviewable "Auto-accepted" block at the top of the
|
||||
suggestions section. Lower values accept more tags (riskier);
|
||||
values above 1.0 effectively disable the feature. Default is 0.95.
|
||||
</p>
|
||||
<div class="mt-sm">
|
||||
<input type="number" id="autoAcceptThresholdInput" class="settings-input"
|
||||
min="0" max="1.1" step="0.01" style="width: 7rem;" />
|
||||
<button type="button" id="autoAcceptThresholdSaveBtn" class="btn secondary-btn">Save threshold</button>
|
||||
<span id="autoAcceptThresholdStatus" class="settings-hint" style="margin-left: 0.5rem;"></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h3>Suggestion blocklist</h3>
|
||||
<p class="settings-hint">
|
||||
@@ -465,8 +482,11 @@
|
||||
post-canonicalization WD14 output (e.g. <code>1girl</code>,
|
||||
<code>breasts</code>). Blocklisted names never appear in the
|
||||
modal's auto-suggestions — either WD14 or embedding-similarity
|
||||
sourced. You can also add entries inline by clicking the ⊘ button
|
||||
on any suggestion chip.
|
||||
sourced. Adding a name also enqueues a cleanup task that removes
|
||||
the tag from every image that has it attached (kind='user' only;
|
||||
deliberate character / artist / fandom tags sharing the same name
|
||||
are left alone). You can also add entries inline by clicking the
|
||||
⊘ button on any suggestion chip.
|
||||
</p>
|
||||
<textarea id="suggestionBlocklistArea" class="settings-textarea" rows="10"
|
||||
placeholder="1girl 2girls breasts nipples penis"></textarea>
|
||||
@@ -521,6 +541,54 @@
|
||||
saveBtn.addEventListener('click', save);
|
||||
load();
|
||||
})();
|
||||
|
||||
(function(){
|
||||
const input = document.getElementById('autoAcceptThresholdInput');
|
||||
const saveBtn = document.getElementById('autoAcceptThresholdSaveBtn');
|
||||
const status = document.getElementById('autoAcceptThresholdStatus');
|
||||
if (!input || !saveBtn) return;
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const r = await fetch('/api/suggestions/config/auto-accept-threshold');
|
||||
const j = await r.json();
|
||||
if (j.ok) input.value = String(j.threshold);
|
||||
} catch { /* leave empty */ }
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saveBtn.disabled = true;
|
||||
status.textContent = 'Saving…';
|
||||
const threshold = parseFloat(input.value);
|
||||
if (isNaN(threshold) || threshold < 0) {
|
||||
status.textContent = 'Enter a number >= 0 (use > 1 to disable).';
|
||||
saveBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r = await fetch('/api/suggestions/config/auto-accept-threshold', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ threshold }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
status.textContent = `Saved (threshold = ${j.threshold}).`;
|
||||
input.value = String(j.threshold);
|
||||
} else {
|
||||
status.textContent = 'Save failed: ' + (j.error || 'unknown');
|
||||
}
|
||||
} catch (e) {
|
||||
status.textContent = 'Save failed: ' + e;
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
setTimeout(() => { status.textContent = ''; }, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
saveBtn.addEventListener('click', save);
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user