|
|
@@ -278,6 +278,14 @@ def evaluate_temporal_status(
|
|
|
}
|
|
|
|
|
|
if publish_at is None:
|
|
|
+ if _can_compensate_temporal_unknown(candidate, text=text):
|
|
|
+ return {
|
|
|
+ "temporal_type": inferred_type or "evergreen",
|
|
|
+ "status": "pass",
|
|
|
+ "reason_code": None,
|
|
|
+ "reason": "缺少发布时间,但无时效敏感表述且其他指标支持通过",
|
|
|
+ "evidence": {**evidence, "compensated": True},
|
|
|
+ }
|
|
|
return {
|
|
|
"temporal_type": inferred_type,
|
|
|
"status": "unknown",
|
|
|
@@ -409,6 +417,96 @@ def _number(value: Any) -> float | None:
|
|
|
return None
|
|
|
|
|
|
|
|
|
+def _semantic_score(candidate: Mapping[str, Any], field: str) -> float | None:
|
|
|
+ return _number(candidate.get(field))
|
|
|
+
|
|
|
+
|
|
|
+def _has_strong_semantic_scores(candidate: Mapping[str, Any]) -> bool:
|
|
|
+ """R/E/S 综合优秀,或 V 足够高,可用于缺失字段补偿。"""
|
|
|
+ relevance = _semantic_score(candidate, "relevance_score")
|
|
|
+ elder = _semantic_score(candidate, "elder_score")
|
|
|
+ share = _semantic_score(candidate, "share_score")
|
|
|
+ value = _semantic_score(candidate, "value_score")
|
|
|
+ if (
|
|
|
+ relevance is not None
|
|
|
+ and elder is not None
|
|
|
+ and share is not None
|
|
|
+ and relevance >= 0.70
|
|
|
+ and elder >= 0.70
|
|
|
+ and share >= 0.65
|
|
|
+ ):
|
|
|
+ return True
|
|
|
+ return value is not None and value >= 0.65
|
|
|
+
|
|
|
+
|
|
|
+def _text_has_temporal_markers(text: str) -> bool:
|
|
|
+ for _, terms, _, _ in _DAYPART_RULES:
|
|
|
+ if any(term in text for term in terms):
|
|
|
+ return True
|
|
|
+ known_names = set(_FIXED_FESTIVALS)
|
|
|
+ for yearly in _LUNAR_FESTIVALS.values():
|
|
|
+ known_names.update(yearly)
|
|
|
+ if any(name in text for name in known_names):
|
|
|
+ return True
|
|
|
+ if any(marker in text for marker in _EVENT_MARKERS + _RELATIVE_DATE_MARKERS):
|
|
|
+ return True
|
|
|
+ return False
|
|
|
+
|
|
|
+
|
|
|
+def _can_compensate_missing_portrait(
|
|
|
+ candidate: Mapping[str, Any],
|
|
|
+ account_ratio: float | None,
|
|
|
+ *,
|
|
|
+ min_account_ratio: float,
|
|
|
+) -> bool:
|
|
|
+ if account_ratio is not None and account_ratio >= max(min_account_ratio, 0.35):
|
|
|
+ return True
|
|
|
+ if account_ratio is not None and account_ratio >= min_account_ratio:
|
|
|
+ return _has_strong_semantic_scores(candidate)
|
|
|
+ elder = _semantic_score(candidate, "elder_score")
|
|
|
+ return elder is not None and elder >= 0.75 and _has_strong_semantic_scores(candidate)
|
|
|
+
|
|
|
+
|
|
|
+def _can_compensate_missing_duration(
|
|
|
+ candidate: Mapping[str, Any],
|
|
|
+ shares: float | None,
|
|
|
+ *,
|
|
|
+ min_share_count: float,
|
|
|
+) -> bool:
|
|
|
+ if shares is not None and shares >= min_share_count * 1.5:
|
|
|
+ return True
|
|
|
+ relevance = _semantic_score(candidate, "relevance_score")
|
|
|
+ return (
|
|
|
+ relevance is not None
|
|
|
+ and relevance >= 0.80
|
|
|
+ and _has_strong_semantic_scores(candidate)
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _can_compensate_missing_shares(candidate: Mapping[str, Any]) -> bool:
|
|
|
+ likes = _number(candidate.get("like_count"))
|
|
|
+ plays = _number(candidate.get("play_count"))
|
|
|
+ if likes is not None and likes >= 5000:
|
|
|
+ return True
|
|
|
+ if plays is not None and plays >= 50000:
|
|
|
+ return True
|
|
|
+ share = _semantic_score(candidate, "share_score")
|
|
|
+ return share is not None and share >= 0.75 and _has_strong_semantic_scores(candidate)
|
|
|
+
|
|
|
+
|
|
|
+def _can_compensate_temporal_unknown(
|
|
|
+ candidate: Mapping[str, Any],
|
|
|
+ *,
|
|
|
+ text: str,
|
|
|
+) -> bool:
|
|
|
+ if _text_has_temporal_markers(text):
|
|
|
+ return False
|
|
|
+ if _has_strong_semantic_scores(candidate):
|
|
|
+ return True
|
|
|
+ shares = _number(candidate.get("share_count"))
|
|
|
+ return shares is not None and shares >= 2000
|
|
|
+
|
|
|
+
|
|
|
def evaluate_candidate_gate(
|
|
|
candidate: Mapping[str, Any],
|
|
|
rule_snapshot: Mapping[str, Any] | str | None,
|
|
|
@@ -429,6 +527,11 @@ def evaluate_candidate_gate(
|
|
|
}
|
|
|
]
|
|
|
|
|
|
+ min_duration = float(rules["min_duration_seconds"])
|
|
|
+ min_shares = float(rules["min_share_count"])
|
|
|
+ min_content_ratio = float(rules["min_content_50_plus_ratio"])
|
|
|
+ min_account_ratio = float(rules["min_account_50_plus_ratio"])
|
|
|
+
|
|
|
def threshold_check(
|
|
|
name: str,
|
|
|
actual: float | None,
|
|
|
@@ -436,10 +539,17 @@ def evaluate_candidate_gate(
|
|
|
*,
|
|
|
missing_code: str,
|
|
|
low_code: str,
|
|
|
+ compensate_missing: bool = False,
|
|
|
) -> None:
|
|
|
+ compensated = False
|
|
|
if actual is None:
|
|
|
- status = "fail"
|
|
|
- reason_code = missing_code
|
|
|
+ if compensate_missing:
|
|
|
+ status = "pass"
|
|
|
+ reason_code = None
|
|
|
+ compensated = True
|
|
|
+ else:
|
|
|
+ status = "fail"
|
|
|
+ reason_code = missing_code
|
|
|
elif actual < threshold:
|
|
|
status = "fail"
|
|
|
reason_code = low_code
|
|
|
@@ -453,29 +563,41 @@ def evaluate_candidate_gate(
|
|
|
"reason_code": reason_code,
|
|
|
"actual": actual,
|
|
|
"threshold": threshold,
|
|
|
+ "compensated": compensated,
|
|
|
}
|
|
|
)
|
|
|
|
|
|
threshold_check(
|
|
|
"duration_seconds",
|
|
|
duration,
|
|
|
- float(rules["min_duration_seconds"]),
|
|
|
+ min_duration,
|
|
|
missing_code="DURATION_UNKNOWN",
|
|
|
low_code="DURATION_TOO_SHORT",
|
|
|
+ compensate_missing=_can_compensate_missing_duration(
|
|
|
+ candidate,
|
|
|
+ shares,
|
|
|
+ min_share_count=min_shares,
|
|
|
+ ),
|
|
|
)
|
|
|
threshold_check(
|
|
|
"share_count",
|
|
|
shares,
|
|
|
- float(rules["min_share_count"]),
|
|
|
+ min_shares,
|
|
|
missing_code="SHARE_COUNT_UNKNOWN",
|
|
|
low_code="SHARE_COUNT_TOO_LOW",
|
|
|
+ compensate_missing=_can_compensate_missing_shares(candidate),
|
|
|
)
|
|
|
threshold_check(
|
|
|
"content_50_plus_ratio",
|
|
|
content_ratio,
|
|
|
- float(rules["min_content_50_plus_ratio"]),
|
|
|
+ min_content_ratio,
|
|
|
missing_code="CONTENT_PORTRAIT_MISSING",
|
|
|
low_code="CONTENT_50_PLUS_TOO_LOW",
|
|
|
+ compensate_missing=_can_compensate_missing_portrait(
|
|
|
+ candidate,
|
|
|
+ account_ratio,
|
|
|
+ min_account_ratio=min_account_ratio,
|
|
|
+ ),
|
|
|
)
|
|
|
|
|
|
failed_codes = [
|