diff --git a/.planning/phases/14-robustness-cost/14-00-PLAN.md b/.planning/phases/14-robustness-cost/14-00-PLAN.md
new file mode 100644
index 0000000..138046e
--- /dev/null
+++ b/.planning/phases/14-robustness-cost/14-00-PLAN.md
@@ -0,0 +1,510 @@
+---
+phase: 14-robustness-cost
+plan: 00
+type: execute
+wave: 0
+depends_on: []
+files_modified:
+ - tests/test_semantic_robustness.py
+ - tests/conftest.py
+ - tests/test_srt_processor.py
+autonomous: true
+requirements:
+ - ROBUST-01
+ - ROBUST-02
+ - ROBUST-03
+ - ROBUST-04
+ - COST-01
+ - COST-02
+ - COST-03
+must_haves:
+ truths:
+ - "所有计划任务都有对应的自动化测试"
+ - "测试框架配置正确,可以运行测试"
+ - "Mock fixtures 可用,模拟 LLM 客户端行为"
+ artifacts:
+ - path: "tests/test_semantic_robustness.py"
+ provides: "鲁棒性和成本控制测试套件"
+ exports: ["test_*"]
+ - path: "tests/conftest.py"
+ provides: "共享测试 fixtures"
+ exports: ["mock_ai_client", "sample_segments", "sample_metadata"]
+ - path: "tests/test_srt_processor.py"
+ provides: "SRTProcessor 集成测试"
+ exports: ["test_semantic_segmentation_toggle"]
+ key_links:
+ - from: "test_semantic_robustness.py"
+ to: "conftest.py"
+ via: "pytest fixtures"
+ pattern: "@pytest.fixture"
+---
+
+
+创建阶段 14 的测试基础设施,覆盖所有鲁棒性和成本控制功能。
+
+Purpose: 确保所有计划任务都有对应的自动化测试,遵循 TDD 原则。
+Output: 完整的测试套件,包括鲁棒性测试、成本控制测试、集成测试。
+
+
+
+@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
+@/home/starmen1265/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/14-robustness-cost/14-CONTEXT.md
+@.planning/phases/14-robustness-cost/14-RESEARCH.md
+@.planning/phases/14-robustness-cost/14-VALIDATION.md
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@.planning/STATE.md
+
+@tests/conftest.py
+@tests/test_semantic_engine.py
+@kb_workflow/srt/semantic_engine.py
+
+
+
+
+
+From tests/conftest.py (existing fixtures):
+```python
+import pytest
+from unittest.mock import Mock, MagicMock
+from kb_workflow.srt.parser import Segment
+
+@pytest.fixture
+def mock_ai_client():
+ """Mock AI client"""
+ client = Mock()
+ client.api_key = "test_key"
+ client.generate = Mock(return_value="Test response")
+ return client
+
+@pytest.fixture
+def sample_segments():
+ """示例字幕分段"""
+ return [
+ Segment(index=1, start_time=0.0, end_time=1.0, text="第一句"),
+ Segment(index=2, start_time=1.0, end_time=2.0, text="第二句"),
+ ]
+
+@pytest.fixture
+def sample_metadata():
+ """示例视频元数据"""
+ return {
+ "title": "测试视频",
+ "duration": 120,
+ "category": "知识"
+ }
+```
+
+From 14-VALIDATION.md (Wave 0 Requirements):
+```python
+# Wave 0 Requirements
+- [ ] tests/test_semantic_robustness.py — 覆盖 ROBUST-01, ROBUST-02, ROBUST-03, ROBUST-04, COST-01, COST-02
+- [ ] tests/test_srt_processor.py — 覆盖 COST-03(配置开关)
+- [ ] tests/conftest.py — 共享 fixtures(mock LLM client, 样本字幕数据)
+- [ ] 框架安装: 无需安装,pytest 已配置
+
+# Mock Strategy
+使用 unittest.mock.patch 模拟 SimpleAIClient.generate(),测试不同失败场景(网络错误、解析失败、超时等)
+```
+
+From 14-RESEARCH.md (Test Requirements):
+| Req ID | Behavior | Test Type | Automated Command |
+|--------|----------|-----------|-------------------|
+| ROBUST-01 | LLM 调用失败时自动降级 | unit | test_llm_failure_fallback |
+| ROBUST-02 | 解析失败时重试或降级 | unit | test_parse_failure_retry |
+| ROBUST-03 | AI 不可用时使用规则分段 | unit | test_ai_unavailable_fallback |
+| ROBUST-04 | 降级日志记录 | unit | test_fallback_logging |
+| COST-01 | 长文本分块处理 | unit | test_long_text_chunking |
+| COST-02 | 缓存命中避免重复调用 | unit | test_cache_hit |
+| COST-03 | 配置开关控制 | integration | test_semantic_segmentation_toggle |
+
+
+
+
+
+ Task 1: 创建 test_semantic_robustness.py 测试文件
+ tests/test_semantic_robustness.py
+
+ 创建 tests/test_semantic_robustness.py,包含所有鲁棒性和成本控制测试:
+
+ 1. 导入依赖:
+ ```python
+ import pytest
+ import json
+ from unittest.mock import Mock, patch, MagicMock
+ from pathlib import Path
+ from requests.exceptions import ConnectionError, Timeout, HTTPError
+
+ from kb_workflow.srt.semantic_engine import (
+ UnifiedSegmentationStrategy,
+ SemanticSegmentationEngine,
+ VideoSegment,
+ SegmentationResult
+ )
+ from kb_workflow.srt.cache import SemanticCache, compute_content_hash
+ ```
+
+ 2. 创建测试类和方法(遵循 14-VALIDATION.md 的测试映射):
+
+ **TestLLMFailureFallback (ROBUST-01)**:
+ - test_llm_failure_fallback: 模拟 LLM 调用失败(网络错误),验证降级到规则分段
+ - test_recoverable_error_retry: 模拟可恢复错误(ConnectionError),验证重试 1 次
+ - test_non_recoverable_error_no_retry: 模拟不可恢复错误(HTTPError 400),验证直接降级
+
+ **TestParseFailureRetry (ROBUST-02, ROBUST-04)**:
+ - test_parse_failure_retry: 模拟 JSON 解析失败(JSONDecodeError),验证重试后降级
+ - test_validation_error_no_retry: 模拟 Pydantic 验证失败(ValidationError),验证直接降级
+ - test_fallback_logging: 验证降级时记录 warning 级别日志,包含错误类型和原因
+
+ **TestAIAvailabilityFallback (ROBUST-03)**:
+ - test_ai_unavailable_fallback: 模拟 AI 客户端未配置(api_key 为空),验证使用规则分段
+ - test_ai_client_none: 模拟 AI 客户端初始化失败,验证使用规则分段
+
+ **TestLongTextChunking (COST-01)**:
+ - test_long_text_chunking: 使用超过 5000 字符的文本,验证分块处理
+ - test_chunk_sentence_boundary: 验证分块在句子边界(标点符号后)分割
+ - test_chunk_index_correction: 验证 VideoSegment.start_index/end_index 相对于原文
+
+ **TestCacheHit (COST-02)**:
+ - test_cache_hit: 第一次调用 LLM,第二次调用从缓存读取
+ - test_cache_key_hash: 验证缓存键使用 SHA256 哈希
+ - test_cache_miss_content_change: 内容变化时缓存未命中
+ - test_cache_file_format: 验证缓存文件格式(JSON + version 字段)
+
+ 3. 使用 Mock 模拟 LLM 客户端:
+ ```python
+ @pytest.fixture
+ def mock_ai_client_with_failure():
+ """模拟失败的 AI 客户端"""
+ client = Mock()
+ client.api_key = "test_key"
+ client.generate = Mock(side_effect=ConnectionError("Network error"))
+ return client
+
+ @pytest.fixture
+ def mock_ai_client_with_parse_error():
+ """模拟解析失败的 AI 客户端"""
+ client = Mock()
+ client.api_key = "test_key"
+ # 第一次返回无效 JSON,第二次返回有效 JSON
+ client.generate = Mock(side_effect=[
+ "Invalid JSON",
+ '{"video_type": "知识类", "segments": [], "actual_segment_count": 0}'
+ ])
+ return client
+ ```
+
+ 4. 示例测试实现:
+ ```python
+ def test_llm_failure_fallback(sample_segments, sample_metadata):
+ """测试 LLM 调用失败时降级到规则分段"""
+ strategy = UnifiedSegmentationStrategy(
+ ai_client=mock_ai_client_with_failure()
+ )
+
+ result = strategy.segment(sample_segments, sample_metadata)
+
+ # 验证降级到规则分段(返回 VideoSegment 列表)
+ assert isinstance(result, list)
+ assert all(isinstance(seg, VideoSegment) for seg in result)
+ # 规则分段没有标题
+ assert all(seg.title is None for seg in result)
+
+ def test_recoverable_error_retry(sample_segments, sample_metadata):
+ """测试可恢复错误重试 1 次"""
+ client = Mock()
+ client.api_key = "test_key"
+ # 第一次失败,第二次成功
+ client.generate = Mock(side_effect=[
+ ConnectionError("Network error"),
+ '{"video_type": "知识类", "segments": [], "actual_segment_count": 0}'
+ ])
+
+ strategy = UnifiedSegmentationStrategy(ai_client=client)
+ result = strategy.segment(sample_segments, sample_metadata)
+
+ # 验证重试了 2 次
+ assert client.generate.call_count == 2
+ ```
+
+ 确保所有测试都是 RED 状态(功能尚未实现)。
+
+
+ pytest tests/test_semantic_robustness.py -v --tb=no
+
+
+ - test_semantic_robustness.py 文件创建,包含所有测试
+ - 所有测试都是 RED 状态(预期失败)
+ - 测试覆盖所有需求(ROBUST-01 到 COST-03)
+ - Mock fixtures 正确模拟 LLM 客户端行为
+
+
+
+
+ Task 2: 更新 conftest.py 添加新 fixtures
+ tests/conftest.py
+
+ 更新 tests/conftest.py,添加阶段 14 需要的新 fixtures:
+
+ 1. 添加长文本样本(用于分块测试):
+ ```python
+ @pytest.fixture
+ def long_text_sample():
+ """长文本样本(>5000 字符,用于分块测试)"""
+ # 生成一个超过 5000 字符的文本,包含多个句子
+ sentences = ["这是一个测试句子。" * 10] * 15 # 约 6000 字符
+ return " ".join(sentences)
+ ```
+
+ 2. 添加模拟失败的 AI 客户端 fixtures:
+ ```python
+ @pytest.fixture
+ def mock_ai_client_network_error():
+ """模拟网络错误的 AI 客户端"""
+ client = Mock()
+ client.api_key = "test_key"
+ client.generate = Mock(side_effect=ConnectionError("Network error"))
+ return client
+
+ @pytest.fixture
+ def mock_ai_client_timeout():
+ """模拟超时的 AI 客户端"""
+ client = Mock()
+ client.api_key = "test_key"
+ client.generate = Mock(side_effect=Timeout("Request timeout"))
+ return client
+
+ @pytest.fixture
+ def mock_ai_client_rate_limit():
+ """模拟限流的 AI 客户端"""
+ response = Mock()
+ response.status_code = 429
+ client = Mock()
+ client.api_key = "test_key"
+ client.generate = Mock(side_effect=HTTPError(response=response))
+ return client
+
+ @pytest.fixture
+ def mock_ai_client_api_error():
+ """模拟 API 错误的 AI 客户端(不可恢复)"""
+ response = Mock()
+ response.status_code = 400
+ client = Mock()
+ client.api_key = "test_key"
+ client.generate = Mock(side_effect=HTTPError(response=response))
+ return client
+
+ @pytest.fixture
+ def mock_ai_client_parse_error():
+ """模拟解析失败的 AI 客户端"""
+ client = Mock()
+ client.api_key = "test_key"
+ # 第一次返回无效 JSON
+ client.generate = Mock(return_value="Invalid JSON response")
+ return client
+
+ @pytest.fixture
+ def mock_ai_client_success():
+ """模拟成功的 AI 客户端"""
+ client = Mock()
+ client.api_key = "test_key"
+ client.generate = Mock(return_value='''{
+ "video_type": "知识类",
+ "segments": [
+ {
+ "title": "测试标题",
+ "content": "测试内容",
+ "start_index": 0,
+ "end_index": 10
+ }
+ ],
+ "actual_segment_count": 1
+ }''')
+ return client
+ ```
+
+ 3. 添加缓存相关的 fixtures:
+ ```python
+ @pytest.fixture
+ def temp_cache_dir(tmp_path):
+ """临时缓存目录"""
+ cache_dir = tmp_path / "cache" / "semantic"
+ return cache_dir
+
+ @pytest.fixture
+ def semantic_cache(temp_cache_dir):
+ """语义分段缓存实例"""
+ from kb_workflow.srt.cache import SemanticCache
+ return SemanticCache(cache_dir=temp_cache_dir)
+ ```
+
+ 4. 添加长字幕分段样本:
+ ```python
+ @pytest.fixture
+ def long_subtitle_segments():
+ """长字幕分段样本(>5000 字符)"""
+ segments = []
+ for i in range(100):
+ segments.append(Segment(
+ index=i + 1,
+ start_time=i * 1.0,
+ end_time=(i + 1) * 1.0,
+ text=f"这是第 {i + 1} 句字幕内容。" * 5
+ ))
+ return segments
+ ```
+
+
+ pytest tests/conftest.py --collect-only
+
+
+ - conftest.py 添加所有需要的 fixtures
+ - Fixtures 包括: 模拟失败的 AI 客户端、长文本样本、缓存目录
+ - 所有 fixtures 可以正确导入和使用
+
+
+
+
+ Task 3: 在 test_srt_processor.py 中添加配置开关测试
+ tests/test_srt_processor.py
+
+ 在 tests/test_srt_processor.py 中添加配置开关测试(COST-03):
+
+ 1. 如果文件不存在,创建基础文件:
+ ```python
+ import pytest
+ from unittest.mock import Mock, patch
+ from pathlib import Path
+
+ from kb_workflow.srt.processor import SRTProcessor
+ from kb_workflow.srt.parser import SRTParser
+ ```
+
+ 2. 添加 TestSemanticSegmentationToggle 测试类:
+ ```python
+ class TestSemanticSegmentationToggle:
+ """测试语义分段配置开关 (COST-03)"""
+
+ def test_semantic_segmentation_enabled(self, sample_srt_file):
+ """测试启用语义分段"""
+ with patch('kb_workflow.srt.semantic_engine.SimpleAIClient') as mock_client:
+ mock_client.return_value = Mock(api_key="test_key")
+
+ processor = SRTProcessor(
+ config={},
+ enable_semantic_segmentation=True
+ )
+
+ # 验证创建了 SemanticSegmentationEngine
+ assert processor.semantic_engine is not None
+ assert processor.segmenter is None
+
+ def test_semantic_segmentation_disabled(self, sample_srt_file):
+ """测试禁用语义分段(默认)"""
+ processor = SRTProcessor(
+ config={},
+ enable_semantic_segmentation=False
+ )
+
+ # 验证使用规则分段器
+ assert processor.semantic_engine is None
+ assert processor.segmenter is not None
+
+ def test_semantic_segmentation_with_cache(self, sample_srt_file):
+ """测试启用语义分段和缓存"""
+ with patch('kb_workflow.srt.semantic_engine.SimpleAIClient') as mock_client:
+ mock_client.return_value = Mock(api_key="test_key")
+
+ processor = SRTProcessor(
+ config={},
+ enable_semantic_segmentation=True,
+ enable_cache=True
+ )
+
+ # 验证创建了缓存
+ assert processor.semantic_engine is not None
+ # 验证策略使用了缓存
+ assert processor.semantic_engine.strategy.cache is not None
+
+ def test_semantic_segmentation_without_cache(self, sample_srt_file):
+ """测试启用语义分段但禁用缓存"""
+ with patch('kb_workflow.srt.semantic_engine.SimpleAIClient') as mock_client:
+ mock_client.return_value = Mock(api_key="test_key")
+
+ processor = SRTProcessor(
+ config={},
+ enable_semantic_segmentation=True,
+ enable_cache=False
+ )
+
+ # 验证没有创建缓存
+ assert processor.semantic_engine is not None
+ assert processor.semantic_engine.strategy.cache is None
+
+ @pytest.fixture
+ def sample_srt_file(self, tmp_path):
+ """创建示例 SRT 文件"""
+ srt_content = """1
+ 00:00:00,000 --> 00:00:01,000
+ 第一句字幕
+
+ 2
+ 00:00:01,000 --> 00:00:02,000
+ 第二句字幕
+ """
+ srt_file = tmp_path / "test.srt"
+ srt_file.write_text(srt_content, encoding='utf-8')
+ return srt_file
+ ```
+
+ 3. 确保测试是 RED 状态(功能尚未实现)。
+
+
+ pytest tests/test_srt_processor.py::test_semantic_segmentation_toggle -v --tb=no
+
+
+ - test_srt_processor.py 添加配置开关测试
+ - 测试覆盖启用/禁用语义分段、启用/禁用缓存
+ - 所有测试都是 RED 状态(预期失败)
+
+
+
+
+
+
+## 整体验证标准
+
+1. **测试文件创建**: test_semantic_robustness.py 包含所有鲁棒性和成本控制测试
+2. **Fixtures 更新**: conftest.py 添加所有需要的 fixtures
+3. **集成测试**: test_srt_processor.py 添加配置开关测试
+4. **测试状态**: 所有测试都是 RED 状态(功能尚未实现)
+
+运行测试验证文件结构:
+```bash
+# 验证测试文件存在
+ls tests/test_semantic_robustness.py tests/conftest.py tests/test_srt_processor.py
+
+# 验证测试可以收集
+python3 -m pytest tests/test_semantic_robustness.py --collect-only
+python3 -m pytest tests/test_srt_processor.py::test_semantic_segmentation_toggle --collect-only
+
+# 验证测试是 RED 状态
+python3 -m pytest tests/test_semantic_robustness.py -v --tb=no
+```
+
+
+
+1. test_semantic_robustness.py 文件创建,包含 6 个测试类
+2. conftest.py 添加 10+ 个新 fixtures
+3. test_srt_processor.py 添加 TestSemanticSegmentationToggle 测试类
+4. 所有测试都是 RED 状态(预期失败)
+5. 测试覆盖所有需求(ROBUST-01 到 COST-03)
+
+
+
diff --git a/.planning/phases/14-robustness-cost/14-01-PLAN.md b/.planning/phases/14-robustness-cost/14-01-PLAN.md
new file mode 100644
index 0000000..70385d8
--- /dev/null
+++ b/.planning/phases/14-robustness-cost/14-01-PLAN.md
@@ -0,0 +1,184 @@
+---
+phase: 14-robustness-cost
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - kb_workflow/srt/semantic_engine.py
+autonomous: true
+requirements:
+ - ROBUST-01
+ - ROBUST-03
+must_haves:
+ truths:
+ - "LLM 调用失败时系统自动降级到规则分段,不影响整体流程"
+ - "AI 客户端不可用时(未配置 API key)使用规则分段"
+ - "LLM 响应解析失败时降级到规则分段"
+ artifacts:
+ - path: "kb_workflow/srt/semantic_engine.py"
+ provides: "UnifiedSegmentationStrategy.segment() 带重试和降级逻辑"
+ exports: ["segment", "_is_recoverable_error"]
+ key_links:
+ - from: "UnifiedSegmentationStrategy.segment()"
+ to: "规则分段器"
+ via: "fallback_segmenter.segment()"
+ pattern: "self.fallback_segmenter.segment"
+ - from: "UnifiedSegmentationStrategy.segment()"
+ to: "SimpleAIClient.generate()"
+ via: "LLM API 调用"
+ pattern: "self.ai_client.generate"
+---
+
+
+实现 LLM 调用的优雅降级策略,确保 AI 不可用时系统仍能正常运行。
+
+Purpose: 提高系统稳定性,避免 LLM 故障导致整个处理流程失败。
+Output: 带重试和降级逻辑的 UnifiedSegmentationStrategy.segment() 方法。
+
+
+
+@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
+@/home/starmen1265/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/14-robustness-cost/14-CONTEXT.md
+@.planning/phases/14-robustness-cost/14-RESEARCH.md
+@.planning/phases/14-robustness-cost/14-VALIDATION.md
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@.planning/STATE.md
+
+@kb_workflow/srt/semantic_engine.py
+@kb_workflow/srt/corrector.py
+@kb_workflow/deep_analyzer/__init__.py
+
+
+
+
+
+From kb_workflow/srt/semantic_engine.py:
+```python
+class UnifiedSegmentationStrategy:
+ def __init__(
+ self,
+ ai_client: Optional[AIClientProtocol] = None,
+ fallback_segmenter: Optional['SemanticSegmenter'] = None
+ ):
+ # Lazy AI Client 初始化
+ if self.ai_client is None:
+ try:
+ from kb_workflow.deep_analyzer import SimpleAIClient
+ self.ai_client = SimpleAIClient()
+ if not self.ai_client.api_key:
+ logger.info("AI API key not configured")
+ self.ai_client = None
+ except Exception as e:
+ logger.warning(f"Failed to initialize AI client: {e}")
+ self.ai_client = None
+
+ def segment(
+ self,
+ segments: List,
+ metadata: dict
+ ) -> List['VideoSegment']:
+ # AI 不可用,直接使用规则分段
+ if self.ai_client is None:
+ logger.info("AI client unavailable, using fallback segmenter")
+ paragraphs = self.fallback_segmenter.segment(segments)
+ return [
+ VideoSegment(title=None, content=para, start_index=0, end_index=0)
+ for para in paragraphs
+ ]
+
+ # 调用 LLM
+ try:
+ llm_output = self.ai_client.generate(prompt, max_tokens=8000, disable_reasoning=True)
+ video_segments = self._parse_result(llm_output, fallback_segments=segments)
+ return video_segments
+ except Exception as e:
+ logger.error(f"LLM segmentation failed: {e}, using fallback")
+ paragraphs = self.fallback_segmenter.segment(segments)
+ return [VideoSegment(title=None, content=para, start_index=0, end_index=0) for para in paragraphs]
+```
+
+From kb_workflow/deep_analyzer/__init__.py (SimpleAIClient 重试模式):
+- SimpleAIClient 已有内置重试逻辑 (max_retries=3)
+- 使用 requests 库进行 HTTP 调用
+- 可能抛出的异常: ConnectionError, Timeout, HTTPError
+
+
+
+
+
+ Task 1: 实现可恢复错误检测和重试逻辑
+ kb_workflow/srt/semantic_engine.py
+
+ - 添加 _is_recoverable_error() 方法判断错误类型
+ - 可恢复错误: ConnectionError, Timeout, HTTPError with status 429/500/502/503/504
+ - 不可恢复错误: 其他 HTTPError (4xx), JSONDecodeError, ValidationError
+ - 重试逻辑: 仅对可恢复错误重试 1 次
+ - 重试延迟: 立即重试(无延迟)
+ - 最后一次失败或不可恢复错误: 降级到规则分段
+
+
+ 在 UnifiedSegmentationStrategy 类中添加:
+
+ 1. _is_recoverable_error() 静态方法:
+ - 导入 requests.exceptions (ConnectionError, Timeout, HTTPError)
+ - 判断异常类型和状态码
+ - 返回 bool
+
+ 2. 修改 segment() 方法的 LLM 调用部分:
+ - 添加 for 循环 (max_attempts = 2)
+ - 在 try-except 中捕获异常
+ - 检查是否可恢复且还有重试机会
+ - 可恢复: logger.info 重试, continue
+ - 不可恢复或最后一次: logger.warning 降级原因, break
+
+ 3. 改进日志记录:
+ - 重试: info 级别, "LLM call failed (attempt 1/2): {error}, retrying..."
+ - 降级: warning 级别, "{error_type}: {error_message}, 降级到规则分段"
+
+ 遵守用户决策: 重试 1 次, 仅可恢复错误重试, 立即重试。
+
+
+ pytest tests/test_semantic_robustness.py::test_llm_failure_fallback -x
+
+
+ - _is_recoverable_error() 方法正确识别可恢复/不可恢复错误
+ - LLM 调用失败时重试 1 次(可恢复错误)或直接降级(不可恢复错误)
+ - 降级时记录具体的错误类型和消息 (warning 级别)
+ - AI 不可用时 (ai_client is None) 直接使用规则分段,无重试
+
+
+
+
+
+
+## 整体验证标准
+
+1. **功能验证**: LLM 调用失败时系统仍能返回分段结果(使用规则分段)
+2. **重试验证**: 可恢复错误(网络错误、超时、限流)触发重试
+3. **降级验证**: 不可恢复错误(API 错误、解析失败)直接降级,不重试
+4. **日志验证**: 降级时记录具体原因,便于问题诊断
+
+运行完整测试套件:
+```bash
+python3 -m pytest tests/test_semantic_robustness.py::test_llm_failure_fallback -v -x
+```
+
+
+
+1. LLM 调用失败时自动降级到规则分段,不中断处理流程
+2. 可恢复错误(网络、超时、限流)重试 1 次后降级
+3. 不可恢复错误(API 错误、解析失败)直接降级,不浪费配额
+4. AI 客户端未配置时直接使用规则分段,无 LLM 调用
+5. 降级时记录 warning 级别日志,包含错误类型和原因
+6. 所有测试通过
+
+
+
diff --git a/.planning/phases/14-robustness-cost/14-02-PLAN.md b/.planning/phases/14-robustness-cost/14-02-PLAN.md
new file mode 100644
index 0000000..9184b45
--- /dev/null
+++ b/.planning/phases/14-robustness-cost/14-02-PLAN.md
@@ -0,0 +1,240 @@
+---
+phase: 14-robustness-cost
+plan: 02
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - kb_workflow/srt/semantic_engine.py
+autonomous: true
+requirements:
+ - ROBUST-02
+ - ROBUST-04
+must_haves:
+ truths:
+ - "LLM 响应解析失败时自动重试或降级"
+ - "降级过程记录详细日志,包含错误类型和原因"
+ - "Pydantic 验证失败时直接降级(不可恢复错误)"
+ artifacts:
+ - path: "kb_workflow/srt/semantic_engine.py"
+ provides: "_parse_result() 带解析失败处理"
+ exports: ["_parse_result"]
+ key_links:
+ - from: "_parse_result()"
+ to: "extract_json_from_llm_response()"
+ via: "JSON 提取"
+ pattern: "extract_json_from_llm_response"
+ - from: "_parse_result()"
+ to: "SegmentationResult"
+ via: "Pydantic 验证"
+ pattern: "SegmentationResult(**data)"
+---
+
+
+增强 LLM 响应解析的错误处理,区分解析失败类型并采取不同策略。
+
+Purpose: 提高 LLM 输出格式不稳定时的容错能力,记录详细日志便于诊断。
+Output: 改进的 _parse_result() 方法,带详细的错误日志和重试逻辑。
+
+
+
+@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
+@/home/starmen1265/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/14-robustness-cost/14-CONTEXT.md
+@.planning/phases/14-robustness-cost/14-RESEARCH.md
+@.planning/phases/14-robustness-cost/14-VALIDATION.md
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@.planning/STATE.md
+
+@kb_workflow/srt/semantic_engine.py
+@.planning/phases/14-robustness-cost/14-01-PLAN.md
+
+
+
+
+
+From kb_workflow/srt/semantic_engine.py:
+```python
+def extract_json_from_llm_response(response: str) -> dict:
+ """从 LLM 响应中提取 JSON
+
+ 处理以下情况:
+ - 纯 JSON
+ - 包裹在 ```json ``` 中的 JSON
+ - 前后包含额外说明文本的 JSON
+
+ Raises:
+ ValueError: 无法提取有效 JSON
+ """
+ # 尝试直接解析
+ try:
+ return json.loads(response.strip())
+ except json.JSONDecodeError:
+ pass
+
+ # 尝试提取 ```json ``` 代码块
+ json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL)
+ if json_match:
+ try:
+ return json.loads(json_match.group(1))
+ except json.JSONDecodeError:
+ pass
+
+ # 尝试提取任何 JSON 对象
+ json_match = re.search(r'\{.*\}', response, re.DOTALL)
+ if json_match:
+ try:
+ return json.loads(json_match.group())
+ except json.JSONDecodeError as e:
+ raise ValueError(f"无法从 LLM 响应中提取有效 JSON: {e}")
+
+ raise ValueError("LLM 响应中未找到 JSON 内容")
+
+class SegmentationResult(BaseModel):
+ video_type: str
+ segments: List[VideoSegment]
+ actual_segment_count: int
+
+def _parse_result(
+ self,
+ llm_output: str,
+ fallback_segments: List
+) -> List[VideoSegment]:
+ """解析 LLM 输出并验证
+
+ Raises:
+ Exception: 解析或验证失败时
+ """
+ try:
+ # 提取 JSON
+ data = extract_json_from_llm_response(llm_output)
+
+ # Pydantic 验证
+ result = SegmentationResult(**data)
+
+ logger.info(
+ f"LLM 分段成功: 类型={result.video_type}, "
+ f"分段数={result.actual_segment_count}"
+ )
+
+ return result.segments
+
+ except (json.JSONDecodeError, ValidationError) as e:
+ logger.error(f"LLM 分段结果验证失败: {e}")
+ logger.error(f"原始 LLM 输出: {llm_output[:500]}...")
+
+ # 降级到规则分段
+ logger.info("降级到规则分段")
+ raise
+
+ except Exception as e:
+ logger.error(f"未知错误: {e}")
+ raise
+```
+
+关键观察:
+- extract_json_from_llm_response() 可能抛出 ValueError (无法提取 JSON)
+- SegmentationResult(**data) 可能抛出 ValidationError (Pydantic 验证失败)
+- 当前实现: 所有解析失败都抛出异常,由 segment() 方法的 try-except 捕获并降级
+
+
+
+
+
+ Task 1: 改进解析失败的错误处理和日志
+ kb_workflow/srt/semantic_engine.py
+
+ - 区分 JSONDecodeError (无法提取 JSON) 和 ValidationError (Pydantic 验证失败)
+ - JSONDecodeError 是可恢复错误(可能 LLM 输出格式问题),可以重试
+ - ValidationError 是不可恢复错误(数据结构错误),直接降级
+ - 所有解析失败都记录详细日志:
+ - 错误类型 (JSONDecodeError/ValidationError/ValueError)
+ - 错误消息
+ - LLM 输出前 500 字符(用于诊断)
+
+
+ 修改 UnifiedSegmentationStrategy._parse_result() 方法:
+
+ 1. 拆分 except 块,区分不同的错误类型:
+
+ ```python
+ except json.JSONDecodeError as e:
+ # JSON 解析失败(可恢复)
+ logger.error(
+ f"JSON 解析失败: {e}\n"
+ f"LLM 输出片段: {llm_output[:500]}..."
+ )
+ # 抛出特定异常,由 segment() 决定是否重试
+ raise
+
+ except ValidationError as e:
+ # Pydantic 验证失败(不可恢复)
+ logger.error(
+ f"Pydantic 验证失败: {e}\n"
+ f"LLM 输出片段: {llm_output[:500]}..."
+ )
+ # 直接降级,不重试
+ raise
+
+ except ValueError as e:
+ # 无法提取 JSON(可恢复)
+ logger.error(
+ f"JSON 提取失败: {e}\n"
+ f"LLM 输出片段: {llm_output[:500]}..."
+ )
+ raise
+ ```
+
+ 2. 在 segment() 方法中,修改 LLM 调用部分的重试逻辑:
+ - 对 JSONDecodeError 和 ValueError 进行重试
+ - 对 ValidationError 不重试,直接降级
+ - 使用 _is_recoverable_error() 的模式,创建 _is_recoverable_parse_error()
+
+ 3. 统一日志格式:
+ - 重试: info 级别
+ - 降级: warning 级别,包含 "降级到规则分段"
+
+ 遵守用户决策: 解析失败直接降级(不重试),但 JSONDecodeError 可以重试 1 次。
+
+
+ pytest tests/test_semantic_robustness.py::test_parse_failure_retry -x
+
+
+ - JSONDecodeError 和 ValueError 触发重试 1 次
+ - ValidationError 直接降级,不重试
+ - 所有解析失败都记录详细日志(错误类型 + LLM 输出片段)
+ - 测试覆盖各种解析失败场景
+
+
+
+
+
+
+## 整体验证标准
+
+1. **JSON 解析失败**: JSONDecodeError 触发重试 1 次,仍失败则降级
+2. **Pydantic 验证失败**: ValidationError 直接降级,不重试
+3. **JSON 提取失败**: ValueError 触发重试 1 次,仍失败则降级
+4. **日志完整性**: 所有解析失败都记录错误类型和 LLM 输出片段
+
+运行完整测试套件:
+```bash
+python3 -m pytest tests/test_semantic_robustness.py::test_parse_failure_retry -v -x
+```
+
+
+
+1. JSONDecodeError 重试 1 次后降级
+2. ValidationError 直接降级,不重试
+3. 所有解析失败都记录 warning 级别日志
+4. 日志包含错误类型、错误消息、LLM 输出前 500 字符
+5. 所有测试通过
+
+
+
diff --git a/.planning/phases/14-robustness-cost/14-03-PLAN.md b/.planning/phases/14-robustness-cost/14-03-PLAN.md
new file mode 100644
index 0000000..377fab2
--- /dev/null
+++ b/.planning/phases/14-robustness-cost/14-03-PLAN.md
@@ -0,0 +1,272 @@
+---
+phase: 14-robustness-cost
+plan: 03
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - kb_workflow/srt/cache/semantic_cache.py
+ - kb_workflow/srt/cache/__init__.py
+autonomous: true
+requirements:
+ - COST-02
+must_haves:
+ truths:
+ - "相同内容的字幕使用缓存结果,避免重复 LLM 调用"
+ - "缓存键基于内容哈希(SHA256),精确匹配"
+ - "缓存存储在本地文件系统 (.planning/cache/semantic/)"
+ artifacts:
+ - path: "kb_workflow/srt/cache/semantic_cache.py"
+ provides: "SemanticCache 缓存管理器"
+ exports: ["SemanticCache", "compute_content_hash"]
+ - path: "kb_workflow/srt/cache/__init__.py"
+ provides: "缓存模块导出"
+ exports: ["SemanticCache"]
+ key_links:
+ - from: "SemanticCache.get()"
+ to: ".planning/cache/semantic/"
+ via: "文件系统读取"
+ pattern: "Path.read_text"
+ - from: "SemanticCache.set()"
+ to: ".planning/cache/semantic/"
+ via: "文件系统写入"
+ pattern: "Path.write_text"
+---
+
+
+实现基于内容哈希的智能缓存,避免重复 LLM 调用,降低成本和延迟。
+
+Purpose: 相同内容的字幕直接使用缓存结果,减少 LLM API 调用次数。
+Output: SemanticCache 缓存管理器,支持内容哈希、文件存储、TTL 永久有效。
+
+
+
+@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
+@/home/starmen1265/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/14-robustness-cost/14-CONTEXT.md
+@.planning/phases/14-robustness-cost/14-RESEARCH.md
+@.planning/phases/14-robustness-cost/14-VALIDATION.md
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@.planning/STATE.md
+
+@kb_workflow/srt/semantic_engine.py
+@kb_workflow/srt/corrector.py
+
+
+
+
+
+From 14-CONTEXT.md (User Decisions):
+```python
+# 缓存机制
+- 缓存键: 字幕内容哈希(SHA256)— 精确匹配
+- 缓存存储: 本地文件系统 — .planning/cache/semantic/
+- 缓存 TTL: 永久有效 — 手动清理
+- 缓存内容: 完整的 SegmentationResult(JSON 格式)
+```
+
+From kb_workflow/srt/semantic_engine.py:
+```python
+class SegmentationResult(BaseModel):
+ video_type: str
+ segments: List[VideoSegment]
+ actual_segment_count: int
+
+class VideoSegment(BaseModel):
+ title: Optional[str]
+ content: str
+ start_index: int
+ end_index: int
+```
+
+关键观察:
+- SegmentationResult 是 Pydantic 模型,可以序列化为 JSON
+- 需要缓存完整的 SegmentationResult 对象
+- 缓存键应该仅基于内容哈希,不包含元数据
+
+
+
+
+
+ Task 1: 创建 SemanticCache 缓存管理器
+ kb_workflow/srt/cache/semantic_cache.py, kb_workflow/srt/cache/__init__.py
+
+ - compute_content_hash(): 计算内容的 SHA256 哈希
+ - get(): 从缓存读取 SegmentationResult
+ - set(): 保存 SegmentationResult 到缓存
+ - 缓存目录不存在时自动创建
+ - 缓存文件命名: {hash}.json
+ - 缓存内容包含 version 字段(未来兼容性)
+ - 缓存命中/未命中记录 debug 日志
+
+
+ 创建 kb_workflow/srt/cache/semantic_cache.py:
+
+ 1. 导入依赖:
+ ```python
+ import hashlib
+ import json
+ import logging
+ from pathlib import Path
+ from typing import Optional
+
+ from kb_workflow.srt.semantic_engine import SegmentationResult
+ ```
+
+ 2. 实现 compute_content_hash():
+ ```python
+ def compute_content_hash(content: str) -> str:
+ """计算内容的 SHA256 哈希作为缓存键"""
+ return hashlib.sha256(content.encode('utf-8')).hexdigest()
+ ```
+
+ 3. 实现 SemanticCache 类:
+ ```python
+ class SemanticCache:
+ """语义分段结果缓存管理器
+
+ 使用内容哈希(SHA256)作为缓存键,存储在本地文件系统。
+ """
+
+ CACHE_VERSION = "1.0"
+ DEFAULT_CACHE_DIR = Path(".planning/cache/semantic")
+
+ def __init__(self, cache_dir: Optional[Path] = None):
+ """初始化缓存管理器
+
+ Args:
+ cache_dir: 缓存目录路径,默认为 .planning/cache/semantic/
+ """
+ self.cache_dir = cache_dir or self.DEFAULT_CACHE_DIR
+ self.logger = logging.getLogger(__name__)
+
+ def get(self, content: str) -> Optional[SegmentationResult]:
+ """从缓存读取分段结果
+
+ Args:
+ content: 字幕内容
+
+ Returns:
+ 缓存的 SegmentationResult,如果不存在则返回 None
+ """
+ cache_key = compute_content_hash(content)
+ cache_file = self.cache_dir / f"{cache_key}.json"
+
+ if not cache_file.exists():
+ self.logger.debug(f"Cache miss: {cache_key}")
+ return None
+
+ try:
+ data = json.loads(cache_file.read_text(encoding='utf-8'))
+
+ # 验证版本
+ if data.get('version') != self.CACHE_VERSION:
+ self.logger.warning(
+ f"Cache version mismatch: {data.get('version')} != {self.CACHE_VERSION}"
+ )
+ return None
+
+ # 移除 version 字段,转换为 SegmentationResult
+ data.pop('version', None)
+ result = SegmentationResult(**data)
+
+ self.logger.debug(f"Cache hit: {cache_key}")
+ return result
+
+ except (json.JSONDecodeError, IOError, ValidationError) as e:
+ self.logger.warning(f"Failed to load cache: {e}")
+ return None
+
+ def set(self, content: str, result: SegmentationResult) -> None:
+ """保存分段结果到缓存
+
+ Args:
+ content: 字幕内容
+ result: 分段结果
+ """
+ cache_key = compute_content_hash(content)
+ cache_file = self.cache_dir / f"{cache_key}.json"
+
+ try:
+ # 确保目录存在
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
+
+ # 序列化为 JSON,添加 version 字段
+ data = result.model_dump()
+ data['version'] = self.CACHE_VERSION
+
+ cache_file.write_text(
+ json.dumps(data, ensure_ascii=False, indent=2),
+ encoding='utf-8'
+ )
+
+ self.logger.debug(f"Cache saved: {cache_key}")
+
+ except IOError as e:
+ self.logger.warning(f"Failed to save cache: {e}")
+ ```
+
+ 4. 创建 kb_workflow/srt/cache/__init__.py:
+ ```python
+ """语义分段缓存模块"""
+
+ from kb_workflow.srt.cache.semantic_cache import (
+ SemanticCache,
+ compute_content_hash
+ )
+
+ __all__ = ['SemanticCache', 'compute_content_hash']
+ ```
+
+ 遵守用户决策:
+ - 缓存键: SHA256 内容哈希
+ - 缓存存储: 本地文件系统 (.planning/cache/semantic/)
+ - 缓存 TTL: 永久有效
+ - 缓存内容: 完整的 SegmentationResult
+
+
+ pytest tests/test_semantic_robustness.py::test_cache_hit -x
+
+
+ - SemanticCache 类实现完整的缓存读写功能
+ - compute_content_hash() 使用 SHA256 计算内容哈希
+ - 缓存目录不存在时自动创建
+ - 缓存文件包含 version 字段,支持未来兼容性
+ - 缓存命中/未命中记录 debug 日志
+ - 缓存加载失败时返回 None,不中断流程
+
+
+
+
+
+
+## 整体验证标准
+
+1. **缓存键计算**: 相同内容产生相同的 SHA256 哈希
+2. **缓存命中**: 第二次调用时从缓存读取,不调用 LLM
+3. **缓存未命中**: 首次调用时保存结果到缓存
+4. **版本控制**: 缓存包含 version 字段,支持未来格式变更
+5. **错误处理**: 缓存读写失败时返回 None,不中断流程
+
+运行完整测试套件:
+```bash
+python3 -m pytest tests/test_semantic_robustness.py::test_cache_hit -v -x
+```
+
+
+
+1. 相同内容第二次调用时从缓存读取
+2. 缓存文件存储在 .planning/cache/semantic/ 目录
+3. 缓存文件命名格式: {sha256_hash}.json
+4. 缓存内容包含完整的 SegmentationResult (JSON 格式)
+5. 缓存命中时记录 debug 级别日志
+6. 所有测试通过
+
+
+
diff --git a/.planning/phases/14-robustness-cost/14-04-PLAN.md b/.planning/phases/14-robustness-cost/14-04-PLAN.md
new file mode 100644
index 0000000..0fb9d2a
--- /dev/null
+++ b/.planning/phases/14-robustness-cost/14-04-PLAN.md
@@ -0,0 +1,352 @@
+---
+phase: 14-robustness-cost
+plan: 04
+type: execute
+wave: 1
+depends_on:
+ - 14-03
+files_modified:
+ - kb_workflow/srt/semantic_engine.py
+autonomous: true
+requirements:
+ - COST-01
+must_haves:
+ truths:
+ - "长视频字幕(>5000字符)自动分块处理"
+ - "分块按句子边界分割,保持语义完整性"
+ - "每个分块独立调用 LLM 分段,结果直接拼接"
+ artifacts:
+ - path: "kb_workflow/srt/semantic_engine.py"
+ provides: "UnifiedSegmentationStrategy 分块处理逻辑"
+ exports: ["_split_into_chunks", "_merge_segment_results"]
+ key_links:
+ - from: "_split_into_chunks()"
+ to: "ASRCorrector._correct_long_text()"
+ via: "复用分批模式"
+ pattern: "按句子边界分割"
+ - from: "segment()"
+ to: "_split_into_chunks()"
+ via: "长文本分块"
+ pattern: "len(content) > 5000"
+---
+
+
+实现长视频字幕的分块处理,避免 LLM 上下文窗口溢出。
+
+Purpose: 处理超过 5000 字符的长字幕,按句子边界分割后独立分段。
+Output: 带分块处理的 UnifiedSegmentationStrategy.segment() 方法。
+
+
+
+@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
+@/home/starmen1265/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/14-robustness-cost/14-CONTEXT.md
+@.planning/phases/14-robustness-cost/14-RESEARCH.md
+@.planning/phases/14-robustness-cost/14-VALIDATION.md
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@.planning/STATE.md
+
+@kb_workflow/srt/semantic_engine.py
+@kb_workflow/srt/corrector.py
+@.planning/phases/14-robustness-cost/14-03-PLAN.md
+
+
+
+
+
+From kb_workflow/srt/corrector.py (分批处理模式):
+```python
+def _correct_long_text(self, text: str) -> str:
+ """分批处理长文本
+
+ 按句子分割,每批不超过 batch_size 字符。
+ """
+ # 按句子分割(简单分割,基于句号、问号、感叹号)
+ sentences = []
+ current = ""
+ for char in text:
+ current += char
+ if char in '。!?\n':
+ sentences.append(current)
+ current = ""
+
+ # 添加最后一个不完整的句子
+ if current:
+ sentences.append(current)
+
+ # 分批处理
+ batches = []
+ current_batch = []
+ current_length = 0
+
+ for sentence in sentences:
+ if current_length + len(sentence) > self.batch_size:
+ if current_batch:
+ batches.append(''.join(current_batch))
+ current_batch = [sentence]
+ current_length = len(sentence)
+ else:
+ current_batch.append(sentence)
+ current_length += len(sentence)
+
+ if current_batch:
+ batches.append(''.join(current_batch))
+
+ # 处理每个批次
+ corrected_batches = []
+ for batch in batches:
+ corrected = self._correct_batch(batch)
+ corrected_batches.append(corrected)
+
+ return ''.join(corrected_batches)
+```
+
+关键观察:
+- 按标点符号(。!?\n)分割句子
+- 每批不超过 batch_size (5000)
+- 处理完每批后直接拼接结果
+
+From 14-CONTEXT.md (User Decisions):
+```python
+# 分块处理
+- 分块阈值: 5000 字符(约 10-15 分钟字幕)
+- 分块方式: 按句子边界分割(句号、问号、感叹号等)
+- 结果合并: 直接拼接各分块结果
+- 分块策略: 长字幕按句子边界分成多个 5000 字符以内的块
+```
+
+From 14-RESEARCH.md (Open Questions):
+> **分块后的分段索引如何计算?**
+> - What we know: 每个分块独立分段,结果直接拼接
+> - What's unclear: VideoSegment.start_index 和 end_index 是相对于原文还是分块?
+> - Recommendation: 相对于原文,需要在分块时记录每个分块的起始偏移量,分段后累加偏移量
+
+决策: start_index/end_index 相对于原文,需要记录分块偏移量。
+
+
+
+
+
+ Task 1: 实现长文本分块处理
+ kb_workflow/srt/semantic_engine.py
+
+ - 添加 _split_into_chunks() 方法,按句子边界分割长文本
+ - 分块阈值: 5000 字符
+ - 添加 _merge_segment_results() 方法,合并分块结果
+ - 修正 VideoSegment.start_index 和 end_index 为相对于原文的位置
+ - 短文本(<= 5000 字符)直接处理,不分块
+
+
+ 在 UnifiedSegmentationStrategy 类中添加分块处理逻辑:
+
+ 1. 添加 _split_into_chunks() 方法(复用 ASRCorrector 模式):
+ ```python
+ def _split_into_chunks(self, text: str, max_chunk_size: int = 5000) -> List[tuple[str, int]]:
+ """按句子边界分割文本为多个块
+
+ Args:
+ text: 待分割的文本
+ max_chunk_size: 每块的最大字符数
+
+ Returns:
+ List of (chunk_text, offset) 元组,offset 是该块在原文中的起始位置
+ """
+ # 按句子分割
+ sentences = []
+ current = ""
+ for char in text:
+ current += char
+ if char in '。!?\n':
+ sentences.append(current)
+ current = ""
+
+ # 添加最后一个不完整的句子
+ if current:
+ sentences.append(current)
+
+ # 分块
+ chunks = []
+ current_chunk = []
+ current_length = 0
+ current_offset = 0
+
+ for sentence in sentences:
+ sentence_len = len(sentence)
+ if current_length + sentence_len > max_chunk_size:
+ if current_chunk:
+ chunk_text = ''.join(current_chunk)
+ chunks.append((chunk_text, current_offset))
+ current_offset += len(chunk_text)
+ current_chunk = [sentence]
+ current_length = sentence_len
+ else:
+ current_chunk.append(sentence)
+ current_length += sentence_len
+
+ # 添加最后一个块
+ if current_chunk:
+ chunk_text = ''.join(current_chunk)
+ chunks.append((chunk_text, current_offset))
+
+ return chunks
+ ```
+
+ 2. 添加 _merge_segment_results() 方法:
+ ```python
+ def _merge_segment_results(
+ self,
+ chunk_results: List[tuple[List[VideoSegment], int]]
+ ) -> List[VideoSegment]:
+ """合并分块的分段结果
+
+ Args:
+ chunk_results: List of (segments, offset) 元组
+
+ Returns:
+ 合并后的分段列表,修正 start_index 和 end_index 为相对于原文的位置
+ """
+ all_segments = []
+
+ for segments, offset in chunk_results:
+ for seg in segments:
+ # 修正索引为相对于原文的位置
+ corrected_seg = VideoSegment(
+ title=seg.title,
+ content=seg.content,
+ start_index=seg.start_index + offset,
+ end_index=seg.end_index + offset
+ )
+ all_segments.append(corrected_seg)
+
+ return all_segments
+ ```
+
+ 3. 修改 segment() 方法,添加分块逻辑:
+ ```python
+ def segment(self, segments: List, metadata: dict) -> List[VideoSegment]:
+ # AI 不可用,直接使用规则分段
+ if self.ai_client is None:
+ logger.info("AI client unavailable, using fallback segmenter")
+ paragraphs = self.fallback_segmenter.segment(segments)
+ return [
+ VideoSegment(title=None, content=para, start_index=0, end_index=0)
+ for para in paragraphs
+ ]
+
+ # 合并字幕文本
+ content = " ".join(seg.text for seg in segments)
+
+ # 检查是否需要分块
+ if len(content) <= 5000:
+ # 短文本直接处理
+ return self._segment_single_chunk(content, metadata, segments)
+ else:
+ # 长文本分块处理
+ return self._segment_multiple_chunks(content, metadata, segments)
+ ```
+
+ 4. 添加 _segment_single_chunk() 和 _segment_multiple_chunks() 辅助方法:
+ ```python
+ def _segment_single_chunk(
+ self,
+ content: str,
+ metadata: dict,
+ fallback_segments: List
+ ) -> List[VideoSegment]:
+ """处理单个文本块(不分块)"""
+ prompt = self._build_prompt(content=content, metadata=metadata)
+
+ # 调用 LLM(带重试和降级)
+ # ... (现有的 LLM 调用逻辑)
+
+ def _segment_multiple_chunks(
+ self,
+ content: str,
+ metadata: dict,
+ fallback_segments: List
+ ) -> List[VideoSegment]:
+ """处理多个文本块(分块)"""
+ logger.info(f"Content too long ({len(content)} chars), splitting into chunks")
+
+ # 分块
+ chunks = self._split_into_chunks(content, max_chunk_size=5000)
+ logger.info(f"Split into {len(chunks)} chunks")
+
+ # 处理每个分块
+ chunk_results = []
+ for i, (chunk_text, offset) in enumerate(chunks, 1):
+ logger.info(f"Processing chunk {i}/{len(chunks)} (offset={offset}, length={len(chunk_text)})")
+
+ # 构建该分块的 prompt
+ chunk_metadata = metadata.copy()
+ chunk_metadata['chunk_info'] = f"第 {i}/{len(chunks)} 块"
+ prompt = self._build_prompt(content=chunk_text, metadata=chunk_metadata)
+
+ try:
+ # 调用 LLM
+ llm_output = self.ai_client.generate(prompt, max_tokens=8000, disable_reasoning=True)
+ segments = self._parse_result(llm_output, fallback_segments=fallback_segments)
+ chunk_results.append((segments, offset))
+ logger.info(f"Chunk {i} segmented successfully: {len(segments)} segments")
+ except Exception as e:
+ logger.warning(f"Chunk {i} failed: {e}, using fallback for this chunk")
+ # 该分块降级到规则分段
+ chunk_segments = self._fallback_chunk(chunk_text, offset)
+ chunk_results.append((chunk_segments, offset))
+
+ # 合并结果
+ return self._merge_segment_results(chunk_results)
+ ```
+
+ 遵守用户决策:
+ - 分块阈值: 5000 字符
+ - 按句子边界分割(。!?\n)
+ - 直接拼接各分块结果
+ - start_index/end_index 相对于原文
+
+
+ pytest tests/test_semantic_robustness.py::test_long_text_chunking -x
+
+
+ - 长文本(>5000字符)自动分块处理
+ - 按句子边界分割,保持语义完整性
+ - 每个分块独立调用 LLM 分段
+ - 分块结果直接拼接,start_index/end_index 相对于原文
+ - 分块失败时该分块降级到规则分段
+ - 短文本(<=5000字符)不分块,直接处理
+
+
+
+
+
+
+## 整体验证标准
+
+1. **分块触发**: 超过 5000 字符的文本自动分块
+2. **句子边界**: 分块在标点符号(。!?\n)后分割
+3. **索引修正**: VideoSegment.start_index 和 end_index 相对于原文
+4. **分块降级**: 单个分块失败时降级到规则分段,不影响其他分块
+5. **日志记录**: 记录分块信息(块数、偏移量、长度)
+
+运行完整测试套件:
+```bash
+python3 -m pytest tests/test_semantic_robustness.py::test_long_text_chunking -v -x
+```
+
+
+
+1. 长文本(>5000字符)自动分块处理
+2. 每个分块不超过 5000 字符
+3. 分块边界在句子末尾(标点符号后)
+4. VideoSegment.start_index/end_index 相对于原文位置
+5. 分块失败时该分块降级到规则分段
+6. 所有测试通过
+
+
+
diff --git a/.planning/phases/14-robustness-cost/14-05-PLAN.md b/.planning/phases/14-robustness-cost/14-05-PLAN.md
new file mode 100644
index 0000000..db8f103
--- /dev/null
+++ b/.planning/phases/14-robustness-cost/14-05-PLAN.md
@@ -0,0 +1,381 @@
+---
+phase: 14-robustness-cost
+plan: 05
+type: execute
+wave: 2
+depends_on:
+ - 14-01
+ - 14-02
+ - 14-03
+ - 14-04
+files_modified:
+ - kb_workflow/srt/semantic_engine.py
+ - kb_workflow/srt/processor.py
+autonomous: true
+requirements:
+ - COST-03
+must_haves:
+ truths:
+ - "用户可通过配置开关启用/禁用 LLM 分段"
+ - "禁用时使用简单规则分段,不影响其他平台"
+ - "SRTProcessor.enable_semantic_segmentation 参数控制开关"
+ artifacts:
+ - path: "kb_workflow/srt/processor.py"
+ provides: "SRTProcessor 配置开关"
+ exports: ["SRTProcessor.__init__"]
+ key_links:
+ - from: "SRTProcessor.__init__()"
+ to: "SemanticSegmentationEngine"
+ via: "enable_semantic_segmentation 参数"
+ pattern: "if enable_semantic_segmentation"
+---
+
+
+集成所有鲁棒性和成本控制功能到 SRTProcessor,提供配置开关控制 LLM 分段。
+
+Purpose: 完成阶段 14 的集成,确保所有功能可通过配置开关控制。
+Output: 集成缓存、分块、重试、降级的 SRTProcessor,支持配置开关。
+
+
+
+@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
+@/home/starmen1265/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/phases/14-robustness-cost/14-CONTEXT.md
+@.planning/phases/14-robustness-cost/14-RESEARCH.md
+@.planning/phases/14-robustness-cost/14-VALIDATION.md
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@.planning/STATE.md
+
+@kb_workflow/srt/processor.py
+@kb_workflow/srt/semantic_engine.py
+@.planning/phases/14-robustness-cost/14-01-PLAN.md
+@.planning/phases/14-robustness-cost/14-02-PLAN.md
+@.planning/phases/14-robustness-cost/14-03-PLAN.md
+@.planning/phases/14-robustness-cost/14-04-PLAN.md
+
+
+
+
+
+From kb_workflow/srt/processor.py (Phase 13 Plan 07):
+```python
+class SRTProcessor(BaseProcessor):
+ def __init__(
+ self,
+ config: Optional[dict] = None,
+ enable_semantic_segmentation: bool = False,
+ **kwargs
+ ):
+ """初始化 SRT 处理器
+
+ Args:
+ config: 配置字典
+ enable_semantic_segmentation: 是否启用语义分段(默认 False)
+ """
+ super().__init__(config=config, **kwargs)
+
+ # 初始化子模块
+ self.parser = SRTParser()
+ self.corrector = ASRCorrector()
+
+ # 语义分段开关
+ self.enable_semantic_segmentation = enable_semantic_segmentation
+ if self.enable_semantic_segmentation:
+ self.semantic_engine = SemanticSegmentationEngine()
+ else:
+ self.semantic_engine = None
+ self.segmenter = SemanticSegmenter()
+
+ def _segment_subtitles(self, segments: List, metadata: dict) -> List[str]:
+ """分段字幕
+
+ Args:
+ segments: 字幕分段列表
+ metadata: 视频元数据
+
+ Returns:
+ 分段后的文本列表
+ """
+ if self.enable_semantic_segmentation and self.semantic_engine:
+ # 使用 LLM 语义分段
+ video_segments = self.semantic_engine.segment(segments, metadata)
+ # 转换为纯文本列表(保留标题)
+ paragraphs = []
+ for seg in video_segments:
+ if seg.title:
+ paragraphs.append(f"## {seg.title}\n\n{seg.content}")
+ else:
+ paragraphs.append(seg.content)
+ return paragraphs
+ else:
+ # 使用规则分段
+ return self.segmenter.segment(segments)
+```
+
+关键观察:
+- enable_semantic_segmentation 参数控制是否启用 LLM 分段
+- True: 使用 SemanticSegmentationEngine
+- False: 使用 SemanticSegmenter (规则分段)
+- 输出格式: 带标题的分段使用 "## {title}\n\n{content}" 格式
+
+From 14-CONTEXT.md (User Decisions):
+```python
+# Claude's Discretion
+- 具体的重试逻辑实现细节 (Plan 01)
+- 缓存文件命名规则和目录结构 (Plan 03)
+- 分块算法的具体实现 (Plan 04)
+- 日志消息的具体措辞和格式 (Plan 01, 02)
+```
+
+本计划需要:
+1. 集成 SemanticCache 到 UnifiedSegmentationStrategy
+2. 确保配置开关 (enable_semantic_segmentation) 正常工作
+3. 添加集成测试验证完整流程
+
+
+
+
+
+ Task 1: 集成缓存到 UnifiedSegmentationStrategy
+ kb_workflow/srt/semantic_engine.py
+
+ - UnifiedSegmentationStrategy 接受可选的 cache 参数
+ - 在 segment() 方法中,调用 LLM 前先检查缓存
+ - 缓存命中: 直接返回缓存结果
+ - 缓存未命中: 调用 LLM,然后保存结果到缓存
+ - 缓存键仅基于字幕内容,不包含元数据
+
+
+ 修改 UnifiedSegmentationStrategy 类,集成缓存:
+
+ 1. 修改 __init__() 方法,添加 cache 参数:
+ ```python
+ def __init__(
+ self,
+ ai_client: Optional[AIClientProtocol] = None,
+ fallback_segmenter: Optional['SemanticSegmenter'] = None,
+ cache: Optional['SemanticCache'] = None
+ ):
+ """初始化统一分段策略
+
+ Args:
+ ai_client: AI 客户端实例
+ fallback_segmenter: 降级时使用的规则分段器
+ cache: 缓存管理器,如果为 None 则不使用缓存
+ """
+ self.ai_client = ai_client
+ self.fallback_segmenter = fallback_segmenter
+ self.cache = cache
+
+ # Lazy AI Client 初始化(保持不变)
+ if self.ai_client is None:
+ try:
+ from kb_workflow.deep_analyzer import SimpleAIClient
+ self.ai_client = SimpleAIClient()
+ if not self.ai_client.api_key:
+ logger.info("AI API key not configured")
+ self.ai_client = None
+ except Exception as e:
+ logger.warning(f"Failed to initialize AI client: {e}")
+ self.ai_client = None
+
+ # 初始化降级分段器
+ if self.fallback_segmenter is None:
+ from kb_workflow.srt.segmenter import SemanticSegmenter
+ self.fallback_segmenter = SemanticSegmenter()
+ ```
+
+ 2. 修改 segment() 方法,添加缓存逻辑:
+ ```python
+ def segment(self, segments: List, metadata: dict) -> List[VideoSegment]:
+ # AI 不可用,直接使用规则分段
+ if self.ai_client is None:
+ logger.info("AI client unavailable, using fallback segmenter")
+ paragraphs = self.fallback_segmenter.segment(segments)
+ return [
+ VideoSegment(title=None, content=para, start_index=0, end_index=0)
+ for para in paragraphs
+ ]
+
+ # 合并字幕文本
+ content = " ".join(seg.text for seg in segments)
+
+ # 检查缓存
+ if self.cache is not None:
+ cached_result = self.cache.get(content)
+ if cached_result is not None:
+ logger.info("Using cached segmentation result")
+ return cached_result.segments
+
+ # 检查是否需要分块
+ if len(content) <= 5000:
+ # 短文本直接处理
+ video_segments = self._segment_single_chunk(content, metadata, segments)
+ else:
+ # 长文本分块处理
+ video_segments = self._segment_multiple_chunks(content, metadata, segments)
+
+ # 保存到缓存
+ if self.cache is not None:
+ # 构建完整的 SegmentationResult
+ result = SegmentationResult(
+ video_type=self._infer_video_type(metadata),
+ segments=video_segments,
+ actual_segment_count=len(video_segments)
+ )
+ self.cache.set(content, result)
+ logger.debug("Saved segmentation result to cache")
+
+ return video_segments
+ ```
+
+ 3. 修改 SemanticSegmentationEngine.__init__(),传递 cache 参数:
+ ```python
+ def __init__(
+ self,
+ strategy: Optional[SegmentationStrategy] = None,
+ fallback_segmenter: Optional['SemanticSegmenter'] = None,
+ cache: Optional['SemanticCache'] = None
+ ):
+ """初始化语义分段引擎
+
+ Args:
+ strategy: 分段策略实例
+ fallback_segmenter: 降级时使用的规则分段器(已弃用)
+ cache: 缓存管理器
+ """
+ if strategy is None:
+ self.strategy = UnifiedSegmentationStrategy(
+ fallback_segmenter=fallback_segmenter,
+ cache=cache
+ )
+ else:
+ self.strategy = strategy
+ ```
+
+ 遵守用户决策:
+ - 缓存是可选的,通过构造函数传入
+ - 缓存键仅基于内容哈希
+ - 缓存命中时记录 info 日志
+
+
+ pytest tests/test_semantic_robustness.py::test_cache_hit -x
+
+
+ - UnifiedSegmentationStrategy 支持可选缓存
+ - 缓存命中时直接返回,不调用 LLM
+ - 缓存未命中时调用 LLM 并保存结果
+ - SemanticSegmentationEngine 支持传递 cache 参数
+
+
+
+
+ Task 2: 在 SRTProcessor 中集成缓存和验证配置开关
+ kb_workflow/srt/processor.py
+
+ - SRTProcessor 接受可选的 enable_cache 参数
+ - 启用语义分段时创建 SemanticCache 实例
+ - 禁用语义分段时不创建缓存
+ - 配置开关正确控制 LLM 分段和规则分段
+
+
+ 修改 SRTProcessor 类,集成缓存:
+
+ 1. 修改 __init__() 方法,添加缓存支持:
+ ```python
+ def __init__(
+ self,
+ config: Optional[dict] = None,
+ enable_semantic_segmentation: bool = False,
+ enable_cache: bool = True,
+ **kwargs
+ ):
+ """初始化 SRT 处理器
+
+ Args:
+ config: 配置字典
+ enable_semantic_segmentation: 是否启用语义分段(默认 False)
+ enable_cache: 是否启用缓存(默认 True,仅在启用语义分段时有效)
+ """
+ super().__init__(config=config, **kwargs)
+
+ # 初始化子模块
+ self.parser = SRTParser()
+ self.corrector = ASRCorrector()
+
+ # 语义分段开关
+ self.enable_semantic_segmentation = enable_semantic_segmentation
+ if self.enable_semantic_segmentation:
+ # 初始化缓存
+ cache = None
+ if enable_cache:
+ from kb_workflow.srt.cache import SemanticCache
+ cache = SemanticCache()
+
+ self.semantic_engine = SemanticSegmentationEngine(cache=cache)
+ self.segmenter = None
+ else:
+ self.semantic_engine = None
+ self.segmenter = SemanticSegmenter()
+
+ logger.info("Using rule-based segmenter (semantic segmentation disabled)")
+ ```
+
+ 2. 验证 _segment_subtitles() 方法正确使用配置开关:
+ - 检查 enable_semantic_segmentation 为 True 时使用 SemanticSegmentationEngine
+ - 检查 enable_semantic_segmentation 为 False 时使用 SemanticSegmenter
+ - 输出格式正确(带标题的分段使用 "## {title}\n\n{content}")
+
+ 遵守用户决策:
+ - 配置开关控制是否启用 LLM 分段
+ - 默认禁用语义分段(向后兼容)
+ - 缓存默认启用(当语义分段启用时)
+
+
+ pytest tests/test_srt_processor.py::test_semantic_segmentation_toggle -x
+
+
+ - SRTProcessor 支持配置开关控制语义分段
+ - 启用时创建 SemanticCache 和 SemanticSegmentationEngine
+ - 禁用时使用 SemanticSegmenter,不创建缓存
+ - 集成测试验证完整流程
+
+
+
+
+
+
+## 整体验证标准
+
+1. **配置开关**: enable_semantic_segmentation=True 启用 LLM 分段,False 使用规则分段
+2. **缓存集成**: 启用语义分段时缓存工作,禁用时不创建缓存
+3. **向后兼容**: 默认禁用语义分段,不影响现有功能
+4. **完整流程**: 从 SRT 处理到 Markdown 生成的完整流程正常工作
+
+运行完整测试套件:
+```bash
+python3 -m pytest tests/test_srt_processor.py::test_semantic_segmentation_toggle -v -x
+```
+
+运行鲁棒性测试:
+```bash
+python3 -m pytest tests/test_semantic_robustness.py -v -x
+```
+
+
+
+1. enable_semantic_segmentation=True 时使用 LLM 分段
+2. enable_semantic_segmentation=False 时使用规则分段
+3. 缓存在启用语义分段时正常工作
+4. 禁用语义分段时不创建缓存
+5. 所有功能集成测试通过
+6. 所有鲁棒性测试通过
+
+
+