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