fix(suggestions): resolve bare WD14 character names to existing "(Fandom)" tags
WD14 emits bare names (e.g. "Ruby Rose") while curated character/fandom
tags carry a fandom suffix ("Ruby Rose (RWBY)"). Two gaps caused a
duplicate, fandom-less tag to be created when users accepted a WD14
suggestion for an image that already had the curated version:
- Suggestion compute: bare character/copyright suggestions are now
suppressed when the image already carries a `Name (Fandom)` variant,
so the duplicate chip no longer surfaces.
- Accept endpoint: if the exact-name lookup misses for character/fandom
kinds, we try a `Name (%)`-scoped lookup and attach the existing tag
only when exactly one candidate matches. Zero or multiple matches
fall through to prior behavior to avoid silently guessing across
ambiguous fandoms.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+21
-3
@@ -767,9 +767,27 @@ def accept_image_suggestion(image_id):
|
|||||||
tag = Tag.query.filter_by(name=tag_name).first()
|
tag = Tag.query.filter_by(name=tag_name).first()
|
||||||
if tag is None:
|
if tag is None:
|
||||||
kind = _WD14_CATEGORY_TO_KIND.get(category) # None for 'general'
|
kind = _WD14_CATEGORY_TO_KIND.get(category) # None for 'general'
|
||||||
tag = Tag(name=tag_name, kind=kind)
|
# WD14 emits bare names ("Ruby Rose") while curated character/fandom tags
|
||||||
db.session.add(tag)
|
# carry a fandom suffix ("Ruby Rose (RWBY)"). Try to auto-resolve a bare
|
||||||
db.session.flush()
|
# suggestion to an existing fandom-scoped tag — but only when exactly one
|
||||||
|
# candidate exists; zero/multiple falls through to creating a new tag so
|
||||||
|
# we don't silently guess across ambiguous matches (e.g. same name in
|
||||||
|
# multiple fandoms).
|
||||||
|
if kind in ('character', 'fandom'):
|
||||||
|
candidates = (
|
||||||
|
Tag.query
|
||||||
|
.filter(Tag.kind == kind)
|
||||||
|
.filter(Tag.name.ilike(f'{tag_name} (%)'))
|
||||||
|
.limit(2)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
if len(candidates) == 1:
|
||||||
|
tag = candidates[0]
|
||||||
|
tag_name = tag.name # feedback row logs the resolved name
|
||||||
|
if tag is None:
|
||||||
|
tag = Tag(name=tag_name, kind=kind)
|
||||||
|
db.session.add(tag)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
if tag not in image.tags:
|
if tag not in image.tags:
|
||||||
image.tags.append(tag)
|
image.tags.append(tag)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ The result is grouped by category and annotated with whether each tag already
|
|||||||
exists in the Tag table (the modal dims disallowed auto-creations).
|
exists in the Tag table (the modal dims disallowed auto-creations).
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import re
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -106,6 +107,21 @@ def _existing_tag_names() -> set[str]:
|
|||||||
return {row[0] for row in db.session.query(Tag.name).all()}
|
return {row[0] for row in db.session.query(Tag.name).all()}
|
||||||
|
|
||||||
|
|
||||||
|
# Matches `Name (Fandom)` — used to recognize that an applied character/copyright
|
||||||
|
# tag covers a bare WD14 suggestion like `Name`.
|
||||||
|
_FANDOM_SUFFIX_RE = re.compile(r'^(.+?) \([^()]+\)$')
|
||||||
|
|
||||||
|
|
||||||
|
def _fandom_suffix_prefixes(names: Iterable[str]) -> set[str]:
|
||||||
|
"""Extract the `Name` portion from any `Name (Fandom)` entries in `names`."""
|
||||||
|
out: set[str] = set()
|
||||||
|
for n in names:
|
||||||
|
m = _FANDOM_SUFFIX_RE.match(n)
|
||||||
|
if m:
|
||||||
|
out.add(m.group(1))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _wd14_suggestions(image_id: int, cfg: dict, already: set[str]) -> list[dict]:
|
def _wd14_suggestions(image_id: int, cfg: dict, already: set[str]) -> list[dict]:
|
||||||
wd14_ver = cfg.get('wd14_model_version', _DEFAULTS['wd14_model_version'])
|
wd14_ver = cfg.get('wd14_model_version', _DEFAULTS['wd14_model_version'])
|
||||||
preds = (
|
preds = (
|
||||||
@@ -113,6 +129,11 @@ def _wd14_suggestions(image_id: int, cfg: dict, already: set[str]) -> list[dict]
|
|||||||
.filter_by(image_id=image_id, model_version=wd14_ver)
|
.filter_by(image_id=image_id, model_version=wd14_ver)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
# Bare names covered by an already-applied `Name (Fandom)` tag. WD14 emits
|
||||||
|
# bare names (e.g. "Ruby Rose") even when the curated tag is fandom-scoped
|
||||||
|
# ("Ruby Rose (RWBY)"); without this, the same character resurfaces as a
|
||||||
|
# duplicate suggestion and accepting it creates a second, fandom-less tag.
|
||||||
|
applied_fandom_prefixes = _fandom_suffix_prefixes(already)
|
||||||
out: list[dict] = []
|
out: list[dict] = []
|
||||||
for p in preds:
|
for p in preds:
|
||||||
threshold = _category_threshold(p.tag_category, cfg)
|
threshold = _category_threshold(p.tag_category, cfg)
|
||||||
@@ -121,6 +142,8 @@ def _wd14_suggestions(image_id: int, cfg: dict, already: set[str]) -> list[dict]
|
|||||||
canonical = _canonicalize_wd14_name(p.tag_name, p.tag_category)
|
canonical = _canonicalize_wd14_name(p.tag_name, p.tag_category)
|
||||||
if canonical in already:
|
if canonical in already:
|
||||||
continue
|
continue
|
||||||
|
if p.tag_category in ('character', 'copyright') and canonical in applied_fandom_prefixes:
|
||||||
|
continue
|
||||||
out.append({
|
out.append({
|
||||||
'name': canonical,
|
'name': canonical,
|
||||||
'category': p.tag_category,
|
'category': p.tag_category,
|
||||||
|
|||||||
Reference in New Issue
Block a user