fix(tags): preserve acronym casing in tag normalization (DC, NSFW)

normalize_tag_name now only capitalizes the first letter of each word and
leaves the rest of the word untouched (was lowercasing the tail, which turned
DC→Dc / NSFW→Nsfw). This matches ml/tag_name._title_word, so a Camie-suggested
tag keeps the exact casing the suggestion UI showed when it round-trips through
POST /api/tags on Accept — addressing "auto-suggested tags must obey
capitalization" and "don't mangle acronyms" in one rule.

Trade-off (operator-chosen): all-caps input no longer folds to Title Case, so
case-variant merging in #714 still folds the dominant lowercase-vs-Title case
but leaves all-caps stylizations distinct (protecting acronyms wins). Tests
updated + a new test documenting acronym preservation / non-folding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 17:39:54 -04:00
parent 9374f63953
commit df2310bc70
4 changed files with 39 additions and 10 deletions
+9 -5
View File
@@ -17,15 +17,19 @@ log = logging.getLogger(__name__)
def normalize_tag_name(name: str) -> str:
"""Canonical tag form (#701): collapse whitespace + Title Case.
"""Canonical tag form (#701): collapse whitespace + capitalize the first
letter of each word, PRESERVING the rest of the word.
Per-word capitalize (NOT str.title(), which mangles apostrophes:
don't → Don'T). Lowercasing the word tail also folds ALL-CAPS input
(HATSUNE MIKU → Hatsune Miku); acronym casing is sacrificed, an accepted
Title-Case trade-off (operator-chosen 2026-06-06).
don't → Don'T). The word tail is left untouched so acronyms survive
(DC stays DC, NSFW stays NSFW) — operator-revised 2026-06-06: protecting
acronyms matters more than folding ALL-CAPS input. This MATCHES
ml/tag_name._title_word, so a tag suggested by the Camie tagger keeps the
exact casing the suggestion UI showed when it round-trips through the
create endpoint on Accept.
"""
return " ".join(
w[:1].upper() + w[1:].lower() for w in (name or "").split()
w[:1].upper() + w[1:] for w in (name or "").split()
)