fix(suggestions): rejected tags stay rejected per image

Adds a per-image rejection filter to get_suggestions so once the user
clicks ✕ on an auto-accepted chip (or rejects a regular suggestion
via the existing ✕ button), that tag name stops reappearing in
suggestions — including auto_accept candidates.

Without this filter, the undo flow was cosmetic: the user's ✕
removed the tag from this image and logged a SuggestionFeedback
decision='rejected' row, but the next modal load re-ran auto-accept
against the same unchanged WD14 predictions and re-attached the same
tag. Rinse, repeat.

Implementation: _rejected_names_for_image(image_id) reads distinct
tag_names from suggestion_feedback where decision='rejected' for
this image. get_suggestions filters merged results against that set
alongside the existing blocklist filter. Both auto-accept candidates
and the three core categories respect the filter.

Tested on image 222 at threshold=0.9: first load auto-applied 23
general tags; rejected '1girl'; second load auto_accepted.count=0,
general suggestions had 10 new items, '1girl' absent from both.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 20:02:07 -04:00
parent 2c486b898c
commit 2347f5eaa2
+29
View File
@@ -114,6 +114,27 @@ def _blocklisted_names() -> set[str]:
return {row[0] for row in db.session.query(TagSuggestionBlocklistEntry.name).all()}
def _rejected_names_for_image(image_id: int) -> set[str]:
"""Tag names the user has rejected for this specific image. Includes
both explicit ✕ rejections on raw suggestions and the ✕/⊘ undo actions
on auto-accepted tags (both go through /suggestions/reject which writes
decision='rejected' rows). Once rejected for an image, a name stays
suppressed for that image so auto-accept doesn't re-apply it on the
next modal open."""
from app.models import SuggestionFeedback
rows = (
db.session
.query(SuggestionFeedback.tag_name)
.filter(
SuggestionFeedback.image_id == image_id,
SuggestionFeedback.decision == 'rejected',
)
.distinct()
.all()
)
return {r[0] for r in rows}
def _wd14_suggestions(image_id: int, cfg: dict, already: set[str]) -> list[dict]:
@@ -247,6 +268,14 @@ def get_suggestions(
if blocklist:
merged = [s for s in merged if s['name'] not in blocklist]
# Drop names the user has already rejected for THIS image. Without this,
# auto-accept re-applies the same WD14 prediction every time the modal
# loads, and the user's ✕ click on an auto-accepted chip is immediately
# undone on the next view.
per_image_rejected = _rejected_names_for_image(image_id)
if per_image_rejected:
merged = [s for s in merged if s['name'] not in per_image_rejected]
if existing_all is None:
existing_all = _existing_tag_names()