diff --git a/scripts/prune_category_performers.py b/scripts/prune_category_performers.py new file mode 100644 index 0000000..8e6486b --- /dev/null +++ b/scripts/prune_category_performers.py @@ -0,0 +1,147 @@ +"""Zdejmuje przypisania „performerów", którzy są nazwami KATEGORII, nie ludźmi. + +W tabeli `performers` siedzą pozycje w rodzaju „Natural Tits", „Big Black", „Anal Milf", +„Cum Swallow" czy „Fat Ass". Biorą się z tagów tubów wciąganych jako obsada. Ugryzły +2026-08-14 przy rozplataniu zlepków: reguła uznała „Big Black" za osobę wymienioną +w tytule i dała mu 124 sceny. + +**Trzy oczywiste heurystyki sprawdzone i ODRZUCONE**, każda z konkretnym kontrprzykładem: + +1. „nazwa jest też tagiem" — prawdziwe dla Lauren Phillips, Julii Ann i Nikki Benz, + bo tuby robią tagi z nazwisk (7625 z 74150 nazw). +2. „nie ma refów kanonicznych" — **TPDB SAM MA** „Big Black" i „Lesbian Love" jako + performerów, a realna „Mena Mason" refów nie ma. +3. „tag ma dużo więcej scen niż performer" — działa dla „Natural Tits" (2147×) i + „Rough Sex" (1552×), ale „Big Black" ma 0,2×, a „Lesbian Love" 0,0×. + +Dlatego kryterium jest STRUKTURALNE i nie zależy od żadnej z tych baz: **cała nazwa +składa się wyłącznie ze słów opisujących treść**. „Big Black" to big + black, „Natural +Tits" to natural + tits, a „Lauren Phillips" nie ma ani jednego takiego członu. + +Kandydatów wychodzi 101, łącznie ~12 tys. przypisań, i lista jest na tyle krótka, że +została **przejrzana ręcznie**. Stąd `_JEDNAK_LUDZIE`: słownik trafia też w prawdziwe +osoby o „opisowych" pseudonimach. + +Uruchomienie (kontener worker): + python -m scripts.prune_category_performers # dry-run + python -m scripts.prune_category_performers --yes # wykonaj +""" +from __future__ import annotations + +import argparse +import logging +import re + +from sqlalchemy import text + +from app.db import session_scope + +log = logging.getLogger(__name__) + +#: Słowa opisujące TREŚĆ, nie osobę. Świadomie bez imion i nazwisk. +_SLOWA_KATEGORII = set( + """ + anal ass tits boobs cock dick pussy cum cumshot creampie blowjob handjob footjob + milf teen mature granny mom stepmom stepsis sister brother wife girlfriend + black white asian latina ebony indian arab russian czech japanese korean thai + big small huge tiny massive giant thick fat skinny petite busty curvy + hardcore softcore rough gentle deep double triple first best hot sexy horny nasty + lesbian gay straight bi trans shemale ladyboy + fuck fucking fucked sex porn xxx video clip scene movie + blonde brunette redhead ginger + threesome foursome gangbang orgy bukkake facial swallow + squirt squirting shower golden piss pee + bdsm bondage domination submission fetish kink + natural fake real amateur pro professional + queen king lover girl boy babe slut whore bitch + pov casting audition massage yoga gym office nurse teacher student + young old + """.split() +) + +#: Wyjątki znalezione przy ręcznym przeglądzie 101 kandydatów. To PRAWDZIWE osoby, +#: które mają pecha do pseudonimu: +#: „Dick Nasty" — refy z SZEŚCIU źródeł, w tym StashDB; realny męski performer, +#: widziany w obsadzie „Sugar Daddy 17" obok Dave'a Cummingsa. +#: „King Cock" — cztery źródła i niejednoznaczne sceny; wykluczony z ostrożności, +#: bo koszt błędu (skasowanie realnej osoby) jest wyższy niż zysk. +_JEDNAK_LUDZIE = frozenset({"dick nasty", "king cock"}) + + +def _norm(s: str | None) -> str: + return re.sub(r"[^a-z0-9]+", " ", (s or "").lower()).strip() + + +def _kandydaci(session) -> list[tuple]: + out = [] + for pid, nm in session.execute( + text("SELECT id, canonical_name FROM performers") + ).all(): + n = _norm(nm) + toks = n.split() + if len(toks) < 2 or n in _JEDNAK_LUDZIE: + continue + if all(t in _SLOWA_KATEGORII for t in toks): + out.append((pid, nm)) + return out + + +def run(*, apply: bool) -> dict[str, int]: + with session_scope() as session: + kand = _kandydaci(session) + ids = [k[0] for k in kand] + if not ids: + print("brak kandydatow") + return {"performers": 0, "attributions": 0} + + n = session.execute( + text( + "SELECT count(*) FROM scene_performers WHERE performer_id = ANY(:ids)" + ), + {"ids": ids}, + ).scalar_one() + print(f"kategorii: {len(kand)} | ich przypisan: {n}") + + if not apply: + for _pid, nm in sorted(kand, key=lambda x: x[1])[:12]: + print(f" {nm}") + print(" ... (dry-run)") + return {"performers": len(kand), "attributions": int(n)} + + # Archiwum przed kasowaniem, ta sama tabela co reszta sprzatania. + session.execute( + text( + """ + INSERT INTO scene_performers_pruned + (scene_id, performer_id, source_id, performer_name, scene_title) + SELECT sp.scene_id, sp.performer_id, sp.source_id, p.canonical_name, s.title + FROM scene_performers sp + JOIN performers p ON p.id = sp.performer_id + JOIN scenes s ON s.id = sp.scene_id + WHERE sp.performer_id = ANY(:ids) + ON CONFLICT DO NOTHING + """ + ), + {"ids": ids}, + ) + deleted = session.execute( + text("DELETE FROM scene_performers WHERE performer_id = ANY(:ids)"), + {"ids": ids}, + ).rowcount + # Same rekordy performerow ZOSTAJA (scene_count spadnie do 0 przy najblizszym + # `refresh_taxonomy_counts`, wiec i tak wypadna z list z `only_with_content`). + # Kasowanie rekordu zerwaloby refy kanoniczne, a tego nie chcemy przy czyms, + # co TPDB u siebie trzyma jako performera. + print(f"zdjeto {deleted} przypisan od {len(kand)} kategorii") + return {"performers": len(kand), "attributions": int(deleted or 0)} + + +def main() -> None: + logging.basicConfig(level=logging.INFO, format="%(message)s") + ap = argparse.ArgumentParser() + ap.add_argument("--yes", action="store_true", help="wykonaj (bez tego dry-run)") + run(apply=ap.parse_args().yes) + + +if __name__ == "__main__": + main()