项目文件夹

文件
2026-07-13 12:28:59 +08:00

510 行
19 KiB
Python
可执行文件

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#!/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()