主要更新: - 更新代理商端文档,明确项目由品牌方分配流程 - 新增Brief配置详情页(已配置)设计稿 - 完善工作台紧急待办中品牌新任务功能 - 整理Pencil设计文件中代理商端页面顺序 - 新增后端FastAPI框架及核心API - 新增前端Next.js页面和组件库 - 添加.gitignore排除构建和缓存文件 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
313 lines
12 KiB
Python
313 lines
12 KiB
Python
"""
|
||
审核相关的 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="修改建议")
|
||
|
||
# 文本审核字段
|
||
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 ScriptReviewRequest(BaseModel):
|
||
"""脚本预审请求"""
|
||
content: str = Field(..., min_length=1, description="脚本内容")
|
||
platform: Platform = Field(..., description="投放平台")
|
||
brand_id: str = Field(..., description="品牌 ID")
|
||
required_points: Optional[list[str]] = Field(None, description="必要卖点列表")
|
||
soft_risk_context: Optional[SoftRiskContext] = Field(None, description="软性风控上下文")
|
||
|
||
|
||
class ScriptReviewResponse(BaseModel):
|
||
"""
|
||
脚本预审响应
|
||
|
||
结构:
|
||
- score: 合规分数 0-100
|
||
- summary: 整体摘要
|
||
- violations: 违规项列表,每项包含 suggestion
|
||
- missing_points: 遗漏的卖点(可选)
|
||
"""
|
||
score: int = Field(..., ge=0, le=100, description="合规分数")
|
||
summary: str = Field(..., description="审核摘要")
|
||
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="软性风控提示")
|
||
|
||
|
||
# ==================== 视频审核 ====================
|
||
|
||
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="软性风控提示")
|
||
|
||
|
||
# ==================== 一致性指标 ====================
|
||
|
||
class ConsistencyWindow(str, Enum):
|
||
"""一致性指标计算周期"""
|
||
ROLLING_30D = "rolling_30d"
|
||
SNAPSHOT_WEEK = "snapshot_week"
|
||
SNAPSHOT_MONTH = "snapshot_month"
|
||
|
||
|
||
class RuleConsistencyMetric(BaseModel):
|
||
"""按规则类型的指标"""
|
||
rule_type: ViolationType = Field(..., description="规则类型")
|
||
total_reviews: int = Field(..., ge=0, description="总审核数")
|
||
violation_count: int = Field(..., ge=0, description="违规数")
|
||
violation_rate: float = Field(..., ge=0, le=1, description="违规率(0-1)")
|
||
|
||
|
||
class ConsistencyMetricsResponse(BaseModel):
|
||
"""一致性指标响应"""
|
||
influencer_id: str = Field(..., description="达人 ID")
|
||
window: ConsistencyWindow = Field(..., description="计算周期")
|
||
period_start: datetime = Field(..., description="周期起始时间")
|
||
period_end: datetime = Field(..., description="周期结束时间")
|
||
metrics: list[RuleConsistencyMetric] = Field(default_factory=list)
|
||
|
||
|
||
# ==================== 特例审批(风控豁免) ====================
|
||
|
||
class RiskTargetType(str, Enum):
|
||
"""特例目标类型"""
|
||
INFLUENCER = "influencer"
|
||
ORDER = "order"
|
||
CONTENT = "content"
|
||
|
||
|
||
class RiskExceptionStatus(str, Enum):
|
||
"""特例审批状态"""
|
||
PENDING = "pending"
|
||
APPROVED = "approved"
|
||
REJECTED = "rejected"
|
||
EXPIRED = "expired"
|
||
REVOKED = "revoked"
|
||
|
||
|
||
class RiskExceptionCreateRequest(BaseModel):
|
||
"""创建特例请求"""
|
||
applicant_id: str = Field(..., description="申请人")
|
||
target_type: RiskTargetType = Field(..., description="目标类型")
|
||
target_id: str = Field(..., description="目标 ID")
|
||
risk_rule_id: str = Field(..., description="豁免规则 ID")
|
||
reason_category: str = Field(..., description="原因分类")
|
||
justification: str = Field(..., min_length=1, description="详细理由")
|
||
attachment_url: Optional[str] = Field(None, description="附件链接")
|
||
current_approver_id: str = Field(..., description="当前审批人")
|
||
valid_start_time: datetime = Field(..., description="生效开始时间")
|
||
valid_end_time: datetime = Field(..., description="生效结束时间")
|
||
|
||
|
||
class RiskExceptionRecord(BaseModel):
|
||
"""特例记录"""
|
||
record_id: str = Field(..., description="记录 ID")
|
||
applicant_id: str = Field(..., description="申请人")
|
||
apply_time: datetime = Field(..., description="申请时间")
|
||
target_type: RiskTargetType = Field(..., description="目标类型")
|
||
target_id: str = Field(..., description="目标 ID")
|
||
risk_rule_id: str = Field(..., description="豁免规则 ID")
|
||
status: RiskExceptionStatus = Field(..., description="状态")
|
||
valid_start_time: datetime = Field(..., description="生效开始时间")
|
||
valid_end_time: datetime = Field(..., description="生效结束时间")
|
||
reason_category: str = Field(..., description="原因分类")
|
||
justification: str = Field(..., description="详细理由")
|
||
attachment_url: Optional[str] = Field(None, description="附件链接")
|
||
current_approver_id: Optional[str] = Field(None, description="当前审批人")
|
||
approval_chain_log: list[dict] = Field(default_factory=list, description="审批流转日志")
|
||
auto_rejected: bool = Field(default=False, description="是否超时自动拒绝")
|
||
rejection_reason: Optional[str] = Field(None, description="驳回原因")
|
||
last_status_at: Optional[datetime] = Field(None, description="最近状态变更时间")
|
||
|
||
|
||
class RiskExceptionDecisionRequest(BaseModel):
|
||
"""特例审批决策请求"""
|
||
approver_id: str = Field(..., description="审批人")
|
||
comment: Optional[str] = Field(None, description="审批备注")
|
||
|
||
|
||
# ==================== 审核任务 ====================
|
||
|
||
class TaskCreateRequest(BaseModel):
|
||
"""创建任务请求"""
|
||
platform: Platform = Field(..., description="投放平台")
|
||
creator_id: str = Field(..., description="达人 ID")
|
||
video_url: Optional[HttpUrl] = Field(None, description="视频 URL")
|
||
script_content: Optional[str] = Field(None, min_length=1, description="脚本内容")
|
||
script_file_url: Optional[HttpUrl] = Field(None, description="脚本文档 URL")
|
||
|
||
|
||
class TaskScriptUploadRequest(BaseModel):
|
||
"""上传脚本请求"""
|
||
script_content: Optional[str] = Field(None, min_length=1, description="脚本内容")
|
||
script_file_url: Optional[HttpUrl] = Field(None, description="脚本文档 URL")
|
||
|
||
|
||
class TaskVideoUploadRequest(BaseModel):
|
||
"""上传视频请求"""
|
||
video_url: HttpUrl = Field(..., description="视频 URL")
|
||
|
||
|
||
class TaskResponse(BaseModel):
|
||
"""任务响应"""
|
||
task_id: str = Field(..., description="任务 ID")
|
||
video_url: Optional[str] = Field(None, description="视频 URL")
|
||
script_content: Optional[str] = Field(None, description="脚本内容")
|
||
script_file_url: Optional[str] = Field(None, description="脚本文档 URL")
|
||
has_script: bool = Field(..., description="是否已上传脚本")
|
||
has_video: bool = Field(..., description="是否已上传视频")
|
||
platform: Platform = Field(..., description="投放平台")
|
||
creator_id: str = Field(..., description="达人 ID")
|
||
status: TaskStatus = Field(..., description="任务状态")
|
||
created_at: str = Field(..., description="创建时间")
|
||
|
||
|
||
class TaskListResponse(BaseModel):
|
||
"""任务列表响应"""
|
||
items: list[TaskResponse] = Field(default_factory=list)
|
||
total: int = Field(..., description="总数")
|
||
page: int = Field(..., description="当前页")
|
||
page_size: int = Field(..., description="每页数量")
|
||
|
||
|
||
class TaskApproveRequest(BaseModel):
|
||
"""通过任务请求"""
|
||
comment: Optional[str] = Field(None, description="备注")
|
||
|
||
|
||
class TaskRejectRequest(BaseModel):
|
||
"""驳回任务请求"""
|
||
reason: str = Field(..., min_length=1, description="驳回原因")
|
||
violations: list[str] = Field(default_factory=list, description="违规类型列表")
|