feat: 添加全面的 TDD 测试套件框架

基于项目需求文档(PRD.md, FeatureSummary.md, DevelopmentPlan.md,
UIDesign.md, User_Role_Interfaces.md)编写的 TDD 测试用例。

后端测试 (Python/pytest):
- 单元测试: rule_engine, brief_parser, timestamp_alignment,
  video_auditor, validators
- 集成测试: API Brief, Video, Review 端点
- AI 模块测试: ASR, OCR, Logo 检测服务
- 全局 fixtures 和 pytest 配置

前端测试 (TypeScript/Vitest):
- 工具函数测试: utils.test.ts
- 组件测试: Button, VideoPlayer, ViolationList
- Hooks 测试: useVideoAudit, useVideoPlayer, useAppeal
- MSW mock handlers 配置

E2E 测试 (Playwright):
- 认证流程测试
- 视频上传流程测试
- 视频审核流程测试
- 申诉流程测试

所有测试当前使用 pytest.skip() / it.skip() 作为占位符,
遵循 TDD 红灯阶段 - 等待实现代码后运行。

验收标准覆盖:
- ASR WER ≤ 10%
- OCR 准确率 ≥ 95%
- Logo F1 ≥ 0.85
- 时间戳误差 ≤ 0.5s
- 频次统计准确率 ≥ 95%

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-02-02 17:22:24 +08:00
co-authored by Claude Opus 4.5
parent 18fe22ce8a
commit 040aada160
26 changed files with 6185 additions and 0 deletions
+339
View File
@@ -0,0 +1,339 @@
"""
Brief 解析模块单元测试
TDD 测试用例 - 基于 FeatureSummary.md (F-01, F-02) 的验收标准
验收标准:
- 图文混排解析准确率 > 90%
- 支持 PDF/Word/Excel/PPT/图片格式
- 支持飞书/Notion 在线文档链接
"""
import pytest
from typing import Any
from pathlib import Path
# 导入待实现的模块(TDD 红灯阶段)
# from app.services.brief_parser import BriefParser, BriefParsingResult
class TestBriefParser:
"""
Brief 解析器测试
验收标准 (FeatureSummary.md F-01):
- 解析准确率 > 90%
"""
@pytest.mark.unit
def test_extract_selling_points(self) -> None:
"""测试卖点提取"""
brief_content = """
产品核心卖点:
1. 24小时持妆
2. 天然成分
3. 敏感肌适用
"""
# TODO: 实现 BriefParser
# parser = BriefParser()
# result = parser.extract_selling_points(brief_content)
#
# assert len(result.selling_points) >= 3
# assert "24小时持妆" in [sp.text for sp in result.selling_points]
# assert "天然成分" in [sp.text for sp in result.selling_points]
# assert "敏感肌适用" in [sp.text for sp in result.selling_points]
pytest.skip("待实现:BriefParser.extract_selling_points")
@pytest.mark.unit
def test_extract_forbidden_words(self) -> None:
"""测试禁忌词提取"""
brief_content = """
禁止使用的词汇:
- 药用
- 治疗
- 根治
- 最有效
"""
# TODO: 实现 BriefParser
# parser = BriefParser()
# result = parser.extract_forbidden_words(brief_content)
#
# expected = {"药用", "治疗", "根治", "最有效"}
# assert set(w.word for w in result.forbidden_words) == expected
pytest.skip("待实现:BriefParser.extract_forbidden_words")
@pytest.mark.unit
def test_extract_timing_requirements(self) -> None:
"""测试时序要求提取"""
brief_content = """
拍摄要求:
- 产品同框时长 > 5秒
- 品牌名提及次数 ≥ 3次
- 产品使用演示 ≥ 10秒
"""
# TODO: 实现 BriefParser
# parser = BriefParser()
# result = parser.extract_timing_requirements(brief_content)
#
# assert len(result.timing_requirements) >= 3
#
# product_visible = next(
# (t for t in result.timing_requirements if t.type == "product_visible"),
# None
# )
# assert product_visible is not None
# assert product_visible.min_duration_seconds == 5
#
# brand_mention = next(
# (t for t in result.timing_requirements if t.type == "brand_mention"),
# None
# )
# assert brand_mention is not None
# assert brand_mention.min_frequency == 3
pytest.skip("待实现:BriefParser.extract_timing_requirements")
@pytest.mark.unit
def test_extract_brand_tone(self) -> None:
"""测试品牌调性提取"""
brief_content = """
品牌调性:
- 风格:年轻活力、专业可信
- 目标人群:18-35岁女性
- 表达方式:亲和、不做作
"""
# TODO: 实现 BriefParser
# parser = BriefParser()
# result = parser.extract_brand_tone(brief_content)
#
# assert result.brand_tone is not None
# assert "年轻活力" in result.brand_tone.style
# assert "专业可信" in result.brand_tone.style
pytest.skip("待实现:BriefParser.extract_brand_tone")
@pytest.mark.unit
def test_full_brief_parsing_accuracy(self) -> None:
"""
测试完整 Brief 解析准确率
验收标准:准确率 > 90%
"""
brief_content = """
# 品牌 Brief - XX美妆产品
## 产品卖点
1. 24小时持妆效果
2. 添加天然植物成分
3. 通过敏感肌测试
## 禁用词汇
- 药用、治疗、根治
- 最好、第一、绝对
## 拍摄要求
- 产品正面展示 ≥ 5秒
- 品牌名提及 ≥ 3次
## 品牌调性
年轻、时尚、专业
"""
# TODO: 实现 BriefParser
# parser = BriefParser()
# result = parser.parse(brief_content)
#
# # 验证解析完整性
# assert len(result.selling_points) >= 3
# assert len(result.forbidden_words) >= 4
# assert len(result.timing_requirements) >= 2
# assert result.brand_tone is not None
#
# # 验证准确率
# assert result.accuracy_rate >= 0.90
pytest.skip("待实现:BriefParser.parse")
class TestBriefFileFormats:
"""
Brief 文件格式支持测试
验收标准 (FeatureSummary.md F-01):
- 支持 PDF/Word/Excel/PPT/图片
"""
@pytest.mark.unit
@pytest.mark.parametrize("file_format,mime_type", [
("pdf", "application/pdf"),
("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"),
("png", "image/png"),
("jpg", "image/jpeg"),
])
def test_supported_file_formats(self, file_format: str, mime_type: str) -> None:
"""测试支持的文件格式"""
# TODO: 实现文件格式验证
# validator = BriefFileValidator()
# assert validator.is_supported(file_format)
# assert validator.get_mime_type(file_format) == mime_type
pytest.skip("待实现:BriefFileValidator")
@pytest.mark.unit
@pytest.mark.parametrize("file_format", [
"exe", "zip", "rar", "mp4", "mp3",
])
def test_unsupported_file_formats(self, file_format: str) -> None:
"""测试不支持的文件格式"""
# TODO: 实现文件格式验证
# validator = BriefFileValidator()
# assert not validator.is_supported(file_format)
pytest.skip("待实现:不支持的格式验证")
class TestOnlineDocumentImport:
"""
在线文档导入测试
验收标准 (FeatureSummary.md F-02):
- 支持飞书/Notion 分享链接
- 仅支持授权的分享链接
"""
@pytest.mark.unit
@pytest.mark.parametrize("url,expected_valid", [
# 飞书文档
("https://docs.feishu.cn/docs/abc123", True),
("https://abc.feishu.cn/docx/xyz789", True),
# Notion 文档
("https://www.notion.so/workspace/page-abc123", True),
("https://notion.so/page-xyz789", True),
# 不支持的链接
("https://google.com/doc/123", False),
("https://docs.google.com/document/d/123", False), # Google Docs 暂不支持
("https://example.com/brief.pdf", False),
])
def test_online_document_url_validation(self, url: str, expected_valid: bool) -> None:
"""测试在线文档 URL 验证"""
# TODO: 实现 URL 验证器
# validator = OnlineDocumentValidator()
# assert validator.is_valid(url) == expected_valid
pytest.skip("待实现:OnlineDocumentValidator")
@pytest.mark.unit
def test_unauthorized_link_returns_error(self) -> None:
"""测试无权限链接返回明确错误"""
unauthorized_url = "https://docs.feishu.cn/docs/restricted-doc"
# TODO: 实现在线文档导入
# importer = OnlineDocumentImporter()
# result = importer.import_document(unauthorized_url)
#
# assert result.status == "failed"
# assert result.error_code == "ACCESS_DENIED"
# assert "权限" in result.error_message or "access" in result.error_message.lower()
pytest.skip("待实现:OnlineDocumentImporter")
class TestBriefParsingEdgeCases:
"""
Brief 解析边界情况测试
"""
@pytest.mark.unit
def test_encrypted_pdf_handling(self) -> None:
"""测试加密 PDF 处理 - 应降级提示手动输入"""
# TODO: 实现加密 PDF 检测
# parser = BriefParser()
# result = parser.parse_file("encrypted.pdf")
#
# assert result.status == "failed"
# assert result.error_code == "ENCRYPTED_FILE"
# assert "手动输入" in result.fallback_suggestion
pytest.skip("待实现:加密 PDF 处理")
@pytest.mark.unit
def test_empty_brief_handling(self) -> None:
"""测试空 Brief 处理"""
# TODO: 实现空内容处理
# parser = BriefParser()
# result = parser.parse("")
#
# assert result.status == "failed"
# assert result.error_code == "EMPTY_CONTENT"
pytest.skip("待实现:空 Brief 处理")
@pytest.mark.unit
def test_non_chinese_brief_handling(self) -> None:
"""测试非中文 Brief 处理"""
english_brief = """
Product Features:
1. 24-hour long-lasting
2. Natural ingredients
"""
# TODO: 实现多语言检测
# parser = BriefParser()
# result = parser.parse(english_brief)
#
# # 应该能处理英文,但提示语言
# assert result.detected_language == "en"
pytest.skip("待实现:多语言 Brief 处理")
@pytest.mark.unit
def test_image_brief_with_text_extraction(self) -> None:
"""测试图片 Brief 的文字提取 (OCR)"""
# TODO: 实现图片 Brief OCR
# parser = BriefParser()
# result = parser.parse_image("brief_screenshot.png")
#
# assert result.status == "success"
# assert len(result.extracted_text) > 0
pytest.skip("待实现:图片 Brief OCR")
class TestBriefParsingOutput:
"""
Brief 解析输出格式测试
"""
@pytest.mark.unit
def test_output_json_structure(self) -> None:
"""测试输出 JSON 结构符合规范"""
brief_content = "测试 Brief 内容"
# TODO: 实现 BriefParser
# parser = BriefParser()
# result = parser.parse(brief_content)
# output = result.to_json()
#
# # 验证必需字段
# assert "selling_points" in output
# assert "forbidden_words" in output
# assert "brand_tone" in output
# assert "timing_requirements" in output
# assert "platform" in output
# assert "region" in output
#
# # 验证字段类型
# assert isinstance(output["selling_points"], list)
# assert isinstance(output["forbidden_words"], list)
pytest.skip("待实现:输出 JSON 结构验证")
@pytest.mark.unit
def test_selling_point_structure(self) -> None:
"""测试卖点数据结构"""
# TODO: 实现卖点结构验证
# expected_fields = ["text", "priority", "evidence_snippet"]
#
# parser = BriefParser()
# result = parser.parse("卖点测试")
#
# for sp in result.selling_points:
# for field in expected_fields:
# assert hasattr(sp, field)
pytest.skip("待实现:卖点结构验证")
+278
View File
@@ -0,0 +1,278 @@
"""
规则引擎单元测试
TDD 测试用例 - 基于 FeatureSummary.md (F-03, F-04, F-05-A, F-06) 的验收标准
验收标准:
- 违禁词召回率 ≥ 95%
- 违禁词误报率 ≤ 5%
- 语境理解误报率 ≤ 5%
- 规则冲突提示清晰可追溯
"""
import pytest
from typing import Any
# 导入待实现的模块(TDD 红灯阶段 - 模块尚未实现)
# from app.services.rule_engine import RuleEngine, ProhibitedWordDetector, RuleConflictDetector
class TestProhibitedWordDetector:
"""
违禁词检测器测试
验收标准 (FeatureSummary.md):
- 召回率 ≥ 95%
- 误报率 ≤ 5%
"""
@pytest.mark.unit
@pytest.mark.parametrize("text,context,expected_violations,should_detect", [
# 广告语境 - 应检出
("这是全网销量第一的产品", "advertisement", ["第一"], True),
("我们是行业领导者", "advertisement", ["领导者"], True),
("史上最低价促销", "advertisement", ["", "史上"], True),
("绝对有效果", "advertisement", ["绝对"], True),
# 日常语境 - 不应检出 (语境感知)
("今天是我最开心的一天", "daily", [], False),
("这是我第一次来这里", "daily", [], False),
("我最喜欢吃苹果", "daily", [], False),
# 边界情况
("", "advertisement", [], False),
("普通的产品介绍,没有违禁词", "advertisement", [], False),
])
def test_detect_prohibited_words(
self,
text: str,
context: str,
expected_violations: list[str],
should_detect: bool,
) -> None:
"""测试违禁词检测的准确性"""
# TODO: 实现 ProhibitedWordDetector
# detector = ProhibitedWordDetector()
# result = detector.detect(text, context=context)
#
# if should_detect:
# assert len(result.violations) > 0
# for word in expected_violations:
# assert any(word in v.content for v in result.violations)
# else:
# assert len(result.violations) == 0
pytest.skip("待实现:ProhibitedWordDetector")
@pytest.mark.unit
def test_recall_rate_above_threshold(
self,
prohibited_word_test_cases: list[dict[str, Any]],
) -> None:
"""
验证召回率 ≥ 95%
召回率 = 正确检出数 / 应检出总数
"""
# TODO: 使用完整测试集验证召回率
# detector = ProhibitedWordDetector()
# positive_cases = [c for c in prohibited_word_test_cases if c["should_detect"]]
#
# true_positives = 0
# for case in positive_cases:
# result = detector.detect(case["text"], context=case["context"])
# if result.violations:
# true_positives += 1
#
# recall = true_positives / len(positive_cases)
# assert recall >= 0.95, f"召回率 {recall:.2%} 低于阈值 95%"
pytest.skip("待实现:召回率测试")
@pytest.mark.unit
def test_false_positive_rate_below_threshold(
self,
prohibited_word_test_cases: list[dict[str, Any]],
) -> None:
"""
验证误报率 ≤ 5%
误报率 = 错误检出数 / 不应检出总数
"""
# TODO: 使用完整测试集验证误报率
# detector = ProhibitedWordDetector()
# negative_cases = [c for c in prohibited_word_test_cases if not c["should_detect"]]
#
# false_positives = 0
# for case in negative_cases:
# result = detector.detect(case["text"], context=case["context"])
# if result.violations:
# false_positives += 1
#
# fpr = false_positives / len(negative_cases)
# assert fpr <= 0.05, f"误报率 {fpr:.2%} 超过阈值 5%"
pytest.skip("待实现:误报率测试")
class TestContextUnderstanding:
"""
语境理解测试
验收标准 (DevelopmentPlan.md 第 8 章):
- 广告极限词与非广告语境区分误报率 ≤ 5%
- 不将「最开心的一天」误判为违规
"""
@pytest.mark.unit
@pytest.mark.parametrize("text,expected_context,should_flag", [
("这款产品是最好的选择", "advertisement", True),
("最近天气真好", "daily", False),
("今天心情最棒了", "daily", False),
("我们的产品效果最显著", "advertisement", True),
("这是我见过最美的风景", "daily", False),
("全网销量第一,值得信赖", "advertisement", True),
("我第一次尝试这个运动", "daily", False),
])
def test_context_classification(
self,
text: str,
expected_context: str,
should_flag: bool,
) -> None:
"""测试语境分类准确性"""
# TODO: 实现语境分类器
# classifier = ContextClassifier()
# result = classifier.classify(text)
#
# assert result.context == expected_context
# if should_flag:
# assert result.is_advertisement_context
# else:
# assert not result.is_advertisement_context
pytest.skip("待实现:ContextClassifier")
@pytest.mark.unit
def test_happy_day_not_flagged(self) -> None:
"""
关键测试:「最开心的一天」不应被误判
这是 DevelopmentPlan.md 明确要求的测试用例
"""
text = "今天是我最开心的一天"
# TODO: 实现检测器
# detector = ProhibitedWordDetector()
# result = detector.detect(text, context="auto") # 自动识别语境
#
# assert len(result.violations) == 0, "「最开心的一天」被误判为违规"
pytest.skip("待实现:语境感知检测")
class TestRuleConflictDetector:
"""
规则冲突检测测试
验收标准 (FeatureSummary.md F-03):
- 规则冲突提示清晰可追溯
"""
@pytest.mark.unit
def test_detect_brief_platform_conflict(
self,
sample_brief_rules: dict[str, Any],
sample_platform_rules: dict[str, Any],
) -> None:
"""测试 Brief 规则与平台规则冲突检测"""
# 构造冲突场景:Brief 允许使用「最佳效果」,但平台禁止「最」
brief_rules = {
**sample_brief_rules,
"allowed_words": ["最佳效果"],
}
# TODO: 实现冲突检测器
# detector = RuleConflictDetector()
# conflicts = detector.detect(brief_rules, sample_platform_rules)
#
# assert len(conflicts) > 0
# assert any("最" in c.conflicting_term for c in conflicts)
# assert all(c.resolution_suggestion is not None for c in conflicts)
pytest.skip("待实现:RuleConflictDetector")
@pytest.mark.unit
def test_no_conflict_when_compatible(
self,
sample_brief_rules: dict[str, Any],
sample_platform_rules: dict[str, Any],
) -> None:
"""测试规则兼容时无冲突"""
# TODO: 实现冲突检测器
# detector = RuleConflictDetector()
# conflicts = detector.detect(sample_brief_rules, sample_platform_rules)
#
# # 标准 Brief 规则应与平台规则兼容
# assert len(conflicts) == 0
pytest.skip("待实现:规则兼容性测试")
class TestRuleVersioning:
"""
规则版本管理测试
验收标准 (FeatureSummary.md F-06):
- 规则变更历史可追溯
- 支持回滚到历史版本
"""
@pytest.mark.unit
def test_rule_version_tracking(self) -> None:
"""测试规则版本追踪"""
# TODO: 实现规则版本管理
# rule_manager = RuleVersionManager()
#
# # 创建规则
# rule_v1 = rule_manager.create_rule({"word": "最", "severity": "hard"})
# assert rule_v1.version == "v1.0.0"
#
# # 更新规则
# rule_v2 = rule_manager.update_rule(rule_v1.id, {"severity": "soft"})
# assert rule_v2.version == "v1.1.0"
#
# # 查看历史
# history = rule_manager.get_history(rule_v1.id)
# assert len(history) == 2
pytest.skip("待实现:RuleVersionManager")
@pytest.mark.unit
def test_rule_rollback(self) -> None:
"""测试规则回滚"""
# TODO: 实现规则回滚
# rule_manager = RuleVersionManager()
#
# rule_v1 = rule_manager.create_rule({"word": "最", "severity": "hard"})
# rule_v2 = rule_manager.update_rule(rule_v1.id, {"severity": "soft"})
#
# # 回滚到 v1
# rolled_back = rule_manager.rollback(rule_v1.id, "v1.0.0")
# assert rolled_back.severity == "hard"
pytest.skip("待实现:规则回滚")
class TestPlatformRuleSync:
"""
平台规则同步测试
验收标准 (PRD.md):
- 平台规则变更后 ≤ 1 工作日内更新
"""
@pytest.mark.unit
def test_platform_rule_update_notification(self) -> None:
"""测试平台规则更新通知"""
# TODO: 实现平台规则同步
# sync_service = PlatformRuleSyncService()
#
# # 模拟抖音规则更新
# new_rules = {"forbidden_words": [{"word": "新违禁词", "category": "ad_law"}]}
# result = sync_service.sync_platform_rules("douyin", new_rules)
#
# assert result.updated
# assert result.notification_sent
pytest.skip("待实现:PlatformRuleSyncService")
@@ -0,0 +1,365 @@
"""
多模态时间戳对齐模块单元测试
TDD 测试用例 - 基于 DevelopmentPlan.md (F-14, F-45) 的验收标准
验收标准:
- 时长统计误差 ≤ 0.5秒
- 频次统计准确率 ≥ 95%
- 时间轴归一化精度 ≤ 0.1秒
- 模糊匹配容差窗口 ±0.5秒
"""
import pytest
from typing import Any
# 导入待实现的模块(TDD 红灯阶段)
# from app.utils.timestamp_align import (
# TimestampAligner,
# MultiModalEvent,
# AlignmentResult,
# )
class TestTimestampAligner:
"""
时间戳对齐器测试
验收标准:
- 时间轴归一化精度 ≤ 0.1秒
- 模糊匹配容差窗口 ±0.5秒
"""
@pytest.mark.unit
@pytest.mark.parametrize("asr_ts,ocr_ts,cv_ts,tolerance,expected_merged,expected_ts", [
# 完全对齐
(1000, 1000, 1000, 500, True, 1000),
# 容差范围内 - 应合并
(1000, 1200, 1100, 500, True, 1100), # 中位数
(1000, 1400, 1200, 500, True, 1200), # 中位数
# 超出容差 - 不应合并
(1000, 2000, 3000, 500, False, None),
(1000, 1600, 1000, 500, False, None), # OCR 超出容差
])
def test_multimodal_event_alignment(
self,
asr_ts: int,
ocr_ts: int,
cv_ts: int,
tolerance: int,
expected_merged: bool,
expected_ts: int | None,
) -> None:
"""测试多模态事件对齐"""
events = [
{"source": "asr", "timestamp_ms": asr_ts, "content": "测试文本"},
{"source": "ocr", "timestamp_ms": ocr_ts, "content": "字幕内容"},
{"source": "cv", "timestamp_ms": cv_ts, "content": "product_detected"},
]
# TODO: 实现 TimestampAligner
# aligner = TimestampAligner(tolerance_ms=tolerance)
# result = aligner.align_events(events)
#
# if expected_merged:
# assert len(result.merged_events) == 1
# assert abs(result.merged_events[0].timestamp_ms - expected_ts) <= 100
# else:
# # 未合并时,每个事件独立
# assert len(result.merged_events) == 3
pytest.skip("待实现:TimestampAligner")
@pytest.mark.unit
def test_timestamp_normalization_precision(self) -> None:
"""
测试时间戳归一化精度
验收标准:精度 ≤ 0.1秒 (100ms)
"""
# 不同来源的时间戳格式
asr_event = {"source": "asr", "timestamp_ms": 1500} # 毫秒
cv_event = {"source": "cv", "frame": 45, "fps": 30} # 帧号 (45/30 = 1.5秒)
ocr_event = {"source": "ocr", "timestamp_seconds": 1.5} # 秒
# TODO: 实现时间戳归一化
# aligner = TimestampAligner()
# normalized = aligner.normalize_timestamps([asr_event, cv_event, ocr_event])
#
# # 所有归一化后的时间戳应在 100ms 误差范围内
# timestamps = [e.timestamp_ms for e in normalized]
# assert max(timestamps) - min(timestamps) <= 100
pytest.skip("待实现:时间戳归一化")
@pytest.mark.unit
def test_fuzzy_matching_window(self) -> None:
"""
测试模糊匹配容差窗口
验收标准:容差 ±0.5秒
"""
# TODO: 实现模糊匹配
# aligner = TimestampAligner(tolerance_ms=500)
#
# # 1000ms 和 1499ms 应该匹配(差值 < 500ms
# assert aligner.is_within_tolerance(1000, 1499)
#
# # 1000ms 和 1501ms 不应匹配(差值 > 500ms
# assert not aligner.is_within_tolerance(1000, 1501)
pytest.skip("待实现:模糊匹配容差")
class TestDurationCalculation:
"""
时长统计测试
验收标准 (FeatureSummary.md F-45):
- 时长统计误差 ≤ 0.5秒
"""
@pytest.mark.unit
@pytest.mark.parametrize("start_ms,end_ms,expected_duration_ms,tolerance_ms", [
(0, 5000, 5000, 500),
(1000, 6500, 5500, 500),
(0, 10000, 10000, 500),
(500, 3200, 2700, 500),
])
def test_duration_calculation_accuracy(
self,
start_ms: int,
end_ms: int,
expected_duration_ms: int,
tolerance_ms: int,
) -> None:
"""测试时长计算准确性 - 误差 ≤ 0.5秒"""
events = [
{"timestamp_ms": start_ms, "type": "object_appear"},
{"timestamp_ms": end_ms, "type": "object_disappear"},
]
# TODO: 实现时长计算
# aligner = TimestampAligner()
# duration = aligner.calculate_duration(events)
#
# assert abs(duration - expected_duration_ms) <= tolerance_ms
pytest.skip("待实现:时长计算")
@pytest.mark.unit
def test_product_visible_duration(
self,
sample_cv_result: dict[str, Any],
) -> None:
"""测试产品可见时长统计"""
# sample_cv_result 包含 start_frame=30, end_frame=180, fps=30
# 预期时长: (180-30)/30 = 5 秒
# TODO: 实现产品时长统计
# aligner = TimestampAligner()
# duration = aligner.calculate_object_duration(
# sample_cv_result["detections"],
# object_type="product"
# )
#
# expected_duration_ms = 5000
# assert abs(duration - expected_duration_ms) <= 500
pytest.skip("待实现:产品可见时长统计")
@pytest.mark.unit
def test_multiple_segments_duration(self) -> None:
"""测试多段时长累加"""
# 产品在视频中多次出现
segments = [
{"start_ms": 0, "end_ms": 3000}, # 3秒
{"start_ms": 10000, "end_ms": 12000}, # 2秒
{"start_ms": 25000, "end_ms": 30000}, # 5秒
]
# 总时长应为 10秒
# TODO: 实现多段时长累加
# aligner = TimestampAligner()
# total_duration = aligner.calculate_total_duration(segments)
#
# assert abs(total_duration - 10000) <= 500
pytest.skip("待实现:多段时长累加")
class TestFrequencyCount:
"""
频次统计测试
验收标准 (FeatureSummary.md F-45):
- 频次统计准确率 ≥ 95%
"""
@pytest.mark.unit
def test_brand_mention_frequency(
self,
sample_asr_result: dict[str, Any],
) -> None:
"""测试品牌名提及频次统计"""
# TODO: 实现频次统计
# counter = FrequencyCounter()
# count = counter.count_mentions(
# sample_asr_result["segments"],
# keyword="品牌"
# )
#
# # 验证统计准确性
# assert count >= 0
pytest.skip("待实现:品牌名提及频次")
@pytest.mark.unit
@pytest.mark.parametrize("text_segments,keyword,expected_count", [
# 简单情况
(
[{"text": "这个品牌真不错"}, {"text": "品牌介绍"}, {"text": "品牌故事"}],
"品牌",
3
),
# 无匹配
(
[{"text": "产品介绍"}, {"text": "使用方法"}],
"品牌",
0
),
# 同一句多次出现
(
[{"text": "品牌品牌品牌"}],
"品牌",
3
),
])
def test_keyword_frequency_accuracy(
self,
text_segments: list[dict[str, str]],
keyword: str,
expected_count: int,
) -> None:
"""测试关键词频次准确性"""
# TODO: 实现频次统计
# counter = FrequencyCounter()
# count = counter.count_keyword(text_segments, keyword)
#
# assert count == expected_count
pytest.skip("待实现:关键词频次统计")
@pytest.mark.unit
def test_frequency_count_accuracy_rate(self) -> None:
"""
测试频次统计准确率
验收标准:准确率 ≥ 95%
"""
# TODO: 使用标注测试集验证
# test_cases = load_frequency_test_set()
# counter = FrequencyCounter()
#
# correct = 0
# for case in test_cases:
# count = counter.count_keyword(case["segments"], case["keyword"])
# if count == case["expected_count"]:
# correct += 1
#
# accuracy = correct / len(test_cases)
# assert accuracy >= 0.95
pytest.skip("待实现:频次准确率测试")
class TestMultiModalFusion:
"""
多模态融合测试
"""
@pytest.mark.unit
def test_asr_ocr_cv_fusion(
self,
sample_asr_result: dict[str, Any],
sample_ocr_result: dict[str, Any],
sample_cv_result: dict[str, Any],
) -> None:
"""测试 ASR + OCR + CV 三模态融合"""
# TODO: 实现多模态融合
# aligner = TimestampAligner()
# fused = aligner.fuse_multimodal(
# asr_result=sample_asr_result,
# ocr_result=sample_ocr_result,
# cv_result=sample_cv_result,
# )
#
# # 验证融合结果包含所有模态
# assert fused.has_asr
# assert fused.has_ocr
# assert fused.has_cv
#
# # 验证时间轴统一
# for event in fused.timeline:
# assert event.timestamp_ms is not None
pytest.skip("待实现:多模态融合")
@pytest.mark.unit
def test_cross_modality_consistency(self) -> None:
"""测试跨模态一致性检测"""
# ASR 说"产品名"OCR 显示"产品名"CV 检测到产品
# 三者应该在时间上一致
asr_event = {"source": "asr", "timestamp_ms": 5000, "content": "产品名"}
ocr_event = {"source": "ocr", "timestamp_ms": 5100, "content": "产品名"}
cv_event = {"source": "cv", "timestamp_ms": 5050, "content": "product"}
# TODO: 实现一致性检测
# aligner = TimestampAligner(tolerance_ms=500)
# consistency = aligner.check_consistency([asr_event, ocr_event, cv_event])
#
# assert consistency.is_consistent
# assert consistency.cross_modality_score >= 0.9
pytest.skip("待实现:跨模态一致性")
@pytest.mark.unit
def test_handle_missing_modality(self) -> None:
"""测试缺失模态处理"""
# 视频无字幕时,OCR 结果为空
asr_events = [{"source": "asr", "timestamp_ms": 1000, "content": "测试"}]
ocr_events = [] # 无 OCR 结果
cv_events = [{"source": "cv", "timestamp_ms": 1000, "content": "product"}]
# TODO: 实现缺失模态处理
# aligner = TimestampAligner()
# result = aligner.align_events(asr_events + ocr_events + cv_events)
#
# # 应正常处理,不报错
# assert result.status == "success"
# assert result.missing_modalities == ["ocr"]
pytest.skip("待实现:缺失模态处理")
class TestTimestampOutput:
"""
时间戳输出格式测试
"""
@pytest.mark.unit
def test_unified_timeline_format(self) -> None:
"""测试统一时间轴输出格式"""
# TODO: 实现时间轴输出
# aligner = TimestampAligner()
# timeline = aligner.get_unified_timeline(events)
#
# # 验证输出格式
# for entry in timeline:
# assert "timestamp_seconds" in entry
# assert "multimodal_events" in entry
# assert isinstance(entry["multimodal_events"], list)
pytest.skip("待实现:统一时间轴格式")
@pytest.mark.unit
def test_violation_with_timestamp(self) -> None:
"""测试违规项时间戳标注"""
# TODO: 实现违规时间戳
# violation = {
# "type": "forbidden_word",
# "content": "最好的",
# "timestamp_start": 5.0,
# "timestamp_end": 5.5,
# }
#
# assert violation["timestamp_end"] > violation["timestamp_start"]
pytest.skip("待实现:违规时间戳")
+275
View File
@@ -0,0 +1,275 @@
"""
数据验证器单元测试
TDD 测试用例 - 验证所有输入数据的格式和约束
"""
import pytest
from typing import Any
# 导入待实现的模块(TDD 红灯阶段)
# from app.utils.validators import (
# BriefValidator,
# VideoValidator,
# ReviewDecisionValidator,
# TaskValidator,
# )
class TestBriefValidator:
"""Brief 数据验证测试"""
@pytest.mark.unit
@pytest.mark.parametrize("platform,expected_valid", [
("douyin", True),
("xiaohongshu", True),
("bilibili", True),
("kuaishou", True),
("weibo", False), # 暂不支持
("unknown", False),
("", False),
(None, False),
])
def test_platform_validation(self, platform: str | None, expected_valid: bool) -> None:
"""测试平台验证"""
# TODO: 实现平台验证
# validator = BriefValidator()
# result = validator.validate_platform(platform)
# assert result.is_valid == expected_valid
pytest.skip("待实现:平台验证")
@pytest.mark.unit
@pytest.mark.parametrize("region,expected_valid", [
("mainland_china", True),
("hk_tw", True),
("overseas", True),
("unknown", False),
("", False),
])
def test_region_validation(self, region: str, expected_valid: bool) -> None:
"""测试区域验证"""
# TODO: 实现区域验证
# validator = BriefValidator()
# result = validator.validate_region(region)
# assert result.is_valid == expected_valid
pytest.skip("待实现:区域验证")
@pytest.mark.unit
def test_selling_points_structure(self) -> None:
"""测试卖点结构验证"""
valid_selling_points = [
{"text": "24小时持妆", "priority": "high"},
{"text": "天然成分", "priority": "medium"},
]
invalid_selling_points = [
{"text": ""}, # 缺少 priority,文本为空
"just a string", # 格式错误
]
# TODO: 实现卖点结构验证
# validator = BriefValidator()
#
# assert validator.validate_selling_points(valid_selling_points).is_valid
# assert not validator.validate_selling_points(invalid_selling_points).is_valid
pytest.skip("待实现:卖点结构验证")
class TestVideoValidator:
"""视频数据验证测试"""
@pytest.mark.unit
@pytest.mark.parametrize("duration_seconds,expected_valid", [
(30, True),
(60, True),
(300, True), # 5 分钟
(1800, True), # 30 分钟 - 边界
(3600, False), # 1 小时 - 可能需要警告
(0, False),
(-1, False),
])
def test_duration_validation(self, duration_seconds: int, expected_valid: bool) -> None:
"""测试视频时长验证"""
# TODO: 实现时长验证
# validator = VideoValidator()
# result = validator.validate_duration(duration_seconds)
# assert result.is_valid == expected_valid
pytest.skip("待实现:时长验证")
@pytest.mark.unit
@pytest.mark.parametrize("resolution,expected_valid", [
("1920x1080", True), # 1080p
("1080x1920", True), # 竖屏 1080p
("3840x2160", True), # 4K
("1280x720", True), # 720p
("640x480", False), # 480p - 太低
("320x240", False),
])
def test_resolution_validation(self, resolution: str, expected_valid: bool) -> None:
"""测试分辨率验证"""
# TODO: 实现分辨率验证
# validator = VideoValidator()
# result = validator.validate_resolution(resolution)
# assert result.is_valid == expected_valid
pytest.skip("待实现:分辨率验证")
class TestReviewDecisionValidator:
"""审核决策验证测试"""
@pytest.mark.unit
@pytest.mark.parametrize("decision,expected_valid", [
("passed", True),
("rejected", True),
("force_passed", True),
("pending", False), # 无效决策
("unknown", False),
("", False),
])
def test_decision_type_validation(self, decision: str, expected_valid: bool) -> None:
"""测试决策类型验证"""
# TODO: 实现决策验证
# validator = ReviewDecisionValidator()
# result = validator.validate_decision_type(decision)
# assert result.is_valid == expected_valid
pytest.skip("待实现:决策类型验证")
@pytest.mark.unit
def test_force_pass_requires_reason(self) -> None:
"""测试强制通过必须填写原因"""
# 强制通过但无原因
invalid_request = {
"decision": "force_passed",
"force_pass_reason": "",
}
# 强制通过有原因
valid_request = {
"decision": "force_passed",
"force_pass_reason": "达人玩的新梗,品牌方认可",
}
# TODO: 实现强制通过验证
# validator = ReviewDecisionValidator()
#
# assert not validator.validate(invalid_request).is_valid
# assert "原因" in validator.validate(invalid_request).error_message
#
# assert validator.validate(valid_request).is_valid
pytest.skip("待实现:强制通过原因验证")
@pytest.mark.unit
def test_rejection_requires_violations(self) -> None:
"""测试驳回必须选择违规项"""
# 驳回但无选择违规项
invalid_request = {
"decision": "rejected",
"selected_violations": [],
}
# 驳回并选择违规项
valid_request = {
"decision": "rejected",
"selected_violations": ["violation_001", "violation_002"],
}
# TODO: 实现驳回验证
# validator = ReviewDecisionValidator()
#
# assert not validator.validate(invalid_request).is_valid
# assert validator.validate(valid_request).is_valid
pytest.skip("待实现:驳回违规项验证")
class TestAppealValidator:
"""申诉验证测试"""
@pytest.mark.unit
@pytest.mark.parametrize("reason_length,expected_valid", [
(5, False), # < 10 字
(9, False), # < 10 字
(10, True), # = 10 字
(50, True), # > 10 字
(500, True), # 长文本
])
def test_appeal_reason_length(self, reason_length: int, expected_valid: bool) -> None:
"""测试申诉理由长度 - 必须 ≥ 10 字"""
reason = "" * reason_length
# TODO: 实现申诉验证
# validator = AppealValidator()
# result = validator.validate_reason(reason)
# assert result.is_valid == expected_valid
pytest.skip("待实现:申诉理由长度验证")
@pytest.mark.unit
def test_appeal_token_check(self) -> None:
"""测试申诉令牌检查"""
# TODO: 实现令牌验证
# validator = AppealValidator()
#
# # 有令牌
# result = validator.validate_token_available(user_id="user_001")
# assert result.is_valid
# assert result.remaining_tokens > 0
#
# # 无令牌
# result = validator.validate_token_available(user_id="user_no_tokens")
# assert not result.is_valid
pytest.skip("待实现:申诉令牌验证")
class TestTimestampValidator:
"""时间戳验证测试"""
@pytest.mark.unit
@pytest.mark.parametrize("timestamp_ms,video_duration_ms,expected_valid", [
(0, 60000, True), # 开始
(30000, 60000, True), # 中间
(60000, 60000, True), # 结束
(-1, 60000, False), # 负数
(70000, 60000, False), # 超出视频时长
])
def test_timestamp_range_validation(
self,
timestamp_ms: int,
video_duration_ms: int,
expected_valid: bool,
) -> None:
"""测试时间戳范围验证"""
# TODO: 实现时间戳验证
# validator = TimestampValidator()
# result = validator.validate_range(timestamp_ms, video_duration_ms)
# assert result.is_valid == expected_valid
pytest.skip("待实现:时间戳范围验证")
@pytest.mark.unit
def test_timestamp_order_validation(self) -> None:
"""测试时间戳顺序验证 - start < end"""
# TODO: 实现顺序验证
# validator = TimestampValidator()
#
# assert validator.validate_order(start=1000, end=2000).is_valid
# assert not validator.validate_order(start=2000, end=1000).is_valid
# assert not validator.validate_order(start=1000, end=1000).is_valid
pytest.skip("待实现:时间戳顺序验证")
class TestUUIDValidator:
"""UUID 验证测试"""
@pytest.mark.unit
@pytest.mark.parametrize("uuid_str,expected_valid", [
("550e8400-e29b-41d4-a716-446655440000", True),
("550E8400-E29B-41D4-A716-446655440000", True), # 大写
("not-a-uuid", False),
("", False),
("12345", False),
])
def test_uuid_format_validation(self, uuid_str: str, expected_valid: bool) -> None:
"""测试 UUID 格式验证"""
# TODO: 实现 UUID 验证
# validator = UUIDValidator()
# result = validator.validate(uuid_str)
# assert result.is_valid == expected_valid
pytest.skip("待实现:UUID 格式验证")
+411
View File
@@ -0,0 +1,411 @@
"""
视频审核模块单元测试
TDD 测试用例 - 基于 FeatureSummary.md (F-10~F-18) 的验收标准
验收标准:
- 100MB 视频审核 ≤ 5 分钟
- 竞品 Logo F1 ≥ 0.85
- ASR 字错率 ≤ 10%
- OCR 准确率 ≥ 95%
"""
import pytest
from typing import Any
# 导入待实现的模块(TDD 红灯阶段)
# from app.services.video_auditor import VideoAuditor, AuditReport
class TestVideoUpload:
"""
视频上传测试
验收标准 (FeatureSummary.md F-10):
- 支持 ≤ 100MB 视频
- 支持 MP4/MOV 格式
- 支持断点续传
"""
@pytest.mark.unit
@pytest.mark.parametrize("file_size_mb,expected_valid", [
(50, True),
(100, True),
(101, False),
(200, False),
])
def test_file_size_validation(self, file_size_mb: int, expected_valid: bool) -> None:
"""测试文件大小验证 - 最大 100MB"""
file_size_bytes = file_size_mb * 1024 * 1024
# TODO: 实现文件大小验证
# validator = VideoFileValidator()
# result = validator.validate_size(file_size_bytes)
#
# assert result.is_valid == expected_valid
# if not expected_valid:
# assert "100MB" in result.error_message
pytest.skip("待实现:文件大小验证")
@pytest.mark.unit
@pytest.mark.parametrize("file_format,mime_type,expected_valid", [
("mp4", "video/mp4", True),
("mov", "video/quicktime", True),
("avi", "video/x-msvideo", False),
("mkv", "video/x-matroska", False),
("pdf", "application/pdf", False),
])
def test_file_format_validation(
self,
file_format: str,
mime_type: str,
expected_valid: bool,
) -> None:
"""测试文件格式验证 - 仅支持 MP4/MOV"""
# TODO: 实现格式验证
# validator = VideoFileValidator()
# result = validator.validate_format(file_format, mime_type)
#
# assert result.is_valid == expected_valid
pytest.skip("待实现:文件格式验证")
class TestASRAccuracy:
"""
ASR 语音识别测试
验收标准 (DevelopmentPlan.md):
- 字错率 (WER) ≤ 10%
"""
@pytest.mark.unit
def test_asr_output_format(self) -> None:
"""测试 ASR 输出格式"""
# TODO: 实现 ASR 服务
# asr = ASRService()
# result = asr.transcribe("test_audio.wav")
#
# assert "text" in result
# assert "segments" in result
# for segment in result["segments"]:
# assert "word" in segment
# assert "start_ms" in segment
# assert "end_ms" in segment
# assert "confidence" in segment
# assert segment["end_ms"] >= segment["start_ms"]
pytest.skip("待实现:ASR 输出格式")
@pytest.mark.unit
def test_asr_word_error_rate(self) -> None:
"""
测试 ASR 字错率
验收标准:WER ≤ 10%
"""
# TODO: 使用标注测试集验证
# asr = ASRService()
# test_set = load_asr_test_set() # 标注数据集
#
# total_errors = 0
# total_words = 0
#
# for sample in test_set:
# result = asr.transcribe(sample["audio_path"])
# wer = calculate_wer(result["text"], sample["ground_truth"])
# total_errors += wer * len(sample["ground_truth"].split())
# total_words += len(sample["ground_truth"].split())
#
# overall_wer = total_errors / total_words
# assert overall_wer <= 0.10, f"WER {overall_wer:.2%} 超过阈值 10%"
pytest.skip("待实现:ASR 字错率测试")
@pytest.mark.unit
def test_asr_timestamp_accuracy(self) -> None:
"""测试 ASR 时间戳准确性"""
# TODO: 实现时间戳验证
# asr = ASRService()
# result = asr.transcribe("test_audio.wav")
#
# # 时间戳应递增
# prev_end = 0
# for segment in result["segments"]:
# assert segment["start_ms"] >= prev_end
# prev_end = segment["end_ms"]
pytest.skip("待实现:ASR 时间戳准确性")
class TestOCRAccuracy:
"""
OCR 字幕识别测试
验收标准 (DevelopmentPlan.md):
- 准确率 ≥ 95%(含复杂背景)
"""
@pytest.mark.unit
def test_ocr_output_format(self) -> None:
"""测试 OCR 输出格式"""
# TODO: 实现 OCR 服务
# ocr = OCRService()
# result = ocr.extract_text("video_frame.jpg")
#
# assert "frames" in result
# for frame in result["frames"]:
# assert "timestamp_ms" in frame
# assert "text" in frame
# assert "confidence" in frame
# assert "bbox" in frame
pytest.skip("待实现:OCR 输出格式")
@pytest.mark.unit
def test_ocr_accuracy_rate(self) -> None:
"""
测试 OCR 准确率
验收标准:准确率 ≥ 95%
"""
# TODO: 使用标注测试集验证
# ocr = OCRService()
# test_set = load_ocr_test_set()
#
# correct = 0
# for sample in test_set:
# result = ocr.extract_text(sample["image_path"])
# if result["text"] == sample["ground_truth"]:
# correct += 1
#
# accuracy = correct / len(test_set)
# assert accuracy >= 0.95, f"准确率 {accuracy:.2%} 低于阈值 95%"
pytest.skip("待实现:OCR 准确率测试")
@pytest.mark.unit
def test_ocr_complex_background(self) -> None:
"""测试复杂背景下的 OCR"""
# TODO: 测试复杂背景
# ocr = OCRService()
#
# # 测试不同背景复杂度
# test_cases = [
# {"image": "simple_bg.jpg", "text": "测试文字"},
# {"image": "complex_bg.jpg", "text": "复杂背景"},
# {"image": "gradient_bg.jpg", "text": "渐变背景"},
# ]
#
# for case in test_cases:
# result = ocr.extract_text(case["image"])
# assert result["text"] == case["text"]
pytest.skip("待实现:复杂背景 OCR")
class TestLogoDetection:
"""
竞品 Logo 检测测试
验收标准 (FeatureSummary.md F-12):
- F1 ≥ 0.85(含遮挡 30% 场景)
"""
@pytest.mark.unit
def test_logo_detection_output_format(self) -> None:
"""测试 Logo 检测输出格式"""
# TODO: 实现 Logo 检测服务
# detector = LogoDetector()
# result = detector.detect("video_frame.jpg")
#
# assert "detections" in result
# for detection in result["detections"]:
# assert "logo_id" in detection
# assert "confidence" in detection
# assert "bbox" in detection
# assert detection["confidence"] >= 0 and detection["confidence"] <= 1
pytest.skip("待实现:Logo 检测输出格式")
@pytest.mark.unit
def test_logo_detection_f1_score(self) -> None:
"""
测试 Logo 检测 F1 值
验收标准:F1 ≥ 0.85
"""
# TODO: 使用标注测试集验证
# detector = LogoDetector()
# test_set = load_logo_test_set() # ≥ 200 张图片
#
# predictions = []
# ground_truths = []
#
# for sample in test_set:
# result = detector.detect(sample["image_path"])
# predictions.append(result["detections"])
# ground_truths.append(sample["ground_truth_logos"])
#
# f1 = calculate_f1(predictions, ground_truths)
# assert f1 >= 0.85, f"F1 {f1:.2f} 低于阈值 0.85"
pytest.skip("待实现:Logo F1 测试")
@pytest.mark.unit
def test_logo_detection_with_occlusion(self) -> None:
"""
测试遮挡场景下的 Logo 检测
验收标准:30% 遮挡仍可检测
"""
# TODO: 测试遮挡场景
# detector = LogoDetector()
#
# # 30% 遮挡的 Logo 图片
# result = detector.detect("logo_30_percent_occluded.jpg")
#
# assert len(result["detections"]) > 0
# assert result["detections"][0]["confidence"] >= 0.7
pytest.skip("待实现:遮挡场景 Logo 检测")
@pytest.mark.unit
def test_new_logo_instant_effect(self) -> None:
"""测试新 Logo 上传即刻生效"""
# TODO: 测试动态添加 Logo
# detector = LogoDetector()
#
# # 上传新 Logo
# detector.add_logo("new_competitor_logo.png", brand="New Competitor")
#
# # 立即测试检测
# result = detector.detect("frame_with_new_logo.jpg")
# assert any(d["brand"] == "New Competitor" for d in result["detections"])
pytest.skip("待实现:Logo 动态添加")
class TestAuditPipeline:
"""
审核流水线集成测试
"""
@pytest.mark.unit
def test_audit_processing_time(self) -> None:
"""
测试审核处理时间
验收标准:100MB 视频 ≤ 5 分钟
"""
# TODO: 实现处理时间测试
# import time
#
# auditor = VideoAuditor()
# start_time = time.time()
#
# result = auditor.audit("100mb_test_video.mp4")
#
# processing_time = time.time() - start_time
# assert processing_time <= 300, f"处理时间 {processing_time:.1f}s 超过 5 分钟"
pytest.skip("待实现:处理时间测试")
@pytest.mark.unit
def test_audit_report_structure(self) -> None:
"""测试审核报告结构"""
# TODO: 实现报告结构验证
# auditor = VideoAuditor()
# report = auditor.audit("test_video.mp4")
#
# # 验证报告必需字段
# required_fields = [
# "report_id", "video_id", "processing_status",
# "asr_results", "ocr_results", "cv_results",
# "violations", "brief_compliance"
# ]
# for field in required_fields:
# assert field in report
pytest.skip("待实现:报告结构验证")
@pytest.mark.unit
def test_violation_with_evidence(self) -> None:
"""测试违规项包含证据"""
# TODO: 实现证据验证
# auditor = VideoAuditor()
# report = auditor.audit("video_with_violation.mp4")
#
# for violation in report["violations"]:
# assert "evidence" in violation
# assert violation["evidence"]["url"] is not None
# assert violation["evidence"]["timestamp_start"] is not None
pytest.skip("待实现:违规证据")
class TestBriefCompliance:
"""
Brief 合规检查测试
验收标准 (FeatureSummary.md F-45):
- 时长统计误差 ≤ 0.5秒
- 频次统计准确率 ≥ 95%
"""
@pytest.mark.unit
def test_selling_point_coverage(
self,
sample_brief_rules: dict[str, Any],
) -> None:
"""测试卖点覆盖检测"""
video_content = {
"asr_text": "24小时持妆效果非常好,使用天然成分",
"ocr_text": "24小时持妆",
}
# TODO: 实现卖点覆盖检测
# checker = BriefComplianceChecker()
# result = checker.check_selling_points(
# video_content,
# sample_brief_rules["selling_points"]
# )
#
# # 应检测到 2/3 卖点覆盖
# assert result["coverage_rate"] >= 0.66
# assert "24小时持妆" in result["detected"]
# assert "天然成分" in result["detected"]
pytest.skip("待实现:卖点覆盖检测")
@pytest.mark.unit
def test_duration_requirement_check(
self,
sample_brief_rules: dict[str, Any],
) -> None:
"""测试时长要求检查"""
cv_detections = [
{"object_type": "product", "start_ms": 0, "end_ms": 6000}, # 6秒
]
# 要求: 产品同框 > 5秒
# TODO: 实现时长检查
# checker = BriefComplianceChecker()
# result = checker.check_duration(
# cv_detections,
# sample_brief_rules["timing_requirements"]
# )
#
# assert result["product_visible"]["status"] == "passed"
# assert result["product_visible"]["detected_seconds"] == 6.0
pytest.skip("待实现:时长要求检查")
@pytest.mark.unit
def test_frequency_requirement_check(
self,
sample_brief_rules: dict[str, Any],
) -> None:
"""测试频次要求检查"""
asr_segments = [
{"text": "品牌名产品"},
{"text": "这个品牌名很好"},
{"text": "推荐品牌名"},
]
# 要求: 品牌名提及 ≥ 3次
# TODO: 实现频次检查
# checker = BriefComplianceChecker()
# result = checker.check_frequency(
# asr_segments,
# sample_brief_rules["timing_requirements"],
# brand_keyword="品牌名"
# )
#
# assert result["brand_mention"]["status"] == "passed"
# assert result["brand_mention"]["detected_count"] == 3
pytest.skip("待实现:频次要求检查")