1仕組みと動作原理
4つのステージを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
動画検出にはsearch(『時系列順が保証されない』問題を回避するため)ではなくYouTube Data APIのuploadsプレイリストを使用し、/shorts/のURLリダイレクトによってショート動画を判定します。
# Get the 15 most recent videos from the uploads playlist # The uploads playlist is always in exact upload order (newest first)
文字起こしの取得は、YouTube公式やオープンソースライブラリではなく、実質的にサードパーティの商用API(Supadata)に依存しています。コード内のコメントには『YouTubeによるBANを回避するため』と明記されています。
This script uses the Supadata API to fetch transcripts reliably. Unlike the youtube-transcript-api library, Supadata doesn't get blocked by YouTube.
『雑誌風リライト』はコード内にハードコードされた長文プロンプトです。タイトルの刷新、非専門家向けの表現、専門用語の解説、直感に反する見解や名言の保持を指示し、動画を観なくても読める独立した記事の作成を明確に要求しています。
- 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番ポート経由で添付ファイルとして送信されます。同時にnewsletters/ディレクトリへHTML/EPUB/JSON形式でアーカイブされます。
# Create the ebook book = epub.EpubBook()
重複排除はローカルのJSONトラッカーに依存しています。処理済み動画IDをprocessed_videos.jsonに永続化し、未処理の新規動画のみを処理対象とし、メール送信が成功した場合にのみ処理済みフラグを付けます。
TRACKER_FILE = os.path.join(os.path.dirname(__file__), "processed_videos.json")
2種類の自動化ルートがリポジトリ内に用意されています。macOSのlaunchd(plist + ログリダイレクト付きrun_newsletter.sh)と、毎週水曜日に定期実行されるGitHub Actions(4つのシークレットを使用)です。
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内のプロンプト、スケジュール設定は~/Library/LaunchAgents配下のplistを書き換えた上でlaunchctlを再ロードします。
CHANNELS_FILE = PROJECT_DIR / "get_videos.py" PROMPT_FILE = PROJECT_DIR / "write_articles.py"
2コア機能
3外部依存関係
| Kind | Dependency / Endpoint |
|---|---|
| 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推奨統制措置と判定根拠
- scripts/: ローカルスクリプトまたはコマンドの実行: python main.py(メインパイプライン。README/SKILL.mdのエントリポイント)
- scripts/ 或 SKILL.md: 外部言語パッケージの動的インストールの宣言あり
- 网络端点: 外部ネットワークアクセスの宣言あり: googleapis.com YouTube Data API v3(channels.list / playlistItems.list、APIキー使用)
- 本地工作目录: ファイル読み書きおよび成果物の出力: processed_videos.json(プロジェクトディレクトリ内で処理済み動画を記録)
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…