video-compliance-ai/backend/tests/test_video_review_service.py
Your Name e4959d584f feat: 完善代理商端业务逻辑与前后端框架
主要更新:
- 更新代理商端文档,明确项目由品牌方分配流程
- 新增Brief配置详情页(已配置)设计稿
- 完善工作台紧急待办中品牌新任务功能
- 整理Pencil设计文件中代理商端页面顺序
- 新增后端FastAPI框架及核心API
- 新增前端Next.js页面和组件库
- 添加.gitignore排除构建和缓存文件

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 19:27:31 +08:00

465 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
视频审核服务层测试 (TDD - 红色阶段)
测试覆盖: 违规检测核心逻辑、时长频次校验、风险等级分类
这些测试验证实际检测结果,而非仅 HTTP 状态码
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
class TestCompetitorLogoDetection:
"""竞品 Logo 检测逻辑"""
@pytest.mark.asyncio
async def test_detect_competitor_logo_in_frame(self):
"""检测画面中的竞品 Logo"""
# 导入服务(实现后才能通过)
from app.services.video_review import VideoReviewService
service = VideoReviewService()
# 模拟视频帧数据(包含竞品 Logo
mock_frames = [
{"timestamp": 10.0, "objects": [{"label": "competitor-brand-A", "confidence": 0.95}]},
{"timestamp": 45.0, "objects": [{"label": "competitor-brand-A", "confidence": 0.88}]},
]
violations = await service.detect_competitor_logos(
frames=mock_frames,
competitors=["competitor-brand-A", "competitor-brand-B"]
)
# 应该检测到 2 处竞品露出
assert len(violations) == 2
assert violations[0]["type"] == "competitor_logo"
assert violations[0]["timestamp"] == 10.0
assert violations[0]["risk_level"] == "medium"
@pytest.mark.asyncio
async def test_no_detection_when_no_competitor(self):
"""无竞品时不应检测到违规"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
mock_frames = [
{"timestamp": 10.0, "objects": [{"label": "product-A", "confidence": 0.95}]},
]
violations = await service.detect_competitor_logos(
frames=mock_frames,
competitors=["competitor-brand-X"] # 不在画面中
)
assert len(violations) == 0
@pytest.mark.asyncio
async def test_ignore_low_confidence_detection(self):
"""忽略低置信度检测"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
mock_frames = [
{"timestamp": 10.0, "objects": [{"label": "competitor-brand-A", "confidence": 0.3}]}, # 低置信度
]
violations = await service.detect_competitor_logos(
frames=mock_frames,
competitors=["competitor-brand-A"],
min_confidence=0.7
)
assert len(violations) == 0
class TestForbiddenWordDetectionInSpeech:
"""口播违禁词检测ASR"""
@pytest.mark.asyncio
async def test_detect_forbidden_word_in_transcript(self):
"""检测语音转文字中的违禁词"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
# 模拟 ASR 转写结果
mock_transcript = [
{"text": "这是一款很好的产品", "start": 0.0, "end": 3.0},
{"text": "我们的产品是最好的", "start": 5.0, "end": 8.0}, # 包含"最好"
{"text": "销量第一名", "start": 10.0, "end": 12.0}, # 包含"第一"
]
violations = await service.detect_forbidden_words_in_speech(
transcript=mock_transcript,
forbidden_words=["最好", "第一", "最佳"]
)
# 应该检测到 2 处违规
assert len(violations) == 2
# 验证第一个违规
assert violations[0]["type"] == "forbidden_word"
assert violations[0]["content"] == "最好"
assert violations[0]["timestamp"] == 5.0
assert violations[0]["source"] == "speech"
assert "suggestion" in violations[0]
# 验证第二个违规
assert violations[1]["content"] == "第一"
assert violations[1]["timestamp"] == 10.0
@pytest.mark.asyncio
async def test_context_aware_detection(self):
"""语境感知检测 - 非广告语境不标记"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
# 非广告语境
mock_transcript = [
{"text": "今天是我最开心的一天", "start": 0.0, "end": 3.0}, # 非广告语境
]
violations = await service.detect_forbidden_words_in_speech(
transcript=mock_transcript,
forbidden_words=[""],
context_aware=True # 启用语境感知
)
# 非广告语境不应标记
assert len(violations) == 0
@pytest.mark.asyncio
async def test_ad_context_flagged(self):
"""广告语境应标记"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
# 广告语境
mock_transcript = [
{"text": "我们的产品是最好的选择", "start": 0.0, "end": 3.0},
]
violations = await service.detect_forbidden_words_in_speech(
transcript=mock_transcript,
forbidden_words=["最好"],
context_aware=True
)
# 广告语境应标记
assert len(violations) == 1
class TestForbiddenWordDetectionInSubtitle:
"""字幕违禁词检测OCR"""
@pytest.mark.asyncio
async def test_detect_forbidden_word_in_subtitle(self):
"""检测字幕中的违禁词"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
# 模拟 OCR 结果
mock_subtitles = [
{"text": "限时特惠", "timestamp": 5.0},
{"text": "效果最佳", "timestamp": 15.0}, # 包含"最佳"
{"text": "立即购买", "timestamp": 25.0},
]
violations = await service.detect_forbidden_words_in_subtitle(
subtitles=mock_subtitles,
forbidden_words=["最佳", "第一", "最好"]
)
assert len(violations) == 1
assert violations[0]["content"] == "最佳"
assert violations[0]["timestamp"] == 15.0
assert violations[0]["source"] == "subtitle"
class TestDurationCheck:
"""时长校验"""
@pytest.mark.asyncio
async def test_product_display_duration_sufficient(self):
"""产品同框时长充足时通过"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
# 模拟产品出现时间段
mock_product_appearances = [
{"start": 5.0, "end": 15.0}, # 10 秒
{"start": 30.0, "end": 35.0}, # 5 秒
]
violations = await service.check_product_display_duration(
appearances=mock_product_appearances,
min_seconds=10
)
# 总时长 15 秒 >= 要求 10 秒,应该通过
assert len(violations) == 0
@pytest.mark.asyncio
async def test_product_display_duration_insufficient(self):
"""产品同框时长不足时报违规"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
mock_product_appearances = [
{"start": 5.0, "end": 8.0}, # 3 秒
]
violations = await service.check_product_display_duration(
appearances=mock_product_appearances,
min_seconds=10
)
# 总时长 3 秒 < 要求 10 秒,应该报违规
assert len(violations) == 1
assert violations[0]["type"] == "duration_short"
assert "3" in violations[0]["content"] or "" in violations[0]["content"]
assert violations[0]["risk_level"] == "medium"
class TestBrandMentionFrequency:
"""品牌提及频次校验"""
@pytest.mark.asyncio
async def test_brand_mention_sufficient(self):
"""品牌提及次数充足时通过"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
mock_transcript = [
{"text": "今天介绍品牌A的产品", "start": 0.0, "end": 3.0},
{"text": "品牌A真的很好用", "start": 10.0, "end": 13.0},
{"text": "推荐大家试试品牌A", "start": 20.0, "end": 23.0},
]
violations = await service.check_brand_mention_frequency(
transcript=mock_transcript,
brand_name="品牌A",
min_mentions=3
)
# 提及 3 次 >= 要求 3 次,应该通过
assert len(violations) == 0
@pytest.mark.asyncio
async def test_brand_mention_insufficient(self):
"""品牌提及次数不足时报违规"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
mock_transcript = [
{"text": "今天介绍品牌A的产品", "start": 0.0, "end": 3.0},
]
violations = await service.check_brand_mention_frequency(
transcript=mock_transcript,
brand_name="品牌A",
min_mentions=3
)
# 提及 1 次 < 要求 3 次,应该报违规
assert len(violations) == 1
assert violations[0]["type"] == "mention_missing"
class TestRiskLevelClassification:
"""风险等级分类"""
@pytest.mark.asyncio
async def test_legal_violation_is_high_risk(self):
"""法律违规(广告法)标记为高风险"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
violation = {
"type": "forbidden_word",
"content": "最好",
"category": "absolute_term", # 广告法极限词
}
risk_level = service.classify_risk_level(violation)
assert risk_level == "high"
@pytest.mark.asyncio
async def test_platform_violation_is_medium_risk(self):
"""平台规则违规标记为中风险"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
violation = {
"type": "duration_short",
"category": "platform_rule",
}
risk_level = service.classify_risk_level(violation)
assert risk_level == "medium"
@pytest.mark.asyncio
async def test_brand_guideline_is_low_risk(self):
"""品牌规范违规标记为低风险"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
violation = {
"type": "mention_missing",
"category": "brand_guideline",
}
risk_level = service.classify_risk_level(violation)
assert risk_level == "low"
class TestScoreCalculation:
"""合规分数计算"""
@pytest.mark.asyncio
async def test_perfect_score_no_violations(self):
"""无违规时满分"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
score = service.calculate_score(violations=[])
assert score == 100
@pytest.mark.asyncio
async def test_high_risk_violation_major_deduction(self):
"""高风险违规大幅扣分"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
violations = [
{"type": "forbidden_word", "risk_level": "high"},
]
score = service.calculate_score(violations=violations)
# 高风险违规应该扣 20-30 分
assert score <= 80
@pytest.mark.asyncio
async def test_multiple_violations_cumulative_deduction(self):
"""多个违规累计扣分"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
violations = [
{"type": "forbidden_word", "risk_level": "high"},
{"type": "forbidden_word", "risk_level": "high"},
{"type": "duration_short", "risk_level": "medium"},
]
score = service.calculate_score(violations=violations)
# 多个违规累计,分数应该更低
assert score <= 60
@pytest.mark.asyncio
async def test_score_never_below_zero(self):
"""分数不会低于 0"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
# 大量违规
violations = [{"type": "forbidden_word", "risk_level": "high"} for _ in range(20)]
score = service.calculate_score(violations=violations)
assert score >= 0
class TestFullReviewPipeline:
"""完整审核流程测试"""
@pytest.mark.asyncio
async def test_review_video_with_violations(self):
"""审核包含违规的视频"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
# Mock AI 服务
service.asr_service = AsyncMock()
service.asr_service.transcribe.return_value = [
{"text": "这是最好的产品", "start": 5.0, "end": 8.0},
]
service.cv_service = AsyncMock()
service.cv_service.detect_objects.return_value = [
{"timestamp": 10.0, "objects": [{"label": "competitor-A", "confidence": 0.9}]},
]
service.ocr_service = AsyncMock()
service.ocr_service.extract_subtitles.return_value = []
result = await service.review_video(
video_url="https://example.com/video.mp4",
platform="douyin",
brand_id="brand-001",
competitors=["competitor-A"],
forbidden_words=["最好"],
)
# 验证结果结构
assert "score" in result
assert "summary" in result
assert "violations" in result
# 应该检测到违规
assert len(result["violations"]) >= 2 # 至少:口播违禁词 + 竞品 Logo
assert result["score"] < 100
# 验证违规项结构
for violation in result["violations"]:
assert "type" in violation
assert "content" in violation
assert "timestamp" in violation
assert "risk_level" in violation
assert "suggestion" in violation
@pytest.mark.asyncio
async def test_review_clean_video(self):
"""审核无违规的视频"""
from app.services.video_review import VideoReviewService
service = VideoReviewService()
# Mock AI 服务 - 无违规内容
service.asr_service = AsyncMock()
service.asr_service.transcribe.return_value = [
{"text": "今天给大家分享护肤技巧", "start": 0.0, "end": 3.0},
]
service.cv_service = AsyncMock()
service.cv_service.detect_objects.return_value = []
service.ocr_service = AsyncMock()
service.ocr_service.extract_subtitles.return_value = []
result = await service.review_video(
video_url="https://example.com/clean_video.mp4",
platform="douyin",
brand_id="brand-001",
competitors=[],
forbidden_words=["最好"],
)
# 无违规,满分
assert len(result["violations"]) == 0
assert result["score"] == 100