video_risk.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """承接视频风险标签查询与审核。
  2. 在模块 B 创建 landing plan(xcx/save)之前调用:
  3. video_id → piaoquantv getVideoTagIds → 风险等级 → pass/block
  4. 接口返回的 data 是 tag id 列表。只有 config.VIDEO_RISK_TAG_LEVELS 中的 tag
  5. 参与风险等级计算,其他 tag 保留到 unknown_tag_ids 便于排查,但不默认阻断。
  6. """
  7. from __future__ import annotations
  8. import logging
  9. from dataclasses import dataclass, asdict
  10. from typing import Any
  11. import httpx
  12. from config import (
  13. VIDEO_RISK_API_TIMEOUT_SECONDS,
  14. VIDEO_RISK_API_URL,
  15. VIDEO_RISK_CHECK_ENABLED,
  16. VIDEO_RISK_MAX_ALLOWED_LEVEL,
  17. VIDEO_RISK_TAG_LEVELS,
  18. )
  19. logger = logging.getLogger(__name__)
  20. @dataclass
  21. class VideoRiskResult:
  22. video_id: int
  23. passed: bool
  24. max_level: int
  25. risk_tag_ids: list[str]
  26. unknown_tag_ids: list[str]
  27. reason: str
  28. raw_tag_ids: list[str]
  29. def to_record_fields(self) -> dict[str, Any]:
  30. """转成可合并进候选记录的落表字段 dict。"""
  31. return {
  32. "landing_risk_passed": self.passed,
  33. "landing_risk_level": self.max_level,
  34. "landing_risk_tag_ids": ",".join(self.risk_tag_ids),
  35. "landing_unknown_tag_ids": ",".join(self.unknown_tag_ids),
  36. "landing_risk_reason": self.reason,
  37. "_landing_risk": asdict(self),
  38. }
  39. def _normalize_tag_ids(data: Any) -> list[str]:
  40. """把接口返回的 data 规范化成字符串 tag id 列表。"""
  41. if data is None:
  42. return []
  43. if isinstance(data, list):
  44. return [str(x) for x in data if x is not None]
  45. return [str(data)]
  46. def query_video_risk_tags(video_id: int) -> list[str]:
  47. """查询视频风险 tag ids。
  48. Raises:
  49. RuntimeError: HTTP / 接口 code 非 0 / data 非预期时抛出。
  50. """
  51. resp = httpx.post(
  52. VIDEO_RISK_API_URL,
  53. json={"videoId": int(video_id)},
  54. headers={"Content-Type": "application/json"},
  55. timeout=VIDEO_RISK_API_TIMEOUT_SECONDS,
  56. )
  57. resp.raise_for_status()
  58. payload = resp.json()
  59. if payload.get("code") != 0:
  60. raise RuntimeError(
  61. f"video risk api failed video_id={video_id} "
  62. f"code={payload.get('code')} msg={payload.get('msg')}"
  63. )
  64. return _normalize_tag_ids(payload.get("data"))
  65. def evaluate_video_risk(video_id: int, tag_ids: list[str]) -> VideoRiskResult:
  66. """把 tag ids 映射成最高风险等级并判断是否可用。"""
  67. risk_tag_ids: list[str] = []
  68. unknown_tag_ids: list[str] = []
  69. levels: list[int] = []
  70. for tag_id in tag_ids:
  71. level = VIDEO_RISK_TAG_LEVELS.get(str(tag_id))
  72. if level is None:
  73. unknown_tag_ids.append(str(tag_id))
  74. continue
  75. risk_tag_ids.append(str(tag_id))
  76. levels.append(int(level))
  77. max_level = max(levels) if levels else 0
  78. passed = max_level <= VIDEO_RISK_MAX_ALLOWED_LEVEL
  79. if not VIDEO_RISK_CHECK_ENABLED:
  80. passed = True
  81. reason = "risk check disabled"
  82. elif max_level == 0:
  83. reason = "no mapped risk tags"
  84. elif passed:
  85. reason = f"risk level {max_level} <= allowed {VIDEO_RISK_MAX_ALLOWED_LEVEL}"
  86. else:
  87. reason = f"risk level {max_level} > allowed {VIDEO_RISK_MAX_ALLOWED_LEVEL}"
  88. return VideoRiskResult(
  89. video_id=int(video_id),
  90. passed=passed,
  91. max_level=max_level,
  92. risk_tag_ids=risk_tag_ids,
  93. unknown_tag_ids=unknown_tag_ids,
  94. reason=reason,
  95. raw_tag_ids=[str(x) for x in tag_ids],
  96. )
  97. def check_video_risk(video_id: int) -> VideoRiskResult:
  98. """查询并评估视频风险。
  99. 接口异常时返回 passed=False,让调用方跳过该 landing,避免在风险未知时创建 plan。
  100. """
  101. if not VIDEO_RISK_CHECK_ENABLED:
  102. return evaluate_video_risk(int(video_id), [])
  103. try:
  104. tag_ids = query_video_risk_tags(int(video_id))
  105. result = evaluate_video_risk(int(video_id), tag_ids)
  106. logger.info(
  107. "[video_risk] video_id=%s passed=%s level=%s risk_tags=%s unknown_tags=%s reason=%s",
  108. video_id,
  109. result.passed,
  110. result.max_level,
  111. result.risk_tag_ids,
  112. result.unknown_tag_ids,
  113. result.reason,
  114. )
  115. return result
  116. except Exception as e:
  117. logger.warning("[video_risk] video_id=%s 查询失败,跳过该视频:%s", video_id, e)
  118. return VideoRiskResult(
  119. video_id=int(video_id),
  120. passed=False,
  121. max_level=999,
  122. risk_tag_ids=[],
  123. unknown_tag_ids=[],
  124. reason=f"risk api error: {e}",
  125. raw_tag_ids=[],
  126. )