goon/app/scheduler/junk_performers.py
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

232 lines
10 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)
# 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}