"""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()