45395 - シコウサクゴ -

SlackにYouTube URLを貼るだけ!ローカルLLMで動画を自動要約するBotを作った

2026-03-22
AI駆動開発
AI駆動開発
YouTube
Slack
ローカルLLM
Obsidian
Ollama
MCP
Last updated:2026-03-21
14 Minutes
2620 Words

AIでの開発中の隙間時間にAI系の情報収集はYoutubeでやるのが効率的ですが、後で見た動画を振り返る時にテキストで要点だけ抽出して後で検索できるようにしようと思い、 SlackチャンネルにYouTube URLを貼るだけで、ローカルLLMが自動で動画を要約し、Obsidianにマークダウンとして保存してくれるBotを開発しました。

完全ローカル完結。クラウドAPIの課金は一切なし。月額費用ゼロ。

1
Slack に YouTube URL を投稿
2
→ Bot が即時反応「要約を作成中...」
3
→ 字幕取得 → ローカルLLMで要約生成
4
→ Obsidian Vault に Markdown 保存
5
→ Git commit & push(自動)
6
→ Slack スレッドに要約を返信

この記事では、このBotの設計思想・技術スタック・実装のポイントを解説します。


何ができるのか

デモ:実際の動作

  1. Slackのチャンネルに YouTube URL を投稿する
  2. Bot が即座にスレッドで「:hourglass_flowing_sand: 要約を作成中です…」と返信
  3. 約1〜2分後(30分の動画の場合)、要約がBlock Kit形式で返信される
  4. 同時にObsidian Vault にMarkdownファイルが保存され、Gitで自動push

要約の出力形式

Bot が返すのは、こんな構造化された要約です:

  • TL;DR — 3行以内の核心
  • 主要ポイント — 3〜7個の箇条書き
  • 詳細メモ — セクション分けされた詳細な内容(Obsidian側のみ)
  • 引用・キーフレーズ — 動画内の印象的な発言

Slack上ではTL;DRと主要ポイントをコンパクトに表示。全文はObsidianで読む、という使い分けです。 Gitリポジトリはこちら→YouTube Summarizer


技術スタック

カテゴリ技術選定理由
LLMOllama + Qwen2.5:32Bローカル実行、日本語品質、無料
字幕取得youtube-transcript-apiYouTube公式字幕を高速取得
字幕フォールバックOpenAI Whisper字幕なし動画にも対応
メタデータyt-dlpタイトル・チャンネル名・公開日を取得
Slack連携slack-bolt (Socket Mode)公開サーバ不要、ローカル実行可能
出力Jinja2 → Obsidian MarkdownPKMとの統合
テンプレートJinja2Markdownテンプレートの柔軟な管理
プロセス管理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 — 全体のオーケストレーション
2
def 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用: パスだけ返す
2
def run(url, config) -> Path:
3
return run_with_result(url, config).output_path
4
5
# Slack Bot用: 全中間結果を返す(要約内容をSlackに返信するため)
6
def run_with_result(url, config) -> PipelineResult:
7
...

実装のポイント

1. 長い動画への対応:Map-Reduce要約

LLMにはコンテキスト長の制限があります。Qwen2.5:32Bの場合、32768トークン。30分の動画の字幕は約2万トークンになることもあり、プロンプトと合わせるとギリギリです。

そこで Map-Reduce パターン を実装しました:

1
estimated_tokens = _estimate_tokens(full_text, language)
2
input_budget = int(num_ctx * 0.6) # コンテキストの60%を入力に使う
3
4
if estimated_tokens <= input_budget:
5
# 短い動画: 1回のLLM呼び出しで要約
6
raw_output = _summarize_single(...)
7
else:
8
# 長い動画: チャンク分割 → 各チャンクを要約 → 統合
9
chunks = _split_into_chunks(full_text, input_budget, language)
10
raw_output = _summarize_map_reduce(chunks, ...)

チャンク分割では 文境界 で切り、200文字のオーバーラップ を設けることで、文脈の断絶を防いでいます。

日本語と英語でトークン推定の方法を変えているのもポイントです:

1
def _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文字起こし
1
def _fetch_transcript_with_fallback(video_id, languages, no_whisper, config):
2
try:
3
return fetch_transcript(video_id, languages) # YouTube字幕API
4
except TranscriptNotAvailable:
5
if no_whisper:
6
raise
7
# Whisperをlazy import(重い依存を通常時は読み込まない)
8
from youtube_summarizer.whisper_fallback import transcribe_audio
9
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
# 安全: リスト引数(シェル解釈されない)
2
subprocess.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) # ← これはNG

push失敗時は pull --rebase で自動リトライ。Obsidian Gitプラグインとの競合も安全に処理します:

1
try:
2
_run_git(repo_path, "push", "origin", "main")
3
except GitSyncError:
4
_run_git(repo_path, "pull", "--rebase", "origin", "main")
5
_run_git(repo_path, "push", "origin", "main")

5. 例外設計:エラーの種類ごとにSlackメッセージを出し分け

各パイプラインステージに対応する例外クラスを設計し、Slack Botでは例外の型に応じてユーザーフレンドリーなメッセージを返しています:

slack_bot.py
1
try:
2
result = run_with_result(url, config)
3
except DuplicateVideoError:
4
say(":information_source: この動画は既に要約済みです")
5
except InvalidURLError:
6
say(":x: 有効なYouTube URLではありません")
7
except MetadataFetchError:
8
say(":x: 動画情報を取得できませんでした")
9
except (TranscriptNotAvailable, WhisperError):
10
say(":x: 字幕の取得に失敗しました")
11
except 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で安定して動くテストスイートです。

Terminal window
1
$ python -m pytest tests/ --cov=youtube_summarizer --cov-report=term-missing
2
# 183 passed, 93% coverage

セキュリティテスト

Slackは不特定多数がメッセージを送れるインターフェースです。悪意のある入力を想定したセキュリティテストも組んでいます:

  • コマンドインジェクション: URL に ; rm -rf / を含むメッセージへの耐性
  • Git コミットメッセージインジェクション: ファイル名に特殊文字を含む場合の安全性
  • HTML/Markdown インジェクション: Block Kit内でのエスケープ確認
  • トークン漏洩防止: ログにSlackトークンが出力されないことの確認

設定ファイル

config.toml で全パラメータを制御。ハードコードはほぼゼロです:

1
[ollama]
2
model = "qwen2.5:32b" # LLMモデル(ollama pullで追加可能)
3
base_url = "http://localhost:11434"
4
num_ctx = 32768 # コンテキスト長
5
temperature = 0.3 # 低めで安定した出力
6
7
[whisper]
8
model_size = "medium" # tiny/base/small/medium/large
9
10
[transcript]
11
languages = ["ja", "en"] # 字幕の優先言語
12
13
[output]
14
vault_path = "/Volumes/work/obsidian/YouTube"
15
template = "default" # Jinja2テンプレート名
7 collapsed lines
16
17
[summary]
18
output_language = "ja"
19
20
[slack]
21
channel_ids = ["C0AN3Q0GTE0"] # 空=全チャンネル許可
22
git_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プロジェクトとして取り組めるスケール感です。


技術参考リンク

Article title:SlackにYouTube URLを貼るだけ!ローカルLLMで動画を自動要約するBotを作った
Article author:45395
Release time:2026-03-22