""" 审核相关的 Pydantic 模型(API 契约定义) 所有测试和实现必须遵循此契约 """ from typing import Optional from datetime import datetime from pydantic import BaseModel, Field, HttpUrl from enum import Enum # ==================== 枚举定义 ==================== class Platform(str, Enum): """支持的投放平台""" DOUYIN = "douyin" XIAOHONGSHU = "xiaohongshu" BILIBILI = "bilibili" KUAISHOU = "kuaishou" class TaskStatus(str, Enum): """任务状态""" PENDING = "pending" PROCESSING = "processing" COMPLETED = "completed" FAILED = "failed" APPROVED = "approved" REJECTED = "rejected" class RiskLevel(str, Enum): """风险等级""" HIGH = "high" # 法律违规(广告法极限词) MEDIUM = "medium" # 平台规则违规 LOW = "low" # 品牌规范违规 class ViolationType(str, Enum): """违规类型""" FORBIDDEN_WORD = "forbidden_word" # 违禁词 EFFICACY_CLAIM = "efficacy_claim" # 功效宣称 COMPETITOR_LOGO = "competitor_logo" # 竞品露出 DURATION_SHORT = "duration_short" # 时长不足 MENTION_MISSING = "mention_missing" # 品牌提及不足 BRAND_SAFETY = "brand_safety" # 品牌安全风险 class ViolationSource(str, Enum): """违规来源""" TEXT = "text" # 文本/脚本 SPEECH = "speech" # 语音(ASR) SUBTITLE = "subtitle" # 字幕(OCR) VISUAL = "visual" # 画面(CV) class SoftRiskAction(str, Enum): """软性风控动作""" CONFIRM = "confirm" # 需要二次确认 NOTE = "note" # 需要填写备注 class SoftRiskWarning(BaseModel): """软性风控提示(Warn-only)""" code: str = Field(..., description="提示类型代码") message: str = Field(..., description="提示内容") action_required: SoftRiskAction = Field(..., description="要求动作") blocking: bool = Field(default=False, description="是否阻断(默认不阻断)") context: Optional[dict] = Field(None, description="附加上下文") class SoftRiskContext(BaseModel): """软性风控输入上下文""" violation_rate: Optional[float] = Field(None, ge=0, le=1, description="违规率") violation_threshold: Optional[float] = Field(None, ge=0, le=1, description="违规率阈值") asr_confidence: Optional[float] = Field(None, ge=0, le=1, description="ASR 置信度") ocr_confidence: Optional[float] = Field(None, ge=0, le=1, description="OCR 置信度") has_history_violation: Optional[bool] = Field(None, description="是否有历史类似违规") # ==================== 通用模型 ==================== class Position(BaseModel): """文本位置""" start: int = Field(..., description="起始位置") end: int = Field(..., description="结束位置") class Violation(BaseModel): """违规项(统一结构)""" type: ViolationType = Field(..., description="违规类型") content: str = Field(..., description="违规内容") severity: RiskLevel = Field(..., description="严重程度") suggestion: str = Field(..., description="修改建议") dimension: Optional[str] = Field(None, description="所属维度: legal/platform/brand_safety/brief_match") # 文本审核字段 position: Optional[Position] = Field(None, description="文本位置(脚本审核)") # 视频审核字段 timestamp: Optional[float] = Field(None, description="开始时间戳(秒)") timestamp_end: Optional[float] = Field(None, description="结束时间戳(秒)") source: Optional[ViolationSource] = Field(None, description="违规来源(视频审核)") # ==================== 多维度审核 ==================== class ReviewDimension(BaseModel): """审核维度评分""" score: int = Field(..., ge=0, le=100) passed: bool issue_count: int = 0 class ReviewDimensions(BaseModel): """四维度审核结果""" legal: ReviewDimension # 法规合规(违禁词、功效词、Brief黑名单词) platform: ReviewDimension # 平台规则 brand_safety: ReviewDimension # 品牌安全(竞品、其他品牌词) brief_match: ReviewDimension # Brief 匹配度(卖点覆盖) class SellingPointMatch(BaseModel): """卖点匹配结果""" content: str priority: str # "core" | "recommended" | "reference" matched: bool evidence: Optional[str] = None # AI 给出的匹配依据 class BriefMatchDetail(BaseModel): """Brief 匹配度评分详情""" # 卖点覆盖 total_points: int = Field(0, description="需要检查的卖点总数(core + recommended)") matched_points: int = Field(0, description="实际匹配的卖点数") required_points: int = Field(0, description="代理商要求至少体现的卖点条数(min_selling_points)") coverage_score: int = Field(0, ge=0, le=100, description="卖点覆盖率得分") # AI 整体匹配分析 overall_score: int = Field(0, ge=0, le=100, description="整体 Brief 匹配度得分") highlights: list[str] = Field(default_factory=list, description="内容亮点(AI 分析)") issues: list[str] = Field(default_factory=list, description="问题点(AI 分析)") explanation: str = Field("", description="评分说明(一句话总结)") # ==================== 脚本预审 ==================== class ScriptReviewRequest(BaseModel): """脚本预审请求""" content: str = Field(..., min_length=1, description="脚本内容") platform: Platform = Field(..., description="投放平台") brand_id: str = Field(..., description="品牌 ID") selling_points: Optional[list[dict]] = Field(None, description="卖点列表 [{content, priority}]") min_selling_points: Optional[int] = Field(None, ge=0, description="代理商要求至少体现的卖点条数") blacklist_words: Optional[list[dict]] = Field(None, description="Brief 黑名单词 [{word, reason}]") soft_risk_context: Optional[SoftRiskContext] = Field(None, description="软性风控上下文") file_url: Optional[str] = Field(None, description="脚本文件 URL(用于自动解析文本和提取图片)") file_name: Optional[str] = Field(None, description="原始文件名(用于判断格式)") class ScriptReviewResponse(BaseModel): """ 脚本预审响应 结构: - score: 加权总分(向后兼容) - summary: 整体摘要 - dimensions: 四维度评分(法规/平台/品牌安全/Brief匹配) - selling_point_matches: 卖点匹配详情 - violations: 违规项列表,每项带 dimension 标签 - missing_points: 遗漏的核心卖点(向后兼容) """ score: int = Field(..., ge=0, le=100, description="加权总分") summary: str = Field(..., description="审核摘要") dimensions: ReviewDimensions = Field(..., description="四维度评分") selling_point_matches: list[SellingPointMatch] = Field(default_factory=list, description="卖点匹配详情") brief_match_detail: Optional[BriefMatchDetail] = Field(None, description="Brief 匹配度评分详情") violations: list[Violation] = Field(default_factory=list, description="违规项列表") missing_points: Optional[list[str]] = Field(None, description="遗漏的核心卖点") soft_warnings: list[SoftRiskWarning] = Field(default_factory=list, description="软性风控提示") ai_available: bool = Field(True, description="AI 服务是否可用(False 表示降级为纯关键词检测)") # ==================== 视频审核 ==================== class VideoReviewRequest(BaseModel): """视频审核请求""" video_url: HttpUrl = Field(..., description="视频 URL") platform: Platform = Field(..., description="投放平台") brand_id: str = Field(..., description="品牌 ID") creator_id: str = Field(..., description="达人 ID") competitors: Optional[list[str]] = Field(None, description="竞品列表") requirements: Optional[dict] = Field(None, description="审核要求(时长、频次等)") class VideoReviewSubmitResponse(BaseModel): """视频审核提交响应(202 Accepted)""" review_id: str = Field(..., description="审核任务 ID") status: TaskStatus = Field(default=TaskStatus.PENDING, description="任务状态") class VideoReviewProgressResponse(BaseModel): """视频审核进度响应""" review_id: str = Field(..., description="审核任务 ID") status: TaskStatus = Field(..., description="任务状态") progress: int = Field(..., ge=0, le=100, description="进度百分比") current_step: str = Field(..., description="当前处理步骤") class VideoReviewResultResponse(BaseModel): """ 视频审核结果响应(200 OK) 结构与脚本审核一致: - score: 合规分数 - summary: 整体摘要 - violations: 违规项列表,每项包含 timestamp 和 suggestion """ review_id: str = Field(..., description="审核任务 ID") status: TaskStatus = Field(default=TaskStatus.COMPLETED, description="任务状态") score: int = Field(..., ge=0, le=100, description="合规分数") summary: str = Field(..., description="审核摘要") violations: list[Violation] = Field(default_factory=list, description="违规项列表") soft_warnings: list[SoftRiskWarning] = Field(default_factory=list, description="软性风控提示")