Ranking z poprzedniego commita ujawnil klase, ktorej zaden wczesniejszy sygnal nie widzial: wpisy bedace SAMYM NAZWISKIEM albo samym imieniem, powstale z rozbicia peleno nazwy przez tube-search. "Devine" obok "Shalina Devine", "Kitana" obok "Kitana A", "Rayne" obok "Tiffany Rayne". Taki wpis lapie potem kazda scene z tym slowem, nalezaca do zupelnie innej osoby. Regula jest DOWODLIWIE BEZSTRATNA: kasujemy przypisanie jednotokenowe tylko wtedy, gdy ta sama scena ma juz przypisana osobe pelnoimienna zawierajaca to slowo. Informacja o obsadzie zostaje, znika tylko niejednoznaczny duplikat. Kontrola potwierdzila, ze zadna scena nie zostala przez to bez obsady (0). Prowenancja z migracji 0026 dala zabezpieczenie: z 62 783 zbednych przypisan 62 120 to scraper (skasowane), 73 kanon (chronione), 590 nieznane (zostawione). Bez niej nie dalo by sie tego rozdzielic. Wynik: 62 120 przypisan zdjetych, 2156 wpisow odchudzonych (Devine 170 na 60), 1740 pustych rekordow usunietych. Regula dopieta do joba junk_performers, drugi przebieg daje zera. Zostaje residuum: sceny, na ktorych osoba pelnoimienna NIE byla przypisana, wiec regula nie miala sie o co oprzec (James, Blue, Moon, Crystal, Cruz, Starr, Knight po 60-160 przypisan). Tam trzeba by DOPISAC wlasciwa osobe, a nie kasowac - to osobne, bardziej ryzykowne zadanie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
232 lines
10 KiB
Python
232 lines
10 KiB
Python
"""Kasowanie śmieciowych performerów — kategorii i studiów podszywających się pod osoby.
|
||
|
||
Skąd się biorą: `_search_base` filtruje wyniki tube-searcha warunkiem „slug zawiera
|
||
≥1 token z zapytania", a `resolve_performer` tworzy performera z dowolnej nazwy podanej
|
||
przez tube. W efekcie „Creampie", „Natural tits", „reverse cowgirl", „69", „Brazzers"
|
||
i „Blacked" wylądowały w tabeli `performers`. Audyt 2026-07-27: 277 takich rekordów z
|
||
33 784 przypisaniami do scen.
|
||
|
||
**Kryterium jest wąskie celowo, bo szerokie kasuje ludzi.** Kryterium używane w
|
||
`app/api/scenes.py` do de-rankingu (slug pokrywa się z tagiem I brak refa tpdb/stashdb)
|
||
daje 3111 trafień, ale przy próbkowaniu okazało się mieć ~60% fałszywek dla nazw
|
||
wielowyrazowych: wpadają w nie **Hatano Yui, Davina Raines, Michelle Ferrari, Clanddi,
|
||
SolaZola, Kate Kuray**. Powód: założenie „prawdziwy performer ma ref kanoniczny" nie
|
||
działa dla JAV, amatorek i debiutantek, których nie ma w TPDB. Do de-rankingu to
|
||
wystarcza (fałszywka = gorszy ranking), do kasowania nie.
|
||
|
||
Rozstrzyga dopiero **promiskuityczność taga**: ile RÓŻNYCH innych performerów pojawia
|
||
się na scenach otagowanych tym samym slugiem. Nazwisko współwystępuje głównie ze swoim
|
||
właścicielem i partnerami ze sceny, kategoria z całym katalogiem. Zmierzone:
|
||
|
||
prawdziwe osoby Hatano Yui 0, Clanddi 0, Chyna 0, Kwini 2, Katekuray 9,
|
||
SolaZola 4, Julia Reaves 20 (maksimum 20)
|
||
kategorie Deepthroat 39541, Creampie 33661, riding 16284, Skinny 9443
|
||
|
||
Próg 50 leży ponad dwukrotnie powyżej najwyższej prawdziwej osoby. Dodatkowo chronimy
|
||
nazwy w formacie „Imię Nazwisko", bo sygnał studia (odrzucony) wciągał marki osobiste.
|
||
|
||
**Drugie kryterium: tag przytłacza performera.** Promiskuityczność zawodzi dla śmieci,
|
||
które mają ref kanoniczny — bo TPDB i StashDB SAME zawierają wpisy „Hardcore", „Blonde",
|
||
„Masturbation", „Amateur". Co gorsza, `obcy` mierzy tam popularność, nie śmieciowość:
|
||
przy wartości 50-59 stoją Adriana Chechik (4201 scen), Lauren Phillips i Jane Wilde.
|
||
|
||
Rozstrzyga stosunek `scen z tagiem / scen performera`. Nazwisko-tag siedzi głównie na
|
||
scenach swojej właścicielki, więc stosunek jest ułamkiem; kategoria wisi na całym
|
||
katalogu, więc idzie w setki:
|
||
|
||
Hardcore 1720,6 Masturbation 1570,4 Blonde 770,0 Anal Creampie 31,4
|
||
najwyższy wśród 921 PEWNYCH osób (ref kanoniczny + ≥500 scen): **0,35**
|
||
|
||
**Próg 2 wynika z zasady, nie z próbki.** Nazwisko użyte jako własny tag daje stosunek
|
||
≈1 Z DEFINICJI, bo tag siedzi dokładnie na scenach swojej właścicielki — potwierdzone na
|
||
twórcach z własną marką: Hotwifevictoria 1,02, Rossmexicana 0,99, Sexwithmilfstella 0,87.
|
||
Wielkie gwiazdy mają jeszcze mniej (tuby tagują tylko część ich scen). Stosunek ≥2
|
||
oznacza więc, że słowo jest doklejane do scen, które do tej osoby nie należą.
|
||
|
||
Wymóg ≥50 scen odcina szum przy małych liczbach (przy 11 scenach „Denzel" wychodził na
|
||
161, ale to za mało danych, żeby przesądzać). Przy progu 2 na 78 trafień dwa są
|
||
dyskusyjne („Teddy", „Grey" mogą być nazwiskami) — ale i one mają tag rozlany na 3×
|
||
więcej scen, niż mają przypisań, więc ich atrybucje i tak są w większości słowne.
|
||
|
||
Job, nie bramka w `resolve_performer`: kasowanie cyklem nie dotyka gorącej ścieżki
|
||
ingestu, a śmieci i tak odrastają wolno. Dotyczy to zwłaszcza tego drugiego kryterium,
|
||
bo tamte rekordy odtwarza sam ingest TPDB/StashDB. Ta sama logika co przy
|
||
[[phash_blacklist]].
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
from sqlalchemy import text
|
||
from sqlalchemy.orm import Session
|
||
|
||
log = logging.getLogger(__name__)
|
||
|
||
#: Ponad 2× powyżej najwyższej zmierzonej prawdziwej osoby (Julia Reaves, 20).
|
||
DEFAULT_MIN_FOREIGN_PERFORMERS = 50
|
||
|
||
#: 2× powyżej naturalnej wartości dla nazwiska użytego jako własny tag (≈1).
|
||
DEFAULT_MIN_TAG_RATIO = 2
|
||
#: Poniżej tego stosunek jest szumem — za mało scen, żeby cokolwiek przesądzać.
|
||
DEFAULT_MIN_SCENES_FOR_RATIO = 50
|
||
|
||
#: Nazwy 1-2 znakowe („A", „L", „Ro", „Js") to artefakty parsowania tytułów, ale
|
||
#: kilka to realne krótkie pseudonimy (Cj, PD, JD mają obsadę potwierdzoną w TPDB
|
||
#: i StashDB, z prawdziwymi partnerami scenicznymi). Dlatego NIE kasujemy tu
|
||
#: performera, tylko przypisania, o których WIEMY, że dokleił je scraper — reszta
|
||
#: rozstrzyga się sama: komu nic nie zostanie, ten znika.
|
||
MAX_ARTIFACT_NAME_LEN = 2
|
||
|
||
_SELECT_SQL = """
|
||
WITH junk AS (
|
||
SELECT pf.id, pf.canonical_name, pf.slug
|
||
FROM performers pf
|
||
WHERE EXISTS (SELECT 1 FROM tags t WHERE t.slug = pf.slug)
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM performer_external_refs r JOIN sources s ON s.id = r.source_id
|
||
WHERE r.performer_id = pf.id AND s.kind IN ('tpdb', 'stashdb')
|
||
)
|
||
-- „Imię Nazwisko" zostaje: marki osobiste (Kate Kuray, Julia Reaves) wyglądają
|
||
-- jak studio, ale są osobami.
|
||
AND pf.canonical_name !~ '^[A-Z][a-z]+ [A-Z][a-z]+$'
|
||
)
|
||
SELECT j.id FROM junk j
|
||
WHERE (
|
||
SELECT count(DISTINCT sp.performer_id)
|
||
FROM tags t JOIN scene_tags st ON st.tag_id = t.id
|
||
JOIN scene_performers sp ON sp.scene_id = st.scene_id
|
||
WHERE t.slug = j.slug AND sp.performer_id <> j.id
|
||
) >= :min_foreign
|
||
"""
|
||
|
||
|
||
_RATIO_SQL = """
|
||
SELECT pf.id
|
||
FROM performers pf
|
||
WHERE EXISTS (SELECT 1 FROM tags t WHERE t.slug = pf.slug)
|
||
AND (SELECT count(*) FROM scene_performers sp WHERE sp.performer_id = pf.id) >= :min_scenes
|
||
AND (SELECT count(*) FROM scene_tags st JOIN tags t ON t.id = st.tag_id WHERE t.slug = pf.slug)::numeric
|
||
/ NULLIF((SELECT count(*) FROM scene_performers sp WHERE sp.performer_id = pf.id), 0) >= :min_ratio
|
||
"""
|
||
|
||
|
||
def find_junk_performers(
|
||
session: Session,
|
||
*,
|
||
min_foreign: int = DEFAULT_MIN_FOREIGN_PERFORMERS,
|
||
min_ratio: int = DEFAULT_MIN_TAG_RATIO,
|
||
min_scenes: int = DEFAULT_MIN_SCENES_FOR_RATIO,
|
||
):
|
||
"""Suma dwóch niezależnych kryteriów — pierwsze łapie śmieci bez refa kanonicznego,
|
||
drugie te, które ref mają (bo TPDB/StashDB też zawierają „Hardcore" czy „Blonde")."""
|
||
ids = {r[0] for r in session.execute(text(_SELECT_SQL), {"min_foreign": min_foreign})}
|
||
ids |= {
|
||
r[0]
|
||
for r in session.execute(
|
||
text(_RATIO_SQL), {"min_ratio": min_ratio, "min_scenes": min_scenes}
|
||
)
|
||
}
|
||
return list(ids)
|
||
|
||
|
||
# Jednotokenowy performer jest ZBĘDNY na scenie, która ma już osobę pełnoimienną
|
||
# zawierającą to samo słowo: „Devine" obok „Shalina Devine", „Kitana" obok „Kitana A".
|
||
# To rozbite zapisy tej samej osoby, powstałe z parsowania tytułów przez tube-search —
|
||
# jeden wpis nazwiskiem łapie potem wszystkie sceny z tym nazwiskiem, należące do
|
||
# zupełnie innych ludzi (audyt 2026-07-27: 62 tys. takich przypisań, 2156 wpisów).
|
||
#
|
||
# Kasowanie jest DOWODLIWIE BEZSTRATNE: warunek wymaga, żeby konkretna osoba już była
|
||
# przypisana do tej sceny, więc informacja o obsadzie zostaje. Żadna scena nie może
|
||
# przez to zostać bez obsady (sprawdzone: 0).
|
||
#
|
||
# Ograniczone do `kind='scraper'` — przypisania z kanonu zostają nietknięte (73 takie).
|
||
_PRUNE_SURNAME_SQL = """
|
||
DELETE FROM scene_performers sp
|
||
USING sources s, performers p
|
||
WHERE p.id = sp.performer_id
|
||
AND p.canonical_name !~ ' '
|
||
AND length(p.canonical_name) >= 4
|
||
AND s.id = sp.source_id
|
||
AND s.kind = 'scraper'
|
||
AND EXISTS (
|
||
SELECT 1 FROM scene_performers sp2 JOIN performers q ON q.id = sp2.performer_id
|
||
WHERE sp2.scene_id = sp.scene_id AND q.id <> p.id AND q.canonical_name ~ ' '
|
||
AND q.canonical_name ~* ('\\m' || p.canonical_name || '\\M')
|
||
)
|
||
"""
|
||
|
||
_PRUNE_ARTIFACT_SQL = """
|
||
DELETE FROM scene_performers sp
|
||
USING sources s, performers p
|
||
WHERE p.id = sp.performer_id
|
||
AND length(trim(p.canonical_name)) <= :max_len
|
||
AND s.id = sp.source_id
|
||
AND s.kind = 'scraper'
|
||
"""
|
||
|
||
_DROP_EMPTY_ARTIFACT_SQL = """
|
||
DELETE FROM performers p
|
||
WHERE length(trim(p.canonical_name)) <= :max_len
|
||
AND NOT EXISTS (SELECT 1 FROM scene_performers sp WHERE sp.performer_id = p.id)
|
||
"""
|
||
|
||
|
||
def prune_artifact_name_attributions(
|
||
session: Session, *, max_len: int = MAX_ARTIFACT_NAME_LEN
|
||
) -> tuple[int, int]:
|
||
"""Zdejmij scraperowe przypisania w dwóch przypadkach i usuń tych, którym nic nie
|
||
zostało: (1) nazwy 1-2 znakowe, (2) jednotokenowy performer na scenie, która ma już
|
||
osobę pełnoimienną zawierającą to słowo. Kanoniczne przypisania zostają nietknięte.
|
||
|
||
Wymaga wypełnionego `scene_performers.source_id` (migracja 0026): bez provenance nie
|
||
dałoby się odróżnić „Cj" z obsady TPDB od „Cj" wyłuskanego z tytułu przez tube-search.
|
||
Wiersze z NULL-em (sceny wieloźródłowe) zostawiamy — nie zgadujemy."""
|
||
pruned = int(
|
||
session.execute(text(_PRUNE_ARTIFACT_SQL), {"max_len": max_len}).rowcount or 0
|
||
)
|
||
pruned += int(session.execute(text(_PRUNE_SURNAME_SQL)).rowcount or 0)
|
||
dropped = int(
|
||
session.execute(text(_DROP_EMPTY_ARTIFACT_SQL), {"max_len": max_len}).rowcount or 0
|
||
)
|
||
dropped += int(
|
||
session.execute(
|
||
text(
|
||
"DELETE FROM performers p WHERE p.canonical_name !~ ' ' "
|
||
"AND length(p.canonical_name) >= 4 "
|
||
"AND NOT EXISTS (SELECT 1 FROM scene_performers sp "
|
||
"WHERE sp.performer_id = p.id) "
|
||
"AND NOT EXISTS (SELECT 1 FROM performer_external_refs r "
|
||
"JOIN sources s ON s.id = r.source_id WHERE r.performer_id = p.id "
|
||
"AND s.kind IN ('tpdb','stashdb'))"
|
||
)
|
||
).rowcount or 0
|
||
)
|
||
return pruned, dropped
|
||
|
||
|
||
def run_junk_performer_cleanup(
|
||
*, min_foreign: int = DEFAULT_MIN_FOREIGN_PERFORMERS, commit: bool = True
|
||
) -> dict[str, int]:
|
||
"""Skasuj śmieciowych performerów. `scene_performers` leci kaskadą — sceny i tagi
|
||
zostają, więc treść nie ginie: „creampie" nadal jest tagiem przy 176 tys. scen,
|
||
tylko przestaje udawać osobę."""
|
||
from app.db import session_scope
|
||
|
||
with session_scope() as session:
|
||
ids = find_junk_performers(session, min_foreign=min_foreign)
|
||
deleted = pruned = dropped = 0
|
||
if commit:
|
||
if ids:
|
||
deleted = int(
|
||
session.execute(
|
||
text("DELETE FROM performers WHERE id = ANY(:ids)"), {"ids": ids}
|
||
).rowcount or 0
|
||
)
|
||
pruned, dropped = prune_artifact_name_attributions(session)
|
||
session.commit()
|
||
log.info(
|
||
"junk performers: znaleziono %d, skasowano %d; nazwy 1-2 znakowe: zdjęto %d "
|
||
"przypisań, usunięto %d pustych",
|
||
len(ids), deleted, pruned, dropped,
|
||
)
|
||
return {"found": len(ids), "deleted": deleted, "pruned": pruned, "dropped": dropped}
|