1实现原理 · 为什么它能做到
四个阶段串成一条本地流水线(拉视频 → 取字幕 → Claude 改写成杂志文 → 打包 EPUB 并邮件投递),main.py 只是顺序调用,不含任何分支逻辑。
from get_videos import main as fetch_videos from get_transcripts import get_transcripts_for_videos from write_articles import write_articles_for_videos from send_email import send_newsletter from video_tracker import filter_new_videos, mark_videos_processed, get_processed_count
视频发现走 YouTube Data API 的 uploads 播放列表而非 search(规避『search 不保证时间序』),并靠 /shorts/ URL 重定向判定短视频。
# Get the 15 most recent videos from the uploads playlist # The uploads playlist is always in exact upload order (newest first)
字幕获取现实上依赖第三方商业 API(Supadata),不是 YouTube 官方或开源库——代码注释明确说这是为了『不被 YouTube 封』。
This script uses the Supadata API to fetch transcripts reliably. Unlike the youtube-transcript-api library, Supadata doesn't get blocked by YouTube.
『杂志文改写』是一个写死在代码里的长 prompt:要求更换标题、面向非专业读者、解释术语、保留反直觉观点与金句,并明确要求写成不看视频也能读懂的独立文章。
- Do NOT include phrases like "In this video" - write it as a standalone article. Assume the reader has not watched the video and has zero context about it. This article is meant to be as a replacement, not complement, for watching the video.
成品是真正的 EPUB(ebooklib 生成目录+章节+CSS+阅读顺序)并通过 Gmail SMTP_SSL 465 以附件投递,同时留档 HTML/EPUB/JSON 到 newsletters/。
# Create the ebook book = epub.EpubBook()
去重靠本地 JSON 追踪器:已处理视频 ID 落盘 processed_videos.json,只处理未见过的新视频,且仅在邮件发送成功后才标记。
TRACKER_FILE = os.path.join(os.path.dirname(__file__), "processed_videos.json")
可选的两条自动化通道都写在仓库里:macOS launchd(plist + run_newsletter.sh 重定向日志)与 GitHub Actions 每周三定时跑(含 4 个 secrets)。
env: YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GMAIL_ADDRESS: ${{ secrets.GMAIL_ADDRESS }} GMAIL_APP_PASSWORD: ${{ secrets.GMAIL_APP_PASSWORD }} SUPADATA_API_KEY: ${{ secrets.SUPADATA_API_KEY }}
附带的 Streamlit 管理台是『直接改源码文件』的实现:频道列表改 get_videos.py、写作风格改 write_articles.py 内的 prompt、排期改 ~/Library/LaunchAgents 下的 plist 并调 launchctl 重载。
CHANNELS_FILE = PROJECT_DIR / "get_videos.py" PROMPT_FILE = PROJECT_DIR / "write_articles.py"
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| api | YouTube Data API v3(Google) |
| network | YouTube Shorts URL 探测(HEAD 请求判重定向) |
| api | Supadata 字幕 API(第三方商业服务) |
| api | Anthropic Claude API(文章改写) |
| network | Gmail SMTP(投递 EPUB 附件) |
| package | Python 依赖(含一个已不再被代码使用的库) |
| package | streamlit(dashboard 专用,未列入 requirements.txt) |
| cli | launchctl(macOS 排期,dashboard 内以子进程调用) |
| network | Google Fonts(dashboard 页面样式 @import,浏览器侧发起) |
4风险提醒 风险提醒:橙色 · 评估后使用
- 凭证面集中且含长期邮箱凭证 — 需同时提供 YouTube/Anthropic/Supadata 三把 key 与 Gmail 账号+应用专用密码;.env 若被误提交或被 CI secrets 泄露,影响面含可发信邮箱。Gmail 应用密码应单独申请、限定用途,并定期轮换。
- 第三方内容出境 — 字幕与视频描述会发给 Supadata(商业服务,付费/配额另有条款)与 Anthropic;Supadata 还持有可代表你身份调用的 x-api-key。对敏感频道应重新评估是否接受该链路。
- 间接 prompt injection 直通你的邮箱 — 被监控频道的字幕/描述是外部不可信文本,被原样拼进写作 prompt;生成文章会以邮件投递并留档,若视频内容含指令性文本可污染产出(属内容可信度风险,非代码执行)。
- 文档漂移会让按文档操作者失败 — channels.txt、`python main.py --channels`、youtube-transcript-api 用法、『不要用 GitHub Actions』均与仓库实际不符;.env.example 缺 SUPADATA_API_KEY。新用户照 README 走会在字幕阶段静默失败(脚本打印 ⚠ 后返回 None)。
- dashboard 直接改写源码与 plist — 频道/风格/排期编辑会重写 get_videos.py、write_articles.py 与 ~/Library/LaunchAgents/com.youtube.newsletter.plist 并重载 launchd;在仓库工作区内改代码可能被 git 提交或覆盖,且排期改动对本机影响持久。
- 平台与合规边界 — YouTube 条款对自动抓取字幕(尤其经第三方中转)与再分发视频内容有约束;把整段视频内容改写为文章并邮件分发,需自行确认所监控频道的版权与授权,README/SKILL.md 未涉及此点。
5第二遍独立确认
- [ok] 四阶段流水线与去重逻辑 — main.py 五个 import 与方法体逐行核对;video_tracker.py 的 load/save/is_video_processed/mark_video_processed 与 processed_videos.json 路径均存在。
- [discrepancy] 字幕来自 Supadata(而非 SKILL.md 所述 youtube-transcript-api) — 代码侧唯一实现是 Supadata(get_transcripts.py 顶部注释与 SUPADATA_TRANSCRIPT_URL 常量);但 SKILL.md『Known Pitfalls』#3 仍在教 'ytt_api = YouTubeTranscriptApi(); transcript = ytt_api.fetch(video_id)',requirements.txt 也仍列 youtube-transcript-api——全仓 .py 无一处 import youtube_transcript_api。文档描述的是已被替换掉的旧实现。
- [discrepancy] 『云服务会被 YouTube 封,请本地跑』与仓库自带 Actions 工作流 — SKILL.md『### 6. Cloud Automation Blocked … Solution: Run automation locally on your Mac using launchd』、README『| Cloud servers blocked | Run locally, not GitHub Actions |』,但 .github/workflows/newsletter.yml 仍是完整的 ubuntu-latest 定时任务(cron '0 15 * * 3'),且注入 5 个 secrets。两种说法不能同时成立;从代码看 Supadata 正是为绕开该封锁而引入('Supadata doesn't get blocked by YouTube'),故云路径更可能是『能跑但不保证字幕』。
- [discrepancy] 配置入口 channels.txt 与 `python main.py --channels` — SKILL.md 命令表写 '| `python main.py --channels` | Edit channel list |'、Key Files 与 README 结构表都列 channels.txt;实际 main.py 无 argparse(全仓 .py 无 argparse 引用),频道是 get_videos.py 的 CHANNELS 常量,dashboard 通过 CHANNELS_FILE = get_videos.py 改源码。channels.txt 在仓库中不存在。
- [discrepancy] SUPADATA_API_KEY 的文档覆盖 — .env.example 只有 YOUTUBE_API_KEY / ANTHROPIC_API_KEY / GMAIL_ADDRESS / GMAIL_APP_PASSWORD 四项,而 get_transcripts.py 在无该 key 时直接返回 None(『⚠ SUPADATA_API_KEY not set in .env file』)导致流水线无字幕。文档缺失这把必需 key(CI workflow 里反而有 secrets.SUPADATA_API_KEY)。
- [discrepancy] 许可声明一致性 — README 末段写 'MIT - Use freely, modify as needed.',但 git ls-tree 无 LICENSE 文件,gh api license=null(spdx 未识别)。已在 meta.license 如实标注冲突,未按 MIT 硬写。
- [ok] 邮件投递与留档细节 — SMTP_SSL('smtp.gmail.com', 465) + server.login + sendmail;MIMEBase('application','epub+zip') 附件;save_newsletter_archive 写 html/epub/json 三件套;成功后 os.remove 临时 EPUB——与 SKILL.md『Email Delivery (Optional)』一致。
- [ok] dashboard 的破坏性写回(源码/plist) — save_channels 重写 get_videos.py 的 CHANNELS 块、风格保存重写 write_articles.py 内 prompt、save_schedule 重写 ~/Library/LaunchAgents plist 并 launchctl bootout/bootstrap —— 已在 file_writes 与 injection_surface 记入;另确认 requirements.txt 未含 streamlit(README 要求单独 pip install)。
6结论
2f7c07f82fd3668b…7f67fd6b55