| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- """承接视频风险标签查询与审核。
- 在模块 B 创建 landing plan(xcx/save)之前调用:
- video_id → piaoquantv getVideoTagIds → 风险等级 → pass/block
- 接口返回的 data 是 tag id 列表。只有 config.VIDEO_RISK_TAG_LEVELS 中的 tag
- 参与风险等级计算,其他 tag 保留到 unknown_tag_ids 便于排查,但不默认阻断。
- """
- from __future__ import annotations
- import logging
- from dataclasses import dataclass, asdict
- from typing import Any
- import httpx
- from config import (
- VIDEO_RISK_API_TIMEOUT_SECONDS,
- VIDEO_RISK_API_URL,
- VIDEO_RISK_CHECK_ENABLED,
- VIDEO_RISK_MAX_ALLOWED_LEVEL,
- VIDEO_RISK_TAG_LEVELS,
- )
- logger = logging.getLogger(__name__)
- @dataclass
- class VideoRiskResult:
- video_id: int
- passed: bool
- max_level: int
- risk_tag_ids: list[str]
- unknown_tag_ids: list[str]
- reason: str
- raw_tag_ids: list[str]
- def to_record_fields(self) -> dict[str, Any]:
- return {
- "landing_risk_passed": self.passed,
- "landing_risk_level": self.max_level,
- "landing_risk_tag_ids": ",".join(self.risk_tag_ids),
- "landing_unknown_tag_ids": ",".join(self.unknown_tag_ids),
- "landing_risk_reason": self.reason,
- "_landing_risk": asdict(self),
- }
- def _normalize_tag_ids(data: Any) -> list[str]:
- if data is None:
- return []
- if isinstance(data, list):
- return [str(x) for x in data if x is not None]
- return [str(data)]
- def query_video_risk_tags(video_id: int) -> list[str]:
- """查询视频风险 tag ids。
- Raises:
- RuntimeError: HTTP / 接口 code 非 0 / data 非预期时抛出。
- """
- resp = httpx.post(
- VIDEO_RISK_API_URL,
- json={"videoId": int(video_id)},
- headers={"Content-Type": "application/json"},
- timeout=VIDEO_RISK_API_TIMEOUT_SECONDS,
- )
- resp.raise_for_status()
- payload = resp.json()
- if payload.get("code") != 0:
- raise RuntimeError(
- f"video risk api failed video_id={video_id} "
- f"code={payload.get('code')} msg={payload.get('msg')}"
- )
- return _normalize_tag_ids(payload.get("data"))
- def evaluate_video_risk(video_id: int, tag_ids: list[str]) -> VideoRiskResult:
- """把 tag ids 映射成最高风险等级并判断是否可用。"""
- risk_tag_ids: list[str] = []
- unknown_tag_ids: list[str] = []
- levels: list[int] = []
- for tag_id in tag_ids:
- level = VIDEO_RISK_TAG_LEVELS.get(str(tag_id))
- if level is None:
- unknown_tag_ids.append(str(tag_id))
- continue
- risk_tag_ids.append(str(tag_id))
- levels.append(int(level))
- max_level = max(levels) if levels else 0
- passed = max_level <= VIDEO_RISK_MAX_ALLOWED_LEVEL
- if not VIDEO_RISK_CHECK_ENABLED:
- passed = True
- reason = "risk check disabled"
- elif max_level == 0:
- reason = "no mapped risk tags"
- elif passed:
- reason = f"risk level {max_level} <= allowed {VIDEO_RISK_MAX_ALLOWED_LEVEL}"
- else:
- reason = f"risk level {max_level} > allowed {VIDEO_RISK_MAX_ALLOWED_LEVEL}"
- return VideoRiskResult(
- video_id=int(video_id),
- passed=passed,
- max_level=max_level,
- risk_tag_ids=risk_tag_ids,
- unknown_tag_ids=unknown_tag_ids,
- reason=reason,
- raw_tag_ids=[str(x) for x in tag_ids],
- )
- def check_video_risk(video_id: int) -> VideoRiskResult:
- """查询并评估视频风险。
- 接口异常时返回 passed=False,让调用方跳过该 landing,避免在风险未知时创建 plan。
- """
- if not VIDEO_RISK_CHECK_ENABLED:
- return evaluate_video_risk(int(video_id), [])
- try:
- tag_ids = query_video_risk_tags(int(video_id))
- result = evaluate_video_risk(int(video_id), tag_ids)
- logger.info(
- "[video_risk] video_id=%s passed=%s level=%s risk_tags=%s unknown_tags=%s reason=%s",
- video_id,
- result.passed,
- result.max_level,
- result.risk_tag_ids,
- result.unknown_tag_ids,
- result.reason,
- )
- return result
- except Exception as e:
- logger.warning("[video_risk] video_id=%s 查询失败,跳过该视频:%s", video_id, e)
- return VideoRiskResult(
- video_id=int(video_id),
- passed=False,
- max_level=999,
- risk_tag_ids=[],
- unknown_tag_ids=[],
- reason=f"risk api error: {e}",
- raw_tag_ids=[],
- )
|