feat: Add KB knowledge base series skills

- kb-link-parser: Multi-platform URL parser (Bilibili, YouTube, Zhihu)
- kb-bilibili-metadata: Bilibili video/article/up主 metadata extractor
- kb-zhihu-metadata: Zhihu question/answer/user metadata extractor
- kb-bilibili-content: Bilibili video download and content extraction
- kb-ocr-processor: OCR text extraction from video frames
- kb-deep-analyzer: AI-powered deep content analysis
- kb-workflow: Workflow orchestration (inbox, archive, subscriptions, report)

Also add:
- ARCHITECTURE_FLOWCHART.md: System architecture diagrams
- DETAILED_DESIGN.md: Complete design documentation
This commit is contained in:
OpenClaw Agent
2026-02-10 12:18:52 +08:00
parent 2f5a269866
commit 5507b1d986
28 changed files with 7715 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
# KB Deep Analyzer Skill
**kb-deep-analyzer** - AI-powered deep content analysis.
## Features
- Multi-type analysis (tutorial, documentation, news)
- Template-based output
- Quality scoring
- Automatic tagging
## Commands
```bash
# Analyze content
python deep_analyzer.py --metadata meta.json --content content.json --output analysis.md
# Specify type
python deep_analyzer.py --metadata meta.json --content content.json --type tutorial --output tutorial.md
```
## Output
```markdown
---
title: "分析标题"
source: "bilibili"
quality_score: 95
tags:
- python
- tutorial
---
## 一句话总结
内容摘要...
## 核心要点
1. 要点1
2. 要点2
```
## Installation
```bash
cp -r kb-deep-analyzer/ ~/skills/
```
+514
View File
@@ -0,0 +1,514 @@
"""Obsidian KB - AI客户端
支持多个 AI 提供商和 fallback 机制
"""
import os
import json
import requests
from typing import Dict, Any, List, Optional, Tuple
from src.models import ContentType
from src.config import get_config
from src.content_chunker import ContentChunker, get_chunking_strategy, chunk_content
class AIClient:
"""
AI客户端 - 支持分段分析长视频内容和 fallback 机制
"""
def __init__(self):
self.config = get_config()
self.chunker = ContentChunker()
# 获取 AI 配置
self._load_ai_config()
def _load_ai_config(self) -> None:
"""加载 AI 配置(主 provider + fallbacks)"""
self.ai_configs = []
# 加载主配置
main_provider = self.config.get('ai.provider', 'openai')
main_config = {
'provider': main_provider,
'api_key': self.config.get('ai.api_key'),
'model': self.config.get('ai.model', 'gpt-4'),
'base_url': self.config.get('ai.base_url'),
'is_primary': True
}
self.ai_configs.append(main_config)
# 加载 fallback 配置
fallbacks = self.config.get('ai.fallbacks', [])
for fb in fallbacks:
self.ai_configs.append({
'provider': fb.get('provider'),
'api_key': fb.get('api_key'),
'model': fb.get('model'),
'base_url': fb.get('base_url'),
'is_primary': False
})
def _call_api_with_fallback(self, prompt: str, max_tokens: int = 1000, response_format: str = 'text') -> Tuple[bool, str]:
"""
使用 fallback 机制调用 API
Args:
prompt: 提示词
max_tokens: 最大 token 数
response_format: 响应格式 ('text' 或 'json')
Returns:
(success, response) 元组
"""
last_error = None
for i, ai_config in enumerate(self.ai_configs):
try:
if not ai_config.get('api_key'):
print(f"[AIClient] 跳过 {ai_config.get('provider')} - API Key 为空")
continue
provider_type = ai_config.get('provider')
print(f"[AIClient] 尝试使用 {provider_type} (优先级 {i+1}/{len(self.ai_configs)})...")
response = self._call_single_provider(ai_config, prompt, max_tokens, response_format)
return True, response
except Exception as e:
last_error = e
print(f"[AIClient] {provider_type} 调用失败: {e}")
continue
# 所有 provider 都失败了
error_msg = f"所有 AI 提供商均失败,最后错误: {last_error}" if last_error else "没有可用的 AI 配置"
print(f"[AIClient] {error_msg}")
return False, self._mock_generate(prompt, max_tokens) # 返回 mock 响应作为兜底
def _call_single_provider(self, ai_config: Dict, prompt: str, max_tokens: int, response_format: str) -> str:
"""
调用单个 AI 提供商
Args:
ai_config: AI 配置
prompt: 提示词
max_tokens: 最大 token 数
response_format: 响应格式
Returns:
API 响应文本
"""
provider = ai_config.get('provider')
api_key = ai_config.get('api_key')
model = ai_config.get('model')
base_url = ai_config.get('base_url')
if provider == 'minimax':
return self._call_minimax(api_key, model, base_url, prompt, max_tokens, response_format)
elif provider == 'zhipu':
return self._call_zhipu(api_key, model, base_url, prompt, max_tokens, response_format)
elif provider == 'alibaba':
return self._call_alibaba(api_key, model, base_url, prompt, max_tokens, response_format)
elif provider == 'openai':
return self._call_openai(api_key, model, base_url, prompt, max_tokens, response_format)
else:
raise ValueError(f"不支持的 provider: {provider}")
def _call_minimax(self, api_key: str, model: str, base_url: str, prompt: str, max_tokens: int, response_format: str) -> str:
"""调用 MiniMax API (Anthropic Messages 兼容接口)"""
url = base_url or "https://api.minimaxi.com/anthropic/v1/messages"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"max_tokens": max_tokens,
"messages": [
{"role": "user", "content": prompt}
]
}
if response_format == 'json':
payload['messages'][0]['content'] = prompt + "\n\n请直接返回 JSON 格式,不要有其他文字。"
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
data = response.json()
# MiniMax 返回的是 Anthropic 格式,content 是一个数组
# 需要找到 type='text' 的元素
for item in data.get('content', []):
if item.get('type') == 'text':
return item.get('text', '')
# 如果没有找到 text 类型,返回第一个元素的值(兼容旧格式)
return data.get('content', [{}])[0].get('text', '')
def _call_zhipu(self, api_key: str, model: str, base_url: str, prompt: str, max_tokens: int, response_format: str) -> str:
"""调用智谱 AI API (OpenAI 兼容接口)"""
# 确保 base_url 包含 /chat/completions
if base_url and not base_url.endswith('/chat/completions'):
url = f"{base_url}/chat/completions"
else:
url = base_url or "https://open.bigmodel.cn/api/paas/v4/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"max_tokens": max_tokens,
"messages": [
{"role": "user", "content": prompt}
]
}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
data = response.json()
return data['choices'][0]['message']['content']
def _call_alibaba(self, api_key: str, model: str, base_url: str, prompt: str, max_tokens: int, response_format: str) -> str:
"""调用阿里云通义千问 API (OpenAI 兼容接口)"""
url = f"{base_url}/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"max_tokens": max_tokens,
"messages": [
{"role": "user", "content": prompt}
]
}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
data = response.json()
return data['choices'][0]['message']['content']
def _call_openai(self, api_key: str, model: str, base_url: str, prompt: str, max_tokens: int, response_format: str) -> str:
"""调用 OpenAI API"""
url = base_url or "https://api.openai.com/v1/chat/completions"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"max_tokens": max_tokens,
"messages": [
{"role": "user", "content": prompt}
]
}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
data = response.json()
return data['choices'][0]['message']['content']
def quick_generate(self, prompt: str, max_tokens: int = 100) -> str:
"""快速生成(用于轻量扫描)"""
success, response = self._call_api_with_fallback(prompt, max_tokens, 'text')
return response
def generate_json(self, prompt: str) -> Dict[str, Any]:
"""生成JSON格式响应"""
success, response = self._call_api_with_fallback(prompt, 1000, 'json')
try:
return json.loads(response)
except:
return self._parse_json_from_text(response)
def _deep_analyze(self, content_text: str, content_type: str, duration: Optional[int] = None) -> Dict[str, Any]:
"""
C5.3: 深度分析入口,根据内容时长决定是否分段分析
Args:
content_text: 内容文本
content_type: 内容类型
duration: 视频时长(秒)
Returns:
分析结果字典
"""
duration = duration or 0
print(f"[AIClient] 深度分析: type={content_type}, duration={duration}s")
# 长视频(>=30分钟)且是视频类型,使用分段分析
if duration >= 1800 and content_type == "video":
print(f"[AIClient] 使用分段分析策略")
return self._analyze_in_chunks(content_text, duration)
# 短视频直接分析
print(f"[AIClient] 使用直接分析策略")
return self._analyze_direct(content_text)
def _analyze_in_chunks(self, content_text: str, duration: int) -> Dict[str, Any]:
"""
C5.3: 分段分析长视频
Args:
content_text: SRT字幕文本
duration: 视频时长(秒)
Returns:
合并后的分析结果
"""
# 1. 获取分段策略
strategy = get_chunking_strategy(duration)
print(f"[AIClient] 分段策略: {strategy['description']}")
# 2. 执行分片
chunks = chunk_content(content_text, duration)
print(f"[AIClient] 内容已分为 {len(chunks)} 段")
# 3. 逐段分析
chunk_summaries = []
for i, chunk in enumerate(chunks):
print(f"[AIClient] 分析第 {i+1}/{len(chunks)} 段...")
summary = self._analyze_chunk(chunk.text, chunk.index)
# 处理时间格式(可能是float秒数或字符串)
start_time = chunk.start_time
end_time = chunk.end_time
if isinstance(start_time, (int, float)):
start_time = self._format_time(start_time)
if isinstance(end_time, (int, float)):
end_time = self._format_time(end_time)
chunk_summaries.append({
'index': chunk.index,
'start_time': start_time,
'end_time': end_time,
'summary': summary
})
# 4. 合并所有段摘要
print(f"[AIClient] 合并 {len(chunk_summaries)} 段分析结果")
final_result = self._merge_summaries(chunk_summaries, "video")
# 5. 构建完整时间轴
final_result['timeline'] = self._build_timeline_from_chunks(chunk_summaries)
return final_result
def _analyze_chunk(self, chunk_text: str, chunk_index: int) -> str:
"""
C5.3: 分析单个内容分片
Args:
chunk_text: 分片文本
chunk_index: 分片索引
Returns:
分片摘要(限制500字以内)
"""
# 截断文本避免过长
text_sample = chunk_text[:3000] if len(chunk_text) > 3000 else chunk_text
prompt = f"""请对以下视频片段进行简要分析(限制在300字以内):
片段内容:
{text_sample}
请提供:
1. 这段的核心主题(50字以内)
2. 关键要点(3-5条)
3. 重要细节或引用
请用中文回答,控制在300字以内。"""
try:
summary = self.quick_generate(prompt, max_tokens=500)
# 确保不超过500字
if len(summary) > 500:
summary = summary[:497] + "..."
return summary
except Exception as e:
print(f"[AIClient] 分片 {chunk_index} 分析失败: {e}")
return f"第{chunk_index+1}段分析失败"
def _merge_summaries(self, chunk_summaries: List[Dict], content_type: str) -> Dict[str, Any]:
"""
C5.3: 合并所有段摘要,生成完整分析报告
Args:
chunk_summaries: 各段摘要列表
content_type: 内容类型
Returns:
完整分析结果
"""
# 构建合并提示
summaries_text = "\n\n".join([
f"=== 第{s['index']+1}段 ({s['start_time']}-{s['end_time']}) ===\n{s['summary']}"
for s in chunk_summaries
])
prompt = f"""基于以下各段分析,生成完整的视频分析报告:
{summaries_text}
请提供以下格式的JSON:
{{
"summary": "整体摘要(200字以内)",
"topics": ["主题1", "主题2", "主题3", "主题4"],
"insights": ["深入见解1", "深入见解2", "深入见解3"],
"actions": ["建议行动1", "建议行动2"],
"full_analysis": "详细分析(500字以内)"
}}"""
try:
result = self.generate_json(prompt)
# 确保必要字段存在
if 'topics' not in result:
result['topics'] = ["主题A", "主题B", "主题C"]
if 'insights' not in result:
result['insights'] = ["重要见解1", "重要见解2"]
if 'actions' not in result:
result['actions'] = []
if 'summary' not in result:
result['summary'] = "长视频分析完成"
if 'full_analysis' not in result:
result['full_analysis'] = summaries_text[:500]
return result
except Exception as e:
print(f"[AIClient] 合并摘要失败: {e}")
# 返回基础合并结果
return {
'summary': f'视频共分为{len(chunk_summaries)}个部分进行分析',
'topics': [f"主题{i+1}" for i in range(min(4, len(chunk_summaries)))],
'insights': ['从分段分析中综合得出'],
'actions': [],
'full_analysis': summaries_text[:800]
}
def _analyze_direct(self, content_text: str) -> Dict[str, Any]:
"""直接分析(不分段)"""
# 截断文本
text_sample = content_text[:5000] if len(content_text) > 5000 else content_text
prompt = f"""请分析以下内容,提供JSON格式结果:
{text_sample}
返回格式:
{{
"summary": "整体摘要(150字以内)",
"topics": ["主题1", "主题2", "主题3"],
"insights": ["见解1", "见解2"],
"actions": ["行动建议1"],
"timeline": [
{{"start_time": "00:00", "end_time": "05:00", "title": "第一部分", "summary": "..."}}
]
}}"""
try:
return self.generate_json(prompt)
except Exception as e:
print(f"[AIClient] 直接分析失败: {e}")
return {
'summary': '分析失败',
'topics': ['无法解析'],
'insights': ['请检查内容格式'],
'actions': [],
'timeline': []
}
def _format_time(self, seconds: float) -> str:
"""格式化秒数为时间字符串"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
if hours > 0:
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
return f"{minutes:02d}:{secs:02d}"
def _build_timeline_from_chunks(self, chunk_summaries: List[Dict]) -> List[Dict[str, Any]]:
"""从分片摘要构建时间轴"""
timeline = []
for summary in chunk_summaries:
timeline.append({
'index': summary['index'],
'start_time': summary['start_time'],
'end_time': summary['end_time'],
'title': f"第{summary['index']+1}部分",
'summary': summary['summary'][:200] + "..." if len(summary['summary']) > 200 else summary['summary']
})
return timeline
def _mock_generate(self, prompt: str, max_tokens: int) -> str:
"""模拟AI生成(实际实现应调用真实API)"""
# 根据prompt内容返回模拟响应
prompt_lower = prompt.lower()
if 'json' in prompt_lower or 'json格式' in prompt:
if '主题' in prompt or 'topic' in prompt_lower:
return json.dumps({
'summary': '这是一个关于人工智能和技术发展的内容,涵盖了多个重要主题。',
'topics': ['人工智能', '机器学习', '未来发展', '技术趋势'],
'insights': [
'AI正在改变我们的工作方式',
'机器学习应用越来越广泛',
'未来技术发展趋势值得关注'
],
'actions': ['深入研究相关技术', '关注行业动态'],
'full_analysis': '详细分析内容...'
}, ensure_ascii=False)
return json.dumps({'result': '模拟JSON响应'})
if '摘要' in prompt or 'summary' in prompt_lower:
return '这是一段关于人工智能发展的重要讨论,涵盖了技术趋势和未来应用。'
if '关键词' in prompt or 'keyword' in prompt_lower:
return '人工智能, 机器学习, 深度学习, 神经网络, 自然语言处理'
return '模拟AI响应内容'
def _parse_json_from_text(self, text: str) -> Dict[str, Any]:
"""从文本中解析JSON"""
# 尝试提取JSON块
import re
json_match = re.search(r'\{[\s\S]*\}', text)
if json_match:
try:
return json.loads(json_match.group())
except:
pass
return {'raw_response': text}
class MultiAIClient:
"""
多AI客户端 - 支持多种AI提供商的客户端
"""
def __init__(self):
self.config = get_config()
self.primary_client = AIClient()
self.clients = {
'openai': self.primary_client,
'local': AIClient(), # 可以配置为本地模型
}
def _deep_analyze(self, content_text: str, content_type: str, duration: Optional[int] = None) -> Dict[str, Any]:
"""使用主客户端进行深度分析"""
return self.primary_client._deep_analyze(content_text, content_type, duration)
def quick_generate(self, prompt: str, max_tokens: int = 100, provider: str = None) -> str:
"""快速生成"""
client = self.clients.get(provider, self.primary_client)
return client.quick_generate(prompt, max_tokens)
+771
View File
@@ -0,0 +1,771 @@
#!/usr/bin/env python3
"""Deep Analyzer - AI 深度内容分析工具
支持根据内容时长选择不同分析策略:
- 短视频 (<10分钟): 轻量分析
- 长视频 (>=10分钟): 详细时间轴分析
Usage:
python deep_analyzer.py --metadata meta.json --content content.json --output analysis.md [--type video]
python deep_analyzer.py --help
"""
import argparse
import json
import os
import sys
import re
from datetime import datetime
from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass, field, asdict
# ============== 数据模型 ==============
@dataclass
class TimelineEntry:
"""时间轴条目"""
index: int = 0
start_time: str = ""
end_time: str = ""
title: str = ""
summary: str = ""
@dataclass
class AnalysisResult:
"""分析结果"""
summary: str = ""
main_topics: List[str] = field(default_factory=list)
key_insights: List[str] = field(default_factory=list)
action_items: List[str] = field(default_factory=list)
recommended_tags: List[str] = field(default_factory=list)
full_analysis: str = ""
timeline: List[Dict[str, Any]] = field(default_factory=list)
# ============== AI 客户端 ==============
class SimpleAIClient:
"""简化的 AI 客户端 - 支持多提供商 fallback"""
# 时长阈值(秒)
SHORT_VIDEO_THRESHOLD = 600 # 10分钟
LONG_VIDEO_THRESHOLD = 1800 # 30分钟
def __init__(self, api_key: str = None, provider: str = "mock", model: str = "gpt-4"):
self.api_key = api_key or os.environ.get("AI_API_KEY", "")
self.provider = provider
self.model = model
self.base_url = os.environ.get("AI_BASE_URL", "")
def generate(self, prompt: str, max_tokens: int = 1000) -> str:
"""生成文本"""
if not self.api_key or self.provider == "mock":
return self._mock_generate(prompt, max_tokens)
try:
if self.provider == "openai":
return self._call_openai(prompt, max_tokens)
elif self.provider == "zhipu":
return self._call_zhipu(prompt, max_tokens)
elif self.provider == "minimax":
return self._call_minimax(prompt, max_tokens)
else:
return self._mock_generate(prompt, max_tokens)
except Exception as e:
print(f"[AI Client] 调用失败: {e}")
return self._mock_generate(prompt, max_tokens)
def generate_json(self, prompt: str) -> Dict[str, Any]:
"""生成 JSON 格式响应"""
response = self.generate(prompt, max_tokens=2000)
try:
return json.loads(response)
except json.JSONDecodeError:
# 尝试从文本中提取 JSON
return self._parse_json_from_text(response)
def _call_openai(self, prompt: str, max_tokens: int) -> str:
"""调用 OpenAI API"""
import requests
url = f"{self.base_url}/chat/completions" if self.base_url else "https://api.openai.com/v1/chat/completions"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": self.model,
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": prompt}]
}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
return response.json()['choices'][0]['message']['content']
def _call_zhipu(self, prompt: str, max_tokens: int) -> str:
"""调用智谱 AI API"""
import requests
base_url = self.base_url or "https://open.bigmodel.cn/api/paas/v4"
url = f"{base_url}/chat/completions"
headers = {"Authorization": f"Bearer {self.api_key}"}
payload = {
"model": self.model,
"messages": [{"role": "user", "content": prompt}]
}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
return response.json()['choices'][0]['message']['content']
def _call_minimax(self, prompt: str, max_tokens: int) -> str:
"""调用 MiniMax API"""
import requests
url = self.base_url or "https://api.minimaxi.com/anthropic/v1/messages"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": self.model,
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": prompt}]
}
response = requests.post(url, headers=headers, json=payload, timeout=30)
response.raise_for_status()
data = response.json()
for item in data.get('content', []):
if item.get('type') == 'text':
return item.get('text', '')
return str(data)
def _mock_generate(self, prompt: str, max_tokens: int) -> str:
"""模拟 AI 生成"""
prompt_lower = prompt.lower()
if 'json' in prompt_lower or '请返回' in prompt:
# 返回模拟 JSON 响应
if '主题' in prompt or 'topic' in prompt_lower:
return json.dumps({
'summary': '这是一个模拟的AI分析结果,包含内容的主要主题和关键见解。',
'topics': ['主题一', '主题二', '主题三', '主题四'],
'insights': [
'AI正在深刻改变内容消费方式',
'个性化推荐提升了用户体验',
'技术发展带来新的可能性'
],
'actions': ['深入研究相关技术', '关注行业动态'],
'full_analysis': '详细分析内容基于模拟AI生成。'
}, ensure_ascii=False)
if '标签' in prompt or 'tag' in prompt_lower:
return json.dumps({'tags': ['技术', 'AI', '视频分析', '深度学习']}, ensure_ascii=False)
return json.dumps({'result': '模拟响应'}, ensure_ascii=False)
if '摘要' in prompt or 'summary' in prompt_lower:
return '这是一段关于内容深度分析的重要讨论,涵盖了多个关键主题和见解。'
return '模拟AI响应内容'
def _parse_json_from_text(self, text: str) -> Dict[str, Any]:
"""从文本中解析 JSON"""
json_match = re.search(r'\{[\s\S]*\}', text)
if json_match:
try:
return json.loads(json_match.group())
except:
pass
return {'raw_response': text}
# ============== 深度分析器 ==============
class DeepAnalyzer:
"""深度内容分析器"""
SHORT_VIDEO_THRESHOLD = 600 # 10分钟
LONG_VIDEO_THRESHOLD = 1800 # 30分钟
def __init__(self, ai_client: SimpleAIClient = None):
self.ai_client = ai_client or SimpleAIClient()
def analyze(self, metadata: Dict[str, Any], content: Dict[str, Any],
content_type: str = "video") -> AnalysisResult:
"""
分析内容
Args:
metadata: 元数据字典
content: 内容数据字典(包含 subtitle, description 等)
content_type: 内容类型
Returns:
AnalysisResult 分析结果
"""
title = metadata.get('title', '未知')
duration = metadata.get('duration', 0)
subtitle = content.get('subtitle', '')
description = content.get('description', '')
print(f"[DeepAnalyzer] 开始分析: {title}")
print(f"[DeepAnalyzer] 类型: {content_type}, 时长: {duration}秒")
print(f"[DeepAnalyzer] 字幕存在: {bool(subtitle and subtitle.strip())}")
has_subtitle = bool(subtitle and subtitle.strip())
if has_subtitle:
if duration < self.SHORT_VIDEO_THRESHOLD:
print(f"[DeepAnalyzer] 使用轻量分析策略 (时长 < 10分钟)")
result = self._analyze_short_with_subtitle(title, subtitle, duration)
else:
print(f"[DeepAnalyzer] 使用详细时间轴分析策略 (时长 >= 10分钟)")
result = self._analyze_long_with_subtitle(title, subtitle, duration, content_type)
else:
print(f"[DeepAnalyzer] 降级处理(无字幕)")
result = self._analyze_fallback(title, description, metadata)
return result
def _analyze_short_with_subtitle(self, title: str, subtitle: str, duration: int) -> AnalysisResult:
"""有字幕的短视频轻量分析"""
result = AnalysisResult()
text_sample = self._get_text_sample(subtitle, max_chars=5000)
analysis = self._analyze_with_ai_short_text(text_sample, title, duration)
result.summary = analysis.get('summary', title)
result.main_topics = analysis.get('topics', [])
result.key_insights = analysis.get('insights', [])
result.action_items = analysis.get('actions', [])
result.timeline = self._generate_simple_timeline_from_duration(title, duration)
return result
def _analyze_long_with_subtitle(self, title: str, subtitle: str, duration: int,
content_type: str) -> AnalysisResult:
"""有字幕的长视频详细分析"""
result = AnalysisResult()
if duration >= self.LONG_VIDEO_THRESHOLD and content_type == "video":
# 使用分段分析
result = self._analyze_in_chunks(title, subtitle, duration)
else:
# 直接分析
text_sample = self._get_text_sample(subtitle, max_chars=5000)
analysis = self._analyze_with_ai_short_text(text_sample, title, duration)
result.summary = analysis.get('summary', title)
result.main_topics = analysis.get('topics', [])
result.key_insights = analysis.get('insights', [])
result.action_items = analysis.get('actions', [])
result.full_analysis = analysis.get('full_analysis', '')
result.timeline = self._generate_detailed_timeline(title, duration)
return result
def _analyze_in_chunks(self, title: str, subtitle: str, duration: int) -> AnalysisResult:
"""分段分析长视频"""
result = AnalysisResult()
# 确定分段数
if duration < 3600: # <60分钟
num_segments = 3
elif duration < 7200: # 60-120分钟
num_segments = 4
else: # >120分钟
num_segments = 6
# 生成分段摘要
segment_duration = duration // num_segments
chunk_summaries = []
# 简单按字符数分割字幕
subtitle_chunks = self._chunk_subtitle(subtitle, num_segments)
for i, chunk_text in enumerate(subtitle_chunks):
start_sec = i * segment_duration
end_sec = min((i + 1) * segment_duration, duration)
start_time = self._format_time(start_sec)
end_time = self._format_time(end_sec)
# AI 分析每段
chunk_summary = self._analyze_chunk(chunk_text, i + 1)
chunk_summaries.append({
'index': i,
'start_time': start_time,
'end_time': end_time,
'summary': chunk_summary
})
# 合并结果
result = self._merge_summaries(chunk_summaries, title)
result.timeline = self._build_timeline_from_chunks(chunk_summaries)
return result
def _analyze_chunk(self, chunk_text: str, chunk_index: int) -> str:
"""分析单个分片"""
text_sample = self._get_text_sample(chunk_text, max_chars=3000)
prompt = f"""请对以下视频片段进行简要分析(限制在300字以内):
片段内容:
{text_sample}
请用中文简要回答,包括核心主题和关键要点。"""
try:
return self.ai_client.generate(prompt, max_tokens=500)
except Exception as e:
print(f"[DeepAnalyzer] 分片 {chunk_index} 分析失败: {e}")
return f"第{chunk_index}段内容分析"
def _merge_summaries(self, chunk_summaries: List[Dict], title: str) -> AnalysisResult:
"""合并分段摘要"""
summaries_text = "\n\n".join([
f"=== 第{s['index']+1}段 ({s['start_time']}-{s['end_time']}) ===\n{s['summary']}"
for s in chunk_summaries
])
prompt = f"""基于以下视频各段分析,生成完整的分析报告:
标题:{title}
各段内容:
{summaries_text}
请返回JSON格式:
{{
"summary": "整体摘要(150字以内)",
"topics": ["主题1", "主题2", "主题3", "主题4"],
"insights": ["见解1", "见解2", "见解3"],
"actions": ["建议1", "建议2"],
"full_analysis": "详细分析(500字以内)"
}}"""
try:
analysis = self.ai_client.generate_json(prompt)
result = AnalysisResult()
result.summary = analysis.get('summary', title)
result.main_topics = analysis.get('topics', [])
result.key_insights = analysis.get('insights', [])
result.action_items = analysis.get('actions', [])
result.full_analysis = analysis.get('full_analysis', summaries_text)
return result
except Exception as e:
print(f"[DeepAnalyzer] 合并摘要失败: {e}")
return AnalysisResult(
summary=f"共{len(chunk_summaries)}段分析完成",
main_topics=[f"主题{i+1}" for i in range(min(4, len(chunk_summaries)))],
key_insights=["综合各段分析得出"],
full_analysis=summaries_text[:500]
)
def _analyze_fallback(self, title: str, description: str, metadata: Dict) -> AnalysisResult:
"""无字幕降级处理"""
result = AnalysisResult()
if description and description.strip():
summary = description[:200] + "..." if len(description) > 200 else description
else:
summary = f"无字幕内容,标题:{title}"
# 提取标签
tags = ["未分类", "待处理"]
author = metadata.get('author', '')
source = metadata.get('source', '')
if author:
tags.append(f"作者:{author}")
if source:
tags.append(f"来源:{source}")
result.summary = summary
result.main_topics = tags[:3]
result.recommended_tags = tags
result.key_insights = ["无字幕内容,仅能从描述获取基本信息"]
result.action_items = ["建议补充字幕以便深度分析"]
return result
def _analyze_with_ai_short_text(self, text: str, title: str, duration: int) -> Dict[str, Any]:
"""AI 短视频分析"""
prompt = f"""请分析以下视频内容,以JSON格式返回:
标题:{title}
时长:{self._format_time(duration)}
字幕内容:
{text[:3000]}
请返回以下格式JSON:
{{
"summary": "整体摘要(100字以内)",
"topics": ["主题1", "主题2", "主题3"],
"insights": ["见解1", "见解2"],
"actions": ["建议1"]
}}"""
try:
return self.ai_client.generate_json(prompt)
except Exception as e:
print(f"[DeepAnalyzer] AI分析失败: {e}")
return {
"summary": title,
"topics": ["主题1", "主题2", "主题3"],
"insights": ["无法获取深入见解"],
"actions": []
}
def _get_text_sample(self, text: str, max_chars: int) -> str:
"""获取文本样本(头尾采样)"""
if len(text) <= max_chars:
return text
part_size = max_chars // 3
return (
text[:part_size] +
"\n\n[...省略中间部分...]\n\n" +
text[-part_size:]
)
def _chunk_subtitle(self, subtitle: str, num_chunks: int) -> List[str]:
"""简单分割字幕"""
if not subtitle:
return [""] * num_chunks
chunk_size = len(subtitle) // num_chunks
chunks = []
for i in range(num_chunks):
start = i * chunk_size
end = start + chunk_size if i < num_chunks - 1 else len(subtitle)
chunks.append(subtitle[start:end])
return chunks
def _generate_simple_timeline_from_duration(self, title: str, duration: int) -> List[Dict]:
"""生成简单时间轴"""
segments = 2 if duration < 300 else 3
seg_duration = duration // segments
timeline = []
for i in range(segments):
start = i * seg_duration
end = min((i + 1) * seg_duration, duration)
timeline.append({
'index': i + 1,
'start_time': self._format_time(start),
'end_time': self._format_time(end),
'title': f"段落 {i+1}",
'summary': f"从{self._format_time(start)}到{self._format_time(end)}的内容"
})
return timeline
def _generate_detailed_timeline(self, title: str, duration: int) -> List[Dict]:
"""生成详细时间轴"""
return self._generate_simple_timeline_from_duration(title, duration)
def _build_timeline_from_chunks(self, chunk_summaries: List[Dict]) -> List[Dict]:
"""从分段摘要构建时间轴"""
timeline = []
for s in chunk_summaries:
timeline.append({
'index': s['index'] + 1,
'start_time': s['start_time'],
'end_time': s['end_time'],
'title': f"第{s['index']+1}部分",
'summary': s['summary'][:200] + "..." if len(s['summary']) > 200 else s['summary']
})
return timeline
def _format_time(self, seconds: int) -> str:
"""格式化秒数为时间字符串"""
h = seconds // 3600
m = (seconds % 3600) // 60
s = seconds % 60
if h > 0:
return f"{h}:{m:02d}:{s:02d}"
return f"{m}:{s:02d}"
# ============== 模板系统 ==============
class TemplateLoader:
"""模板加载器"""
TEMPLATES = {
"video": """# {{title}}
## 基本信息
- **时长**: {{duration}}
- **作者**: {{author}}
- **来源**: {{source}}
- **分析时间**: {{analyze_time}}
## 摘要
{{summary}}
## 主要主题
{% for topic in topics %}
- {{topic}}
{% endfor %}
## 关键见解
{% for insight in insights %}
- {{insight}}
{% endfor %}
## 建议行动
{% for action in actions %}
- [ ] {{action}}
{% endfor %}
{% if timeline %}
## 时间轴
{% for entry in timeline %}
### {{entry.index}}. {{entry.start_time}} - {{entry.end_time}}
{{entry.title}}: {{entry.summary}}
{% endfor %}
{% endif %}
{% if tags %}
## 标签
{% for tag in tags %}
- {{tag}}
{% endfor %}
{% endif %}
""",
"article": """# {{title}}
## 基本信息
- **作者**: {{author}}
- **来源**: {{source}}
- **分析时间**: {{analyze_time}}
## 摘要
{{summary}}
## 主要主题
{% for topic in topics %}
- {{topic}}
{% endfor %}
## 关键见解
{% for insight in insights %}
- {{insight}}
{% endfor %}
## 建议行动
{% for action in actions %}
- [ ] {{action}}
{% endfor %}
{% if tags %}
## 标签
{% for tag in tags %}
- {{tag}}
{% endfor %}
{% endif %}
"""
}
def load_template(self, template_type: str = "video") -> str:
"""加载模板"""
# 优先检查环境变量
template_path = os.environ.get(f"TEMPLATE_{template_type.upper()}", "")
if template_path and os.path.exists(template_path):
with open(template_path, 'r', encoding='utf-8') as f:
return f.read()
return self.TEMPLATES.get(template_type, self.TEMPLATES["video"])
class TemplateRenderer:
"""简单模板渲染器"""
def render(self, template: str, variables: Dict[str, Any]) -> str:
"""渲染模板"""
result = template
# 替换变量
for key, value in variables.items():
placeholder = f"{{{{{key}}}}}"
result = result.replace(placeholder, str(value))
# 渲染 for 循环 (简化版本)
# 匹配 {% for item in list %} ... {% endfor %}
pattern = r'\{%\s*for\s+(\w+)\s+in\s+(\w+)\s*%\}(.*?)\{%\s*endfor\s*%\}'
def replace_loop(match):
item_name = match.group(1)
list_name = match.group(2)
content = match.group(3)
items = variables.get(list_name, [])
rendered = []
for item in items:
if isinstance(item, dict):
# 字典项
item_content = content
for k, v in item.items():
item_content = item_content.replace(f"{{{{{item_name}.{k}}}}}", str(v))
rendered.append(item_content)
else:
# 简单项
rendered.append(content.replace(f"{{{{{item_name}}}}}", str(item)))
return '\n'.join(rendered)
result = re.sub(pattern, replace_loop, result, flags=re.DOTALL)
# 移除空的 for 循环块
result = re.sub(r'\{%\s*for\s+\w+\s+in\s+\w+\s*%\}\s*\{%\s*endfor\s*%\}', '', result)
return result
# ============== 工具函数 ==============
def load_json(file_path: str) -> Dict:
"""加载 JSON 文件"""
if not file_path or not os.path.exists(file_path):
return {}
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def format_duration(seconds: int) -> str:
"""格式化时长"""
h = seconds // 3600
m = (seconds % 3600) // 60
s = seconds % 60
if h > 0:
return f"{h}:{m:02d}:{s:02d}"
return f"{m}:{s:02d}"
def result_to_dict(result: AnalysisResult) -> Dict:
"""将分析结果转换为字典"""
return {
"summary": result.summary,
"main_topics": result.main_topics,
"key_insights": result.key_insights,
"action_items": result.action_items,
"recommended_tags": result.recommended_tags,
"full_analysis": result.full_analysis,
"timeline": result.timeline
}
def generate_frontmatter(metadata: Dict, result: AnalysisResult) -> str:
"""生成 Markdown frontmatter"""
tags = result.recommended_tags or result.main_topics[:5]
fm = ["---", "title: \"" + metadata.get('title', '未命名') + "\"",
f"date: {datetime.now().strftime('%Y-%m-%d')}",
f"duration: {format_duration(metadata.get('duration', 0))}",
f"author: {metadata.get('author', '未知')}"]
if tags:
fm.append("tags: [" + ", ".join(f'"{t}"' for t in tags) + "]")
fm.append("---")
return '\n'.join(fm)
# ============== 主程序 ==============
def main():
parser = argparse.ArgumentParser(
description="Deep Analyzer - AI 深度内容分析工具",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python deep_analyzer.py --metadata meta.json --content content.json --output analysis.md
python deep_analyzer.py --metadata meta.json --content content.json --type video --api-key xxx
"""
)
parser.add_argument("--metadata", "-m", required=True,
help="元数据 JSON 文件路径")
parser.add_argument("--content", "-c", required=True,
help="内容 JSON 文件路径")
parser.add_argument("--output", "-o", required=True,
help="输出 Markdown 文件路径")
parser.add_argument("--type", "-t", default="video", choices=["video", "article"],
help="内容类型 (默认: video)")
parser.add_argument("--api-key", "-k", default=None,
help="AI API Key (或设置 AI_API_KEY 环境变量)")
parser.add_argument("--provider", "-p", default="mock",
choices=["openai", "zhipu", "minimax", "mock"],
help="AI 提供商 (默认: mock)")
parser.add_argument("--model", default="gpt-4",
help="AI 模型 (默认: gpt-4)")
parser.add_argument("--no-frontmatter", action="store_true",
help="不生成 frontmatter")
args = parser.parse_args()
# 加载数据
print(f"[Main] 加载元数据: {args.metadata}")
metadata = load_json(args.metadata)
print(f"[Main] 加载内容: {args.content}")
content = load_json(args.content)
# 初始化
ai_client = SimpleAIClient(
api_key=args.api_key,
provider=args.provider,
model=args.model
)
analyzer = DeepAnalyzer(ai_client)
# 执行分析
print(f"[Main] 开始分析...")
result = analyzer.analyze(metadata, content, args.type)
# 渲染模板
template_loader = TemplateLoader()
renderer = TemplateRenderer()
template = template_loader.load_template(args.type)
variables = {
"title": metadata.get('title', '未命名'),
"duration": format_duration(metadata.get('duration', 0)),
"author": metadata.get('author', '未知'),
"source": metadata.get('source', ''),
"analyze_time": datetime.now().strftime('%Y-%m-%d %H:%M'),
"summary": result.summary,
"topics": result.main_topics,
"insights": result.key_insights,
"actions": result.action_items,
"tags": result.recommended_tags,
"timeline": result.timeline
}
markdown_content = renderer.render(template, variables)
# 添加 frontmatter
if not args.no_frontmatter:
frontmatter = generate_frontmatter(metadata, result)
markdown_content = frontmatter + "\n\n" + markdown_content
# 写入输出
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
with open(args.output, 'w', encoding='utf-8') as f:
f.write(markdown_content)
print(f"[Main] 分析完成!输出: {args.output}")
print(f"[Main] 摘要: {result.summary[:100]}...")
print(f"[Main] 主题数: {len(result.main_topics)}")
if __name__ == "__main__":
main()