feat: типизация записей (14 типов, до 3 на запись), чипы и бейджи типов в панели, CSV с типами, /api/types, doctor --only по категориям, волны источников 5-6 (635 всего)

This commit is contained in:
Vlad Gothbreach
2026-10-26 19:01:45 +03:00
parent 4196d66b4e
commit 7b3d708402
10 changed files with 652 additions and 24 deletions

92
omni/taxonomy.py Normal file
View File

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
"""Типизация записей: не «категория источника», а тип самого поста.
Категория отвечает «откуда», тип — «что именно»: слив, триал, софт, промо,
перепродажа, CVE, релиз, гайд, датасет, модель, вакансия, цена, вопрос, новость.
Одна запись может иметь несколько типов, порядок — по весу совпадений.
"""
import re
# (key, label, weight, [regex, ...])
RULES = [
("leak", "слив", 10, [
r"\bслив\w*", r"утечк\w+", r"\bleak(ed|s)?\b", r"\bdump(ed)?\b", r"база\s+(данных|слит)",
r"\bcombo(list)?\b", r"stealer\s*log", r"\bdb\s+dump\b", r"пасс\w*\s+от\s+",
]),
("trial", "триал", 9, [
r"\btrial\b", r"триал\w*", r"пробн\w+\s+период", r"free\s+(credits?|tier|plan)",
r"бесплатн\w+\s+(кредит|токен|доступ|период|тариф)", r"\bfreebies?\b", r"щедр\w+\s+лимит",
r"\$\d+\s*(free|бесплат)", r"no\s+credit\s+card",
]),
("tool", "софт", 8, [
r"\bсофт\w*", r"\btool(s|kit)?\b", r"скрипт\w*", r"\bparser\b", r"\bчекер\b", r"\bбот\w*",
r"\bcrawler\b", r"\bscraper\b", r"\bautomation\b", r"автоматизац\w+", r"\bcli\b",
r"\bsdk\b", r"open[\s-]?source\s+(tool|project)",
]),
("promo", "промо", 7, [
r"промокод\w*", r"\bpromo\w*", r"\bcoupon\b", r"скидк\w+", r"\bdiscount\b", r"giveaway",
r"раздач\w+", r"\bairdrop\b", r"бонус\w*\s+(код|за\s+регистрац)", r"-?\d{1,2}%\s*off",
]),
("resale", "перепродажа", 7, [
r"\bпродам\b", r"\bпродаж\w+", r"\bresell\w*", r"\bв\s+аренду\b", r"\bкуплю\b",
r"\bsell(ing)?\s+(account|key|access)", r"перепрод\w+", r"\bоптом\b", r"\bв\s+наличии\b.*\$",
]),
("cve", "уязвимость", 10, [
r"\bCVE-\d{4}-\d{3,7}\b", r"\bvulnerab\w+", r"уязвим\w+", r"\bRCE\b", r"\bXSS\b",
r"\bSQLi?\b", r"\bexploit\w*", r"\b0day\b", r"\bpatch\w*\s+tuesday", r"security\s+advisory",
]),
("release", "релиз", 6, [
r"\brelease[sd]?\b", r"\bрелиз\w*", r"\bchangelog\b", r"\bv\d+\.\d+(\.\d+)?\b",
r"вышл\w+\s+версия", r"\bnow\s+available\b", r"\bGA\b", r"\bpreview\s+build\b",
]),
("guide", "гайд", 5, [
r"\bгайд\w*", r"\btutorial\b", r"\bhow[\s-]to\b", r"инструкц\w+", r"\bруководств\w+",
r"\bкак\s+(сделать|настроить|поднять|получить)", r"\bwalkthrough\b", r"\bcheat\s*sheet\b",
]),
("dataset", "датасет", 6, [
r"\bdataset\w*", r"датасет\w*", r"\bcorpus\b", r"\bbenchmark\s+suite\b", r"\bannotated\b",
]),
("model", "модель", 5, [
r"\bweights\b", r"\bcheckpoint\w*", r"\.gguf\b", r"\.safetensors\b", r"\bllm\b",
r"\bмодел\w+\s+(вышла|обучен| weights)", r"\bquantiz\w+", r"\bfine-?tun\w+",
]),
("job", "вакансия", 6, [
r"\bваканси\w+", r"\bhiring\b", r"\bjob\s+(offer|post)", r"\bremote\b.*\$",
r"\bsalary\b", r"\$\d+\s*/\s*(hr|hour)", r"\bищу\s+работу\b", r"\bfor\s+hire\b",
]),
("price", "цена", 4, [
r"\bцен[аы]\b", r"\bpricing\b", r"\bтариф\w*", r"\$\d", r"\d+\s*₽", r"\d+\s*€",
r"\bper\s+(month|year|1m)\b", r"/\s*1m\s+tokens",
]),
("question", "вопрос", 3, [
r"\?\s*$", r"^\s*(как|почему|что|где|кто|можно\s+ли)\b", r"\bhelp\b", r"\bподскажите\b",
]),
]
COMPILED = [(k, l, w, [re.compile(p, re.I | re.U) for p in pats]) for k, l, w, pats in RULES]
LABELS = {k: l for k, l, _, _ in RULES}
LABELS["news"] = "новость"
KEYS = [k for k, _, _, _ in RULES]
def classify(title, text, limit=3):
"""Список типов записи по убыванию веса. Если ничего не сработало — ['news']."""
t = (title or "") + "\n" + (text or "")[:2000]
if not t.strip():
return ["news"]
hits = []
for key, label, w, pats in COMPILED:
n = 0
for p in pats:
if p.search(t):
n += 1
if n:
hits.append((w + n, key))
if not hits:
return ["news"]
hits.sort(reverse=True)
return [k for _, k in hits[:limit]]
def labels(keys):
return [LABELS.get(k, k) for k in keys or []]