|
|
@@ -45,7 +45,7 @@ ACTION_LABELS = {
|
|
|
"REJECT_CONTENT": "淘汰",
|
|
|
}
|
|
|
MEDIA_STATUS_LABELS = {
|
|
|
- "oss_uploaded": "已保存到 OSS",
|
|
|
+ "oss_uploaded": "已保存",
|
|
|
"oss_upload_pending": "待补传",
|
|
|
"metadata_only": "尚未拿到视频",
|
|
|
"unavailable": "无可用视频",
|
|
|
@@ -68,8 +68,11 @@ REASON_LABELS = {
|
|
|
"missing_score": "没有足够信息进入打分",
|
|
|
"v4_query_and_platform_pass": "搜索词和平台表现都符合要求",
|
|
|
"v4_query_or_score_below_threshold": "相关性或平台表现未达到要求",
|
|
|
+ "v4_score_review_needed": "分数处在复看区间,需要人工确认",
|
|
|
"v4_technical_retry_needed": "系统需要重试后再判断",
|
|
|
"v4_allow_walk_denied": "不适合继续向外扩展",
|
|
|
+ "budget_exhausted": "本轮扩展名额已用完",
|
|
|
+ "query>=70/platform>=65/score>=70": "评分达到继续扩展门槛",
|
|
|
"no_decision": "暂未产生判断结果",
|
|
|
}
|
|
|
|
|
|
@@ -140,7 +143,7 @@ class FlowLedgerService:
|
|
|
query = self._query_by_id(bundle, query_id)
|
|
|
related_ids = self._query_related_content_ids(query_id, bundle)
|
|
|
actions = [
|
|
|
- self._walk_action(action, query, debug=debug)
|
|
|
+ self._walk_action(action, query, bundle, debug=debug)
|
|
|
for action in bundle["walk_actions"]
|
|
|
if _walk_action_related(action, query_id, related_ids)
|
|
|
]
|
|
|
@@ -254,8 +257,13 @@ class FlowLedgerService:
|
|
|
|
|
|
def _ledger_row(self, query: dict[str, Any], bundle: dict[str, Any], *, debug: bool) -> dict[str, Any]:
|
|
|
query_id = _text(query.get("search_query_id"))
|
|
|
- videos = [self._video(content, bundle, debug=debug) for content in self._query_content(query_id, bundle, first_round_only=True)]
|
|
|
- decisions = [video.get("decision") for video in videos if video.get("decision")]
|
|
|
+ contents = self._query_content(query_id, bundle, first_round_only=True)
|
|
|
+ videos = [self._video(content, bundle, debug=debug) for content in contents]
|
|
|
+ decisions = [
|
|
|
+ decision
|
|
|
+ for decision in (_decision_for_content(content, bundle["decisions"]) for content in contents)
|
|
|
+ if decision
|
|
|
+ ]
|
|
|
related_ids = self._query_related_content_ids(query_id, bundle)
|
|
|
actions = [action for action in bundle["walk_actions"] if _walk_action_related(action, query_id, related_ids)]
|
|
|
source = self._source_summary(query, bundle, debug=debug)
|
|
|
@@ -387,6 +395,7 @@ class FlowLedgerService:
|
|
|
"reason_label": REASON_LABELS.get(reason, "系统原因待补充"),
|
|
|
"score": score,
|
|
|
"score_label": f"{round(float(score))} 分" if isinstance(score, (int, float)) else "",
|
|
|
+ "score_items": _decision_score_items(scorecard, score),
|
|
|
"allow_walk": bool(_record(decision.get("decision_replay_data")).get("allow_walk")),
|
|
|
"v4_explanation": _record(decision.get("v4_explanation") or _record(decision.get("raw_payload")).get("v4_explanation")),
|
|
|
"debug": {"decision": decision} if debug else None,
|
|
|
@@ -412,6 +421,7 @@ class FlowLedgerService:
|
|
|
"reject_count": actions["REJECT_CONTENT"],
|
|
|
"primary_reason": primary_reason,
|
|
|
"primary_reason_label": REASON_LABELS.get(primary_reason, "系统原因待补充"),
|
|
|
+ "score_summary": _score_summary(decisions),
|
|
|
"headline": _rule_headline(actions, len(decisions)),
|
|
|
"debug": {"decisions": decisions} if debug else None,
|
|
|
}
|
|
|
@@ -470,21 +480,29 @@ class FlowLedgerService:
|
|
|
"unavailable_count": media["unavailable"] + media["metadata_only"],
|
|
|
}
|
|
|
|
|
|
- def _walk_action(self, action: dict[str, Any], query: dict[str, Any] | None, *, debug: bool) -> dict[str, Any]:
|
|
|
+ def _walk_action(self, action: dict[str, Any], query: dict[str, Any] | None, bundle: dict[str, Any], *, debug: bool) -> dict[str, Any]:
|
|
|
edge = _walk_lane_id(action)
|
|
|
reason = _text(action.get("reason_code"))
|
|
|
status = _text(action.get("walk_status"), "unknown")
|
|
|
+ gate_reason = _text(action.get("walk_gate_reason_code") or action.get("allow_walk_reason") or reason)
|
|
|
+ trigger_label = _walk_trigger_label(action)
|
|
|
+ from_label = _walk_from_label(action, query, bundle)
|
|
|
+ score_items = _walk_score_items(action)
|
|
|
return {
|
|
|
"id": _text(action.get("walk_action_id")),
|
|
|
"edge_id": edge,
|
|
|
"edge_label": WALK_EDGE_LABELS.get(edge, "系统原因待补充"),
|
|
|
+ "depth": _number(action.get("depth")),
|
|
|
+ "trigger_label": trigger_label,
|
|
|
"status": status,
|
|
|
"status_label": _walk_status_label(status),
|
|
|
- "from_label": _walk_from_label(action, query),
|
|
|
+ "from_label": from_label,
|
|
|
"result": "已带入后续流程" if status == "success" else "没有继续执行",
|
|
|
"reason": reason,
|
|
|
- "reason_label": REASON_LABELS.get(reason, "这一步没有记录额外原因") if reason else "这一步没有记录额外原因",
|
|
|
- "impact": _walk_impact(status, reason),
|
|
|
+ "reason_label": _walk_reason_label(action, reason),
|
|
|
+ "impact": _walk_impact(status, reason, gate_reason),
|
|
|
+ "score_items": score_items,
|
|
|
+ "detail_lines": _walk_detail_lines(action, edge, from_label, trigger_label, score_items),
|
|
|
"debug": {"action": action} if debug else None,
|
|
|
}
|
|
|
|
|
|
@@ -658,20 +676,26 @@ def _walk_status_label(status: str) -> str:
|
|
|
}.get(status, "状态待确认")
|
|
|
|
|
|
|
|
|
-def _walk_from_label(action: dict[str, Any], query: dict[str, Any] | None) -> str:
|
|
|
+def _walk_from_label(action: dict[str, Any], query: dict[str, Any] | None, bundle: dict[str, Any]) -> str:
|
|
|
from_type = _text(action.get("from_node_type"))
|
|
|
+ from_node_id = _text(action.get("from_node_id"))
|
|
|
if "query" in from_type:
|
|
|
- return f"从搜索词“{_text((query or {}).get('search_query'), '搜索词待确认')}”出发"
|
|
|
+ source_query = next((item for item in bundle["queries"] if _text(item.get("search_query_id")) == from_node_id), None) or query
|
|
|
+ return f"从搜索词“{_text((source_query or {}).get('search_query'), '搜索词待确认')}”触发"
|
|
|
if "content" in from_type or "video" in from_type:
|
|
|
- return "从视频内容出发"
|
|
|
+ content = next((item for item in bundle["content_items"] if from_node_id in _content_ids(item)), None)
|
|
|
+ title = _text((content or {}).get("description") or (content or {}).get("title"))
|
|
|
+ return f"从视频“{_short_text(title, 34)}”触发" if title else "从视频内容触发"
|
|
|
if "tag" in from_type:
|
|
|
- return "从视频标签出发"
|
|
|
+ return "从视频标签触发"
|
|
|
return "从上一环节出发"
|
|
|
|
|
|
|
|
|
-def _walk_impact(status: str, reason: str) -> str:
|
|
|
- if reason == "blocked_by_rule_decision":
|
|
|
+def _walk_impact(status: str, reason: str, gate_reason: str) -> str:
|
|
|
+ if reason == "blocked_by_rule_decision" or gate_reason == "v4_allow_walk_denied":
|
|
|
return "这条内容不会继续带出更多视频。"
|
|
|
+ if reason == "budget_exhausted":
|
|
|
+ return "本轮扩展名额已用完,这一步不会继续新增搜索词。"
|
|
|
if status == "success":
|
|
|
return "这一步已经执行,结果会进入后续判断。"
|
|
|
if status == "skipped":
|
|
|
@@ -679,6 +703,158 @@ def _walk_impact(status: str, reason: str) -> str:
|
|
|
return "这一步暂不影响最终沉淀。"
|
|
|
|
|
|
|
|
|
+def _walk_trigger_label(action: dict[str, Any]) -> str:
|
|
|
+ raw = _record(action.get("raw_payload"))
|
|
|
+ hashtag = _text(action.get("hashtag") or raw.get("hashtag"))
|
|
|
+ if hashtag:
|
|
|
+ return f"标签:#{hashtag.lstrip('#')}"
|
|
|
+ author = _text(action.get("author_id") or raw.get("author_id") or action.get("platform_author_id") or raw.get("platform_author_id"))
|
|
|
+ if author:
|
|
|
+ return f"作者:{author}"
|
|
|
+ cursor = _text(action.get("page_cursor") or raw.get("page_cursor") or action.get("cursor") or raw.get("cursor"))
|
|
|
+ if cursor:
|
|
|
+ return f"翻页游标:{cursor}"
|
|
|
+ to_node = _text(action.get("to_node_id"))
|
|
|
+ if to_node and to_node not in {"tag_query_skipped", "author_works_skipped"}:
|
|
|
+ return f"产生结果:{to_node}"
|
|
|
+ return "触发对象待确认"
|
|
|
+
|
|
|
+
|
|
|
+def _walk_reason_label(action: dict[str, Any], reason: str) -> str:
|
|
|
+ gate_reason = _text(action.get("walk_gate_reason_code") or action.get("allow_walk_reason"))
|
|
|
+ if gate_reason == "v4_allow_walk_denied":
|
|
|
+ allow_reason = _text(action.get("allow_walk_reason"))
|
|
|
+ if allow_reason and allow_reason != gate_reason:
|
|
|
+ return f"不适合继续向外扩展:{REASON_LABELS.get(allow_reason, '评分未达到继续扩展门槛')}"
|
|
|
+ return "不适合继续向外扩展"
|
|
|
+ if reason:
|
|
|
+ return REASON_LABELS.get(reason, "这一步没有记录额外原因")
|
|
|
+ return "这一步没有记录额外原因"
|
|
|
+
|
|
|
+
|
|
|
+def _walk_detail_lines(
|
|
|
+ action: dict[str, Any],
|
|
|
+ edge: str,
|
|
|
+ from_label: str,
|
|
|
+ trigger_label: str,
|
|
|
+ score_items: list[dict[str, Any]],
|
|
|
+) -> list[str]:
|
|
|
+ lines = [
|
|
|
+ f"动作:{WALK_EDGE_LABELS.get(edge, '扩展动作')}",
|
|
|
+ f"上一环节:{from_label}",
|
|
|
+ ]
|
|
|
+ if trigger_label != "触发对象待确认":
|
|
|
+ lines.append(f"触发对象:{trigger_label}")
|
|
|
+ if score_items:
|
|
|
+ lines.append("扩展门槛:" + " / ".join(f"{item['label']} {item['score_label']}" for item in score_items[:3]))
|
|
|
+ reason = _walk_reason_label(action, _text(action.get("reason_code")))
|
|
|
+ if reason != "这一步没有记录额外原因":
|
|
|
+ lines.append(f"判断:{reason}")
|
|
|
+ return lines
|
|
|
+
|
|
|
+
|
|
|
+def _walk_score_items(action: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
+ snapshot = _record(action.get("walk_gate_snapshot") or _record(action.get("raw_payload")).get("walk_gate_snapshot"))
|
|
|
+ return _score_items_from_values(
|
|
|
+ total=snapshot.get("score"),
|
|
|
+ query=snapshot.get("query_relevance_score"),
|
|
|
+ platform=snapshot.get("platform_performance_score"),
|
|
|
+ components=[],
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _decision_score_items(scorecard: dict[str, Any], score: Any) -> list[dict[str, Any]]:
|
|
|
+ return _score_items_from_values(
|
|
|
+ total=score if score is not None else scorecard.get("total_score"),
|
|
|
+ query=scorecard.get("query_relevance_score"),
|
|
|
+ platform=scorecard.get("platform_performance_score"),
|
|
|
+ components=_list(scorecard.get("platform_performance_components")),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _score_summary(decisions: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
+ scorecards = [_record(item.get("scorecard")) for item in decisions]
|
|
|
+ return {
|
|
|
+ "items": [
|
|
|
+ _avg_score_item("综合分", [item.get("score") if item.get("score") is not None else _record(item.get("scorecard")).get("total_score") for item in decisions]),
|
|
|
+ _avg_score_item("需求相关", [item.get("query_relevance_score") for item in scorecards]),
|
|
|
+ _avg_score_item("平台表现", [item.get("platform_performance_score") for item in scorecards]),
|
|
|
+ ],
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _score_items_from_values(total: Any, query: Any, platform: Any, components: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
+ items = [
|
|
|
+ _score_item("综合分", total, "最终用于入池 / 复看 / 淘汰的总分"),
|
|
|
+ _score_item("需求相关", query, "视频内容和本次搜索词、需求的匹配程度"),
|
|
|
+ _score_item("平台表现", platform, "点赞、评论、转发、收藏等互动表现"),
|
|
|
+ ]
|
|
|
+ for component in components:
|
|
|
+ items.append(_score_item(
|
|
|
+ _platform_component_label(_text(component.get("field"))),
|
|
|
+ component.get("normalized_score"),
|
|
|
+ f"原始值 {_text(component.get('value'), '0')},权重 {_percent_text(component.get('weight'))}",
|
|
|
+ ))
|
|
|
+ return [item for item in items if item]
|
|
|
+
|
|
|
+
|
|
|
+def _score_item(label: str, value: Any, detail: str) -> dict[str, Any]:
|
|
|
+ score = _float_or_none(value)
|
|
|
+ if score is None:
|
|
|
+ return {}
|
|
|
+ return {
|
|
|
+ "label": label,
|
|
|
+ "score": round(score, 2),
|
|
|
+ "score_label": f"{round(score, 1):g} 分",
|
|
|
+ "detail": detail,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _avg_score_item(label: str, values: list[Any]) -> dict[str, Any]:
|
|
|
+ nums = [value for value in (_float_or_none(item) for item in values) if value is not None]
|
|
|
+ if not nums:
|
|
|
+ return {"label": label, "score": None, "score_label": "暂无评分"}
|
|
|
+ avg = sum(nums) / len(nums)
|
|
|
+ return {
|
|
|
+ "label": label,
|
|
|
+ "score": round(avg, 2),
|
|
|
+ "score_label": f"平均 {round(avg, 1):g} 分",
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _platform_component_label(field: str) -> str:
|
|
|
+ return {
|
|
|
+ "statistics.digg_count": "点赞表现",
|
|
|
+ "statistics.like_count": "点赞表现",
|
|
|
+ "statistics.comment_count": "评论表现",
|
|
|
+ "statistics.share_count": "转发表现",
|
|
|
+ "statistics.collect_count": "收藏表现",
|
|
|
+ "statistics.play_count": "播放表现",
|
|
|
+ }.get(field, field or "互动表现")
|
|
|
+
|
|
|
+
|
|
|
+def _percent_text(value: Any) -> str:
|
|
|
+ number = _float_or_none(value)
|
|
|
+ if number is None:
|
|
|
+ return "待确认"
|
|
|
+ return f"{round(number * 100):g}%"
|
|
|
+
|
|
|
+
|
|
|
+def _float_or_none(value: Any) -> float | None:
|
|
|
+ try:
|
|
|
+ if value is None or value == "":
|
|
|
+ return None
|
|
|
+ return float(value)
|
|
|
+ except (TypeError, ValueError):
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _short_text(value: str, limit: int) -> str:
|
|
|
+ if len(value) <= limit:
|
|
|
+ return value
|
|
|
+ return value[: limit - 1] + "…"
|
|
|
+
|
|
|
+
|
|
|
def _platform_label(platform: str) -> str:
|
|
|
return {
|
|
|
"douyin": "抖音",
|