goon/app/scheduler/junk_performers.py
goon-foss 8d92ecc51b fix(performers): zdjecie scraperowych przypisan z nazw 1-2 znakowych
"A", "L", "Ro", "Be", "Js" mialy po 250-290 scen kazdy. To artefakty parsowania
tytulow przez tube-search, ale NIE wszystkie: "Cj", "PD" i "JD" to realne krotkie
pseudonimy z obsada potwierdzona w TPDB i StashDB, z prawdziwymi partnerami
scenicznymi (Cj z Lucy Belle i Denisem Martim, PD z Victoria Love).

Dlatego nie kasujemy tu performera, tylko przypisania, o ktorych WIEMY ze dokleil je
scraper. Reszta rozstrzyga sie sama: komu nic nie zostanie, ten znika. To pierwsze
uzycie provenance z migracji 0026 - bez niej nie dalo sie odroznic "Cj" z obsady
TPDB od "Cj" wylusknietego z tytulu.

Rozklad byl jednoznaczny: z 9404 przypisan 9232 (98,2 proc.) ze scrapera, 141 z
kanonu, 31 nieznanych. Wiersze z NULL-em zostawiamy, nie zgadujemy.

Wynik: 9232 przypisan zdjetych, 20 performerow zniknelo calkiem, 31 zostalo z sama
prawdziwa obsada (Cj 41, PD 38, JD 20, reszta <=10). Dopiete do joba junk_performers,
zeby nie odroslo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:18:38 +02:00

191 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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)
_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 z nazw 1-2 znakowych i usuń tych, którym nic nie
zostało. Kanoniczne przypisania zostają nietknięte — patrz MAX_ARTIFACT_NAME_LEN.
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
)
dropped = int(
session.execute(text(_DROP_EMPTY_ARTIFACT_SQL), {"max_len": max_len}).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}