fix(suggestions): parse (Fandom) suffix on accept + heal legacy rows
Two-part fix for the reported bug where accepting a character
suggestion named 'Jinx (League Of Legends)' created a single
malformed character tag with fandom_id=NULL and the whole suffix
embedded in the name, and never attached the fandom tag to the
image.
Root cause: WD14 predictions store names like
'jinx_(league_of_legends)'. app.services.tag_suggestions
._canonicalize_wd14_name transforms that to 'Jinx (League Of
Legends)' for character/copyright categories. The suggestion chip
renders that canonicalized string. On ✓, accept_image_suggestion
looked for an existing character with name = 'Jinx (League Of
Legends)' — post-refactor no such row exists (names are bare) —
fell into the 0-candidate branch, and created a fresh malformed
character.
Going forward (main.py):
accept_image_suggestion's character branch now splits a
'(Fandom)' suffix off the incoming name before the 0/1/N lookup.
If it splits, find-or-create the fandom tag, then find-or-create
a character with (kind='character', name=bare, fandom_id=<f>).
If no suffix, unchanged 0/1/N behavior.
Backfill for already-corrupted rows (tasks/maintenance.py):
New _heal_malformed_character_names pass runs before the
existing sync-fandoms-to-images logic. Finds
(kind='character', fandom_id IS NULL, name LIKE '% (%)%'),
parses the suffix via the same regex, and either promotes the
row to (name=bare, fandom_id=<f>) or merges into an existing
canonical character. tag_reference_embedding rows (string PK)
are cascade-renamed alongside. Same auto-merge semantics as
migration j26042101 Phase 2.
Implementation is all raw SQL to sidestep an ORM autoflush
ordering quirk: ORM-level fandom INSERT + subsequent tag UPDATE
referencing its fresh id hit a transient FK violation in worker
task context. Raw SQL with an explicit commit after fandom
insert avoids the ambiguity.
Verified locally:
- Fresh accept of 'CanonicalChar (Canonical Fandom)' against a
pre-existing canonical character: resolved to the canonical
row, fandom attached to image.
- Seeded malformed 'LegacyJinx (League Of Legends)' character
with fandom_id=NULL attached to an image: sync task healed it
to bare 'LegacyJinx' + fandom_id, attached the fandom row to
image_tags, final counts {healed:1, skipped:0,
links_added:1}.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+45
-20
@@ -872,27 +872,52 @@ def accept_image_suggestion(image_id):
|
||||
if tag is None:
|
||||
return jsonify(ok=False, error="tag_not_found"), 404
|
||||
elif kind == "character":
|
||||
candidates = Tag.query.filter_by(kind="character", name=name).all()
|
||||
if len(candidates) == 1:
|
||||
tag = candidates[0]
|
||||
elif len(candidates) == 0:
|
||||
tag = Tag(kind="character", name=name, fandom_id=None)
|
||||
db.session.add(tag)
|
||||
db.session.flush()
|
||||
# WD14 emits character names in 'bare_(fandom)' form. After canonical
|
||||
# case/underscore handling the incoming name may look like
|
||||
# 'Soraka (League Of Legends)'. Before the post-refactor 0/1/N
|
||||
# lookup, split the '(Fandom)' suffix and resolve the fandom to a
|
||||
# fandom_id — otherwise the lookup misses every existing bare-name
|
||||
# character and we'd create a new character whose entire name is
|
||||
# 'Soraka (League Of Legends)' with fandom_id=NULL.
|
||||
import re
|
||||
fandom_suffix_match = re.match(r'^(.+?) \(([^()]+)\)$', name)
|
||||
if fandom_suffix_match:
|
||||
bare_name = fandom_suffix_match.group(1).strip()
|
||||
suffix_fandom_name = fandom_suffix_match.group(2).strip()
|
||||
f_tag = _ensure_fandom_tag(suffix_fandom_name)
|
||||
candidates = (
|
||||
Tag.query
|
||||
.filter_by(kind="character", name=bare_name, fandom_id=f_tag.id)
|
||||
.all()
|
||||
)
|
||||
if len(candidates) == 1:
|
||||
tag = candidates[0]
|
||||
else:
|
||||
tag = Tag(kind="character", name=bare_name, fandom_id=f_tag.id)
|
||||
db.session.add(tag)
|
||||
db.session.flush()
|
||||
else:
|
||||
return jsonify(
|
||||
ok=False,
|
||||
error="ambiguous",
|
||||
candidates=[
|
||||
{
|
||||
"id": c.id,
|
||||
"display_name": c.display_name,
|
||||
"fandom_id": c.fandom_id,
|
||||
"fandom_name": c.fandom.name if c.fandom else None,
|
||||
}
|
||||
for c in candidates
|
||||
],
|
||||
), 409
|
||||
candidates = Tag.query.filter_by(kind="character", name=name).all()
|
||||
if len(candidates) == 1:
|
||||
tag = candidates[0]
|
||||
elif len(candidates) == 0:
|
||||
tag = Tag(kind="character", name=name, fandom_id=None)
|
||||
db.session.add(tag)
|
||||
db.session.flush()
|
||||
else:
|
||||
return jsonify(
|
||||
ok=False,
|
||||
error="ambiguous",
|
||||
candidates=[
|
||||
{
|
||||
"id": c.id,
|
||||
"display_name": c.display_name,
|
||||
"fandom_id": c.fandom_id,
|
||||
"fandom_name": c.fandom.name if c.fandom else None,
|
||||
}
|
||||
for c in candidates
|
||||
],
|
||||
), 409
|
||||
else:
|
||||
effective_kind = kind if kind else "user"
|
||||
tag = Tag.query.filter_by(kind=effective_kind, name=name).first()
|
||||
|
||||
Reference in New Issue
Block a user