Odtworzenie uczenia na 189 tys. zdarzen (wagi zdarzenie po zdarzeniu, bez zagladania w przyszlosc, test na 30% najnowszych): stara punktacja porzadkowala klipy w obrebie urzadzenia z AUC 0,576 (lajk albo >= 10 s vs ucieczka < 2 s). Tagi RedGifs sa liczne i ogolne, wiec ich suma dominowala, a mowila malo; nisze prawie nic, popularnosc z RedGifs slabo, tworca i przyjecie klipu u nas najwiecej. Nowa: log(waga tagu x rzadkosc tagu) + 0,25 log(nisze) + 2,5 log(tworca) + 0,8 ln(1+lajki RedGifs) + 120 (odsetek lajkow u nas - 3%) - 15 (odsetek ucieczek - 50%) + 0,3 za swiezosc. Na tescie 0,652, regresja na tych samych cechach 0,654. Liczniki clips.feed_views/skips/likes (migracja 0052, wypelnione z historii), bumpowane w POST /clips/events. 25% urzadzen (hash id) zostaje na starej punktacji do porownania na zywym ruchu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
4.6 KiB
Python
90 lines
4.6 KiB
Python
"""Klipy do feedu Swipe (sekcja w stylu TikToka). Osobny wertykał, NIE sceny.
|
|
|
|
Klip to 5-120 s pionowego wideo od twórcy-amatora (RedGifs, Pornhub Shorties, nsfwswipe),
|
|
bez obsady z kanonu TPDB, bez dedupu ze scenami i bez filtra 60 s, którym grid ukrywa
|
|
krótkie rzeczy. Dlatego własne tabele zamiast wciskania tego w `scenes`:
|
|
inne kolumny (twórca, audio, lajki, nisze), inny cykl życia (twórcy kasują klipy, więc
|
|
reaper co dobę), inna taksonomia (tagi RedGifs to nie tagi Goona, 1495 vs nasze).
|
|
|
|
`(source, source_id)` identyfikuje klip u źródła (`redgifs` + id gifa). Adresy plików
|
|
RedGifs są publiczne i bez podpisu (`media.redgifs.com/<Id>.mp4`), więc trzymamy je
|
|
wprost i telefon gra direct, bez ekstraktora i bez VPS-a. Pornhub Shorties (etap
|
|
późniejszy) będzie miał `hd_url` puste i resolve na żądanie.
|
|
|
|
Tagi w osobnej tabeli z indeksem `(tag, clip_id)`: feed dobiera kandydatów skanem po tagu
|
|
od losowego uuid (jak `sort=random` w `list_scenes`), więc para w tej kolejności to jego
|
|
główny indeks. Nisze (społeczności RedGifs) jako tablica, bo służą tylko onboardingowi
|
|
i blacklistom, nie skanom.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
DateTime,
|
|
Float,
|
|
ForeignKey,
|
|
Index,
|
|
Integer,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy.dialects.postgresql import ARRAY, UUID
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.models.base import Base, TimestampMixin, UUIDPKMixin
|
|
|
|
|
|
class Clip(UUIDPKMixin, TimestampMixin, Base):
|
|
__tablename__ = "clips"
|
|
__table_args__ = (
|
|
UniqueConstraint("source", "source_id", name="uq_clips_source_source_id"),
|
|
Index("ix_clips_published_at", "published_at"),
|
|
Index("ix_clips_creator", "creator"),
|
|
)
|
|
|
|
#: `redgifs` | `pornhub` | `nsfwswipe`
|
|
source: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
source_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
title: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
#: Pseudonim twórcy u źródła (RedGifs `userName`). Nie łączymy z `performers`.
|
|
creator: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
creator_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
duration_sec: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
width: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
height: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
has_audio: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
#: RedGifs `contentType`, np. "Solo Female".
|
|
content_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
niches: Mapped[list[str]] = mapped_column(ARRAY(String(64)), nullable=False, default=list)
|
|
hd_url: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
sd_url: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
poster_url: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
thumb_url: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
#: Źródło deklaruje wariant HLS (RedGifs `hls`). Informacyjne, gramy mp4.
|
|
hls: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
likes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
views: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
#: Przyjecie klipu w samym Goonie (feed Edge): wyswietlenia, ucieczki < 2 s, lajki.
|
|
#: Licznik bumpuje POST /clips/events, punktacja feedu z tego korzysta (migracja 0052).
|
|
feed_views: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
|
feed_skips: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
|
feed_likes: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
|
#: Data publikacji u źródła (RedGifs `createDate`), do odznaki „nowe" i sortu.
|
|
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
dead_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
dead_reason: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
|
|
|
|
class ClipTag(Base):
|
|
__tablename__ = "clip_tags"
|
|
__table_args__ = (Index("ix_clip_tags_tag_clip", "tag", "clip_id"),)
|
|
|
|
clip_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("clips.id", ondelete="CASCADE"), primary_key=True
|
|
)
|
|
#: Slug tagu u źródła, małe litery, spacje jako myślniki ("big-tits").
|
|
tag: Mapped[str] = mapped_column(String(64), primary_key=True)
|