goon/app/scheduler/phash_blacklist.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

65 lines
2.6 KiB
Python

"""Wykrywanie i czyszczenie zdegenerowanych phashy.
Phash identyfikuje scenę tylko wtedy, gdy miniaturka pokazuje treść sceny. Gdy tube
serwuje zaślepkę, czarną klatkę albo wspólne intro studia, ta sama wartość ląduje
przy setkach niepowiązanych scen i przestaje cokolwiek znaczyć, a `find_by_phash_within`
bierze najbliższą wartość z CAŁEJ tabeli, więc taki wpis jest czystym szumem.
Próg `min_scenes=10` wynika z rozkładu zmierzonego 2026-07-27: 11 000 wartości
występowało przy dokładnie 2 scenach (to realne duplikaty, chcemy je zachować),
1346 przy 3-9, a dopiero od 10 w górę zaczynają się grupy, w których liczba różnych
tytułów równa się liczbie scen, czyli nic ich nie łączy poza obrazkiem.
Kolejność ma znaczenie: najpierw `refresh` (zapisz wartości na stałe), potem `purge`
(skasuj wiersze). Odwrotnie skasowalibyśmy dowody i blacklista wyszłaby pusta.
"""
from __future__ import annotations
import logging
from sqlalchemy import text
from sqlalchemy.orm import Session
log = logging.getLogger(__name__)
DEFAULT_MIN_SCENES = 10
_REFRESH_SQL = """
INSERT INTO phash_blacklist (value, scene_count)
SELECT value, count(DISTINCT scene_id)
FROM scene_fingerprints
WHERE kind = 'phash'
GROUP BY value
HAVING count(DISTINCT scene_id) >= :min_scenes
ON CONFLICT (value) DO UPDATE SET scene_count = EXCLUDED.scene_count
"""
_PURGE_SQL = """
DELETE FROM scene_fingerprints f
USING phash_blacklist b
WHERE f.kind = 'phash' AND f.value = b.value
"""
def refresh_phash_blacklist(session: Session, *, min_scenes: int = DEFAULT_MIN_SCENES) -> int:
"""Dopisz do blacklisty wartości dzielone przez ≥min_scenes scen. Zwraca rozmiar
blacklisty po odświeżeniu (wpisy raz dodane NIE znikają, o to chodzi)."""
session.execute(text(_REFRESH_SQL), {"min_scenes": min_scenes})
return int(session.execute(text("SELECT count(*) FROM phash_blacklist")).scalar_one())
def purge_blacklisted_fingerprints(session: Session) -> int:
"""Skasuj wiersze `scene_fingerprints` o zablacklistowanej wartości. Zwraca liczbę
skasowanych. Sceny zostają nietknięte, znika tylko bezużyteczny odcisk."""
return int(session.execute(text(_PURGE_SQL)).rowcount or 0)
def run_phash_blacklist(*, min_scenes: int = DEFAULT_MIN_SCENES) -> dict[str, int]:
from app.db import session_scope
with session_scope() as session:
size = refresh_phash_blacklist(session, min_scenes=min_scenes)
purged = purge_blacklisted_fingerprints(session)
session.commit()
log.info("phash blacklist: %d wartości na liście, skasowano %d odcisków", size, purged)
return {"blacklist_size": size, "purged": purged}