feat: a teaser's card references the drop it announced and the piece's variants (4402, 4401)
The operator's problem: a Patreon teaser is a pointer, and its card showed the censored crop plus a text link while the content it pointed at sat on another card. The fix is a REFERENCE, not an absorption: "the nested items on the unified post are a duplicate or reference of existing content". Nothing is written. Discord posts keep their own rows, dates and places in the feed. - post_unification: for each teaser with a linked association, the drop's images and text, plus its variant family: Discord images sharing the seed's gated LEADING working name, or a phash near-duplicate, within a window of the teaser. One hop only, oldest first. - Measured on artist 8 before writing it: of 121 message pairs 2-60 days apart that share a gated token, 106 share the leading name and all read as real families. Of the 15 sharing only a trailing word, 14 are sibling pieces and one is a plain collision (`bottom`, 56 days). The family cap is 8, not the pairing cap of 6, because `tentacooler` and `0-k1` (6 posts each) are real families. - The feed drops a linked drop's own card only within discord_link_fold_hours of its teaser (default 24): "only hidden from the post view they're posted the same day". Older referenced posts stay where they landed. - post_association.linked_by records whether FC or a person made the link, so the card can say so. Undo is the existing dismiss. - `image0` (gallery-dl's fallback name) becomes a stopword. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
@@ -112,6 +112,10 @@ _STOPWORDS = frozenset({
|
||||
# `{user[name]}` as it for ~1,600 files, so it is the single most common
|
||||
# "name" in the library and identifies nothing.
|
||||
"none",
|
||||
# gallery-dl's fallback when a Discord attachment has no filename of its
|
||||
# own. Measured on artist 8: four unrelated images across 1,974 days, and
|
||||
# the one false family the leading-name rule admitted inside 60 days.
|
||||
"image0",
|
||||
})
|
||||
|
||||
# A bare year: still needed for the TEXT signal, where words and numbers are
|
||||
@@ -170,19 +174,17 @@ def _strip_prefixes(stem: str) -> str:
|
||||
return _HASH_SUFFIX.sub("", stem)
|
||||
|
||||
|
||||
def working_name_tokens(path: str) -> set[str]:
|
||||
"""The identity-bearing tokens in one image's filename.
|
||||
def _ordered_tokens(path: str) -> list[str]:
|
||||
"""The identity-bearing tokens of one filename, in the order written.
|
||||
|
||||
Returns an EMPTY set for a name that carries no working title — a
|
||||
screenshot, a bare number, a stopword. Empty means "no evidence", which the
|
||||
caller must treat as silence rather than as a weak match; see the module
|
||||
docstring for the false positive that rule exists for.
|
||||
The one tokenizer both public readings share, so the set of names and the
|
||||
leading name cannot disagree about what counts as a name.
|
||||
"""
|
||||
stem = _strip_prefixes(PurePosixPath(path).stem)
|
||||
if _SCREENSHOT.match(stem.strip()):
|
||||
return set()
|
||||
return []
|
||||
|
||||
out: set[str] = set()
|
||||
out: list[str] = []
|
||||
# Hyphens are kept INSIDE tokens — `0-k` is a real working name on the live
|
||||
# instance, and splitting on hyphen would reduce it to a single character
|
||||
# and then discard it for being too short.
|
||||
@@ -192,10 +194,43 @@ def working_name_tokens(path: str) -> set[str]:
|
||||
continue
|
||||
if tok in _STOPWORDS or not _HAS_LETTER.search(tok):
|
||||
continue
|
||||
out.add(tok)
|
||||
if tok not in out:
|
||||
out.append(tok)
|
||||
return out
|
||||
|
||||
|
||||
def working_name_tokens(path: str) -> set[str]:
|
||||
"""The identity-bearing tokens in one image's filename.
|
||||
|
||||
Returns an EMPTY set for a name that carries no working title — a
|
||||
screenshot, a bare number, a stopword. Empty means "no evidence", which the
|
||||
caller must treat as silence rather than as a weak match; see the module
|
||||
docstring for the false positive that rule exists for.
|
||||
"""
|
||||
return set(_ordered_tokens(path))
|
||||
|
||||
|
||||
def leading_name(path: str) -> str | None:
|
||||
"""The FIRST identity token of a filename — the piece, not its decoration.
|
||||
|
||||
Creators lead with what the piece is and trail with what this export of it
|
||||
is: `Year_20k_wip1`, `not_sombra_21-cumpeen`, `Tentacooler_c_ins`. Content
|
||||
words sit at the tail, and they span too FEW posts for any frequency cap
|
||||
to catch — `nude`, `cum` and `top` are on three of artist 8's posts each.
|
||||
Position is what separates them from a name.
|
||||
|
||||
Measured on artist 8, Discord messages 2-60 days apart sharing a gated
|
||||
token: 121 pairs. The 106 sharing the leading name all read as one piece's
|
||||
trickle; of the 15 sharing only a trailing word, 14 are sibling pieces
|
||||
(`Bea_Machamp_Shiny_*` / `Bea_Machoke_Shiny_*`) and one is a plain
|
||||
collision (`Undyne_insert_bottom_only-C` / `Lichgalclc_Lingerie_Bottom_21`).
|
||||
|
||||
None when the name carries no identity at all — see `working_name_tokens`.
|
||||
"""
|
||||
tokens = _ordered_tokens(path)
|
||||
return tokens[0] if tokens else None
|
||||
|
||||
|
||||
def token_frequencies(posts: Iterable[Iterable[str]]) -> Counter[str]:
|
||||
"""How many of ONE ARTIST's POSTS each working-name token appears in.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user