AIでの開発中の隙間時間にAI系の情報収集はYoutubeでやるのが効率的ですが、後で見た動画を振り返る時にテキストで要点だけ抽出して後で検索できるようにしようと思い、 SlackチャンネルにYouTube URLを貼るだけで、ローカルLLMが自動で動画を要約し、Obsidianにマークダウンとして保存してくれるBotを開発しました。
完全ローカル完結。クラウドAPIの課金は一切なし。月額費用ゼロ。
1Slack に YouTube URL を投稿2 → Bot が即時反応「要約を作成中...」3 → 字幕取得 → ローカルLLMで要約生成4 → Obsidian Vault に Markdown 保存5 → Git commit & push(自動)6 → Slack スレッドに要約を返信この記事では、このBotの設計思想・技術スタック・実装のポイントを解説します。
何ができるのか
デモ:実際の動作
- Slackのチャンネルに YouTube URL を投稿する
- Bot が即座にスレッドで「:hourglass_flowing_sand: 要約を作成中です…」と返信
- 約1〜2分後(30分の動画の場合)、要約がBlock Kit形式で返信される
- 同時にObsidian Vault にMarkdownファイルが保存され、Gitで自動push
要約の出力形式
Bot が返すのは、こんな構造化された要約です:
- TL;DR — 3行以内の核心
- 主要ポイント — 3〜7個の箇条書き
- 詳細メモ — セクション分けされた詳細な内容(Obsidian側のみ)
- 引用・キーフレーズ — 動画内の印象的な発言
Slack上ではTL;DRと主要ポイントをコンパクトに表示。全文はObsidianで読む、という使い分けです。 Gitリポジトリはこちら→YouTube Summarizer
技術スタック
| カテゴリ | 技術 | 選定理由 |
|---|---|---|
| LLM | Ollama + Qwen2.5:32B | ローカル実行、日本語品質、無料 |
| 字幕取得 | youtube-transcript-api | YouTube公式字幕を高速取得 |
| 字幕フォールバック | OpenAI Whisper | 字幕なし動画にも対応 |
| メタデータ | yt-dlp | タイトル・チャンネル名・公開日を取得 |
| Slack連携 | slack-bolt (Socket Mode) | 公開サーバ不要、ローカル実行可能 |
| 出力 | Jinja2 → Obsidian Markdown | PKMとの統合 |
| テンプレート | Jinja2 | Markdownテンプレートの柔軟な管理 |
| プロセス管理 | macOS launchd | 常駐デーモン化、自動再起動 |
| バージョン管理 | Git (subprocess) | Obsidian Git連携で自動同期 |
なぜOllamaか
最大の設計判断は ローカルLLM の採用です。
OpenAI APIやClaude APIを使えば実装はもっと簡単です。しかし:
- コスト: 毎日数本の動画を要約すると月額数千円〜数万円
- プライバシー: 字幕データを外部に送信しなくて済む
- レイテンシ: ローカルならネットワーク遅延ゼロ(モデルのロードは初回のみ)
- 可用性: インターネット障害やAPI停止の影響を受けない
Mac Studio (M3 Ultra, 96GB RAM) という環境があったので、32Bパラメータのモデルも快適に動きます。Qwen2.5:32Bは日本語の要約品質が高く、この用途には最適でした。
なぜSocket Modeか
Slack Botの通信方式には2つの選択肢があります:
| 方式 | 必要なもの | 適する環境 |
|---|---|---|
| HTTP Mode | 公開URL、SSL証明書、Webサーバ | クラウドサーバ |
| Socket Mode | なし(アウトバウンド接続のみ) | ローカルマシン |
Socket Modeはslack-bolt側からSlackのWebSocket APIに接続するため、NATやファイアウォールの内側でも動作します。自宅のMac Studioで動かすには理想的な選択です。
アーキテクチャ
全体構成
1┌─────────────────────────────────────────────────┐2│ Slack │3│ ユーザーがYouTube URLを投稿 │4└──────────┬──────────────────────────────────────┘5 │ WebSocket (Socket Mode)6 ▼7┌──────────────────────────────┐8│ slack_bot.py │ ← エントリポイント9│ URL検出 → pipeline呼出 │10└──────────┬───────────────────┘11 ▼12┌──────────────────────────────┐13│ pipeline.py │ ← オーケストレーション14│ │15│ 1. url_parser → 動画ID │18 collapsed lines
16│ 2. metadata → 動画情報 │17│ 3. transcript → 字幕 │18│ 4. summarizer → LLM要約 │19│ 5. formatter → Markdown │20│ 6. write file → 保存 │21└──────────┬───────────────────┘22 ▼23┌──────────┬───────────────────┐24│ git_sync │ slack_message │25│ add/ │ Block Kit │26│ commit/ │ 構築 │27│ push │ │28└──────────┴───────────────────┘29 ▼30┌──────────────────────────────┐31│ Obsidian Vault (Git) │32│ /Volumes/work/obsidian/ │33└──────────────────────────────┘モジュール分割の設計思想
このプロジェクトのモジュールは、パイプラインパターンで設計しています。
1# pipeline.py — 全体のオーケストレーション2def run_with_result(url: str, config: dict) -> PipelineResult:3 video_id = extract_video_id(url) # URL解析4 metadata = fetch_metadata(video_id) # メタデータ取得5 transcript = fetch_transcript(...) # 字幕取得6 summary = summarize(transcript, ...) # LLM要約7 markdown = render_markdown(...) # Markdown生成8 output_path.write_text(markdown) # ファイル保存9 return PipelineResult(output_path, metadata, summary, transcript)各ステップは独立したモジュールで、入力と出力が明確。テストも容易です。pipeline.py はそれらを線形に繋ぐだけの薄い層として機能します。
CLIとSlack Botは、同じ pipeline.py を共有しています:
1# CLI用: パスだけ返す2def run(url, config) -> Path:3 return run_with_result(url, config).output_path4
5# Slack Bot用: 全中間結果を返す(要約内容をSlackに返信するため)6def run_with_result(url, config) -> PipelineResult:7 ...実装のポイント
1. 長い動画への対応:Map-Reduce要約
LLMにはコンテキスト長の制限があります。Qwen2.5:32Bの場合、32768トークン。30分の動画の字幕は約2万トークンになることもあり、プロンプトと合わせるとギリギリです。
そこで Map-Reduce パターン を実装しました:
1estimated_tokens = _estimate_tokens(full_text, language)2input_budget = int(num_ctx * 0.6) # コンテキストの60%を入力に使う3
4if estimated_tokens <= input_budget:5 # 短い動画: 1回のLLM呼び出しで要約6 raw_output = _summarize_single(...)7else:8 # 長い動画: チャンク分割 → 各チャンクを要約 → 統合9 chunks = _split_into_chunks(full_text, input_budget, language)10 raw_output = _summarize_map_reduce(chunks, ...)チャンク分割では 文境界 で切り、200文字のオーバーラップ を設けることで、文脈の断絶を防いでいます。
日本語と英語でトークン推定の方法を変えているのもポイントです:
1def _estimate_tokens(text, language):2 if language in ("ja", "zh", "ko"):3 return int(len(text) * 1.5) # 1文字 ≒ 1.5トークン4 else:5 return int(len(text.split()) * 1.3) # 1単語 ≒ 1.3トークン2. 字幕取得の多段フォールバック
YouTube動画には字幕がないケースが意外と多い。3段階のフォールバックで対応しています:
1手動字幕(優先言語順) → 自動生成字幕 → Whisper文字起こし1def _fetch_transcript_with_fallback(video_id, languages, no_whisper, config):2 try:3 return fetch_transcript(video_id, languages) # YouTube字幕API4 except TranscriptNotAvailable:5 if no_whisper:6 raise7 # Whisperをlazy import(重い依存を通常時は読み込まない)8 from youtube_summarizer.whisper_fallback import transcribe_audio9 return transcribe_audio(video_id, whisper_model)Whisperは openai-whisper パッケージで約1.5GBのモデルを使いますが、lazy import にすることで通常の字幕取得時には一切ロードしません。
3. Slackメッセージのリンク形式への対応
Slackは投稿されたURLを内部的に <https://www.youtube.com/watch?v=xxx|youtube.com/watch?v=xxx> というフォーマットに変換します。普通のURL正規表現では抽出できません。
1_YOUTUBE_URL_PATTERN = re.compile(2 r"<(https?://(?:www\.)?(?:youtube\.com/watch\?[^\s|>]*v=[a-zA-Z0-9_-]{11}"3 r"|youtu\.be/[a-zA-Z0-9_-]{11})[^\s|>]*)(?:\|[^>]*)?>",4)< と > で囲まれたSlack形式のリンクから、| の前の実URLだけを抽出しています。
4. Git操作のセキュリティ
Git操作は subprocess.run をリスト引数で呼び出し、シェルインジェクションを防止しています:
1# 安全: リスト引数(シェル解釈されない)2subprocess.run(["git", "-C", str(repo_path), "add", str(relative_path)], ...)3
4# 危険: shell=True だとコミットメッセージに "; rm -rf /" を入れられる5# subprocess.run(f"git commit -m '{message}'", shell=True) # ← これはNGpush失敗時は pull --rebase で自動リトライ。Obsidian Gitプラグインとの競合も安全に処理します:
1try:2 _run_git(repo_path, "push", "origin", "main")3except GitSyncError:4 _run_git(repo_path, "pull", "--rebase", "origin", "main")5 _run_git(repo_path, "push", "origin", "main")5. 例外設計:エラーの種類ごとにSlackメッセージを出し分け
各パイプラインステージに対応する例外クラスを設計し、Slack Botでは例外の型に応じてユーザーフレンドリーなメッセージを返しています:
1try:2 result = run_with_result(url, config)3except DuplicateVideoError:4 say(":information_source: この動画は既に要約済みです")5except InvalidURLError:6 say(":x: 有効なYouTube URLではありません")7except MetadataFetchError:8 say(":x: 動画情報を取得できませんでした")9except (TranscriptNotAvailable, WhisperError):10 say(":x: 字幕の取得に失敗しました")11except OllamaConnectionError:12 say(":x: LLMが起動していません")6. launchdで常駐デーモン化
macOSではSystemd相当の launchd でプロセスを常駐化します:
1<key>KeepAlive</key>2<dict>3 <key>SuccessfulExit</key>4 <false/>5</dict>SuccessfulExit: false の設定で、異常終了時のみ自動再起動。正常終了(Ctrl+C)では再起動しません。RunAtLoad: true でログイン時に自動起動。
テストの話
183テスト、カバレッジ93%
外部依存(Ollama、YouTube API、yt-dlp、Slack API、subprocess)はすべてモック。CIで安定して動くテストスイートです。
1$ python -m pytest tests/ --cov=youtube_summarizer --cov-report=term-missing2# 183 passed, 93% coverageセキュリティテスト
Slackは不特定多数がメッセージを送れるインターフェースです。悪意のある入力を想定したセキュリティテストも組んでいます:
- コマンドインジェクション: URL に
; rm -rf /を含むメッセージへの耐性 - Git コミットメッセージインジェクション: ファイル名に特殊文字を含む場合の安全性
- HTML/Markdown インジェクション: Block Kit内でのエスケープ確認
- トークン漏洩防止: ログにSlackトークンが出力されないことの確認
設定ファイル
config.toml で全パラメータを制御。ハードコードはほぼゼロです:
1[ollama]2model = "qwen2.5:32b" # LLMモデル(ollama pullで追加可能)3base_url = "http://localhost:11434"4num_ctx = 32768 # コンテキスト長5temperature = 0.3 # 低めで安定した出力6
7[whisper]8model_size = "medium" # tiny/base/small/medium/large9
10[transcript]11languages = ["ja", "en"] # 字幕の優先言語12
13[output]14vault_path = "/Volumes/work/obsidian/YouTube"15template = "default" # Jinja2テンプレート名7 collapsed lines
16
17[summary]18output_language = "ja"19
20[slack]21channel_ids = ["C0AN3Q0GTE0"] # 空=全チャンネル許可22git_auto_push = trueハマったポイント
Slackのプライベートチャンネルで反応しない
Slack Botの Event Subscriptions で message.channels を設定しただけでは、プライベートチャンネル のメッセージを受信できません。
message.channels→ パブリックチャンネルのみmessage.groups→ プライベートチャンネル
同様に OAuth Scopes も:
channels:history→ パブリックチャンネルgroups:history→ プライベートチャンネル
設定変更後は 「Reinstall to Workspace」 が必須です。これを忘れて30分悩みました。
Ollamaのコンテキスト長とプロンプト設計
最初は num_ctx のうち入力に80%を使おうとしましたが、LLMの出力が途中で切れる問題が頻発。60%を入力上限にすることで安定しました。要約出力(TL;DR + 主要ポイント + 詳細メモ + 引用)がそこそこのトークン数を消費するためです。
今後の展望
- LLMモデルの切り替え対応: Gemma 3、Llama 4 など新モデルへの対応(config.toml変更のみ)
- 要約品質の評価: ROUGE スコアや人手評価による定量的な品質測定
- マルチ言語対応: 英語動画を日本語で要約、日本語動画を英語で要約
- 再生リスト一括処理: プレイリストURLから全動画を連続要約
- Obsidianリンク強化: 関連動画の自動リンク、タグの自動生成
まとめ
| 項目 | 内容 |
|---|---|
| 開発期間 | 約2日(CLI 1日 + Slack Bot 1日) |
| コード量 | 本体 約800行 + テスト 約1,800行 |
| テスト | 183件、カバレッジ93% |
| 月額コスト | 0円(ローカルLLM) |
| 実行環境 | Mac Studio M3 Ultra, 96GB RAM |
ローカルLLM + Slack Bot + Obsidianの組み合わせは、個人のナレッジマネジメント自動化として非常に実用的です。Cloud APIに頼らなくても、十分実用的な要約品質が得られる時代になりました。
コードは全体で約800行。Pythonに慣れている方なら、週末の1プロジェクトとして取り組めるスケール感です。
技術参考リンク
- Ollama — ローカルLLM実行環境
- Qwen2.5 — Alibaba Cloud の LLM
- slack-bolt for Python — Slack Bot フレームワーク
- youtube-transcript-api — YouTube字幕取得
- yt-dlp — 動画メタデータ取得
- Obsidian — ナレッジ管理ツール