goon/scripts/fix_composite_performers.py
goon-foss 96d16b5488 fix(performers): rozplac zlepki dwoch osob w jedna nieistniejaca
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).
2026-08-14 11:00:48 +02:00

204 lines
7.9 KiB
Python

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