基于项目需求文档(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>
328 lines
11 KiB
Python
328 lines
11 KiB
Python
"""
|
||
ASR 服务单元测试
|
||
|
||
TDD 测试用例 - 基于 DevelopmentPlan.md 的验收标准
|
||
|
||
验收标准:
|
||
- 字错率 (WER) ≤ 10%
|
||
- 时间戳精度 ≤ 100ms
|
||
"""
|
||
|
||
import pytest
|
||
from typing import Any
|
||
|
||
# 导入待实现的模块(TDD 红灯阶段)
|
||
# from app.services.ai.asr import ASRService, ASRResult, ASRSegment
|
||
|
||
|
||
class TestASRService:
|
||
"""ASR 服务测试"""
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_asr_service_initialization(self) -> None:
|
||
"""测试 ASR 服务初始化"""
|
||
# TODO: 实现 ASR 服务
|
||
# service = ASRService()
|
||
# assert service.is_ready()
|
||
# assert service.model_name is not None
|
||
pytest.skip("待实现:ASR 服务初始化")
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_asr_transcribe_audio_file(self) -> None:
|
||
"""测试音频文件转写"""
|
||
# TODO: 实现音频转写
|
||
# service = ASRService()
|
||
# result = service.transcribe("tests/fixtures/audio/sample.wav")
|
||
#
|
||
# assert result.status == "success"
|
||
# assert result.text is not None
|
||
# assert len(result.text) > 0
|
||
pytest.skip("待实现:音频转写")
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_asr_output_format(self) -> None:
|
||
"""测试 ASR 输出格式"""
|
||
# TODO: 实现 ASR 服务
|
||
# service = ASRService()
|
||
# result = service.transcribe("tests/fixtures/audio/sample.wav")
|
||
#
|
||
# # 验证输出结构
|
||
# assert hasattr(result, "text")
|
||
# assert hasattr(result, "segments")
|
||
# assert hasattr(result, "language")
|
||
# assert hasattr(result, "duration_ms")
|
||
#
|
||
# # 验证 segment 结构
|
||
# for segment in result.segments:
|
||
# assert hasattr(segment, "text")
|
||
# assert hasattr(segment, "start_ms")
|
||
# assert hasattr(segment, "end_ms")
|
||
# assert hasattr(segment, "confidence")
|
||
# assert segment.end_ms >= segment.start_ms
|
||
pytest.skip("待实现:ASR 输出格式")
|
||
|
||
|
||
class TestASRAccuracy:
|
||
"""ASR 准确率测试"""
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_word_error_rate_threshold(self) -> None:
|
||
"""
|
||
测试字错率阈值
|
||
|
||
验收标准:WER ≤ 10%
|
||
"""
|
||
# TODO: 使用标注测试集验证
|
||
# service = ASRService()
|
||
# test_cases = load_asr_labeled_dataset()
|
||
#
|
||
# total_errors = 0
|
||
# total_words = 0
|
||
#
|
||
# for case in test_cases:
|
||
# result = service.transcribe(case["audio_path"])
|
||
# wer = calculate_word_error_rate(
|
||
# result.text,
|
||
# case["ground_truth"]
|
||
# )
|
||
# total_errors += wer * len(case["ground_truth"])
|
||
# total_words += len(case["ground_truth"])
|
||
#
|
||
# overall_wer = total_errors / total_words
|
||
# assert overall_wer <= 0.10, f"WER {overall_wer:.2%} 超过阈值 10%"
|
||
pytest.skip("待实现:WER 测试")
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
@pytest.mark.parametrize("audio_type,expected_wer_threshold", [
|
||
("clean_speech", 0.05), # 清晰语音 WER < 5%
|
||
("background_music", 0.10), # 背景音乐 WER < 10%
|
||
("multiple_speakers", 0.15), # 多人对话 WER < 15%
|
||
("noisy_environment", 0.20), # 嘈杂环境 WER < 20%
|
||
])
|
||
def test_wer_by_audio_type(
|
||
self,
|
||
audio_type: str,
|
||
expected_wer_threshold: float,
|
||
) -> None:
|
||
"""测试不同音频类型的 WER"""
|
||
# TODO: 实现分类型 WER 测试
|
||
# service = ASRService()
|
||
# test_cases = load_asr_test_set_by_type(audio_type)
|
||
#
|
||
# wer = calculate_average_wer(service, test_cases)
|
||
# assert wer <= expected_wer_threshold
|
||
pytest.skip(f"待实现:{audio_type} WER 测试")
|
||
|
||
|
||
class TestASRTimestamp:
|
||
"""ASR 时间戳测试"""
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_timestamp_monotonic_increase(self) -> None:
|
||
"""测试时间戳单调递增"""
|
||
# TODO: 实现时间戳验证
|
||
# service = ASRService()
|
||
# result = service.transcribe("tests/fixtures/audio/sample.wav")
|
||
#
|
||
# prev_end = 0
|
||
# for segment in result.segments:
|
||
# assert segment.start_ms >= prev_end, \
|
||
# f"时间戳不是单调递增: {segment.start_ms} < {prev_end}"
|
||
# prev_end = segment.end_ms
|
||
pytest.skip("待实现:时间戳单调递增")
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_timestamp_precision(self) -> None:
|
||
"""
|
||
测试时间戳精度
|
||
|
||
验收标准:精度 ≤ 100ms
|
||
"""
|
||
# TODO: 使用标注测试集验证
|
||
# service = ASRService()
|
||
# test_cases = load_timestamp_labeled_dataset()
|
||
#
|
||
# total_error = 0
|
||
# total_segments = 0
|
||
#
|
||
# for case in test_cases:
|
||
# result = service.transcribe(case["audio_path"])
|
||
# for i, segment in enumerate(result.segments):
|
||
# if i < len(case["ground_truth_timestamps"]):
|
||
# gt = case["ground_truth_timestamps"][i]
|
||
# start_error = abs(segment.start_ms - gt["start_ms"])
|
||
# end_error = abs(segment.end_ms - gt["end_ms"])
|
||
# total_error += (start_error + end_error) / 2
|
||
# total_segments += 1
|
||
#
|
||
# avg_error = total_error / total_segments if total_segments > 0 else 0
|
||
# assert avg_error <= 100, f"平均时间戳误差 {avg_error:.0f}ms 超过阈值 100ms"
|
||
pytest.skip("待实现:时间戳精度测试")
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_timestamp_within_audio_duration(self) -> None:
|
||
"""测试时间戳在音频时长范围内"""
|
||
# TODO: 实现边界验证
|
||
# service = ASRService()
|
||
# result = service.transcribe("tests/fixtures/audio/sample.wav")
|
||
#
|
||
# for segment in result.segments:
|
||
# assert segment.start_ms >= 0
|
||
# assert segment.end_ms <= result.duration_ms
|
||
pytest.skip("待实现:时间戳边界验证")
|
||
|
||
|
||
class TestASRLanguage:
|
||
"""ASR 语言处理测试"""
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_chinese_mandarin_recognition(self) -> None:
|
||
"""测试普通话识别"""
|
||
# TODO: 实现普通话测试
|
||
# service = ASRService()
|
||
# result = service.transcribe("tests/fixtures/audio/mandarin.wav")
|
||
#
|
||
# assert result.language == "zh-CN"
|
||
# assert "你好" in result.text or len(result.text) > 0
|
||
pytest.skip("待实现:普通话识别")
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_mixed_language_handling(self) -> None:
|
||
"""测试中英混合语音处理"""
|
||
# TODO: 实现混合语言测试
|
||
# service = ASRService()
|
||
# result = service.transcribe("tests/fixtures/audio/mixed_cn_en.wav")
|
||
#
|
||
# # 应能识别中英文混合内容
|
||
# assert result.status == "success"
|
||
pytest.skip("待实现:中英混合识别")
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_dialect_handling(self) -> None:
|
||
"""测试方言处理"""
|
||
# TODO: 实现方言测试
|
||
# service = ASRService()
|
||
#
|
||
# # 方言可能降级处理或提示
|
||
# result = service.transcribe("tests/fixtures/audio/cantonese.wav")
|
||
#
|
||
# if result.status == "success":
|
||
# assert result.language in ["zh-CN", "zh-HK", "yue"]
|
||
# else:
|
||
# assert result.warning == "dialect_detected"
|
||
pytest.skip("待实现:方言处理")
|
||
|
||
|
||
class TestASRSpecialCases:
|
||
"""ASR 特殊情况测试"""
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_silent_audio(self) -> None:
|
||
"""测试静音音频"""
|
||
# TODO: 实现静音测试
|
||
# service = ASRService()
|
||
# result = service.transcribe("tests/fixtures/audio/silent.wav")
|
||
#
|
||
# assert result.status == "success"
|
||
# assert result.text == "" or result.segments == []
|
||
pytest.skip("待实现:静音音频处理")
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_very_short_audio(self) -> None:
|
||
"""测试极短音频 (< 1秒)"""
|
||
# TODO: 实现极短音频测试
|
||
# service = ASRService()
|
||
# result = service.transcribe("tests/fixtures/audio/short_500ms.wav")
|
||
#
|
||
# assert result.status == "success"
|
||
pytest.skip("待实现:极短音频处理")
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_long_audio(self) -> None:
|
||
"""测试长音频 (> 5分钟)"""
|
||
# TODO: 实现长音频测试
|
||
# service = ASRService()
|
||
# result = service.transcribe("tests/fixtures/audio/long_10min.wav")
|
||
#
|
||
# assert result.status == "success"
|
||
# assert result.duration_ms >= 600000 # 10分钟
|
||
pytest.skip("待实现:长音频处理")
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.unit
|
||
def test_corrupted_audio_handling(self) -> None:
|
||
"""测试损坏音频处理"""
|
||
# TODO: 实现错误处理测试
|
||
# service = ASRService()
|
||
# result = service.transcribe("tests/fixtures/audio/corrupted.wav")
|
||
#
|
||
# assert result.status == "error"
|
||
# assert "corrupted" in result.error_message.lower() or \
|
||
# "invalid" in result.error_message.lower()
|
||
pytest.skip("待实现:损坏音频处理")
|
||
|
||
|
||
class TestASRPerformance:
|
||
"""ASR 性能测试"""
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.performance
|
||
def test_transcription_speed(self) -> None:
|
||
"""
|
||
测试转写速度
|
||
|
||
验收标准:实时率 ≤ 0.5 (转写时间 / 音频时长)
|
||
"""
|
||
# TODO: 实现性能测试
|
||
# import time
|
||
#
|
||
# service = ASRService()
|
||
#
|
||
# # 60秒测试音频
|
||
# start_time = time.time()
|
||
# result = service.transcribe("tests/fixtures/audio/60s_sample.wav")
|
||
# processing_time = time.time() - start_time
|
||
#
|
||
# audio_duration = result.duration_ms / 1000
|
||
# real_time_factor = processing_time / audio_duration
|
||
#
|
||
# assert real_time_factor <= 0.5, \
|
||
# f"实时率 {real_time_factor:.2f} 超过阈值 0.5"
|
||
pytest.skip("待实现:转写速度测试")
|
||
|
||
@pytest.mark.ai
|
||
@pytest.mark.performance
|
||
def test_concurrent_transcription(self) -> None:
|
||
"""测试并发转写"""
|
||
# TODO: 实现并发测试
|
||
# import asyncio
|
||
#
|
||
# service = ASRService()
|
||
#
|
||
# async def transcribe_one(audio_path: str):
|
||
# return await service.transcribe_async(audio_path)
|
||
#
|
||
# # 并发处理 5 个音频
|
||
# tasks = [
|
||
# transcribe_one(f"tests/fixtures/audio/sample_{i}.wav")
|
||
# for i in range(5)
|
||
# ]
|
||
# results = await asyncio.gather(*tasks)
|
||
#
|
||
# assert all(r.status == "success" for r in results)
|
||
pytest.skip("待实现:并发转写测试")
|