This repository has been archived on 2026-08-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kb-skills/.planning/phases/14-robustness-cost/14-04-PLAN.md
T

353 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 14-robustness-cost
plan: 04
type: execute
wave: 2
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"
---
<objective>
实现长视频字幕的分块处理,避免 LLM 上下文窗口溢出。
Purpose: 处理超过 5000 字符的长字幕,按句子边界分割后独立分段。
Output: 带分块处理的 UnifiedSegmentationStrategy.segment() 方法。
</objective>
<execution_context>
@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
@/home/starmen1265/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
</context>
<interfaces>
<!-- Key contracts from corrector.py for batch processing -->
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 相对于原文,需要记录分块偏移量。
</interfaces>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: 实现长文本分块处理</name>
<files>kb_workflow/srt/semantic_engine.py</files>
<behavior>
- 添加 _split_into_chunks() 方法,按句子边界分割长文本
- 分块阈值: 5000 字符
- 添加 _merge_segment_results() 方法,合并分块结果
- 修正 VideoSegment.start_index 和 end_index 为相对于原文的位置
- 短文本(<= 5000 字符)直接处理,不分块
</behavior>
<action>
在 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 相对于原文
</action>
<verify>
<automated>pytest tests/test_semantic_robustness.py::test_long_text_chunking -x</automated>
</verify>
<done>
- 长文本(>5000字符)自动分块处理
- 按句子边界分割,保持语义完整性
- 每个分块独立调用 LLM 分段
- 分块结果直接拼接,start_index/end_index 相对于原文
- 分块失败时该分块降级到规则分段
- 短文本(<=5000字符)不分块,直接处理
</done>
</task>
</tasks>
<verification>
## 整体验证标准
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
```
</verification>
<success_criteria>
1. 长文本(>5000字符)自动分块处理
2. 每个分块不超过 5000 字符
3. 分块边界在句子末尾(标点符号后)
4. VideoSegment.start_index/end_index 相对于原文位置
5. 分块失败时该分块降级到规则分段
6. 所有测试通过
</success_criteria>
<output>
执行完成后创建 `.planning/phases/14-robustness-cost/14-04-SUMMARY.md`
</output>