From 96d16b54883663b157bbad7100a858e51865c8b0 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Fri, 14 Aug 2026 11:00:48 +0200 Subject: [PATCH] fix(performers): rozplac zlepki dwoch osob w jedna nieistniejaca MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scraper tworzyl osoby, ktore nie istnieja, biorac imie jednej i nazwisko drugiej osoby z tej samej sceny: "Lilly Blossom" = Lilly Bell + Blake Blossom "Chloe Ryder" = Chloe Temple + Sera Ryder "James Driller" = Kenna James + Ryan Driller "Parker Banks" = Zoe Parker + Cassidy Banks prune_token_overlap_attributions ich nie widzi, bo wymaga INNEGO udokumentowanego performera na scenie, a zlepek jest zwykle JEDYNYM przypisanym. Znowu ODZYSKIWANIE, nie kasowanie: obie prawdziwe osoby stoja w tytule i nie byly przypisane. Wynik: 3993 zlepki zdjete, 7988 prawdziwych przypisan dolozonych. Zyskali m.in. Lauren Phillips +74 sceny, Gianna Dior +45, Jenna Sativa +42, Aaliyah Love +41, Keiran Lee +41. BLAD W PIERWSZYM PRZEBIEGU I JEGO NAPRAWA: regula uznala nazwy-KATEGORIE za "wlasciciela" wymienionego w tytule i "Big Black" dostal 124 sceny, a dziewiec innych po kilka. Zdjete osobno (197 przypisan, do archiwum), plus jawna lista wykluczen w skrypcie. Lista jest JAWNA, bo obie oczywiste heurystyki zawodza: "nazwa jest tez tagiem" jest prawdziwe dla Lauren Phillips i Julii Ann, a "ma kanon" jest prawdziwe dla "Big Black". CZEGO NIE RUSZAM: 3150 zlepkow MAJACYCH kanon. "Nikki Monroe" czy "Valentina Lopez" moga istniec naprawde, a "Lauren Smith" wzielo sie z trojczlonowego nazwiska aktorki ("lauren lee smith"). Istnienie osoby nie przesadza, czy jest w TEJ scenie. Szersza reguła "w tytule stoi ktos inny" ODRZUCONA po probce: 31% trafien to byly kategorie-smieci, a similarity trigramowe nie rozdziela klas (siostry Dellai 0,318 vs Nikki Nevada/Brooks 0,316). --- scripts/fix_composite_performers.py | 204 ++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 scripts/fix_composite_performers.py diff --git a/scripts/fix_composite_performers.py b/scripts/fix_composite_performers.py new file mode 100644 index 0000000..06b77a2 --- /dev/null +++ b/scripts/fix_composite_performers.py @@ -0,0 +1,204 @@ +"""Rozplata performerów-ZLEPKÓW: nazw posklejanych z członów dwóch różnych osób. + +Scraper, szukając po nazwie, potrafił utworzyć osobę, która nie istnieje, biorąc imię +jednej i nazwisko drugiej osoby z tej samej sceny: + + „Lilly Blossom" = Lilly Bell + Blake Blossom + „Chloe Ryder" = Chloe Temple + Sera Ryder + „Parker Banks" = Zoe Parker + Cassidy Banks + „James Driller" = Kenna James + Ryan Driller + +**Dlaczego to osobny skrypt, a nie kolejna reguła w `prune_token_overlap_attributions`.** +Tamten wymaga, żeby na scenie był INNY udokumentowany performer. Zlepek jest zwykle +JEDYNYM przypisanym, więc nie ma z czym go porównać i tamta reguła go nie widzi. + +**I znowu jest to ODZYSKIWANIE.** Obie prawdziwe osoby stoją w tytule i zwykle NIE są +przypisane do sceny, bo scraper wpisał zamiast nich jedną nieistniejącą. + +Kryterium (wszystkie warunki naraz): + +1. przypisanie ze scrapera i zlepek jest JEDYNYM performerem sceny, +2. jego pełnej nazwy NIE MA w tytule, +3. w tytule stoją PEŁNE nazwy co najmniej dwóch innych performerów z naszej bazy, +4. każdy człon nazwy zlepka pochodzi od INNEJ z tych osób, +5. zlepka NIE ZNA kanon (brak refów TPDB/StashDB). + +Warunek 5 jest tym, który powstrzymuje szkodę. Z 4273 wykrytych zlepków aż 3150 MA +odnośniki kanoniczne i tych NIE RUSZAMY: „Nikki Monroe" czy „Valentina Lopez" to nazwy, +które mogą istnieć naprawdę, a „Lauren Smith" wzięło się z trójczłonowego nazwiska +aktorki („lauren lee smith"). Istnienie osoby nie przesądza, czy jest w tej scenie, +więc przy nich potrzeba innego sygnału niż nazwa. + +Zostaje 1123 przypisania (391 osób), których nie zna żadna baza kanoniczna. Próbka +16/16 to czyste zlepki. + +Uruchomienie (kontener worker): + python -m scripts.fix_composite_performers # dry-run + python -m scripts.fix_composite_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__) + +#: Człony krótsze odpadają: „Li", „Vi" trafiłyby w losowe słowa tytułu. +_MIN_TOKEN = 3 +#: Nazwa kandydata na właściciela: min. 2 człony i 9 znaków, żeby nie łapać mononimów +#: ani tagów jednowyrazowych. +_MIN_NAME_LEN = 9 + +#: Nazwy-KATEGORIE siedzące w tabeli performerów. MUSZĄ być wykluczone jako „właściciel", +#: bo spełniają wszystkie warunki formalne (dwa człony, długość) i stoją w co drugim +#: tytule. Przy pierwszym uruchomieniu 2026-08-14 bez tej listy „Big Black" dostał 124 +#: sceny, a dziewięć innych kategorii po kilka; poprawione osobno, 197 przypisań. +#: +#: Lista jest JAWNA, nie heurystyczna, bo obie oczywiste heurystyki zawodzą: „nazwa jest +#: też tagiem" jest prawdziwe dla Lauren Phillips i Julii Ann (tuby robią tagi z nazwisk), +#: a „ma kanon" jest prawdziwe dla „Big Black" i „Lesbian Love". +_NIE_OSOBY = frozenset({ + "big black", "juicy ass", "small tits", "anal lover", "mature milf", + "double anal", "lesbian love", "double pussy", "natural tits", "anal queen", + "first anal", "golden shower", "rough sex", "south asian", "busty girl", +}) + + +def _norm(s: str | None) -> str: + return re.sub(r"[^a-z0-9]+", " ", (s or "").lower()).strip() + + +def _load(session) -> tuple[dict, set, list]: + names: dict[str, tuple] = {} + for pid, nm in session.execute( + text("SELECT id, canonical_name FROM performers") + ).all(): + n = _norm(nm) + if n in _NIE_OSOBY: + continue + if len(n.split()) >= 2 and len(n) >= _MIN_NAME_LEN: + names.setdefault(n, (pid, nm)) + + kanon = { + p + for (p,) in session.execute( + text( + "SELECT DISTINCT r.performer_id FROM performer_external_refs r " + "JOIN sources s ON s.id = r.source_id WHERE s.name IN ('tpdb','stashdb')" + ) + ).all() + } + + rows = session.execute( + text( + """ + SELECT sp.scene_id, sp.performer_id, sp.source_id, p.canonical_name, s.title + FROM scene_performers sp + JOIN sources src ON src.id = sp.source_id AND src.name = 'tube-scraper' + JOIN performers p ON p.id = sp.performer_id + JOIN scenes s ON s.id = sp.scene_id + WHERE (SELECT count(*) FROM scene_performers x WHERE x.scene_id = sp.scene_id) = 1 + """ + ) + ).all() + return names, kanon, rows + + +def _detect(names: dict, kanon: set, rows: list) -> list[tuple]: + out = [] + for sid, pid, source_id, pname, title in rows: + if pid in kanon: + continue # warunek 5 + t = " " + _norm(title) + " " + pn = _norm(pname) + toks = [x for x in pn.split() if len(x) >= _MIN_TOKEN] + if len(toks) < 2 or f" {pn} " in t: + continue # warunek 2 + + w = t.split() + present: list[tuple] = [] + for k in (3, 2): + for i in range(len(w) - k + 1): + cand = " ".join(w[i : i + k]) + hit = names.get(cand) + if hit and hit[0] != pid and all(cand != c for c, _ in present): + present.append((cand, hit[0])) + if len(present) < 2: + continue # warunek 3 + + owners: dict = {} + for tok in toks: + for cand, oid in present: + if tok in cand.split(): + owners[oid] = True + break + if len(owners) >= 2: # warunek 4 + out.append((sid, pid, source_id, list(owners))) + return out + + +def run(*, apply: bool) -> dict[str, int]: + with session_scope() as session: + names, kanon, rows = _load(session) + hits = _detect(names, kanon, rows) + print(f"zlepkow do rozplatania: {len(hits)} przypisan, {len({h[1] for h in hits})} osob") + if not apply or not hits: + if not apply: + print("(dry-run)") + return {"matched": len(hits), "attached": 0, "detached": 0} + + attached = detached = 0 + for sid, pid, source_id, owner_ids in hits: + for oid in owner_ids: + attached += int( + session.execute( + text( + "INSERT INTO scene_performers (scene_id, performer_id, source_id) " + "VALUES (:s, :p, :src) ON CONFLICT DO NOTHING" + ), + {"s": sid, "p": oid, "src": source_id}, + ).rowcount + or 0 + ) + 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.scene_id = :s AND sp.performer_id = :p + ON CONFLICT DO NOTHING + """ + ), + {"s": sid, "p": pid}, + ) + detached += int( + session.execute( + text( + "DELETE FROM scene_performers WHERE scene_id = :s AND performer_id = :p" + ), + {"s": sid, "p": pid}, + ).rowcount + or 0 + ) + print(f"dołożono {attached} prawdziwych przypisań, zdjęto {detached} zlepków") + return {"matched": len(hits), "attached": attached, "detached": detached} + + +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()