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).
204 lines
7.9 KiB
Python
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()
|