feat(kb-deep-analyzer): 添加流式写入器 StreamingWriter
- 支持多线程并发写入到文档不同位置 - 初始化时创建带占位符的文档结构 - 每批完成后写入对应位置 - 支持更新摘要、主题、见解等全局内容
This commit is contained in:
@@ -47,6 +47,182 @@ class AnalysisResult:
|
|||||||
|
|
||||||
# ============== AI 客户端 ==============
|
# ============== AI 客户端 ==============
|
||||||
|
|
||||||
|
# ============== 流式写入器 ==============
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
class StreamingWriter:
|
||||||
|
"""
|
||||||
|
流式写入器 - 支持多线程并发写入到文档不同位置
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
1. 初始化: writer = StreamingWriter(output_path, num_chunks)
|
||||||
|
2. 预先创建带占位符的文档结构
|
||||||
|
3. 每批完成后调用 writer.write_batch(batch_results)
|
||||||
|
4. 完成后调用 writer.mark_complete() 清理占位符
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, output_path: str, num_chunks: int = 0, batch_size: int = 3):
|
||||||
|
self.output_path = Path(output_path)
|
||||||
|
self.num_chunks = num_chunks
|
||||||
|
self.batch_size = batch_size
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.written = {} # 记录已写入的位置
|
||||||
|
self.batch_num = 0
|
||||||
|
|
||||||
|
def initialize_document(self, metadata: Dict, num_chunks: int) -> str:
|
||||||
|
"""
|
||||||
|
初始化文档结构,返回带占位符的内容
|
||||||
|
|
||||||
|
文档结构:
|
||||||
|
---
|
||||||
|
title: "xxx"
|
||||||
|
...
|
||||||
|
---
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
## 主要主题
|
||||||
|
|
||||||
|
## 关键见解
|
||||||
|
|
||||||
|
## 建议行动
|
||||||
|
|
||||||
|
## 时间轴
|
||||||
|
### 第 N 部分 (00:00 - 00:00)
|
||||||
|
[ANALYZING...]
|
||||||
|
"""
|
||||||
|
self.num_chunks = num_chunks
|
||||||
|
|
||||||
|
# 生成时间轴占位符
|
||||||
|
timeline_placeholders = ""
|
||||||
|
for i in range(num_chunks):
|
||||||
|
timeline_placeholders += f"""### 第 {i + 1} 部分 (待分析)
|
||||||
|
[ANALYZING_BATCH_{i // self.batch_size + 1}]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
content = f"""---
|
||||||
|
title: "{metadata.get('title', '未命名')}"
|
||||||
|
date: {datetime.now().strftime('%Y-%m-%d')}
|
||||||
|
duration: {format_duration(metadata.get('duration', 0))}
|
||||||
|
author: {metadata.get('author', '未知')}
|
||||||
|
analyze_time: {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
||||||
|
bvid: {metadata.get('bvid', '')}
|
||||||
|
platform: {metadata.get('platform', 'bilibili')}
|
||||||
|
---
|
||||||
|
|
||||||
|
# {metadata.get('title', '未命名')}
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
[WAITING_FOR_ANALYSIS...]
|
||||||
|
|
||||||
|
## 主要主题
|
||||||
|
[WAITING_FOR_ANALYSIS...]
|
||||||
|
|
||||||
|
## 关键见解
|
||||||
|
[WAITING_FOR_ANALYSIS...]
|
||||||
|
|
||||||
|
## 建议行动
|
||||||
|
[WAITING_FOR_ANALYSIS...]
|
||||||
|
|
||||||
|
## 时间轴
|
||||||
|
{timeline_placeholders}
|
||||||
|
|
||||||
|
---
|
||||||
|
*分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}*
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 写入初始化内容
|
||||||
|
self.output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(self.output_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print(f"[StreamingWriter] 初始化文档: {self.output_path}")
|
||||||
|
return content
|
||||||
|
|
||||||
|
def write_batch(self, batch_results: List[Dict], num_batches: int) -> None:
|
||||||
|
"""
|
||||||
|
写入一批结果
|
||||||
|
|
||||||
|
Args:
|
||||||
|
batch_results: 当前批的结果列表
|
||||||
|
num_batches: 总批次数
|
||||||
|
"""
|
||||||
|
self.batch_num += 1
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
# 读取当前内容
|
||||||
|
if not self.output_path.exists():
|
||||||
|
print(f"[StreamingWriter] 警告: 文件不存在 {self.output_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
content = self.output_path.read_text(encoding='utf-8')
|
||||||
|
|
||||||
|
for result in batch_results:
|
||||||
|
idx = result['index']
|
||||||
|
start_time = result['start_time']
|
||||||
|
end_time = result['end_time']
|
||||||
|
summary = result['summary']
|
||||||
|
|
||||||
|
# 查找并替换占位符
|
||||||
|
old_placeholder = f"### 第 {idx + 1} 部分 ({start_time} - {end_time})\n[ANALYZING_BATCH_{self.batch_num}]"
|
||||||
|
new_content = f"""### 第 {idx + 1} 部分 ({start_time} - {end_time})
|
||||||
|
{summary}"""
|
||||||
|
|
||||||
|
content = content.replace(old_placeholder, new_content, 1)
|
||||||
|
self.written[idx] = True
|
||||||
|
|
||||||
|
print(f"[StreamingWriter] 写入段 {idx + 1}/{self.num_chunks}")
|
||||||
|
|
||||||
|
# 如果这是最后一批,更新其他状态
|
||||||
|
if self.batch_num >= num_batches:
|
||||||
|
# 更新摘要等
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 写回文件
|
||||||
|
self.output_path.write_text(content, encoding='utf-8')
|
||||||
|
print(f"[StreamingWriter] 批次 {self.batch_num}/{num_batches} 完成")
|
||||||
|
|
||||||
|
def update_summary(self, summary_text: str, topics: List[str], insights: List[str]) -> None:
|
||||||
|
"""更新摘要等全局内容"""
|
||||||
|
with self.lock:
|
||||||
|
if not self.output_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
content = self.output_path.read_text(encoding='utf-8')
|
||||||
|
|
||||||
|
# 更新摘要
|
||||||
|
if "[WAITING_FOR_ANALYSIS...]" in content:
|
||||||
|
content = content.replace("## 摘要\n[WAITING_FOR_ANALYSIS...]", f"## 摘要\n{summary_text}")
|
||||||
|
|
||||||
|
# 更新主题
|
||||||
|
topics_text = "\n".join(f"- {t}" for t in topics)
|
||||||
|
if "## 主要主题\n[WAITING_FOR_ANALYSIS...]" in content:
|
||||||
|
content = content.replace("## 主要主题\n[WAITING_FOR_ANALYSIS...]", f"## 主要主题\n{topics_text}")
|
||||||
|
|
||||||
|
# 更新见解
|
||||||
|
insights_text = "\n".join(f"- {i}" for i in insights)
|
||||||
|
if "## 关键见解\n[WAITING_FOR_ANALYSIS...]" in content:
|
||||||
|
content = content.replace("## 关键见解\n[WAITING_FOR_ANALYSIS...]", f"## 关键见解\n{insights_text}")
|
||||||
|
|
||||||
|
self.output_path.write_text(content, encoding='utf-8')
|
||||||
|
print(f"[StreamingWriter] 更新摘要和主题")
|
||||||
|
|
||||||
|
def cleanup_placeholder(self, placeholder: str) -> None:
|
||||||
|
"""清理单个占位符"""
|
||||||
|
with self.lock:
|
||||||
|
if not self.output_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
content = self.output_path.read_text(encoding='utf-8')
|
||||||
|
content = content.replace(placeholder, "")
|
||||||
|
self.output_path.write_text(content, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
class SimpleAIClient:
|
class SimpleAIClient:
|
||||||
"""简化的 AI 客户端 - 支持多提供商 fallback"""
|
"""简化的 AI 客户端 - 支持多提供商 fallback"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user