Docker-сервис, выбор основы и сравнение моделей, сборщик датасета
docker / build (push) Failing after 39s
docker / build (push) Failing after 39s
- 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>
This commit is contained in:
1 parent
d155b8584b
commit
efc9a4f56a
411 files changed
+87495
-27363
No files matched your search
@@ -0,0 +1,109 @@
|
||||
"""Разнообразная выборка раздач: годы, качество, фильмы/сериалы.
|
||||
|
||||
Список раздела идёт «от новых к старым», поэтому чтение подряд даёт только
|
||||
свежие релизы. Здесь:
|
||||
|
||||
* страницы раздела берутся вразброс по всей глубине (последовательность с
|
||||
золотым сечением, уже посещённые пропускаются);
|
||||
* на странице выбираются не все темы, а те, чья «корзина» (годовая группа ×
|
||||
качество × фильм/сериал) в датасете представлена меньше всего.
|
||||
|
||||
Всё работает по заголовкам тем из списка, заходить в раздачу для выбора не нужно.
|
||||
"""
|
||||
|
||||
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
|
||||
Reference in new issue
Block a user