diff --git a/app/services/tag_suggestions.py b/app/services/tag_suggestions.py index 4de9922..36ae260 100644 --- a/app/services/tag_suggestions.py +++ b/app/services/tag_suggestions.py @@ -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()