feat(suggestions): user-managed blocklist for noisy auto-tags
Adds a tag_suggestion_blocklist table + service-layer filter so the
user can permanently suppress specific canonical tag names from the
suggestion stream (WD14 anatomy/composition tags like '1girl',
'breasts', 'nipples' that aren't useful for the discoverability use
case this app targets).
Data model
- migration k26042201: tag_suggestion_blocklist(name TEXT PK, created_at)
- model TagSuggestionBlocklistEntry
Service
- tag_suggestions._blocklisted_names() snapshots the current set
- get_suggestions filters merged results before grouping, so both
WD14- and embedding-sourced suggestions respect the blocklist
- get_bulk_suggestions inherits the filter via its per-image call
to get_suggestions
API
- GET /api/suggestions/blocklist -> {ok, names}
- POST /api/suggestions/blocklist -> add one
- POST /api/suggestions/blocklist/delete -> remove one
- POST /api/suggestions/blocklist/bulk -> replace the whole list
(backs the settings textarea save button)
UI
- modal suggestion chip gets a third action button (⊘) alongside
accept (✓) / reject (✕). Clicking it adds the name to the
blocklist, logs a rejection for ML feedback on this image, and
sweeps every chip on the page carrying that same name.
- Settings -> Maintenance -> Suggestion blocklist section with a
monospaced textarea (one name per line) + Save. Loads current
entries on mount.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+62
@@ -936,6 +936,68 @@ def reject_image_suggestion(image_id):
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Suggestion blocklist — user-managed list of canonical tag names that
|
||||
# should never appear as auto-suggestions (e.g. noisy WD14 anatomy tags).
|
||||
# ---------------------------
|
||||
|
||||
@main.get("/api/suggestions/blocklist")
|
||||
def list_suggestion_blocklist():
|
||||
from app.models import TagSuggestionBlocklistEntry
|
||||
rows = (
|
||||
TagSuggestionBlocklistEntry.query
|
||||
.order_by(TagSuggestionBlocklistEntry.name)
|
||||
.all()
|
||||
)
|
||||
return jsonify(ok=True, names=[r.name for r in rows])
|
||||
|
||||
|
||||
@main.post("/api/suggestions/blocklist")
|
||||
def add_suggestion_blocklist():
|
||||
from app.models import TagSuggestionBlocklistEntry
|
||||
payload = request.get_json(force=True, silent=True) or {}
|
||||
name = (payload.get('name') or '').strip()
|
||||
if not name:
|
||||
return jsonify(ok=False, error='missing_name'), 400
|
||||
existing = TagSuggestionBlocklistEntry.query.get(name)
|
||||
if existing is not None:
|
||||
return jsonify(ok=True, added=False, name=name)
|
||||
db.session.add(TagSuggestionBlocklistEntry(name=name))
|
||||
db.session.commit()
|
||||
return jsonify(ok=True, added=True, name=name)
|
||||
|
||||
|
||||
@main.post("/api/suggestions/blocklist/delete")
|
||||
def remove_suggestion_blocklist():
|
||||
from app.models import TagSuggestionBlocklistEntry
|
||||
payload = request.get_json(force=True, silent=True) or {}
|
||||
name = (payload.get('name') or '').strip()
|
||||
if not name:
|
||||
return jsonify(ok=False, error='missing_name'), 400
|
||||
existing = TagSuggestionBlocklistEntry.query.get(name)
|
||||
if existing is not None:
|
||||
db.session.delete(existing)
|
||||
db.session.commit()
|
||||
return jsonify(ok=True, removed=True, name=name)
|
||||
return jsonify(ok=True, removed=False, name=name)
|
||||
|
||||
|
||||
@main.post("/api/suggestions/blocklist/bulk")
|
||||
def bulk_replace_suggestion_blocklist():
|
||||
"""Replace the entire blocklist with the provided names. Textarea-save path."""
|
||||
from app.models import TagSuggestionBlocklistEntry
|
||||
payload = request.get_json(force=True, silent=True) or {}
|
||||
names = payload.get('names', [])
|
||||
if not isinstance(names, list):
|
||||
return jsonify(ok=False, error='names_must_be_list'), 400
|
||||
cleaned = sorted({str(n).strip() for n in names if str(n) and str(n).strip()})
|
||||
TagSuggestionBlocklistEntry.query.delete()
|
||||
for name in cleaned:
|
||||
db.session.add(TagSuggestionBlocklistEntry(name=name))
|
||||
db.session.commit()
|
||||
return jsonify(ok=True, count=len(cleaned), names=cleaned)
|
||||
|
||||
|
||||
@main.post("/image/<int:image_id>/tags/add")
|
||||
def add_tag(image_id):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user