Files
grabber-ai/tools/diversity.py
T
DenozordecandClaude Sonnet 5.5 efc9a4f56a
docker / build (push) Failing after 39s
Docker-сервис, выбор основы и сравнение моделей, сборщик датасета
- Docker: Dockerfile (CPU), compose с FlareSolverr, настройка через DUB_* переменные,
  токен доступа к /predict, tools/pack_models.py, workflow Gitea Actions, docs/docker.md
- Модель грузится офлайн (config.json рядом с весами); общий код вынесен в src/spans.py,
  сервису не нужны sklearn/matplotlib/pandas
- Обучение: профили tiny2/base/large/modern, запуски в models/runs, реестр и честное
  сравнение на одной тестовой выборке (src/runs.py), пункт меню «Модели»
- Датасет: язык и аудио-параметры дорожек, 18+ по строкам «Аудио», ручные метки по ссылкам,
  переиндексация сохранённого HTML, параллельная сборка, сбалансированная выборка
- Тексты страниц (data/pages) больше не хранятся в git

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 17:19:10 +07:00

110 lines
4.6 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.
"""Разнообразная выборка раздач: годы, качество, фильмы/сериалы.
Список раздела идёт «от новых к старым», поэтому чтение подряд даёт только
свежие релизы. Здесь:
* страницы раздела берутся вразброс по всей глубине (последовательность с
золотым сечением, уже посещённые пропускаются);
* на странице выбираются не все темы, а те, чья «корзина» (годовая группа ×
качество × фильм/сериал) в датасете представлена меньше всего.
Всё работает по заголовкам тем из списка, заходить в раздачу для выбора не нужно.
"""
from __future__ import annotations
import csv
import random
import re
from collections import Counter
from pathlib import Path
RE_YEAR = re.compile(r"(?<!\d)(19[3-9]\d|20[0-3]\d)(?!\d)")
RE_SERIES = re.compile(r"(?i)сезон|сери[ия]|\bS\d{1,2}\b|\bE\d{1,3}\b|эпизод|\bTV\b|выпуск")
GOLDEN = 0.6180339887498949
QUALITY_RULES = [
("4K", re.compile(r"(?i)2160|\b4K\b|\bUHD\b")),
("1080", re.compile(r"(?i)1080|\bFHD\b")),
("720", re.compile(r"(?i)\b720|\bHD(?:TV)?\b|\bHDRip\b")),
("SD", re.compile(r"(?i)DVD|SAT|TVRip|VHS|\b480|\b576|CAMRip|TS\b")),
]
def year_of(title: str) -> int | None:
"""Год выпуска из заголовка: сначала из скобок «[2020, …]» / «(2020)», иначе первый."""
for m in re.finditer(r"[\[(]\s*(19[3-9]\d|20[0-3]\d)(?!\d)", title or ""):
return int(m.group(1))
m = RE_YEAR.search(title or "")
return int(m.group(1)) if m else None
def year_band(year: int | None) -> str:
if year is None:
return "?"
if year < 2010:
return f"{year // 10 * 10}-е"
return str(year)
def quality_of(title: str) -> str:
for name, rx in QUALITY_RULES:
if rx.search(title or ""):
return name
return "прочее"
def bucket(title: str) -> tuple[str, str, str]:
return (year_band(year_of(title)), quality_of(title),
"сериал" if RE_SERIES.search(title or "") else "фильм")
def counts_from_meta(path: Path, site: str | None = None) -> Counter:
"""Сколько релизов каждой корзины уже в датасете (по data/meta.csv)."""
out: Counter = Counter()
try:
with path.open(encoding="utf-8-sig", newline="") as f:
for r in csv.DictReader(f):
if site and r.get("source") != site:
continue
title = r.get("title") or ""
year = int(r["year"]) if str(r.get("year", "")).isdigit() else year_of(title)
q = quality_of(f"{title} {r.get('quality', '')} {r.get('resolution', '')}")
is_s = str(r.get("is_series", "")).lower() in ("true", "1")
out[(year_band(year), q, "сериал" if is_s else "фильм")] += 1
except OSError:
pass
return out
def pick(items: list[dict], n: int, counts: Counter, rng: random.Random) -> list[dict]:
"""n тем из страницы: сначала из самых малочисленных корзин (counts обновляется)."""
pool = list(items)
rng.shuffle(pool) # равные корзины -> случайный порядок
chosen: list[dict] = []
while pool and len(chosen) < n:
best = min(pool, key=lambda it: counts[bucket(it.get("text", ""))])
pool.remove(best)
counts[bucket(best.get("text", ""))] += 1
chosen.append(best)
return chosen
def next_pages(depth: int, visited: set[int], k: int, rng: random.Random) -> list[int]:
"""До k ещё не посещённых страниц, разбросанных по глубине раздела."""
if depth <= 0:
return []
left = depth - len(visited)
out: list[int] = []
x = rng.random()
tries = 0
while len(out) < min(k, left) and tries < depth * 8 + 50:
tries += 1
x = (x + GOLDEN) % 1.0
p = int(x * depth)
if p not in visited and p not in out:
out.append(p)
if len(out) < min(k, left): # хвост добираем подряд
out += [p for p in range(depth) if p not in visited and p not in out][:min(k, left) - len(out)]
return out