video_risk.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  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. return {
  31. "landing_risk_passed": self.passed,
  32. "landing_risk_level": self.max_level,
  33. "landing_risk_tag_ids": ",".join(self.risk_tag_ids),
  34. "landing_unknown_tag_ids": ",".join(self.unknown_tag_ids),
  35. "landing_risk_reason": self.reason,
  36. "_landing_risk": asdict(self),
  37. }
  38. def _normalize_tag_ids(data: Any) -> list[str]:
  39. if data is None:
  40. return []
  41. if isinstance(data, list):
  42. return [str(x) for x in data if x is not None]
  43. return [str(data)]
  44. def query_video_risk_tags(video_id: int) -> list[str]:
  45. """查询视频风险 tag ids。
  46. Raises:
  47. RuntimeError: HTTP / 接口 code 非 0 / data 非预期时抛出。
  48. """
  49. resp = httpx.post(
  50. VIDEO_RISK_API_URL,
  51. json={"videoId": int(video_id)},
  52. headers={"Content-Type": "application/json"},
  53. timeout=VIDEO_RISK_API_TIMEOUT_SECONDS,
  54. )
  55. resp.raise_for_status()
  56. payload = resp.json()
  57. if payload.get("code") != 0:
  58. raise RuntimeError(
  59. f"video risk api failed video_id={video_id} "
  60. f"code={payload.get('code')} msg={payload.get('msg')}"
  61. )
  62. return _normalize_tag_ids(payload.get("data"))
  63. def evaluate_video_risk(video_id: int, tag_ids: list[str]) -> VideoRiskResult:
  64. """把 tag ids 映射成最高风险等级并判断是否可用。"""
  65. risk_tag_ids: list[str] = []
  66. unknown_tag_ids: list[str] = []
  67. levels: list[int] = []
  68. for tag_id in tag_ids:
  69. level = VIDEO_RISK_TAG_LEVELS.get(str(tag_id))
  70. if level is None:
  71. unknown_tag_ids.append(str(tag_id))
  72. continue
  73. risk_tag_ids.append(str(tag_id))
  74. levels.append(int(level))
  75. max_level = max(levels) if levels else 0
  76. passed = max_level <= VIDEO_RISK_MAX_ALLOWED_LEVEL
  77. if not VIDEO_RISK_CHECK_ENABLED:
  78. passed = True
  79. reason = "risk check disabled"
  80. elif max_level == 0:
  81. reason = "no mapped risk tags"
  82. elif passed:
  83. reason = f"risk level {max_level} <= allowed {VIDEO_RISK_MAX_ALLOWED_LEVEL}"
  84. else:
  85. reason = f"risk level {max_level} > allowed {VIDEO_RISK_MAX_ALLOWED_LEVEL}"
  86. return VideoRiskResult(
  87. video_id=int(video_id),
  88. passed=passed,
  89. max_level=max_level,
  90. risk_tag_ids=risk_tag_ids,
  91. unknown_tag_ids=unknown_tag_ids,
  92. reason=reason,
  93. raw_tag_ids=[str(x) for x in tag_ids],
  94. )
  95. def check_video_risk(video_id: int) -> VideoRiskResult:
  96. """查询并评估视频风险。
  97. 接口异常时返回 passed=False,让调用方跳过该 landing,避免在风险未知时创建 plan。
  98. """
  99. if not VIDEO_RISK_CHECK_ENABLED:
  100. return evaluate_video_risk(int(video_id), [])
  101. try:
  102. tag_ids = query_video_risk_tags(int(video_id))
  103. result = evaluate_video_risk(int(video_id), tag_ids)
  104. logger.info(
  105. "[video_risk] video_id=%s passed=%s level=%s risk_tags=%s unknown_tags=%s reason=%s",
  106. video_id,
  107. result.passed,
  108. result.max_level,
  109. result.risk_tag_ids,
  110. result.unknown_tag_ids,
  111. result.reason,
  112. )
  113. return result
  114. except Exception as e:
  115. logger.warning("[video_risk] video_id=%s 查询失败,跳过该视频:%s", video_id, e)
  116. return VideoRiskResult(
  117. video_id=int(video_id),
  118. passed=False,
  119. max_level=999,
  120. risk_tag_ids=[],
  121. unknown_tag_ids=[],
  122. reason=f"risk api error: {e}",
  123. raw_tag_ids=[],
  124. )