feat(15-02): integrate KBSettings config for semantic segmentation

- Modified SRTProcessor to read enable_semantic_segmentation from KBSettings
- Changed constructor parameter default from False to None (None = read from config)
- Added config priority: constructor parameter > KBSettings > default (True)
- Added logging for config status (INFO level)
- Added comprehensive tests for config integration (TestConfigIntegration)
- Added tests for output format compatibility (TestOutputFormatBenefits)
- Fixed MockAIClient to accept disable_reasoning parameter
- All 34 tests passing

Requirements satisfied:
- CONFIG-01: SRTProcessor reads from KBSettings
- CONFIG-02: Disabled config uses rule-based segmentation
- OUTPUT-01: LLM segmentation outputs H2 titles
- OUTPUT-02: title=null skips title output
- OUTPUT-03: Rule-based segmentation outputs "## 内容"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 23:03:54 +08:00
parent 5debdd6f36
commit 5983e95352
2 changed files with 372 additions and 5 deletions
+16 -2
View File
@@ -49,7 +49,7 @@ class SRTProcessor(BaseProcessor):
self, self,
state_manager: Optional[WorkflowStateManager] = None, state_manager: Optional[WorkflowStateManager] = None,
enable_correction: bool = True, enable_correction: bool = True,
enable_semantic_segmentation: bool = False, enable_semantic_segmentation: Optional[bool] = None,
enable_cache: bool = True enable_cache: bool = True
): ):
"""初始化 SRT 处理器 """初始化 SRT 处理器
@@ -57,10 +57,24 @@ class SRTProcessor(BaseProcessor):
Args: Args:
state_manager: 状态管理器 state_manager: 状态管理器
enable_correction: 是否启用 ASR 纠错(默认 True) enable_correction: 是否启用 ASR 纠错(默认 True)
enable_semantic_segmentation: 是否启用 LLM 语义分段(默认 False) enable_semantic_segmentation: 是否启用 LLM 语义分段(None 时从配置读取)
enable_cache: 是否启用缓存(默认 True,仅在启用语义分段时有效) enable_cache: 是否启用缓存(默认 True,仅在启用语义分段时有效)
""" """
super().__init__(state_manager) super().__init__(state_manager)
# 如果未明确指定,从配置读取
if enable_semantic_segmentation is None:
settings = get_settings()
enable_semantic_segmentation = settings.enable_semantic_segmentation
logger.info(
f"SRTProcessor using config: "
f"semantic_segmentation={'enabled' if enable_semantic_segmentation else 'disabled'}"
)
elif enable_semantic_segmentation:
logger.info("SRTProcessor using explicit parameter: semantic_segmentation=enabled")
else:
logger.info("SRTProcessor using explicit parameter: semantic_segmentation=disabled")
self.enable_correction = enable_correction self.enable_correction = enable_correction
self.enable_semantic_segmentation = enable_semantic_segmentation self.enable_semantic_segmentation = enable_semantic_segmentation
self.segmenter = None self.segmenter = None
+356 -3
View File
@@ -9,12 +9,13 @@ Tests for:
import asyncio import asyncio
import json import json
import os
import pytest import pytest
from pathlib import Path from pathlib import Path
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional from typing import List, Optional
from unittest.mock import Mock from unittest.mock import Mock, patch
# ============================================================================= # =============================================================================
@@ -323,7 +324,7 @@ class TestASRCorrector:
call_count = 0 call_count = 0
class MockAIClient: class MockAIClient:
def generate(self, prompt: str, max_tokens: int = 16000) -> str: def generate(self, prompt: str, max_tokens: int = 16000, disable_reasoning: bool = False) -> str:
nonlocal call_count nonlocal call_count
call_count += 1 call_count += 1
# 返回提示词中的文本部分 # 返回提示词中的文本部分
@@ -401,7 +402,11 @@ class TestSRTProcessor:
from kb_workflow.pipeline.processors.srt import SRTProcessor from kb_workflow.pipeline.processors.srt import SRTProcessor
from kb_workflow.pipeline.schemas import SRTInput from kb_workflow.pipeline.schemas import SRTInput
processor = SRTProcessor(enable_correction=False) # 明确禁用语义分段以使用规则分段器(测试规则分段输出)
processor = SRTProcessor(
enable_correction=False,
enable_semantic_segmentation=False
)
input_data = SRTInput( input_data = SRTInput(
content_id="BV1test123", content_id="BV1test123",
@@ -580,3 +585,351 @@ class TestSemanticSegmentationToggle:
srt_file = tmp_path / "test.srt" srt_file = tmp_path / "test.srt"
srt_file.write_text(srt_content, encoding='utf-8') srt_file.write_text(srt_content, encoding='utf-8')
return srt_file return srt_file
# =============================================================================
# Test 6: Configuration Integration (CONFIG-01, CONFIG-02) - Plan 15-02
# =============================================================================
class TestConfigIntegration:
"""测试配置集成 (CONFIG-01, CONFIG-02, OUTPUT-01, OUTPUT-02, OUTPUT-03)"""
@pytest.fixture
def mock_env_base(self):
"""基础环境变量"""
return {
"KB_BASE_PATH": "/tmp/kb",
"KB_ARCHIVE_PATH": "/tmp/archive"
}
def test_srt_processor_reads_from_kbsettings(self, mock_env_base):
"""Test 1: enable_semantic_segmentation=None 时从 KBSettings 读取配置"""
from kb_workflow.pipeline.processors.srt import SRTProcessor
from kb_workflow.pipeline.config import get_settings
# 清除缓存以确保使用新的环境变量
get_settings.cache_clear()
with patch.dict(os.environ, {
**mock_env_base,
"KB_ENABLE_SEMANTIC_SEGMENTATION": "true"
}, clear=False):
get_settings.cache_clear()
processor = SRTProcessor()
# 验证从配置读取
assert processor.enable_semantic_segmentation is True
def test_srt_processor_constructor_overrides_config(self, mock_env_base):
"""Test 2: 显式传入参数时覆盖配置值(参数优先级高于配置)"""
from kb_workflow.pipeline.processors.srt import SRTProcessor
from kb_workflow.pipeline.config import get_settings
get_settings.cache_clear()
with patch.dict(os.environ, {
**mock_env_base,
"KB_ENABLE_SEMANTIC_SEGMENTATION": "true"
}, clear=False):
get_settings.cache_clear()
# 显式传入 False 应该覆盖配置的 True
processor = SRTProcessor(enable_semantic_segmentation=False)
assert processor.enable_semantic_segmentation is False
def test_config_disabled_uses_rule_based(self, mock_env_base):
"""Test 3: 配置为 False 且无参数时使用规则分段"""
from kb_workflow.pipeline.processors.srt import SRTProcessor
from kb_workflow.srt.segmenter import SemanticSegmenter
from kb_workflow.pipeline.config import get_settings
get_settings.cache_clear()
with patch.dict(os.environ, {
**mock_env_base,
"KB_ENABLE_SEMANTIC_SEGMENTATION": "false"
}, clear=False):
get_settings.cache_clear()
processor = SRTProcessor()
# 验证使用规则分段器
assert isinstance(processor.segmenter, SemanticSegmenter)
assert processor.enable_semantic_segmentation is False
def test_config_enabled_uses_llm_segmentation(self, mock_env_base):
"""Test 4: 配置为 True 且无参数时使用 LLM 分段"""
from kb_workflow.pipeline.processors.srt import SRTProcessor
from kb_workflow.srt.semantic_engine import SemanticSegmentationEngine
from kb_workflow.pipeline.config import get_settings
get_settings.cache_clear()
with patch.dict(os.environ, {
**mock_env_base,
"KB_ENABLE_SEMANTIC_SEGMENTATION": "true"
}, clear=False):
get_settings.cache_clear()
with patch('kb_workflow.deep_analyzer.SimpleAIClient') as mock_client:
mock_client.return_value = Mock(api_key="test_key")
processor = SRTProcessor()
# 验证使用 LLM 分段引擎
assert isinstance(processor.segmenter, SemanticSegmentationEngine)
assert processor.enable_semantic_segmentation is True
def test_initialization_logs_config_status(self, mock_env_base, caplog):
"""Test 5: 初始化日志记录配置状态(INFO 级别)"""
from kb_workflow.pipeline.processors.srt import SRTProcessor
from kb_workflow.pipeline.config import get_settings
import logging
get_settings.cache_clear()
with patch.dict(os.environ, {
**mock_env_base,
"KB_ENABLE_SEMANTIC_SEGMENTATION": "false"
}, clear=False):
get_settings.cache_clear()
with caplog.at_level(logging.INFO):
processor = SRTProcessor()
# 验证日志记录了配置状态
assert any("semantic_segmentation=" in record.message for record in caplog.records)
assert any("disabled" in record.message for record in caplog.records)
# =============================================================================
# Test 7: Output Format Compatibility (OUTPUT-01, OUTPUT-02, OUTPUT-03)
# =============================================================================
class TestOutputFormatCompatibility:
"""测试输出格式兼容性"""
@pytest.fixture
def srt_files_for_output(self, tmp_path):
"""创建输出格式测试用的文件"""
# 创建 SRT 文件
srt_content = """1
00:00:00,000 --> 00:00:05,000
这是第一段内容
2
00:00:05,000 --> 00:00:10,000
这是第二段内容
"""
srt_file = tmp_path / "test.srt"
srt_file.write_text(srt_content, encoding="utf-8")
# 创建元数据文件
metadata = {
"title": "输出格式测试",
"author": "测试作者",
"platform": "bilibili"
}
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)
}
def test_output_format_with_semantic_disabled(self, srt_files_for_output):
"""Test 3: 规则分段输出 "## 内容" 标题(OUTPUT-03)"""
from kb_workflow.pipeline.processors.srt import SRTProcessor
from kb_workflow.pipeline.schemas import SRTInput
processor = SRTProcessor(
enable_semantic_segmentation=False,
enable_correction=False
)
input_data = SRTInput(
content_id="test123",
platform="bilibili",
srt_file=srt_files_for_output["srt_file"],
metadata_file=srt_files_for_output["metadata_file"],
output_file=srt_files_for_output["output_file"]
)
result = asyncio.run(processor.process(input_data))
# 验证输出文件
content = Path(result.output_file).read_text(encoding="utf-8")
# 验证 "## 内容" 标题(OUTPUT-03)
assert "## 内容" in content
# 验证段落内容直接输出
assert "这是第一段内容" in content
assert "这是第二段内容" in content
def test_generate_markdown_with_video_segments(self):
"""Test 1: LLM 分段输出带 H2 标题的 Markdown(OUTPUT-01)"""
from kb_workflow.pipeline.processors.srt import SRTProcessor
from kb_workflow.srt.semantic_engine import VideoSegment
processor = SRTProcessor(enable_correction=False)
# 创建 VideoSegment 对象模拟 LLM 分段结果
mock_segments = [
VideoSegment(title="第一段", content="这是第一段内容", start_index=0, end_index=10),
VideoSegment(title=None, content="这是第二段内容", start_index=10, end_index=20),
VideoSegment(title="第三段", content="这是第三段内容", start_index=20, end_index=30)
]
# 创建临时输出文件
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
output_file = Path(f.name)
try:
# 调用 _generate_markdown 方法
processor._generate_markdown(
output_file=output_file,
metadata={"title": "测试"},
paragraphs=mock_segments,
tags=[]
)
# 验证输出文件
content = output_file.read_text(encoding="utf-8")
# 验证 H2 标题(OUTPUT-01)
assert "## 第一段" in content
assert "## 第三段" in content
# 验证 title=None 时不输出标题(OUTPUT-02)
assert "## 第二段" not in content
assert "这是第二段内容" in content
# 验证内容正确输出
assert "这是第一段内容" in content
assert "这是第三段内容" in content
finally:
# 清理临时文件
if output_file.exists():
output_file.unlink()
def test_generate_markdown_with_plain_text(self):
"""Test 3b: 规则分段输出纯文本(带 "## 内容" 标题)"""
from kb_workflow.pipeline.processors.srt import SRTProcessor
processor = SRTProcessor(enable_correction=False)
# 创建纯文本列表模拟规则分段结果
mock_paragraphs = [
"这是第一段内容",
"这是第二段内容"
]
# 创建临时输出文件
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
output_file = Path(f.name)
try:
# 调用 _generate_markdown 方法
processor._generate_markdown(
output_file=output_file,
metadata={"title": "测试"},
paragraphs=mock_paragraphs,
tags=[]
)
# 验证输出文件
content = output_file.read_text(encoding="utf-8")
# 验证 "## 内容" 标题(OUTPUT-03)
assert "## 内容" in content
# 验证段落内容直接输出
assert "这是第一段内容" in content
assert "这是第二段内容" in content
finally:
# 清理临时文件
if output_file.exists():
output_file.unlink()
def test_generate_markdown_with_null_titles(self):
"""Test 2: 所有 title=None 时不输出分段标题(OUTPUT-02)"""
from kb_workflow.pipeline.processors.srt import SRTProcessor
from kb_workflow.srt.semantic_engine import VideoSegment
processor = SRTProcessor(enable_correction=False)
# 创建全部 title=None 的 VideoSegment
mock_segments = [
VideoSegment(title=None, content="第一段内容", start_index=0, end_index=10),
VideoSegment(title=None, content="第二段内容", start_index=10, end_index=20)
]
# 创建临时输出文件
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
output_file = Path(f.name)
try:
# 调用 _generate_markdown 方法
processor._generate_markdown(
output_file=output_file,
metadata={"title": "测试"},
paragraphs=mock_segments,
tags=[]
)
# 验证输出文件
content = output_file.read_text(encoding="utf-8")
# 验证没有分段标题(只有 frontmatter 和内容)
lines = content.split('\n')
# 找到 frontmatter 结束后的内容
in_content = False
for line in lines:
if line == "---":
if in_content:
break
in_content = True
continue
if in_content and line.startswith("## "):
# 只能有 "## 内容" 标题,不能有其他分段标题
assert line == "## 内容"
# 验证内容仍然存在
assert "第一段内容" in content
assert "第二段内容" in content
finally:
# 清理临时文件
if output_file.exists():
output_file.unlink()
def test_both_modes_valid_output(self, srt_files_for_output):
"""Test 4: 两种模式输出结构不同但都有效"""
from kb_workflow.pipeline.processors.srt import SRTProcessor
from kb_workflow.pipeline.schemas import SRTInput
# 测试规则分段模式
processor = SRTProcessor(
enable_semantic_segmentation=False,
enable_correction=False
)
input_data = SRTInput(
content_id="test123",
platform="bilibili",
srt_file=srt_files_for_output["srt_file"],
metadata_file=srt_files_for_output["metadata_file"],
output_file=srt_files_for_output["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 len(content) > 0
assert "---" in content # frontmatter
assert "## 内容" in content