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).
This commit is contained in:
parent
0d4e11bc31
commit
96d16b5488
1 changed files with 204 additions and 0 deletions
204
scripts/fix_composite_performers.py
Normal file
204
scripts/fix_composite_performers.py
Normal file
|
|
@ -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()
|
||||
Loading…
Add table
Reference in a new issue