feat(classify): zero title-only titles, penalize <10min, add discover.candidate_min_score

标题仅含 require_title 不再计分(必须咻咻满+任一正向词);时长 <10 分钟 -1(时长未知不扣);新增 discover.candidate_min_score(默认=notify.min_score)控制是否进候选池,只过标题关的不再入池。库内 796 条:仅过标题关的 8 条从 1/3 分降为 0/-1,其余不变。
This commit is contained in:
kairee committed 2026-11-01 20:18:43 +08:00
1 parent aa9aa7c589
commit c6790f0ba1
10 files changed
+138 -69

No files matched your search

+22 -12
View File
@@ -74,10 +74,13 @@ _DATE_PATTERNS = (
)
_DATE_RE = re.compile("|".join(_DATE_PATTERNS))
# 负向权重(见 project.md §3.3):每个负向词 -2,命中多条额外叠加;日期单独 -4。
# 负向权重(见 project.md §3.3):每个负向词 -2,命中多条额外叠加;日期单独 -4;
# 时长 <10 分钟视作切片,-1(时长未知=0 时不扣)。
NEGATIVE_PER_WORD = 2
DATE_SCORE = 4
SELF_SCORE = 3
SHORT_DURATION_SECONDS = 600
SHORT_DURATION_SCORE = 1
def date_hits(title: str) -> list[str]:
@@ -94,16 +97,18 @@ def score_item(
) -> int:
"""按 project.md §3.3 计算候选分数。
正向按**命中次数**超线性加权:每命中一个正向词(``require_title`` / ``reaction``
/ ``charge`` / ``weak_positive``)记 1 次,正向得分 = 命中次数²(1→1、2→4、
3→9……),使多信号同现的真 reaction 不会被单个负向词打穿(1+1>2、1+1+1>5)。
正向按**命中次数**超线性加权:正向词(``reaction`` / ``charge`` / ``weak_positive``)
每命中一个记 1 次;仅在已有正向命中时,``require_title`` 才额外记 1 次。得分 =
命中次数²(1→1、2→4、3→9……)。也就是说:**只含 ``require_title`` 的标题 0 分**
("咻咻满"单独出现和 "reaction" 单独出现一样没有正向意义),必须
"require_title + 任一正向词" 才得分;多信号同现时不会被单个负向词打穿。
``reaction`` 词表含 ``re`` 这类通用子串(用于兼容 reaction/reactio/reation 等
拼写),因此必须与 ``require_title`` 或 ``充电`` 同现才计一次命中。标题不含
``require_title`` 直接 0 分;时长只在此基础上已有命中时加权。
``require_title`` 直接 0 分;时长只在已有命中时加权。
负向按命中词数递减:每个词 -2,命中 2 个额外 -2、3 个额外 -6(k*(k-1)),
带日期再 -4。
带日期再 -4;时长 <10 分钟(切片/片段)再 -1。
"""
require_title = str(classify.get("require_title") or "")
if require_title and not passes_required_title(title, require_title):
@@ -113,12 +118,13 @@ def score_item(
reaction_gate = passes_required_title(title, require_title) or contains_any(title, ["充电"])
reaction_hit = 1 if (matched_words(title, classify.get("reaction")) and reaction_gate) else 0
hits = 0
if require_title:
hits += 1
hits += reaction_hit
hits += len(matched_words(title, classify.get("charge")))
hits += len(matched_words(title, classify.get("weak_positive")))
positive_hits = (
reaction_hit
+ len(matched_words(title, classify.get("charge")))
+ len(matched_words(title, classify.get("weak_positive")))
)
# 只过标题关(标题仅含 require_title)不给分;require_title 仅在已有正向命中时才算 1 次。
hits = positive_hits + (1 if (require_title and positive_hits) else 0)
score = hits * hits
@@ -131,6 +137,10 @@ def score_item(
elif duration_seconds > 30 * 60:
score += gt_30
# 时长 <10 分钟视作切片/片段,扣 1 分(时长未知 0 不扣)。
if 0 < duration_seconds < SHORT_DURATION_SECONDS:
score -= SHORT_DURATION_SCORE
neg_words = matched_words(title, classify.get("negative"))
k = len(neg_words)
if k:
+6
View File
@@ -190,6 +190,7 @@ class DiscoverConfig:
keywords: tuple[str, ...]
pages: int
window_days: int
candidate_min_score: int
@dataclass(frozen=True)
@@ -485,6 +486,11 @@ def _build_config(data: Mapping[str, Any], path: Path) -> AppConfig:
window_days=_as_int(
discover_raw.get("window_days", 14), "discover.window_days", minimum=1
),
candidate_min_score=_as_int(
discover_raw.get("candidate_min_score", notify.min_score),
"discover.candidate_min_score",
minimum=0,
),
)
classify_raw = data["classify"]
+2 -1
View File
@@ -157,7 +157,8 @@ class DiscoveryService:
classify=cfg.classify,
)
summary.scored += 1
if score < 0:
# 低于候选线直接丢弃:只过标题关(无正向命中)的不再进候选池。
if score < cfg.discover.candidate_min_score:
return
candidate = self.repo.upsert_candidate(