feat(tags): zdejmij znaczniki jakosci + przytnij ogon tagow rule34video

Znaczniki jakosci (hd, 4k, 720p, HD Videos, wzorzec ^\d{3,4}p$) trafiaja na stoplist
i znikaja z katalogu: 323 379 przypisan, 16 tagow. Powod: apka ma osobny filtr jakosci
liczony ze ZRODEL ODTWARZANIA, ktory jest wiarygodny, a tag jest deklaracja tuba i bywa
falszywy. Dwa rownolegle sygnaly o tym samym, z ktorych jeden klamie, sa gorsze niz jeden.

Ogon rule34video: 356 scen wnosilo 1112 roznych tagow do WSPOLNEJ listy uzywanej przez
caly katalog, z czego 602 wystepowaly dokladnie raz (nazwy postaci i serii, tagowanie
w stylu booru). Nowy job co 24h zostawia tagi z >=3 scen i najwyzej 15 na scene,
uporzadkowane po czestosci w obrebie serwisu.

Wazny szczegol reguly, wychwycony dopiero w symulacji: ochrona tagow wspolnych dotyczy
TYLKO progu, nie limitu. Pierwsza wersja wylaczala tagi wspolne rowniez z limitu i przez
to prawie nie dzialala, rekord spadal 161 -> 147 zamiast do 15, bo najbardziej obsypana
scena skladala sie glownie z tagow wspolnych.

Wynik na produkcji: srednia na scenie rule34video 13,5 -> 9,1, rekord 161 -> 15, zadna
scena nie zostala bez tagow. Usuniete 1554 przypisania i 2475 osieroconych tagow.
VACUUM ANALYZE po obu operacjach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-08-14 12:28:47 +02:00
parent 38a2724963
commit 8c8f22d388
5 changed files with 175 additions and 7 deletions

View file

@ -150,6 +150,11 @@ class Settings(BaseSettings):
sched_junk_performers_hours: int = Field( sched_junk_performers_hours: int = Field(
default=24, validation_alias="GOON_SCHED_JUNK_PERFORMERS_HOURS" default=24, validation_alias="GOON_SCHED_JUNK_PERFORMERS_HOURS"
) )
# Przycinanie ogona tagow rule34video (tagowanie booru: nazwy postaci/serii).
# 356 scen wnosilo 1112 tagow do WSPOLNEJ listy, 602 z nich uzyte raz. 24h. 0 = off.
sched_tag_tail_trim_hours: int = Field(
default=24, validation_alias="GOON_SCHED_TAG_TAIL_TRIM_HOURS"
)
# Ingest freshness watchdog, alert do Sentry gdy aktywny tube (origin # Ingest freshness watchdog, alert do Sentry gdy aktywny tube (origin
# tube:<sitetag>) przestał dawać nowe sceny > próg. Łapie zamrożenie # tube:<sitetag>) przestał dawać nowe sceny > próg. Łapie zamrożenie
# pojedynczego origin, którego globalny monitor (jeden Source "tube-scraper") nie # pojedynczego origin, którego globalny monitor (jeden Source "tube-scraper") nie

View file

@ -15,15 +15,18 @@ Kryterium wpisu: tag nie zawezaja wyszukiwania, bo pasuje do niemal wszystkiego
w katalogu ("porn", "sex", "hot"), albo opisuje sposob dystrybucji, a nie tresc w katalogu ("porn", "sex", "hot"), albo opisuje sposob dystrybucji, a nie tresc
("premium", "exclusive", "leaked", "download"). ("premium", "exclusive", "leaked", "download").
Swiadomie NIE na liscie: Znaczniki jakosci (`hd`, `4k`, `720p`, `HD Videos`) tez sa na liscie, decyzja z 2026-08-14.
- `onlyfans` (71 972 przypisan) - to realny typ tresci, ludzie tego szukaja. Powod: apka ma osobny filtr jakosci liczony ze ZRODEL ODTWARZANIA, ktory jest wiarygodny,
- `4k`, `hd`, `720p`, `1080p` (ok. 110 tys.) - niosa jakas informacje, choc a tag jest deklaracja tuba i bywa falszywy. Dwa rownolegle sygnaly o tym samym, z ktorych
niepewna. Apka ma osobny, wiarygodny filtr jakosci liczony ze zrodel odtwarzania, jeden klamie, sa gorsze niz jeden. Samo `HD Videos` mialo 205 488 przypisan.
wiec te tagi sa zbedne, ale usuniecie ich to decyzja produktowa, nie sprzatanie
smieci. Do przegadania osobno. Swiadomie NIE na liscie: `onlyfans` (71 972 przypisan). To realny typ tresci, nie chwyt
marketingowy, ludzie tego szukaja.
""" """
from __future__ import annotations from __future__ import annotations
import re
#: Porownanie po slugu (lowercase, dywizy), bo ta sama rzecz przychodzi jako #: Porownanie po slugu (lowercase, dywizy), bo ta sama rzecz przychodzi jako
#: "Full HD", "full-hd" i "FullHD". #: "Full HD", "full-hd" i "FullHD".
JUNK_TAG_SLUGS: frozenset[str] = frozenset( JUNK_TAG_SLUGS: frozenset[str] = frozenset(
@ -65,10 +68,30 @@ JUNK_TAG_SLUGS: frozenset[str] = frozenset(
"streaming", "streaming",
"online", "online",
"full-video", "full-video",
# znaczniki jakosci, patrz docstring: dubluja filtr liczony ze zrodel i klamia
"hd",
"hd-porn",
"hd-videos",
"sd",
"fhd",
"uhd",
"full-hd",
"fullhd",
"ultra-hd",
"high-definition",
"hi-res",
"4k",
"8k",
"4k-porn",
} }
) )
#: Rozdzielczosc jako tag: 240p, 480p, 720p, 1080p, 2160p... Wzorzec zamiast wyliczanki,
#: bo tuby wymyslaja kolejne warianty przy kazdym re-scrape.
_RESOLUTION_RE = re.compile(r"^\d{3,4}p$")
def is_junk_tag(slug: str) -> bool: def is_junk_tag(slug: str) -> bool:
"""True dla tagu, ktorego nie chcemy trzymac w katalogu.""" """True dla tagu, ktorego nie chcemy trzymac w katalogu."""
return slug.strip().lower() in JUNK_TAG_SLUGS s = slug.strip().lower()
return s in JUNK_TAG_SLUGS or bool(_RESOLUTION_RE.match(s))

View file

@ -308,6 +308,21 @@ def _job_phash_blacklist() -> None:
log.exception("[scheduler] phash blacklist failed") log.exception("[scheduler] phash blacklist failed")
def _job_tag_tail_trim() -> None:
"""Przycina ogon tagow rule34video (booru: nazwy postaci, serii, autorow).
356 scen wnosilo 1112 roznych tagow do wspolnej listy, 602 uzyte dokladnie raz.
Zostawia tagi z >=3 scen i najwyzej 15 na scene; tagi uzywane takze poza
rule34video sa chronione, bo naleza do wspolnego slownika."""
log.info("[scheduler] tag tail trim starting")
try:
from app.scheduler.tag_tail_trim import run
stats = run()
log.info("[scheduler] tag tail trim done: %s", stats)
except Exception:
log.exception("[scheduler] tag tail trim failed")
def _job_junk_performers() -> None: def _job_junk_performers() -> None:
"""Kasuje kategorie i studia podszywające się pod performerów („Creampie", """Kasuje kategorie i studia podszywające się pod performerów („Creampie",
„Natural tits", „Brazzers"). Audyt 2026-07-27: 277 rekordów, 33 784 przypisania. „Natural tits", „Brazzers"). Audyt 2026-07-27: 277 rekordów, 33 784 przypisania.
@ -550,6 +565,17 @@ def build_scheduler(cfg: dict[str, Any]) -> BlockingScheduler:
cfg["phash_backfill_minutes"], _batch, cfg["phash_backfill_minutes"], _batch,
) )
if cfg.get("tag_tail_trim_hours"):
sched.add_job(
_job_tag_tail_trim,
IntervalTrigger(hours=cfg["tag_tail_trim_hours"], start_date=INTERVAL_ANCHOR),
id="tag_tail_trim",
replace_existing=True,
max_instances=1,
coalesce=True,
)
log.info("scheduler: tag tail trim every %dh", cfg["tag_tail_trim_hours"])
if cfg.get("junk_performers_hours"): if cfg.get("junk_performers_hours"):
sched.add_job( sched.add_job(
_job_junk_performers, _job_junk_performers,

View file

@ -0,0 +1,112 @@
"""Przycinanie ogona tagow z rule34video (tagowanie w stylu booru).
Skad problem: rule34video taguje jak booru, czyli nazwami postaci, serii i autorow.
Zmierzone 2026-08-14: **356 scen ma 1112 roznych tagow**, srednio 13,5 na scene, rekord
161 (zgloszenie uzytkownika o scenie "TEMPTATION - Episode 14"). Z tych 1112 tagow
**602 wystepuja dokladnie raz**, a 763 najwyzej dwa razy. To sa nazwy wlasne.
Same w sobie nie sa bledne, ludzie ich szukaja, ale 356 scen wrzuca ponad tysiac pozycji
do WSPOLNEJ listy tagow, ktorej uzywa caly katalog (1,9 mln scen). Stad przyciecie,
a nie skasowanie zrodla.
Dwie reguly, obie potrzebne (zmierzone osobno):
1. **Prog czestosci**: tag musi wystepowac na >=3 scenach rule34video. Sam prog zbija
srednia 13,5 -> 10,9 i rekord 161 -> 104, wiec NIE wystarcza.
2. **Limit na scene**: najwyzej 15 tagow, zostawiamy najczestsze w obrebie serwisu.
Dopiero to domyka rekord.
Ochrona tagow wspolnych dotyczy TYLKO progu, nie limitu, i to jest istotne. Tag uzywany
takze poza rule34video (np. "Bondage") nalezy do wspolnego slownika, wiec nie kasujemy go
z powodu niskiej czestosci w obrebie serwisu. Ale limit na scene obejmuje go normalnie:
scena nie powinna miec 161 tagow niezaleznie od tego, czyje one sa.
Pierwsza wersja tej reguly wylaczala tagi wspolne takze z limitu i przez to prawie nie
dzialala: rekord spadal 161 -> 147 zamiast do 15, bo najbardziej obsypana scena skladala
sie glownie z tagow wspolnych.
Uwaga o zbieznosci: limit na scene obniza liczniki tagow, wiec kolejny przebieg moze
zejsc nieco nizej, zanim sie ustabilizuje. Liczniki bierzemy ze SNAPSHOTU sprzed zmian
w danym przebiegu, zeby jeden przebieg nie kaskadowal sam na sobie.
"""
from __future__ import annotations
import logging
from sqlalchemy import text
from app.db import session_scope
log = logging.getLogger(__name__)
ORIGIN = "tube:rule34video"
MIN_SCENES_PER_TAG = 3
MAX_TAGS_PER_SCENE = 15
_SQL = text(
"""
WITH r34 AS (
SELECT DISTINCT p.scene_id AS id
FROM playback_sources p
WHERE p.origin = :origin
),
-- snapshot licznikow SPRZED zmian w tym przebiegu
freq AS (
SELECT st.tag_id, count(*) AS c
FROM scene_tags st JOIN r34 ON r34.id = st.scene_id
GROUP BY st.tag_id
),
-- tag uzywany takze poza rule34video nalezy do wspolnego slownika, nie ruszamy
shared AS (
SELECT DISTINCT st.tag_id
FROM scene_tags st
WHERE st.tag_id IN (SELECT tag_id FROM freq)
AND st.scene_id NOT IN (SELECT id FROM r34)
),
ranked AS (
SELECT st.scene_id, st.tag_id, f.c AS cnt,
(st.tag_id IN (SELECT tag_id FROM shared)) AS is_shared,
row_number() OVER (PARTITION BY st.scene_id ORDER BY f.c DESC, st.tag_id) AS rn
FROM scene_tags st
JOIN r34 ON r34.id = st.scene_id
JOIN freq f ON f.tag_id = st.tag_id
),
doomed AS (
SELECT scene_id, tag_id FROM ranked
-- limit obowiazuje wszystkie tagi; prog tylko te wylacznie rule34video
WHERE rn > :max_tags OR (cnt < :min_scenes AND NOT is_shared)
)
DELETE FROM scene_tags st
USING doomed d
WHERE st.scene_id = d.scene_id AND st.tag_id = d.tag_id
"""
)
# Tagi, ktore po przycieciu nie wisza juz nigdzie. Zostawione zasmiecalyby liste tagow
# mimo zerowej liczby scen.
_SQL_ORPHANS = text(
"""
DELETE FROM tags t
WHERE NOT EXISTS (SELECT 1 FROM scene_tags st WHERE st.tag_id = t.id)
AND NOT EXISTS (SELECT 1 FROM movie_tags mt WHERE mt.tag_id = t.id)
AND NOT EXISTS (SELECT 1 FROM blacklisted_tags bt WHERE bt.tag_id = t.id)
"""
)
def run() -> dict[str, int]:
"""Przytnij ogon i skasuj osierocone tagi. Zwraca licznik zmian."""
with session_scope() as session:
removed = session.execute(
_SQL,
{
"origin": ORIGIN,
"min_scenes": MIN_SCENES_PER_TAG,
"max_tags": MAX_TAGS_PER_SCENE,
},
).rowcount
orphans = session.execute(_SQL_ORPHANS).rowcount
session.commit()
log.info(
"tag_tail_trim: usunieto %s przypisan, %s osieroconych tagow", removed, orphans
)
return {"removed": removed or 0, "orphans": orphans or 0}

View file

@ -224,6 +224,8 @@ def run_forever() -> int:
# Śmieciowi performerzy, kategorie/studia udające osoby (audyt 2026-07-27). # Śmieciowi performerzy, kategorie/studia udające osoby (audyt 2026-07-27).
# Kryterium wąskie celowo: szerokie kasuje realnych ludzi bez refa w TPDB. # Kryterium wąskie celowo: szerokie kasuje realnych ludzi bez refa w TPDB.
"junk_performers_hours": getattr(settings, "sched_junk_performers_hours", 24) or None, "junk_performers_hours": getattr(settings, "sched_junk_performers_hours", 24) or None,
# Ogon tagow rule34video (nazwy wlasne w stylu booru zalewajace liste tagow).
"tag_tail_trim_hours": getattr(settings, "sched_tag_tail_trim_hours", 24) or None,
# Taxonomy scene_count refresh, denormalizacja liczników (perf fix 0019). # Taxonomy scene_count refresh, denormalizacja liczników (perf fix 0019).
"taxonomy_counts_hours": getattr(settings, "sched_taxonomy_counts_hours", 3) or None, "taxonomy_counts_hours": getattr(settings, "sched_taxonomy_counts_hours", 3) or None,
# Ingest freshness watchdog, per-origin alert do Sentry (report 14f3a655). # Ingest freshness watchdog, per-origin alert do Sentry (report 14f3a655).