fix(sxyprn): koniec z przypisywaniem scen po samym imieniu
Zgloszenie e28f2517: scena "Bridgette B, Gigi Rivera, Julia Ann, Phoenix Marie, ..."
miala przypisana Julie B., ktorej w tytule nie ma.
Mechanizm: scraper wola strone performera i sprawdza, czy scena naprawde go dotyczy,
porownujac czlony nazwiska z tytulem. Robil to jednak KAZDY Z OSOBNA i tylko od trzech
znakow. Dla "Julia B." czlon "b." odpadal na dlugosci, zostawalo samo "julia", i
przypisanie lapalo kazda scene z tym imieniem w tytule. Stad tez "Chloe S" przy
"Chloe Kreams", "Sharon D" przy "Sharon White", "May E" przy "Danielle Maye".
Nowa regula (`person_named_in` w `app/normalize/text.py`): czlony musza stac OBOK
SIEBIE i w tej samej kolejnosci. Od trzech znakow wystarczy zgodnosc przedrostka, zeby
przezyly warianty zapisu ("Savanna Bond" przy tytule "Savannah Bond", "Jyn Maze" przy
"Jynx Maze"). Inicjaly musza zgadzac sie dokladnie. Tytul tniemy wczesniej na segmenty
po przecinkach i myslnikach, bo bez tego smieciowy performer "Rope Bondage" dopasowywal
sie do doklejonego ogona tagow "- rope - bondage".
Swiadomy koszt: odwrocona kolejnosc ("Goldy Kim" wobec "Kim Gold") nie przejdzie, a
performer z inicjalem zamiast nazwiska nie dostanie ze sxyprn nic. Przy wyborze miedzy
pominieciem sceny a przypisaniem jej niewlasciwej osoby wolimy pominac, bo to bledne
przypisanie widac na ekranie performera i to ono jest zglaszane.
Smoke test po zmianie: Lana Rhoades, Julia Ann, Angela White i Abella Danger dalej po
30 scen ze strony performera, wiec scraper sie nie zamrozil (to sie juz raz zdarzilo
przy zbyt ostrym filtrze). "Julia B." oddaje teraz zero zamiast cudzych scen.
Dolozony `scripts/sxyprn_fix_overattribution.py`, ktory stosuje te sama regule wstecz.
Uruchomiony na produkcji: skasowane 85 379 z 239 686 przypisan (35,6%). Ruszamy tylko
sceny majace WYLACZNIE zrodlo sxyprn i tylko przypisania od scrapera; kanon
(tpdb/stashdb) nietkniety. Regula sprawdzona recznie na dwoch probkach po ~20 pozycji,
wszystkie kasowane byly faktycznie bledne.
Skutek uboczny: scen bez zadnego performera przybylo z 3 622 do 23 891. To sceny,
ktorych jedyne przypisanie bylo bledne, wiec brak jest uczciwszy niz cudze nazwisko.
This commit is contained in:
parent
5a48175a85
commit
b7a8911441
3 changed files with 192 additions and 8 deletions
|
|
@ -35,7 +35,7 @@ from app.connectors.base import (
|
||||||
)
|
)
|
||||||
from app.connectors.direct_scrapers._search_base import BaseSearchScraper
|
from app.connectors.direct_scrapers._search_base import BaseSearchScraper
|
||||||
from app.extractors import browser_get
|
from app.extractors import browser_get
|
||||||
from app.normalize.text import slugify
|
from app.normalize.text import person_named_in, slugify
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -112,9 +112,6 @@ class SxyPrnScraper(BaseSearchScraper):
|
||||||
return
|
return
|
||||||
|
|
||||||
text = r.text
|
text = r.text
|
||||||
query_tokens = {
|
|
||||||
tok for tok in query.lower().split() if len(tok) >= self._query_token_min_len
|
|
||||||
}
|
|
||||||
anchors = list(_CARD_ANCHOR_RE.finditer(text))
|
anchors = list(_CARD_ANCHOR_RE.finditer(text))
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
yielded = 0
|
yielded = 0
|
||||||
|
|
@ -130,10 +127,13 @@ class SxyPrnScraper(BaseSearchScraper):
|
||||||
if not tm:
|
if not tm:
|
||||||
continue
|
continue
|
||||||
title, tags = _clean_title(tm.group(1))
|
title, tags = _clean_title(tm.group(1))
|
||||||
title_l = title.lower()
|
# Scena musi realnie dotyczyć performera. Sprawdzamy CAŁE nazwisko jako ciąg
|
||||||
# Strict: scena musi realnie dotyczyć performera (oba tokeny w tytule),
|
# sąsiadujących członów, bo poprzedni wariant (każdy człon od trzech znaków
|
||||||
# chroni przed śmieciem na stronie i przed over-attribution.
|
# gdziekolwiek w tytule) degenerował się do samego imienia, gdy nazwisko było
|
||||||
if query_tokens and not all(tok in title_l for tok in query_tokens):
|
# krótsze: „Julia B." → człon „b." odpada na długości, zostaje „julia", i
|
||||||
|
# przypisanie łapało każdą scenę ze słowem Julia. Zgłoszenie `e28f2517`,
|
||||||
|
# 85 tys. błędnych przypisań na samym sxyprn.
|
||||||
|
if not person_named_in(title, query):
|
||||||
continue
|
continue
|
||||||
if not title:
|
if not title:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -59,3 +59,57 @@ def slugify(value: str | None) -> str:
|
||||||
if not value:
|
if not value:
|
||||||
return ""
|
return ""
|
||||||
return _slugify(value, lowercase=True, max_length=200)
|
return _slugify(value, lowercase=True, max_length=200)
|
||||||
|
|
||||||
|
|
||||||
|
# Minimalna długość członu, przy której dopuszczamy dopasowanie po przedrostku.
|
||||||
|
# Poniżej wymagamy zgodności co do znaku, bo inicjał („s", „b") pasuje do połowy słów
|
||||||
|
# w dowolnym tytule i to jest dokładnie źródło over-atrybucji opisanej niżej.
|
||||||
|
_PREFIX_FUZZ_MIN = 3
|
||||||
|
|
||||||
|
# Granice, przez które nazwisko nie może się przelewać. Tytuły tubów to listy osób
|
||||||
|
# rozdzielone przecinkiem albo myślnikiem, a na końcu doklejone tagi. Bez tego cięcia
|
||||||
|
# śmieciowy performer „Rope Bondage" dopasowywał się do ogona „- rope - bondage",
|
||||||
|
# bo po normalizacji separator znika i człony stają się sąsiednie.
|
||||||
|
_PERSON_SEPARATOR_RE = re.compile(r"\s[-–—]\s|[,|&/;:()\[\]]|feat\.?|ft\.?", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _person_token_match(name_tok: str, text_tok: str) -> bool:
|
||||||
|
if name_tok == text_tok:
|
||||||
|
return True
|
||||||
|
if len(name_tok) >= _PREFIX_FUZZ_MIN and len(text_tok) >= _PREFIX_FUZZ_MIN:
|
||||||
|
return name_tok.startswith(text_tok) or text_tok.startswith(name_tok)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def person_named_in(haystack: str | None, person: str | None) -> bool:
|
||||||
|
"""Czy `person` występuje w `haystack` jako ciąg SĄSIADUJĄCYCH członów.
|
||||||
|
|
||||||
|
Po co osobno, skoro jest zwykłe `in`: tube wołany nazwiskiem oddaje całą stronę
|
||||||
|
wyników, więc scraper musi sam sprawdzić, czy scena naprawdę dotyczy tej osoby.
|
||||||
|
Sprawdzanie członów osobno („julia" gdziekolwiek i „b" gdziekolwiek) przypisywało
|
||||||
|
„Julia B." do każdej sceny, w której tytule stało „Julia Ann" obok „Bridgette B".
|
||||||
|
Na samym sxyprn zrobiło się z tego 85 tys. błędnych przypisań (zgłoszenie
|
||||||
|
`e28f2517`, 2026-08-24).
|
||||||
|
|
||||||
|
Człony muszą stać obok siebie i w tej samej kolejności. Od trzech znaków wystarczy
|
||||||
|
zgodność przedrostka, żeby przeżyły warianty zapisu („Savanna Bond" przy tytule
|
||||||
|
„Savannah Bond", „Jyn Maze" przy „Jynx Maze"). Inicjały muszą zgadzać się dokładnie.
|
||||||
|
|
||||||
|
Świadomy koszt: odwrócona kolejność („Goldy Kim" wobec „Kim Gold") nie przejdzie.
|
||||||
|
Przy wyborze między pominięciem sceny a przypisaniem jej niewłaściwej osobie wolimy
|
||||||
|
pominąć, bo błędne przypisanie widać na ekranie performera i to ono jest zgłaszane.
|
||||||
|
"""
|
||||||
|
name_toks = normalize_person(person).split()
|
||||||
|
span = len(name_toks)
|
||||||
|
if not span:
|
||||||
|
return False
|
||||||
|
for segment in _PERSON_SEPARATOR_RE.split(haystack or ""):
|
||||||
|
text_toks = normalize_person(segment).split()
|
||||||
|
if len(text_toks) < span:
|
||||||
|
continue
|
||||||
|
if any(
|
||||||
|
all(_person_token_match(n, t) for n, t in zip(name_toks, text_toks[i : i + span]))
|
||||||
|
for i in range(len(text_toks) - span + 1)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
|
||||||
130
scripts/sxyprn_fix_overattribution.py
Normal file
130
scripts/sxyprn_fix_overattribution.py
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
"""Kasuje błędne przypisania performerów do scen z sxyprn (over-atrybucja).
|
||||||
|
|
||||||
|
Zgłoszenie `e28f2517` z 2026-08-24. Scena „Bridgette B, Gigi Rivera, Julia Ann, Phoenix
|
||||||
|
Marie, ..." miała przypisaną **Julię B.**, której w tytule nie ma.
|
||||||
|
|
||||||
|
Skąd się to brało: scraper sxyprn woła stronę performera i sprawdza, czy scena naprawdę
|
||||||
|
go dotyczy, porównując człony nazwiska z tytułem. Sprawdzał je jednak KAŻDY Z OSOBNA i
|
||||||
|
tylko od trzech znaków. Dla „Julia B." człon „b." odpadał na długości, zostawało samo
|
||||||
|
„julia" i przypisanie łapało każdą scenę, w której tytule pada to imię. Ten sam
|
||||||
|
mechanizm dał „Chloe S" przy „Chloe Kreams", „Iris N" przy „Iris Rodriguez" i tak dalej.
|
||||||
|
|
||||||
|
Poprawka w samym scraperze siedzi w `person_named_in` (`app/normalize/text.py`) i wymaga,
|
||||||
|
by człony stały OBOK SIEBIE, z tolerancją na wariant zapisu od trzech znaków. Ten skrypt
|
||||||
|
stosuje dokładnie tę samą regułę wstecz, do tego, co już jest w bazie.
|
||||||
|
|
||||||
|
Zakres: TYLKO sceny, których jedynym źródłem jest sxyprn, i TYLKO przypisania od
|
||||||
|
scrapera. Kanon (tpdb/stashdb) nie jest ruszany, bo jest wiarygodniejszy niż tytuł z
|
||||||
|
tuba. Sceny mające obok sxyprn inne źródło zostawiamy, bo performer mógł tam trafić z
|
||||||
|
tamtego tuba i tytuł sxyprn nic o tym nie mówi.
|
||||||
|
|
||||||
|
Skala przed uruchomieniem (2026-08-24): 125 524 scen wyłącznie z sxyprn, 244 309
|
||||||
|
przypisań od scrapera, z czego 85 711 bez nazwiska w tytule.
|
||||||
|
|
||||||
|
Weryfikacja reguły przed kasowaniem: dwie próbki po ~22 pozycje obejrzane ręcznie,
|
||||||
|
wszystkie do skasowania były faktycznie błędne. Wyjątki, które reguła CELOWO zachowuje,
|
||||||
|
to warianty zapisu w rodzaju „Savanna Bond" przy tytule „Savannah Bond".
|
||||||
|
|
||||||
|
Czego ten skrypt NIE robi: nie rusza śmieciowych performerów nazwanych od tagów
|
||||||
|
(„Rope Bondage", „Sub Girl"). To osobny problem, od niego jest kryterium
|
||||||
|
promiskuityczności taga.
|
||||||
|
|
||||||
|
Użycie (w kontenerze workera):
|
||||||
|
python3 /srv/scripts/sxyprn_fix_overattribution.py --dry-run
|
||||||
|
python3 /srv/scripts/sxyprn_fix_overattribution.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from app.db import session_scope
|
||||||
|
from app.normalize.text import person_named_in
|
||||||
|
|
||||||
|
log = logging.getLogger("sxyprn-overattr")
|
||||||
|
|
||||||
|
_ORIGIN = "tube:sxyprncom"
|
||||||
|
_BATCH = 2000
|
||||||
|
|
||||||
|
_SELECT = """
|
||||||
|
WITH tylko_sx AS (
|
||||||
|
SELECT DISTINCT ps.scene_id
|
||||||
|
FROM playback_sources ps
|
||||||
|
WHERE ps.origin = :origin
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM playback_sources p2
|
||||||
|
WHERE p2.scene_id = ps.scene_id AND p2.origin <> :origin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SELECT sp.scene_id, sp.performer_id, p.canonical_name, s.title
|
||||||
|
FROM scene_performers sp
|
||||||
|
JOIN tylko_sx t ON t.scene_id = sp.scene_id
|
||||||
|
JOIN performers p ON p.id = sp.performer_id
|
||||||
|
JOIN scenes s ON s.id = sp.scene_id
|
||||||
|
JOIN sources src ON src.id = sp.source_id AND src.name = 'tube-scraper'
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument("--dry-run", action="store_true", help="nie kasuj, tylko policz i pokaż próbkę")
|
||||||
|
p.add_argument("--limit", type=int, default=0, help="ogranicz liczbę sprawdzanych wierszy")
|
||||||
|
p.add_argument("--sample", type=int, default=15, help="ile przykładów wypisać")
|
||||||
|
a = p.parse_args(argv)
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", stream=sys.stdout
|
||||||
|
)
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
sql = _SELECT + (f" LIMIT {int(a.limit)}" if a.limit else "")
|
||||||
|
with session_scope() as s:
|
||||||
|
rows = s.execute(text(sql), {"origin": _ORIGIN}).fetchall()
|
||||||
|
log.info("przypisań do sprawdzenia: %d (%.0f s)", len(rows), time.time() - t0)
|
||||||
|
|
||||||
|
do_kasacji: list[tuple] = []
|
||||||
|
przyklady: list[str] = []
|
||||||
|
for scene_id, performer_id, nazwa, tytul in rows:
|
||||||
|
if person_named_in(tytul, nazwa):
|
||||||
|
continue
|
||||||
|
do_kasacji.append((scene_id, performer_id))
|
||||||
|
if len(przyklady) < a.sample:
|
||||||
|
przyklady.append(f" {nazwa!r:32} nie występuje w {str(tytul)[:72]!r}")
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"do skasowania: %d z %d (%.1f%%)",
|
||||||
|
len(do_kasacji), len(rows), 100.0 * len(do_kasacji) / max(1, len(rows)),
|
||||||
|
)
|
||||||
|
for line in przyklady:
|
||||||
|
log.info("%s", line)
|
||||||
|
|
||||||
|
if a.dry_run:
|
||||||
|
log.info("dry-run, nic nie skasowane")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
skasowane = 0
|
||||||
|
for i in range(0, len(do_kasacji), _BATCH):
|
||||||
|
chunk = do_kasacji[i : i + _BATCH]
|
||||||
|
with session_scope() as s:
|
||||||
|
skasowane += s.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
DELETE FROM scene_performers sp
|
||||||
|
USING unnest(CAST(:sids AS uuid[]), CAST(:pids AS uuid[])) AS u(sid, pid)
|
||||||
|
WHERE sp.scene_id = u.sid AND sp.performer_id = u.pid
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"sids": [str(x[0]) for x in chunk], "pids": [str(x[1]) for x in chunk]},
|
||||||
|
).rowcount
|
||||||
|
log.info("skasowane %d/%d (%.0f s)", skasowane, len(do_kasacji), time.time() - t0)
|
||||||
|
|
||||||
|
log.info("KONIEC: skasowane %d przypisań w %.0f s", skasowane, time.time() - t0)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Loading…
Add table
Reference in a new issue