Files
FabledCurator/tests/test_post_naming.py
T
bvandeusenandClaude Opus 5 c67a8c313e
CI and images / lint (push) Failing after 3s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 23s
CI and images / backend-lint-and-test (push) Successful in 32s
CI and images / integration (push) Successful in 2m19s
CI and images / sign-extension (push) Skipped
CI and images / build-web (push) Skipped
CI and images / smoke-web (push) Skipped
CI and images / promote (push) Skipped
CI and images / build-agent (push) Skipped
fix: count the POSTS a working name spans, not the files (4392)
Counting files punished a piece for the one thing a working name is
guaranteed to do — have several exports. Measured across the operator's four
dual-platform artists: knuxy carries `p217` on four files spanning exactly two
posts, the Patreon post and the Discord drop, and ~200 comic-page tokens have
that shape. Every one scored half strength. Counting posts scores them 1.00
and still catches the habits, which span many posts rather than many files:
tamadaheijun's `comic2` spans 8, conto's `seth2` 5, `maid` 4. Knuxy's
ambiguous band drops from 268 tokens to 55.

Three false positives found the same way — by running the module over the real
library rather than reading it:

  * The screenshot guard matches from the start of the stem, so it never fired
    on the legacy `<post id>_media_<media id>_` era. `Screenshot 2025-07-27
    182450ab` sailed through and contributed `2025-07-27` — the same-day date
    collision this module's docstring exists to refuse.
  * tamadaheijun names screenshots in Japanese. A guard that knows only the
    English word is a guard for one artist.
  * `timeline 3-0002` spans three unrelated posts. Keeping hyphens inside
    tokens for `0-k`'s sake let the number survive whole, so an identity token
    now has to contain a letter — which is the property behind the old
    bare-year and all-digits rules anyway, and leaves `0-k`, `680lc` and `p59`
    untouched.

Also stopwords for `the`, `gif`, `main`, `patreon`, `capture`, `timeline`,
each measured carrying a false match inside the admitted band.

IDENTITY_FLOOR names what a shared name must reach to propose a link with no
corroboration. At 0.75, 13 of artist 8's 15 name-sharing pairs clear it,
including the operator's own example. The two that do not are real pairs this
signal will not carry alone — the stated cost of refusing the four-post band,
where conto's `illustration9` and `maid` also sit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
2026-09-24 08:00:36 -04:00

308 lines
12 KiB
Python

"""Milestone 388: the creator's own working name, recovered from a filename.
Two of the operator's artists post a censored or cropped teaser on Patreon and
the real release in their Discord. That pair is the one a whole-image
comparison handles WORST — a crop with a censor bar is exactly the local edit
that moves a perceptual hash and blurs a semantic embedding — and it is the
pair the creator names identically on both platforms.
Everything here was calibrated against the live library (artist 8, 520 images,
409 distinct tokens) rather than invented, so the cases carry their measured
numbers. The property the whole module serves is that a WRONG link is worse
than no link: no link leaves the operator where they already were, a wrong one
tells them two different pieces are the same piece.
"""
from collections import Counter
import pytest
from backend.app.services.post_naming import (
IDENTITY_FLOOR,
MAX_MARKER_POSTS,
MAX_TOKEN_POSTS,
marker_frequencies,
marker_overlap,
shared_identity,
text_markers,
token_frequencies,
working_name_tokens,
)
# --- what survives each platform's framing -----------------------------------
#
# Three naming eras reach this (#4002), and a token has to come out the same
# from all of them or the signal only works on whichever era it was written
# against.
@pytest.mark.parametrize(
"path, expected, era",
[
(
"/images/yellowroom/patreon/2026-09-23_170392790_Anya/01_((0-k.jpg",
"0-k",
"current Patreon: a per-post media index",
),
(
"20260923_1552424119805673604_01_0-k_base.jpg",
"0-k",
"Discord: gallery-dl's date_messageid_num_ prefix (#3999)",
),
(
"85317841_media_212565911_0071 NoHeart__c3118a69f3__c3118a69f3.jpg",
"noheart",
"legacy: flat at the artist root, post id + media id + doubled hash",
),
],
)
def test_one_working_name_survives_every_naming_era(path, expected, era):
assert expected in working_name_tokens(path), era
def test_a_hyphen_stays_inside_a_token():
"""`0-k` is a real working name on the live instance — it is the operator's
own example pair. Splitting on hyphen reduces it to two single characters
and then discards both for being too short, which silently loses the one
case this was built to catch."""
assert working_name_tokens("01_((0-k.jpg") == {"0-k"}
def test_a_work_in_progress_matches_the_piece_it_became():
"""Measured: `cnni6600 wip1` (Patreon) and `cnni6600_Base` (Discord) are
one piece. A decoration is a stage, not a name."""
assert working_name_tokens("01_cnni6600 wip1.png") == working_name_tokens(
"20250514_1372285581945471087_01_cnni6600_Base.jpg"
)
@pytest.mark.parametrize(
"path, why",
[
(
"109078417_media_334848471_Screenshot 2025-07-27 182450ab.png",
"the guard matches from the START of the stem, so while the legacy "
"prefix was left on, a legacy screenshot never looked like one — "
"and contributed `2025-07-27`, the exact same-day date collision "
"this module refuses",
),
(
"136070668_media_513155924_\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8 2025-07-27 9.31.png",
"tamadaheijun's screenshots are named in Japanese; a guard that "
"only knows the English word is a guard for one artist",
),
(
"129421439_media_469882823_timeline 3-0002.jpg",
"keeping hyphens inside tokens for `0-k`'s sake let `3-0002` "
"survive whole, and it was MEASURED spanning three unrelated posts",
),
],
)
def test_measured_false_positives_contribute_nothing(path, why):
"""Each of these was found by running the module against the operator's
real library, not by reading it — which is the only way any of them would
have been found."""
assert working_name_tokens(path) == set(), why
def test_an_identity_token_must_contain_a_letter():
"""The property behind refusing bare years, bare numbers and date
fragments, stated once. `0-k`, `680lc` and `p59` all keep a letter."""
assert working_name_tokens("01_2025-07-27.jpg") == set()
assert working_name_tokens("01_0-k.jpg") == {"0-k"}
def test_the_literal_string_none_is_not_a_name():
"""#3999 rendered `{user[name]}` as "None" for ~1,600 files, which made it
the single most common "name" in the library and an identity for nothing."""
assert working_name_tokens("20180508_None_DCthingwhat__0123456789.jpg") == {
"dcthingwhat"
}
# --- the false positive that shaped the design -------------------------------
@pytest.mark.parametrize(
"path",
[
"01_Screenshot 2026-08-13 000004.png", # Patreon's spelling
"20260813_1234567890123_01_Screenshot_2026-08-13_032144.png", # Discord's
],
)
def test_a_screenshot_contributes_nothing_at_all(path):
"""A first pass matched these two to each other on the token `2026-08-13`.
A screenshot filename is a camera artifact carrying no working name, and
its date collides across platforms BY CONSTRUCTION: the teaser and the
release go out the same day, so their screenshot names always share one.
That is a signal firing precisely when it is least informative.
Both spellings matter. `\\b` after "shot" was the first guard and it caught
only the Patreon one, because `_` is a WORD character so
`Screenshot_2026-08-13` never presented a word boundary there. Found by
running against the live library, not by reading the regex.
"""
assert working_name_tokens(path) == set()
# --- rarity, which both signals now share ------------------------------------
def test_a_token_on_two_images_is_full_strength():
"""Two is the floor rather than one: a genuine match means the token is on
at least two files, so demanding uniqueness rejects every real pair."""
freqs = Counter({"connfront": 2})
assert shared_identity({"connfront"}, {"connfront"}, freqs) == (1.0, "connfront")
def test_a_character_name_can_never_link_two_posts():
"""THE false-positive guard, and the reason the cap is set where it is.
Measured on artist 8: `anya` is on 8 images, `riju` 9, `undyne` 8, `bea` 16.
Ungated, every Anya post would match every Anya drop."""
freqs = Counter({"anya": 8})
assert shared_identity({"anya"}, {"anya"}, freqs) == (0.0, None)
def test_a_token_at_the_cap_names_nothing():
"""It decays to zero, and reporting it anyway would hand the review queue a
reason with no weight behind it — "matched on loislanetb2", with nothing
there. A token is named only while it is doing work."""
freqs = Counter({"tok": MAX_TOKEN_POSTS})
assert shared_identity({"tok"}, {"tok"}, freqs) == (0.0, None)
def test_the_rarest_shared_token_decides_not_the_count_of_them():
"""One decisive token beats three vague ones. A pair sharing a piece name
AND two habits is evidence of the piece name."""
freqs = Counter({"rare": 2, "goo": 9, "shiny": 7})
strength, token = shared_identity(
{"rare", "goo", "shiny"}, {"rare", "goo", "shiny"}, freqs
)
assert (strength, token) == (1.0, "rare")
def test_frequencies_are_counted_per_artist_not_per_library():
"""A working name belongs to the person who chose it; the same string is
one creator's piece and another's boilerplate."""
counts = token_frequencies([
["01_ConnFront.jpg"],
["20230222_1078078245695664148_01_ConnFront.jpg"],
])
assert counts["connfront"] == 2
# --- the text marker, gated the same way -------------------------------------
def test_the_same_marker_reads_the_same_through_both_platforms():
"""The operator's pair carries `\U0001F348\U0001F348` in the Patreon title and
`@everyone \U0001F348 \U0001F348` in the Discord message. Spacing is a platform's
rendering, not the creator's intent."""
assert "\U0001F348" in text_markers("Anya -- \U0001F348\U0001F348")
assert "\U0001F348" in text_markers("@everyone \U0001F348 \U0001F348")
def test_one_marker_the_creator_uses_nowhere_else_is_the_whole_signal():
"""Measured: `\U0001F317` is on exactly two of this artist's 300 posts, and they
are the pair. An earlier pass halved every total so a long body could not
out-vote a short one — which the rarity gate now does properly, and which
meant this case could reach only 0.5."""
freqs = Counter({"\U0001F317": 2})
assert marker_overlap("\U0001F317", "\U0001F317 drop", freqs) == 1.0
def test_a_marker_the_creator_uses_habitually_is_worth_nothing():
"""The bug this gate exists for. Measured on artist 8: \U0001F4A6 is in 13 of 300
posts — punctuation, about one post in twenty-five. Ungated it scored a
full 1.00 and was the DECIDING term in a proposal that proximity alone
could not carry, which is proximity wearing a hat."""
freqs = Counter({"\U0001F4A6": 13})
assert marker_overlap("Drizzle \U0001F4A6", "\U0001F4A6", freqs) == 0.0
def test_a_shared_ordinary_word_is_worth_nothing():
"""Also measured, also a real proposal: the shared "marker" between a
teaser and a drop was the word `like`, which is in 43 of this artist's
posts."""
freqs = Counter({"like": 43})
assert marker_overlap("like this", "like that", freqs) == 0.0
def test_a_symbol_outweighs_a_word_at_equal_rarity():
"""Prose shares words by accident; a creator who writes "commission" in
both posts on a Tuesday has said nothing the timestamps did not."""
freqs = Counter({"\U0001F317": 2, "commission": 2})
symbol = marker_overlap("\U0001F317", "\U0001F317", freqs)
word = marker_overlap("commission", "commission", freqs)
assert word < symbol
def test_marker_frequencies_count_posts_not_occurrences():
"""A creator who repeats an emoji six times in one body has used it once as
far as identity goes."""
counts = marker_frequencies(["\U0001F348 \U0001F348 \U0001F348 \U0001F348", "\U0001F348"])
assert counts["\U0001F348"] == 2
def test_the_marker_gate_is_tighter_than_the_filename_gate():
"""Both caps count POSTS, so they are directly comparable and the gap is a
claim: a working name is the creator's private label for one piece and may
honestly recur as they revisit it, while a marker is public decoration and
stops being evidence the moment it is reused."""
assert MAX_MARKER_POSTS < MAX_TOKEN_POSTS
def test_a_name_must_clear_the_floor_to_link_on_its_own():
"""The floor is the whole two-route design in one number: identity may
propose alone, circumstance never may. Pinned against the threshold it
guards so the two cannot drift apart silently."""
assert 0.0 < IDENTITY_FLOOR <= 1.0
assert IDENTITY_FLOOR > 0.60 # the matcher's default threshold
# --- the count is of POSTS, which is what makes the cap mean anything --------
def test_a_piece_with_many_exports_is_not_penalised_for_having_them():
"""Counting FILES punishes a piece for the one thing a working name is
guaranteed to do. Measured: knuxy carries `p217` on four files across
exactly two posts — the Patreon post and the Discord drop — and roughly
two hundred comic-page tokens have that shape. File-counting scored every
one of them at half strength."""
counts = token_frequencies([
["p217.jpg", "p217-clean.jpg"], # the Patreon post
["20240101_123456789_01_p217.jpg", "..._02_p217-clean.jpg"], # the drop
])
assert counts["p217"] == 2
assert shared_identity({"p217"}, {"p217"}, counts) == (1.0, "p217")
def test_a_name_reused_across_many_posts_is_still_caught():
"""The other half of the same property — the cap has to keep working once
the unit changes. Measured habits: tamadaheijun's `comic2` spans 8 posts,
conto's `seth2` 5."""
counts = token_frequencies([["comic2_%02d.jpg" % i] for i in range(8)])
assert counts["comic2"] == 8
assert shared_identity({"comic2"}, {"comic2"}, counts) == (0.0, None)
def test_marker_overlap_cannot_be_called_without_the_frequencies():
"""An ungated call is the bug the signature exists to make impossible to
write by accident. A default would have kept it one keyword away."""
with pytest.raises(TypeError):
marker_overlap("\U0001F348", "\U0001F348")