From 0d4e11bc31e228d12749f113671fdfa716c4e7e2 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Fri, 14 Aug 2026 10:34:14 +0200 Subject: [PATCH] fix(performers): odzyskaj sceny przypisane do samego czlonu nazwiska MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Siedem performerow o nazwie JEDNOCZLONOWEJ ("James", "Blue", "Moon", "Crystal", "Cruz", "Starr", "Knight") zbieralo sceny wielu roznych osob dzielacych ten czlon: "Blue" mial sceny Anity, Delilah i Bonnie Blue, "Starr" mial Violet, Natashe i Bobbi Starr. prune_token_overlap_attributions z zalozenia ich NIE RUSZAL i to nie byl niedopatrzenie: dla nazwy jednoczlonowej za dowod uznaje samo wystapienie tego slowa w tytule, wiec "James" jest "udokumentowany" przez kazda scene wspominajaca jakiegokolwiek Jamesa. Stad 0 skasowanych z tej siodemki. To NIE bylo kasowanie, tylko ODZYSKIWANIE: w probce 14/14 wlasciwa osoba NIE byla przypisana do sceny, czyli Bonnie Blue nie miala tych scen na swojej stronie. Kryterium: przypisanie ze scrapera (kanon nietykalny — z 605 przypisan tych siedmiu 518 jest ze scrapera) + istnieje performer, ktorego nazwa konczy sie tym czlonem po spacji + ta PELNA nazwa stoi w tytule sceny jako ciag calych slow. Ostatni warunek odroznia odzyskanie od zgadywania. Wynik: dolozono 271 wlasciwych przypisan, zdjeto 268 mononimow (roznica to sceny, gdzie w tytule stoja DWIE takie osoby, np. Bonnie i Mick Blue — obie sa dokladane, mononim schodzi raz). Zyskali m.in.: Violet Starr +22 sceny, Mick Blue +16, Stacy Cruz +15, Rachel Starr +13, Alexis Crystal +12, Natasha Starr +8. Mononimy spadly: Starr 93->28, Cruz 45->8, Moon 64->23, Blue 113->73. Reszta zostaje nietknieta, bo nie ma w tytule wskazanego wlasciciela — "Blue" i "Cruz" maja refy StashDB, wiec czesc z nich moze byc prawdziwa. Zdjete wiersze ida do scene_performers_pruned, cofniecie to INSERT..SELECT. --- scripts/fix_mononym_attributions.py | 132 ++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 scripts/fix_mononym_attributions.py diff --git a/scripts/fix_mononym_attributions.py b/scripts/fix_mononym_attributions.py new file mode 100644 index 0000000..ef234ce --- /dev/null +++ b/scripts/fix_mononym_attributions.py @@ -0,0 +1,132 @@ +"""Przypisuje WŁAŚCIWĄ osobę tam, gdzie scena dostała sam człon jej nazwiska. + +W bazie siedzi siedem performerów o nazwie jednoczłonowej („James", „Blue", „Moon", +„Crystal", „Cruz", „Starr", „Knight"), którzy zbierają sceny wielu różnych osób +dzielących ten człon: „Blue" ma sceny Anity Blue, Delilah Blue i Bonnie Blue, „Starr" +ma Violet, Natashę i Bobbi Starr. + +**To NIE jest ten sam przypadek co `prune_token_overlap_attributions`** i tamten skrypt +z założenia ich nie rusza: dla nazwy jednoczłonowej za dowód uznaje samo wystąpienie +tego słowa w tytule, więc „James" jest „udokumentowany" przez każdą scenę wspominającą +jakiegokolwiek Jamesa. Stąd 0 skasowanych z tych siedmiu przy sprzątaniu 2026-08-13. + +**I nie jest to kasowanie, tylko ODZYSKIWANIE.** W próbce 14/14 właściwa osoba NIE była +przypisana do sceny, czyli Bonnie Blue nie miała tych scen na swojej stronie. Skrypt +dokłada właściwego performera i zdejmuje mononim. + +Kryterium (wszystkie warunki naraz): + +1. przypisanie mononimu pochodzi ze scrapera (kanon nietykalny; z 605 przypisań tych + siedmiu 518 jest ze scrapera, reszta z TPDB/StashDB i tej nie dotykamy), +2. w bazie istnieje performer, którego nazwa KOŃCZY SIĘ tym członem po spacji + („% blue" łapie „Bonnie Blue", nie łapie „Blue"), +3. ta pełna nazwa występuje w tytule sceny jako ciąg całych słów. + +Warunek 3 jest tym, co odróżnia odzyskanie od zgadywania: nie wystarczy, że ktoś taki +istnieje, jego nazwisko musi stać w tytule tej konkretnej sceny. + +Gdy w tytule stoją DWIE takie osoby („Bonnie Blue and Mick Blue"), dokładamy obie +i zdejmujemy mononim raz. To poprawne, bo scena faktycznie ma ich obu. + +Uruchomienie (kontener worker): + python -m scripts.fix_mononym_attributions # dry-run + python -m scripts.fix_mononym_attributions --yes # wykonaj +""" +from __future__ import annotations + +import argparse +import logging + +from sqlalchemy import text + +from app.db import session_scope + +log = logging.getLogger(__name__) + +#: Nazwy jednoczłonowe zbierające cudze sceny. Lista jawna, NIE heurystyka: mononimy +#: bywają prawdziwe (StashDB ma performerki „Blue" i „Cruz"), więc nie chcemy tu trafić +#: przypadkiem w kogoś, kto naprawdę występuje pod jednym imieniem. +MONONYMS = ("James", "Blue", "Moon", "Crystal", "Cruz", "Starr", "Knight") + +_BUILD = """ +CREATE TEMP TABLE kand ON COMMIT DROP AS +SELECT sp.scene_id, + sp.performer_id AS mono_id, + sp.source_id, + p2.id AS wlasciwy_id +FROM scene_performers sp +JOIN sources src ON src.id = sp.source_id AND src.name = 'tube-scraper' +JOIN performers m ON m.id = sp.performer_id AND m.canonical_name = ANY(:names) +JOIN scenes s ON s.id = sp.scene_id +JOIN performers p2 + ON lower(p2.canonical_name) LIKE '% ' || lower(m.canonical_name) + AND p2.id <> m.id + -- pełna nazwa musi stać w tytule jako ciąg CAŁYCH słów; `strpos` po znormalizowanym, + -- obustronnie odspacjowanym tekście, żeby „Mia Blue" nie łapało się w „Miami Blues" + AND strpos(' ' || regexp_replace(lower(s.title), '[^a-z0-9]+', ' ', 'g') || ' ', + ' ' || regexp_replace(lower(p2.canonical_name), '[^a-z0-9]+', ' ', 'g') || ' ') > 0 +""" + +#: Dokładamy właściwą osobę. `ON CONFLICT DO NOTHING`, bo bywa już przypisana. +#: `source_id` przepisujemy z wiersza mononimu: przypisanie pochodzi z tego samego +#: scrapera, tylko trafiło w zły rekord. +_ATTACH = """ +INSERT INTO scene_performers (scene_id, performer_id, source_id) +SELECT DISTINCT scene_id, wlasciwy_id, source_id FROM kand +ON CONFLICT DO NOTHING +""" + +_ARCHIVE = """ +INSERT INTO scene_performers_pruned + (scene_id, performer_id, source_id, performer_name, scene_title) +SELECT DISTINCT sp.scene_id, sp.performer_id, sp.source_id, p.canonical_name, s.title +FROM kand k +JOIN scene_performers sp ON sp.scene_id = k.scene_id AND sp.performer_id = k.mono_id +JOIN performers p ON p.id = sp.performer_id +JOIN scenes s ON s.id = sp.scene_id +ON CONFLICT DO NOTHING +""" + +_DETACH = """ +DELETE FROM scene_performers sp +USING (SELECT DISTINCT scene_id, mono_id FROM kand) d +WHERE sp.scene_id = d.scene_id AND sp.performer_id = d.mono_id +""" + +_REPORT = """ +SELECT m.canonical_name AS mononim, + count(*) AS przypisan, + count(DISTINCT k.wlasciwy_id) AS roznych_osob +FROM kand k JOIN performers m ON m.id = k.mono_id +GROUP BY m.canonical_name ORDER BY 2 DESC +""" + + +def run(*, apply: bool) -> dict[str, int]: + with session_scope() as session: + session.execute(text(_BUILD), {"names": list(MONONYMS)}) + rows = session.execute(text(_REPORT)).all() + for name, n, people in rows: + print(f" {name:9} {n:>4} przypisań → {people} różnych osób", flush=True) + total = sum(r[1] for r in rows) + + if not apply: + print(f"\nRAZEM: {total} do naprawy (dry-run)") + return {"matched": total, "attached": 0, "detached": 0} + + attached = session.execute(text(_ATTACH)).rowcount or 0 + session.execute(text(_ARCHIVE)) + detached = session.execute(text(_DETACH)).rowcount or 0 + print(f"\nRAZEM: dołożono {attached} właściwych przypisań, zdjęto {detached} mononimów") + return {"matched": total, "attached": attached, "detached": detached} + + +def main() -> None: + logging.basicConfig(level=logging.INFO, format="%(message)s") + ap = argparse.ArgumentParser() + ap.add_argument("--yes", action="store_true", help="wykonaj (bez tego dry-run)") + run(apply=ap.parse_args().yes) + + +if __name__ == "__main__": + main()