"""SRTProcessor 集成测试 - LLM 语义分段 测试 SRTProcessor 与 SemanticSegmentationEngine 的集成: - 语义分段模式 - 规则分段模式(向后兼容) - AI 不可用时降级到规则分段 - 带标题的分段输出 """ import asyncio import json import pytest from pathlib import Path from typing import List, Optional from unittest.mock import Mock, MagicMock, patch # ============================================================================= # Test Fixtures # ============================================================================= @pytest.fixture def sample_srt_content() -> str: """标准 SRT 格式内容""" return """1 00:00:00,000 --> 00:00:05,000 机器学习是人工智能的一个核心分支。 2 00:00:05,000 --> 00:00:10,000 它使计算机能够从数据中学习并改进。 3 00:00:10,000 --> 00:00:15,000 深度学习是机器学习的一个子领域。 4 00:00:15,000 --> 00:00:20,000 神经网络是深度学习的基础。 5 00:00:20,000 --> 00:00:25,000 这些技术已经广泛应用于图像识别和自然语言处理。 """ @pytest.fixture def srt_test_files(tmp_path: Path, sample_srt_content: str) -> dict: """创建测试用的 SRT 和元数据文件""" # 创建 SRT 文件 srt_file = tmp_path / "test.srt" srt_file.write_text(sample_srt_content, encoding="utf-8") # 创建元数据文件 metadata = { "title": "机器学习入门教程", "author": "测试作者", "platform": "bilibili", "bvid": "BV1test123", "duration": 60, "url": "https://www.bilibili.com/video/BV1test123", "pubtime": "2026-03-22" } metadata_file = tmp_path / "metadata.json" metadata_file.write_text(json.dumps(metadata, ensure_ascii=False), encoding="utf-8") # 输出文件路径 output_file = tmp_path / "output.md" return { "srt_file": str(srt_file), "metadata_file": str(metadata_file), "output_file": str(output_file), "metadata": metadata } @pytest.fixture def mock_ai_client(): """Mock AI 客户端,返回语义分段结果""" mock_client = Mock() mock_client.api_key = "test-api-key" # Mock LLM 响应 llm_response = """```json { "video_type": "知识类", "segments": [ { "title": "机器学习概述", "content": "机器学习是人工智能的一个核心分支。它使计算机能够从数据中学习并改进。", "start_index": 0, "end_index": 42 }, { "title": "深度学习基础", "content": "深度学习是机器学习的一个子领域。神经网络是深度学习的基础。", "start_index": 43, "end_index": 92 }, { "title": null, "content": "这些技术已经广泛应用于图像识别和自然语言处理。", "start_index": 93, "end_index": 138 } ], "actual_segment_count": 3 } ```""" mock_client.generate = Mock(return_value=llm_response) return mock_client # ============================================================================= # Test 1: 语义分段模式 # ============================================================================= class TestSRTProcessorSemanticMode: """测试 SRTProcessor 语义分段模式""" @patch('kb_workflow.deep_analyzer.SimpleAIClient') def test_semantic_segmentation_enabled(self, mock_simple_ai_cls, srt_test_files, mock_ai_client): """启用语义分段时使用 SemanticSegmentationEngine""" from kb_workflow.pipeline.processors.srt import SRTProcessor from kb_workflow.pipeline.schemas import SRTInput from kb_workflow.srt.semantic_engine import SemanticSegmentationEngine # 让 SimpleAIClient() 返回我们的 mock 客户端 mock_simple_ai_cls.return_value = mock_ai_client processor = SRTProcessor(enable_semantic_segmentation=True, enable_correction=False) # 验证使用了 SemanticSegmentationEngine assert isinstance(processor.segmenter, SemanticSegmentationEngine) input_data = SRTInput( content_id="BV1test123", platform="bilibili", srt_file=srt_test_files["srt_file"], metadata_file=srt_test_files["metadata_file"], output_file=srt_test_files["output_file"] ) result = asyncio.run(processor.process(input_data)) assert result.success assert Path(result.output_file).exists() @patch('kb_workflow.deep_analyzer.SimpleAIClient') def test_semantic_segmentation_generates_titles(self, mock_simple_ai_cls, srt_test_files, mock_ai_client): """语义分段生成的 Markdown 包含标题""" from kb_workflow.pipeline.processors.srt import SRTProcessor from kb_workflow.pipeline.schemas import SRTInput mock_simple_ai_cls.return_value = mock_ai_client processor = SRTProcessor(enable_semantic_segmentation=True, enable_correction=False) input_data = SRTInput( content_id="BV1test123", platform="bilibili", srt_file=srt_test_files["srt_file"], metadata_file=srt_test_files["metadata_file"], output_file=srt_test_files["output_file"] ) result = asyncio.run(processor.process(input_data)) # 验证输出包含标题 content = Path(result.output_file).read_text(encoding="utf-8") assert "## 机器学习概述" in content assert "## 深度学习基础" in content # 最后一个分段没有标题(title: null) assert content.count("## ") == 2 @patch('kb_workflow.deep_analyzer.SimpleAIClient') def test_semantic_segmentation_uses_metadata(self, mock_simple_ai_cls, srt_test_files, mock_ai_client): """语义分段使用元数据构建 Prompt""" from kb_workflow.pipeline.processors.srt import SRTProcessor from kb_workflow.pipeline.schemas import SRTInput mock_simple_ai_cls.return_value = mock_ai_client processor = SRTProcessor(enable_semantic_segmentation=True, enable_correction=False) input_data = SRTInput( content_id="BV1test123", platform="bilibili", srt_file=srt_test_files["srt_file"], metadata_file=srt_test_files["metadata_file"], output_file=srt_test_files["output_file"] ) result = asyncio.run(processor.process(input_data)) # 验证 AI 客户端被调用 assert mock_ai_client.generate.called # 验证 Prompt 包含元数据 call_args = mock_ai_client.generate.call_args prompt = call_args[0][0] assert "机器学习入门教程" in prompt assert "知识类" in prompt # ============================================================================= # Test 2: 规则分段模式(向后兼容) # ============================================================================= class TestSRTProcessorRuleBasedMode: """测试 SRTProcessor 规则分段模式(向后兼容)""" def test_rule_based_segmentation_default(self, srt_test_files): """默认使用规则分段(enable_semantic_segmentation=False)""" from kb_workflow.pipeline.processors.srt import SRTProcessor from kb_workflow.pipeline.schemas import SRTInput from kb_workflow.srt.segmenter import SemanticSegmenter processor = SRTProcessor(enable_correction=False) # 验证使用了规则分段器 assert isinstance(processor.segmenter, SemanticSegmenter) assert not processor.enable_semantic_segmentation input_data = SRTInput( content_id="BV1test123", platform="bilibili", srt_file=srt_test_files["srt_file"], metadata_file=srt_test_files["metadata_file"], output_file=srt_test_files["output_file"] ) result = asyncio.run(processor.process(input_data)) assert result.success assert Path(result.output_file).exists() # 验证输出格式(规则分段没有标题) content = Path(result.output_file).read_text(encoding="utf-8") assert "## 内容" in content # 规则分段不会有 "## 机器学习概述" 这样的标题 def test_rule_based_segmentation_explicit(self, srt_test_files): """显式禁用语义分段""" from kb_workflow.pipeline.processors.srt import SRTProcessor from kb_workflow.pipeline.schemas import SRTInput from kb_workflow.srt.segmenter import SemanticSegmenter processor = SRTProcessor(enable_semantic_segmentation=False, enable_correction=False) assert isinstance(processor.segmenter, SemanticSegmenter) input_data = SRTInput( content_id="BV1test123", platform="bilibili", srt_file=srt_test_files["srt_file"], metadata_file=srt_test_files["metadata_file"], output_file=srt_test_files["output_file"] ) result = asyncio.run(processor.process(input_data)) assert result.success # ============================================================================= # Test 3: AI 不可用时降级 # ============================================================================= class TestSRTProcessorFallback: """测试 AI 不可用时降级到规则分段""" def test_ai_unavailable_falls_back_to_rule_based(self, srt_test_files): """AI 客户端不可用时自动降级到规则分段""" from kb_workflow.pipeline.processors.srt import SRTProcessor from kb_workflow.pipeline.schemas import SRTInput from kb_workflow.srt.segmenter import SemanticSegmenter # Mock SimpleAIClient 没有配置 API key class MockSimpleAIClientNoKey: def __init__(self): self.api_key = None original_simple_client = None def mock_import(): nonlocal original_simple_client # 保存原始导入 import kb_workflow.deep_analyzer as original_module if hasattr(original_module, 'SimpleAIClient'): original_simple_client = original_module.SimpleAIClient # 返回 mock 客户端类 return MockSimpleAIClientNoKey # 使用 monkeypatch 修改导入 import sys if 'kb_workflow.deep_analyzer' in sys.modules: original_module = sys.modules['kb_workflow.deep_analyzer'] original_client = getattr(original_module, 'SimpleAIClient', None) with pytest.MonkeyPatch().context() as m: # 设置 mock m.setattr(original_module, 'SimpleAIClient', MockSimpleAIClientNoKey) processor = SRTProcessor(enable_semantic_segmentation=True, enable_correction=False) # 验证降级到规则分段器 assert isinstance(processor.segmenter, SemanticSegmenter) input_data = SRTInput( content_id="BV1test123", platform="bilibili", srt_file=srt_test_files["srt_file"], metadata_file=srt_test_files["metadata_file"], output_file=srt_test_files["output_file"] ) result = asyncio.run(processor.process(input_data)) assert result.success def test_semantic_engine_init_failure_falls_back(self, srt_test_files): """SemanticSegmentationEngine 初始化失败时降级""" from kb_workflow.pipeline.processors.srt import SRTProcessor from kb_workflow.pipeline.schemas import SRTInput from kb_workflow.srt.segmenter import SemanticSegmenter # Mock 导入失败 def mock_import_error(name, *args, **kwargs): if name == "kb_workflow.srt.semantic_engine": raise ImportError("Mock import error") return __import__(name, *args, **kwargs) with pytest.MonkeyPatch().context() as m: # 注意:这个测试比较复杂,因为 import 缓存 # 实际使用中,如果 semantic_engine.py 不存在或有语法错误,会自动降级 pass # ============================================================================= # Test 4: 输出格式验证 # ============================================================================= class TestSRTProcessorOutputFormat: """测试输出格式兼容性""" @patch('kb_workflow.deep_analyzer.SimpleAIClient') def test_semantic_output_has_valid_frontmatter(self, mock_simple_ai_cls, srt_test_files, mock_ai_client): """语义分段输出的 frontmatter 格式正确""" from kb_workflow.pipeline.processors.srt import SRTProcessor from kb_workflow.pipeline.schemas import SRTInput mock_simple_ai_cls.return_value = mock_ai_client processor = SRTProcessor(enable_semantic_segmentation=True, enable_correction=False) input_data = SRTInput( content_id="BV1test123", platform="bilibili", srt_file=srt_test_files["srt_file"], metadata_file=srt_test_files["metadata_file"], output_file=srt_test_files["output_file"] ) result = asyncio.run(processor.process(input_data)) content = Path(result.output_file).read_text(encoding="utf-8") # 验证 frontmatter assert content.startswith("---") assert "title:" in content assert "author:" in content assert "platform:" in content assert "bvid:" in content def test_rule_based_output_unchanged(self, srt_test_files): """规则分段输出格式保持不变""" from kb_workflow.pipeline.processors.srt import SRTProcessor from kb_workflow.pipeline.schemas import SRTInput processor = SRTProcessor(enable_correction=False) input_data = SRTInput( content_id="BV1test123", platform="bilibili", srt_file=srt_test_files["srt_file"], metadata_file=srt_test_files["metadata_file"], output_file=srt_test_files["output_file"] ) result = asyncio.run(processor.process(input_data)) content = Path(result.output_file).read_text(encoding="utf-8") # 验证格式与之前一致 assert "---" in content assert "## 内容" in content assert "第一句" in content or "机器学习" in content