#!/usr/bin/env python3 """qiaomu-anything-to-notebooklm - 多源内容智能处理器 自动识别输入类型,上传到 NotebookLM 并生成指定格式 支持深度分析模式:三轮递进提问(概览→深度挖掘→综合反刍) """ import sys import os import subprocess import tempfile import json import time import re from pathlib import Path def detect_input_type(input_path): """检测输入类型""" if input_path.startswith('http'): if 'mp.weixin.qq.com' in input_path: return 'weixin' elif 'youtube.com' in input_path or 'youtu.be' in input_path: return 'youtube' elif 'xiaoyuzhoufm.com' in input_path or 'ximalaya.com' in input_path or 'bilibili.com' in input_path: return 'podcast' elif 'x.com' in input_path or 'twitter.com' in input_path: return 'x_twitter' else: return 'url' path = Path(input_path).expanduser() if not path.exists(): return 'search' # 不是文件路径,当作搜索关键词 suffix = path.suffix.lower() if suffix == '.epub': return 'epub' elif suffix in ['.pdf', '.txt', '.md']: return 'document' elif suffix in ['.docx', '.pptx', '.xlsx']: return 'office' elif suffix in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: return 'image' elif suffix in ['.mp3', '.wav']: return 'audio' elif suffix == '.zip': return 'zip' else: return 'unknown' def extract_epub_to_txt(epub_path): """提取 EPUB 到 TXT""" import ebooklib from ebooklib import epub from bs4 import BeautifulSoup book = epub.read_epub(str(epub_path)) content = [] for item in book.get_items(): if item.get_type() == ebooklib.ITEM_DOCUMENT: soup = BeautifulSoup(item.get_content(), 'html.parser') content.append(soup.get_text()) # 保存到临时文件 txt_path = tempfile.mktemp(suffix='.txt', prefix='epub_') with open(txt_path, 'w', encoding='utf-8') as f: f.write('\n\n'.join(content)) return txt_path def upload_to_notebooklm(file_path, title): """上传文件到 NotebookLM""" # 创建笔记本 result = subprocess.run( ['notebooklm', 'create', title], capture_output=True, text=True ) if result.returncode != 0: print(f"❌ 创建笔记本失败: {result.stderr}", file=sys.stderr) return False # 上传文件 result = subprocess.run( ['notebooklm', 'source', 'add', file_path, '--title', title], capture_output=True, text=True ) if result.returncode != 0: print(f"❌ 上传文件失败: {result.stderr}", file=sys.stderr) return False print(f"✅ 已上传到 NotebookLM: {title}") return True def label_for(content_type): """根据内容类型返回合适的中文指代词""" labels = { 'epub': '本书', 'document': '这份文档', 'podcast': '这期播客', 'x_twitter': '这条推文', 'youtube': '这个视频', 'url': '这篇文章', 'weixin': '这篇文章', 'search': '这份内容', } return labels.get(content_type, '这份内容') def generate_questions_progressive(content_type): """ 生成三轮回合递进的深度问题。 本函数与具体内容解耦,不使用 {title} 等占位符, 统一用 label_for(content_type) 生成的指代词(如"本书""这个视频")。 设计原则: - 第一轮(4题):建立整体认知框架 - 第二轮(5题):深入挖掘细节与矛盾 - 第三轮(3题):综合反刍与认知升级 - NotebookLM 在同一 conversation 中保持上下文,后续回合受益于前序回答 问题设计技巧: - "请基于提供的文档内容回答" 防止 NotebookLM 触发网络搜索 - "列出、拆解、指出、提取" 等动作词引导结构化回答 - 避免 yes/no 式问题 """ name = label_for(content_type) # ── 第一轮:概览与框架 ── round1 = [ f"请用一段话概括{name}的核心主题和写作目的。注意:完全基于已上传的文档内容回答,不要搜索网络。", f"{name}的整体结构是什么?请按章节或逻辑模块逐一列出,每个模块用2-3句话概括核心内容。完全基于文档回答。", f"{name}提出了哪些核心论点或主张?请逐一列出并用文档中的具体内容支撑每个论点。完全基于文档回答。", f"{name}中最具颠覆性或反常识的内容是什么?请列出3-5条,并解释每条为什么让人意外。完全基于文档回答。", ] # ── 第二轮:深度挖掘 ── if content_type in ['epub', 'document']: # 书籍/文档类:侧重论证逻辑与文本细读 round2 = [ f"请拆解{name}的核心论证逻辑:作者的前提假设是什么?推理过程是怎样的?最终结论是什么?请引用具体文本段落说明。", f"{name}中引用了哪些关键案例、数据或文本证据?请逐一列出并说明每个证据在整体论证中起到什么作用。", f"{name}中是否存在内部矛盾或值得商榷的观点?如果有,请指出并分析矛盾的根源。如果没有,请说明为什么论证站得住脚。", f"{name}最独特的贡献或核心洞察是什么?如果只能用一句话概括,应该是什么?为什么这句话重要?", f"如果要对{name}提出一个最尖锐的批评,会是什么?请从论证完整性、证据充分性、视角局限性等角度分析。", ] elif content_type == 'youtube': round2 = [ f"这个视频的核心论点是什么?演讲者用哪些论据来支撑?请拆解其论证结构。", f"视频中提到了哪些具体案例、数据或研究?请逐一列出并说明它们在论证中的作用。", f"这个视频的立场是否存在偏向或漏洞?哪些观点可能经不起推敲?", f"这个视频最独特的信息或洞察是什么?有没有在其他地方看不到的内容?", f"如果请一位持反对立场的专家来回应,他最可能提出的三个反驳点是什么?", ] else: # 文章/网页/播客/推文类:侧重叙事与观点分析 round2 = [ f"请拆解{name}的论证或叙事结构:开头如何建立框架?中间如何展开?结尾如何收束?使用了哪些修辞或论证手法?", f"{name}中引用了哪些关键案例、数据或引用?请逐一列出并评估其可信度和相关性。", f"{name}的立场或视角是否存在局限?有没有重要的反例或未被讨论的维度?", f"{name}最令人印象深刻的一个洞察或观点是什么?为什么它具有冲击力?", f"如果要给{name}的作者写一封简短的反馈信,你会提出哪三个建设性意见或质疑?", ] # ── 第三轮:综合与反刍 ── round3 = [ f"读完{name}后,读者最应该带走的一个认知改变是什么?哪些观点可能颠覆读者的既有认知?", f"从{name}中可以提取出哪些可操作的行动指南、实践建议或决策原则?请列出3-5条。", f"请用三个最有力的理由,说服一个没接触过{name}的人去认真阅读它。每个理由用一句话概括。", ] # 合并所有轮次,每轮之间加一个分隔标识(便于后续处理和展示) all_questions = [] all_questions.append(("【第一轮:概览与框架】", round1)) all_questions.append(("【第二轮:深度挖掘】", round2)) all_questions.append(("【第三轮:综合与反刍】", round3)) return all_questions def ask_notebooklm(question, max_retries=1): """向 NotebookLM 提问并获取答案,带重试机制""" for attempt in range(max_retries + 1): result = subprocess.run( ['notebooklm', 'ask', question], capture_output=True, text=True ) if result.returncode == 0: answer = result.stdout.strip() if answer and len(answer) > 10: # 有实质内容的回答才算成功 return answer if attempt < max_retries: print(f" 重试中...", end=" ") time.sleep(2) print(f"⚠️ 提问失败(已重试{max_retries}次)", file=sys.stderr) return None def format_feishu_markdown(title, questions, answers): """将问答结果格式化为飞书 Markdown""" lines = [ f"# {title} - 深度解读", "", "> 本文档由 NotebookLM 深度分析生成", "", ] for i, (q, a) in enumerate(zip(questions, answers), 1): lines.append(f"## {i}. {q}") lines.append("") if a: lines.append(a) else: lines.append("*(未回答)*") lines.append("") return "\n".join(lines) def create_feishu_doc(title, markdown_content): """创建飞书文档""" print("\n📝 创建飞书文档...") # 调用 lark-cli docs +create result = subprocess.run( ['lark-cli', 'docs', '+create', '--title', title, '--markdown', markdown_content], capture_output=True, text=True ) if result.returncode != 0: print(f"❌ 创建飞书文档失败: {result.stderr}", file=sys.stderr) return None # 从输出中提取文档 URL(如果有) output = result.stdout print(f"✅ 飞书文档已创建") print(output) return True def ask_round(round_label, questions, title): """执行一轮提问,返回 (questions, answers) 列表""" print(f"\n📌 {round_label}") answers = [] asked = [] for i, q in enumerate(questions, 1): print(f" [{i}/{len(questions)}] {q[:60]}...") answer = ask_notebooklm(q) if answer: print(f" ✅ 回答长度: {len(answer)} 字符") answers.append(answer) else: print(f" ⚠️ 跳过") answers.append("") asked.append(q) time.sleep(1.5) # 避免请求过快 return asked, answers def deep_analysis(file_path, title, content_type, to_feishu=False): """深度分析模式:三轮递进提问""" print("\n" + "="*60) print("🔍 启动深度分析模式") print("="*60 + "\n") # 1. 上传到 NotebookLM print("📤 上传内容到 NotebookLM...") if not upload_to_notebooklm(file_path, title): return None print("⏳ 等待 NotebookLM 处理内容...") time.sleep(3) # 2. 生成三轮递进问题 print("\n📝 生成深度分析问题...") rounds = generate_questions_progressive(content_type) total_questions = sum(len(qs) for _, qs in rounds) print(f"✅ 共 {len(rounds)} 轮 {total_questions} 个问题\n") # 3. 逐轮提问(NotebookLM 保持对话上下文,后轮受益于前轮回答) print("💬 开始三轮递进提问...\n") all_questions = [] all_answers = [] for round_label, questions in rounds: asked, answers = ask_round(round_label, questions, title) all_questions.extend(asked) all_answers.extend(answers) # 4. 返回结构化数据 result = { "status": "success", "title": title, "content_type": content_type, "rounds": len(rounds), "questions": all_questions, "answers": all_answers, "total_questions": len(all_questions), "answered": len([a for a in all_answers if a]), } # 5. 如果指定了 --to-feishu,创建飞书文档 if to_feishu: markdown = format_feishu_markdown(title, all_questions, all_answers) create_feishu_doc(f"{title} - 深度解读", markdown) return result def main(): if len(sys.argv) < 2: print("用法: main.py <输入路径或URL> [--deep-analysis] [--to-feishu]", file=sys.stderr) sys.exit(1) input_arg = sys.argv[1] deep_mode = '--deep-analysis' in sys.argv to_feishu = '--to-feishu' in sys.argv input_type = detect_input_type(input_arg) print(f"📋 检测到输入类型: {input_type}") # 根据类型处理 if input_type == 'epub': epub_path = Path(input_arg).expanduser() print(f"📚 处理 EPUB: {epub_path.name}") # 提取文本 txt_path = extract_epub_to_txt(epub_path) print(f"✅ 文本已提取: {txt_path}") title = epub_path.stem if deep_mode: result = deep_analysis(txt_path, title, input_type) if result: # 保存结果到文件 output_file = f"/tmp/{title}_analysis.json" with open(output_file, 'w', encoding='utf-8') as f: json.dump(result, f, ensure_ascii=False, indent=2) print(f"\n✅ 分析完成!结果已保存到: {output_file}") else: upload_to_notebooklm(txt_path, title) elif input_type == 'document': doc_path = Path(input_arg).expanduser() print(f"📄 处理文档: {doc_path.name}") title = doc_path.stem if deep_mode: result = deep_analysis(str(doc_path), title, input_type) if result: output_file = f"/tmp/{title}_analysis.json" with open(output_file, 'w', encoding='utf-8') as f: json.dump(result, f, ensure_ascii=False, indent=2) print(f"\n✅ 分析完成!结果已保存到: {output_file}") else: upload_to_notebooklm(str(doc_path), title) elif input_type == 'podcast': print(f"🎙️ 处理播客/视频: {input_arg}") print(" 通过 Get笔记 API 获取转写(可能需要 2-5 分钟)...") script = os.path.join(os.path.dirname(__file__), 'scripts', 'get_podcast_transcript.py') result = subprocess.run( ['python3', script, input_arg], capture_output=True, text=True ) if result.returncode != 0: print(f"❌ 获取转写失败: {result.stderr}", file=sys.stderr) sys.exit(1) # Parse JSON output from script try: data = json.loads(result.stdout.strip()) except json.JSONDecodeError: print(f"❌ 解析输出失败: {result.stdout}", file=sys.stderr) sys.exit(1) txt_path = data['txt_path'] title = data['title'] content_length = data['content_length'] print(f"✅ 转写完成: {title} ({content_length} 字符)") print(f" TXT: {txt_path}") if deep_mode: result_data = deep_analysis(txt_path, title, 'podcast') if result_data: safe_title = re.sub(r'[::/\\?|<>*"\']', '_', title).strip('_')[:60] output_file = f"/tmp/{safe_title}_analysis.json" with open(output_file, 'w', encoding='utf-8') as f: json.dump(result_data, f, ensure_ascii=False, indent=2) print(f"\n✅ 分析完成!结果已保存到: {output_file}") else: upload_to_notebooklm(txt_path, title) elif input_type == 'x_twitter': print(f"🐦 处理 X/Twitter: {input_arg}") print(" 通过代理级联获取推文内容...") fetch_script = os.path.join(os.path.dirname(__file__), 'scripts', 'fetch_url.sh') result = subprocess.run( ['bash', fetch_script, input_arg], capture_output=True, text=True, timeout=60 ) if result.returncode != 0: print(f"❌ 获取推文失败: {result.stderr}", file=sys.stderr) sys.exit(1) content = result.stdout.strip() if not content: print("❌ 获取到空内容", file=sys.stderr) sys.exit(1) # Extract title from content or URL title = input_arg.split('/')[-1] or 'x_post' # Try to extract first line as title first_line = content.split('\n')[0].strip() if first_line and len(first_line) < 100: title = first_line.lstrip('#').strip() safe_title = re.sub(r'[::/\\?|<>*"\']', '_', title).strip('_')[:60] txt_path = tempfile.mktemp(suffix='.txt', prefix=f'x_{safe_title}_') with open(txt_path, 'w', encoding='utf-8') as f: f.write(f"# {title}\n\n") f.write(f"来源: {input_arg}\n") f.write(f"获取时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n---\n\n") f.write(content) print(f"✅ 推文内容已获取: {safe_title} ({len(content)} 字符)") print(f" TXT: {txt_path}") if deep_mode: result_data = deep_analysis(txt_path, safe_title, 'x_twitter') if result_data: output_file = f"/tmp/{safe_title}_analysis.json" with open(output_file, 'w', encoding='utf-8') as f: json.dump(result_data, f, ensure_ascii=False, indent=2) print(f"\n✅ 分析完成!结果已保存到: {output_file}") else: upload_to_notebooklm(txt_path, safe_title) elif input_type == 'url': print(f"🌐 处理 URL: {input_arg}") # 添加 URL 作为 source result = subprocess.run( ['notebooklm', 'source', 'add', input_arg], capture_output=True, text=True ) if result.returncode != 0: print(f"❌ 添加失败: {result.stderr}", file=sys.stderr) sys.exit(1) print("✅ URL 已添加到 NotebookLM") if deep_mode: title = input_arg.split('/')[-1] or 'web_content' print("⏳ 等待 NotebookLM 处理内容...") time.sleep(3) # 使用通用三轮递进提问 rounds = generate_questions_progressive(input_type) total_questions = sum(len(qs) for _, qs in rounds) print(f"\n📝 开始提问(共 {total_questions} 个问题,{len(rounds)} 轮)...") all_questions = [] all_answers = [] for round_label, questions in rounds: asked, answers = ask_round(round_label, questions, title) all_questions.extend(asked) all_answers.extend(answers) result_data = { "status": "success", "title": title, "url": input_arg, "content_type": input_type, "rounds": len(rounds), "questions": all_questions, "answers": all_answers, "total_questions": len(all_questions), "answered": len([a for a in all_answers if a]), } if to_feishu: md = format_feishu_markdown(title, all_questions, all_answers) create_feishu_doc(f"{title} - 深度解读", md) output_file = f"/tmp/{title}_analysis.json" with open(output_file, 'w', encoding='utf-8') as f: json.dump(result_data, f, ensure_ascii=False, indent=2) print(f"\n✅ 分析完成!结果已保存到: {output_file}") else: print(f"❌ 不支持的输入类型: {input_type}", file=sys.stderr) print("提示: 请使用 EPUB、PDF、TXT、MD 文件或 URL", file=sys.stderr) sys.exit(1) if __name__ == '__main__': main()