#!/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", base_url: str = ""): self.api_key = api_key or os.environ.get("AI_API_KEY", "") self.provider = provider self.model = model self.base_url = base_url or 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/coding/paas/v4" url = f"{base_url}/chat/completions" headers = {"Authorization": f"Bearer {self.api_key}"} payload = { "model": self.model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens } 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.SHORT_VIDEO_THRESHOLD and content_type == "video": # 10分钟以上使用分段分析 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() # 常量配置 CHUNK_MAX_CHARS = 3000 # 每个chunk最大字符数 MIN_CHUNKS = 3 # 最少分段数 MAX_API_CHUNKS = 20 # 最多API调用次数 total_chars = len(subtitle) # 1. 动态计算分段数 # 计算需要多少个chunk(每个不超过3000字) min_required = total_chars // CHUNK_MAX_CHARS + 1 # 根据时长限制最大分段 if duration < 1800: # <30分钟: 最多6段 max_chunks = 6 elif duration < 7200: # <2小时: 最多12段 max_chunks = 12 else: # >2小时: 最多20段 max_chunks = 20 num_segments = min(max(min_required, MIN_CHUNKS), MAX_API_CHUNKS) print(f"[DeepAnalyzer] 字幕{total_chars}字符,分{num_segments}段分析") # 2. 按字符数分段(保持语义完整) chunk_size = total_chars // num_segments chunks = [] chunk_boundaries = [] # 记录每段的字符边界 for i in range(num_segments): start = i * chunk_size if i == num_segments - 1: # 最后一段取到结尾 end = total_chars chunk_text = subtitle[start:] else: # 在自然断点处分割 end = self._find_breakpoint(subtitle, start, start + chunk_size) chunk_text = subtitle[start:end] chunks.append(chunk_text) chunk_boundaries.append((start, end)) print(f"[DeepAnalyzer] 各段字符: {[len(c) for c in chunks]}") # 3. 用字符比例估算时间 char_per_second = total_chars / duration if duration > 0 else 1 # 合并结果 chunk_summaries = [] for i, chunk_text in enumerate(chunks): start_char, end_char = chunk_boundaries[i] # 用字符位置估算时间 start_sec = max(0, int(start_char / char_per_second)) end_sec = min(duration, int(end_char / char_per_second)) 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, 'start_sec': start_sec, 'end_sec': end_sec, 'summary': chunk_summary }) # 合并结果 result = self._merge_summaries(chunk_summaries, title) result.timeline = self._build_timeline_from_chunks(chunk_summaries) return result def _find_breakpoint(self, text: str, start: int, preferred_end: int) -> int: """ 在自然断点处分割文本 优先级: 1. 双换行(段落分隔) 2. 句号+换行(句末) 3. 句号(句末) 4. 换行(行末) 5. 回到preferred_end Args: text: 完整文本 start: 起始位置 preferred_end: 理想结束位置 Returns: 实际结束位置 """ if not text or start >= len(text): return start # 搜索范围:理想位置前后各500字符 search_start = max(start, preferred_end - 500) search_end = min(len(text), preferred_end + 500) # 优先级1: 双换行 double_newline = text.rfind('\n\n', search_start, search_end) if double_newline != -1: return double_newline # 优先级2: 句号+换行 period_newline = text.rfind('。\n', search_start, search_end) if period_newline != -1: return period_newline + 1 # 包含句号 # 优先级3: 句号(中文或英文) last_period = max( text.rfind('。', search_start, search_end), text.rfind('.', search_start, search_end) ) if last_period != -1: return last_period + 1 # 包含句号 # 优先级4: 换行 last_newline = text.rfind('\n', search_start, search_end) if last_newline != -1: return last_newline # 没找到自然断点,返回理想位置 return preferred_end 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"], "tags": ["标签1", "标签2", "标签3", "标签4", "标签5"] }}""" 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": [], "tags": [] } 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 _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}} --- title: "{{title}}" date: {{date}} duration: {{duration}} author: {{author}} source: {{source}} tags: [{{tags_string}}] bvid: {{bvid}} platform: {{platform}} --- ## 基本信息 - **时长**: {{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 %} --- *元数据* - **平台**: {{platform}} - **BV号**: {{bvid}} - **原始文件**: {{source_file}} """, "article": """# {{title}} --- title: "{{title}}" date: {{date}} author: {{author}} source: {{source}} tags: [{{tags_string}}] cvid: {{cvid}} platform: {{platform}} --- ## 基本信息 - **作者**: {{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 %} --- *元数据* - **平台**: {{platform}} - **CV号**: {{cvid}} - **原始文件**: {{source_file}} """ } 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("--base-url", "-u", default=None, help="API Base URL (或设置 AI_BASE_URL 环境变量)") 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 or os.environ.get("AI_API_KEY", ""), provider=args.provider or os.environ.get("AI_PROVIDER", "mock"), model=args.model or os.environ.get("AI_MODEL", "gpt-4"), base_url=args.base_url or os.environ.get("AI_BASE_URL", "") ) 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', '未命名'), "date": datetime.now().strftime('%Y-%m-%d'), "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, "tags_string": ', '.join(f'"{t}"' for t in (result.recommended_tags or [])), "timeline": result.timeline, # 扩展元数据 "bvid": metadata.get('bvid', ''), "cvid": metadata.get('cvid', ''), "mid": metadata.get('mid', ''), "platform": metadata.get('platform', 'bilibili'), "source_file": metadata.get('source_file', ''), "aid": str(metadata.get('aid', '')), } 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()