From 470d70c11e7ef54c34a9e2b777d971121e790248 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Mon, 27 Jul 2026 15:39:51 +0200 Subject: [PATCH] feat(performers): narzedzie do przegladu over-attribution (wzorzec Rapture) Ostatnia klasa smieci: REALNA osoba o nazwie bedacej pospolitym slowem, do ktorej tube-search dokleja wszystko, co ma to slowo w tytule. Rapture miala 30 z 72 przypisan falszywych (film "The Rapture" z Mimi Rogers, literowka "hymen raptured", tytuly scen). NIE MA na to sygnalu automatycznego. Przetestowalem piec i zaden nie rozstrzyga: - sama provenance ze scrapera: realni tworcy spoza TPDB (Amouranth, MollyRedWolf) tez maja 100 procent przypisan ze scrapera - obsada z kanonu na scenie: backfill 0026 wypelnil tylko sceny jednozrodlowe, wiec sceny mieszane maja NULL - 1 trafienie na 560 tys. - udzial nazwy w tytule: u niszowych prawdziwych tez ~100 procent (Amouranth 100, Clanddi 100) - obce sceny ze slowem: myli generyczne uzycie z pominieta atrybucja - stosunek tag/performer >=1: wyklucza 79 pozycji, ale samej Rapture BY NIE ZLAPAL Zostaje czytanie tytulow. Skrypt robi z tego kilka minut zamiast pol godziny: listuje komplet z provenance i kontekstem, liczy wskaznik pospolitosci nazwy, przyjmuje liste prefiksow do odpiecia i DOMYSLNIE nie rusza przypisan potwierdzonych przez kanon. Bez trybu rankingu wszystkich kandydatow: bez indeksu trigramowego na scenes.title to skan 3,4 mln tytulow RAZY ~800 kandydatow. Sprobowalem, ubilem po 10 minutach. Przy okazji przeglad zdemaskowal "Precious" (119 scen, 0 z kanonu) - wszystkie przypisania to przymiotnik ("precious stepdaughter", "her ass is precious"). Usuniete. Skala reszty: 1335 kandydatow z >=20 scenami, 539 z >=100. Nie ma sensu przerabiac wszystkich - narzedzie jest na zglaszane przypadki. Co-Authored-By: Claude Opus 5 --- scripts/review_performer_attributions.py | 161 +++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 scripts/review_performer_attributions.py diff --git a/scripts/review_performer_attributions.py b/scripts/review_performer_attributions.py new file mode 100644 index 0000000..294b3b8 --- /dev/null +++ b/scripts/review_performer_attributions.py @@ -0,0 +1,161 @@ +"""Przegląd i przycinanie przypisań JEDNEGO performera (wzorzec „Rapture"). + +Dotyczy klasy, której NIE DA SIĘ posprzątać hurtowo: realna osoba (jest w TPDB/StashDB) +o nazwie będącej pospolitym słowem, do której tube-search dokleja wszystko, co ma to +słowo w tytule. „Rapture" miała 30 z 72 przypisań fałszywych — mainstreamowy film „The +Rapture" z Mimi Rogers, literówkę „hymen raptured", tytuły scen („LucidFlix – Tru Kait – +Rapture") i zwykłe użycia („complete rapture", „moments of rapture"). + +**Dlaczego ręcznie.** Przetestowałem pięć sygnałów automatycznych i żaden nie rozstrzyga: + + - sama provenance ze scrapera — realni twórcy spoza TPDB (Amouranth, MollyRedWolf) + też mają 100% przypisań ze scrapera + - obsada z kanonu na scenie — backfill 0026 wypełnił tylko sceny jednoźródłowe, więc + sceny mieszane, w których leży problem, mają NULL (1 trafienie na 560 tys.) + - udział nazwy w tytule — u prawdziwych niszowych osób też ~100% (Amouranth 100%, + Clanddi 100%) + - „obce" sceny ze słowem w tytule — myli generyczne użycie z pominiętą atrybucją + (Amouranth ma 137 takich, a to po prostu jej sceny bez przypisanej obsady) + - stosunek tag/performer ≥1 — wyklucza 79 pozycji, ale samej Rapture BY NIE ZŁAPAŁ + +Zostaje przeczytanie tytułów. Ten skrypt robi z tego operację na kilka minut zamiast +pół godziny: wypisuje komplet z kontekstem, przyjmuje listę prefiksów do odpięcia i +domyślnie NIE rusza przypisań potwierdzonych przez kanon. + +Skala problemu (2026-07-27): 1335 kandydatów z ≥20 scenami, 539 z ≥100 (105 tys. +przypisań). Nie ma sensu przerabiać ich wszystkich — używaj, gdy konkretna osoba +zwróci uwagę, tak jak Rapture wyszła przy audycie phashy. + +**Wskaźnik pospolitości** liczony przy każdym przeglądzie: ile scen ma tę nazwę w +tytule, ale NIE jest do niej przypisanych, podzielone przez liczbę jej przypisań. + + Addiction 14,3 Rogue 5,0 Mystery 4,3 Gypsy 3,2 Precious 3,1 Rapture 2,5 + Kwini 1,8 Chyna 0,8 Amouranth 0,45 + +Wskaźnik NIE mówi, czy osoba jest prawdziwa — mówi tylko, że jej przypisania są +niewiarygodne i trzeba je przeczytać. Rapture przy 2,5 okazała się realna (zostały 42 +sceny), „Precious" przy 3,1 okazała się wyłącznie przymiotnikiem („precious +stepdaughter", „her ass is precious") i poszła w całości. + +Świadomie NIE ma tu trybu rankingu wszystkich kandydatów: bez indeksu trigramowego na +`scenes.title` to skan 3,4 mln tytułów RAZY ~800 kandydatów, czyli godziny obciążania +bazy. Dla jednej osoby ten sam skan trwa sekundy. + +Użycie (kontener worker): + python -m scripts.review_performer_attributions "Rapture" + python -m scripts.review_performer_attributions "Rapture" --detach-file /tmp/zle.txt + python -m scripts.review_performer_attributions "Rapture" --detach-file /tmp/zle.txt --yes + +`--detach-file` to zwykły plik tekstowy: jedna linia = początek tytułu (bez wielkości +liter). Puste linie i `#` pomijane. +""" +from __future__ import annotations + +import argparse +import sys + +from sqlalchemy import text + +from app.db import session_scope + +_LIST_SQL = """ +SELECT sc.title, + sc.duration_sec, + coalesce(src.kind::text, '?') AS zrodlo, + (SELECT count(*) FROM scene_performers x WHERE x.scene_id = sc.id) AS ilu_perf, + sc.id +FROM scene_performers sp +JOIN scenes sc ON sc.id = sp.scene_id +LEFT JOIN sources src ON src.id = sp.source_id +WHERE sp.performer_id = :pid +ORDER BY lower(sc.title) +""" + + +_FOREIGN_SQL = """ +SELECT count(*) FROM scenes s +WHERE s.title ~* ('\\m' || :name || '\\M') + AND NOT EXISTS (SELECT 1 FROM scene_performers sp + WHERE sp.scene_id = s.id AND sp.performer_id = :pid) +""" + + +def _find_performer(session, name: str): + row = session.execute( + text( + "SELECT id, canonical_name FROM performers " + "WHERE lower(canonical_name) = lower(:n) ORDER BY id LIMIT 1" + ), + {"n": name}, + ).first() + return row + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("performer", nargs="?", help="canonical_name performera") + ap.add_argument("--detach-file", help="plik z prefiksami tytułów do odpięcia") + ap.add_argument("--yes", action="store_true", help="wykonaj odpięcie (bez tego dry-run)") + ap.add_argument( + "--include-canonical", + action="store_true", + help="pozwól odpiąć też przypisania z tpdb/stashdb (domyślnie chronione)", + ) + args = ap.parse_args() + + if not args.performer: + print("podaj canonical_name performera") + sys.exit(1) + + prefixes: list[str] = [] + if args.detach_file: + with open(args.detach_file, encoding="utf-8") as fh: + prefixes = [ + ln.strip().lower() + for ln in fh + if ln.strip() and not ln.lstrip().startswith("#") + ] + + with session_scope() as s: + perf = _find_performer(s, args.performer) + if perf is None: + print(f"nie znalazłem performera: {args.performer!r}") + sys.exit(1) + pid, name = perf[0], perf[1] + rows = s.execute(text(_LIST_SQL), {"pid": pid}).fetchall() + obce = int(s.execute(text(_FOREIGN_SQL), {"name": name, "pid": pid}).scalar_one()) + + wsk = obce / len(rows) if rows else 0.0 + print(f"{name} — {len(rows)} przypisań, obcych scen ze słowem: {obce}, " + f"wskaźnik pospolitości: {wsk:.1f}") + print("(wysoki = nazwa jest zwykłym słowem, przypisania trzeba przeczytać)\n") + hit_ids: list[str] = [] + for title, dur, zrodlo, ilu, sid in rows: + t = (title or "").lower() + matched = any(t.startswith(p) for p in prefixes) + protected = matched and zrodlo in ("tpdb", "stashdb") and not args.include_canonical + if matched and not protected: + hit_ids.append(sid) + mark = "ODPNIJ" if (matched and not protected) else ("CHRONIONE" if protected else " ") + w_tytule = "T" if name.lower() in t else "-" + print(f" {mark:10} [{zrodlo:8}] nazwa-w-tytule={w_tytule} perf={ilu} " + f"{dur or '?'}s {(title or '')[:58]!r}") + + if not prefixes: + print("\n(sam przegląd — podaj --detach-file, żeby coś odpiąć)") + return + + print(f"\ndo odpięcia: {len(hit_ids)} / {len(rows)}") + if not args.yes: + print("(dry-run — uruchom z --yes)") + return + n = s.execute( + text("DELETE FROM scene_performers WHERE performer_id = :pid AND scene_id = ANY(:ids)"), + {"pid": pid, "ids": hit_ids}, + ).rowcount + s.commit() + print(f"ODPIĘTE: {n}") + + +if __name__ == "__main__": + main()