はじめに
毎朝、目が覚めるとポッドキャストの台本が出来上がっている。
テック系ニュース、エンタメ情報、海外メディアの最新記事——それらを自動収集し、実家で母親と観ていた大好きな海外ドラマの登場人物たちが掛け合いトークで紹介してくれる。しかも毎回出演者はランダムで変わる。
これは「スタラジ(スターラジオ)」——Raspberry Pi と Claude Code を組み合わせて構築した、完全自動のポッドキャスト台本生成システムの話です。
何ができるのか
毎朝 07:45、Raspberry Pi 上で以下が全自動で実行されます。
- 国内外80以上のメディアからRSSで最新ニュースを自動収集
- AIがニュースをクラスタリングし、トレンドを分析
- 海外ドラマ「ER緊急救命室」の登場人物たちが掛け合う7〜8分のポッドキャスト台本を自動生成
- 台本からプレゼンテーション用スライドの仕様書(JSON)を自動生成
- 台本・スライド・DBを Google Drive に自動同期
- 土曜日は20分の増刊号(週間振り返り)を自動生成
- 日曜・祝日はお休み(でもニュース収集だけは続ける)
生成された台本は1万文字超。記事のソース名、著者名、具体的なタイトルまで正確に紹介される本格的なトーク番組の台本です。
システム全体像
開発はWindows PC上でClaude Codeと対話しながらTDD(テスト駆動開発)で進め、GitHubを経由してRaspberry Piにデプロイ。Pi上ではClaude Codeが --dangerously-skip-permissions モードで自律実行し、ニュースの分析から台本の執筆まですべてを自分で判断します。
パイプライン:ニュース収集から配信までの全自動フロー
毎朝の処理を1つのフローで表すとこうなります。
台本生成のアーキテクチャ:調査と生成物のフロー
Claude Code が台本を生成するまでの「調査→分析→生成」の流れです。
スライド生成のアーキテクチャ:2ソース分離設計
スライドJSONは「事実」と「トーク」を完全に分離する設計です。
ポイントはスライド本体(content)にはDBとソース記事から抽出した客観的事実だけを載せ、出演者の感情・意見は speaker_notes にだけ記載すること。視聴者が見るスライドは正確な情報、裏側のトーク原稿は個性豊かなキャラクターの掛け合い——この分離が品質の鍵です。
技術スタック
| 要素 | 技術 |
|---|---|
| 実行環境 | Raspberry Pi(常時稼働・SSHホスト名: green-lab) |
| 言語 | Node.js |
| データベース | SQLite(news.db: 記事 + 台本) |
| ニュース収集 | rss-parser(RSS/Atom対応、80+フィード) |
| 台本生成 | Claude Code CLI(自律モード) |
| スライド仕様 | Claude Code CLI(2回目呼出、JSON出力) |
| クラウド同期 | rclone(Google Drive OAuth認証) |
| テスト | Jest(14スイート / 108テスト) |
| 祝日判定 | @holiday-jp/holiday_jp |
| 音声合成 | Google Cloud TTS(Chirp3-HD × 6声) |
| 音声結合 | ffmpeg(concat demuxer) |
| 読み仮名制御 | ruby_text.js(ルビ記法変換) |
| スライド画像(準備済み) | Playwright(HTML→PNG 1920x1080) |
| 動画合成(準備済み) | ffmpeg(filter_complex) |
こだわりポイント
1. ニュースは「クラスタリング」して紹介する
100件以上のニュースを1つずつ紹介していたら聞いてられません。Claude Codeにはこう指示しています。
取得した大量のニュースを1つ1つ順に紹介するのではなく、まずは背後にある「技術」や「サービス」ごとに分類(クラスタリング)し、全体を俯瞰してください。その中から盛り上がっているクラスタを選定し、トークテーマとして構成してください。
これにより「今週はAIコーディング界隈が熱い」「大手ITのレイオフが加速」といったトレンド主導の構成になります。
2. 出演者は毎回ランダム
実家で母親と観ていた大好きな海外ドラマ「ER緊急救命室」。そのキャラクターから名前をいただいた6人のレギュラーメンバーがいます。
// bin/select_personas.js
function selectPersonas(personas, count = 3) {
const shuffled = [...personas].sort(() => Math.random() - 0.5);
return shuffled.slice(0, count);
}
毎回この中から3人がランダムに選ばれます。情熱的なロスが出る日もあれば、クールなベントンが仕切る日もある。同じニュースでもメンバーの組み合わせでトーンが変わるのが面白いところです。
| キャラクター | タイプ |
|---|---|
| グリーン | 不満爆発・自虐ツッコミ型 |
| ベントン | クール・一刀両断型 |
| ハサウェイ | ポジティブギャル型 |
| ロス | 情熱・行動派型 |
| カーター | 真面目・勉強家型 |
| ルイス | バランス・仲裁型 |
3. ソース情報の正確性にこだわる
AIに台本を書かせると、存在しない数値を「捏造」しがちです。これを明確に禁止しています。
DBに存在しない情報(いいね数・インプレッション数など)は絶対に捏造しないでください。
代わりに、記事の著者名やソースメディア名はDBに格納し、必ず正確に紹介させます。
// bin/fetch_rss.js - RSSから著者情報を抽出
function extractAuthor(item) {
const author = item.author || item.creator || null;
if (author && author.trim() === '') return null;
return author || null;
}
4. 障害からの自動リカバリ
Raspberry Piは電源ケーブルが抜かれたり、ネットワークが落ちたりすることがあります。数日間止まっていても、復帰時に自動でリカバリします。
// src/db.js - 停止期間に応じてウィンドウを動的拡張
function calcDailyWindowDays(lastGeneratedAt) {
if (!lastGeneratedAt) return 7; // 初回は7日分
const elapsedDays = Math.floor(
(Date.now() - lastGeneratedAt.getTime()) / (24 * 60 * 60 * 1000)
);
return Math.min(7, Math.max(3, elapsedDays + 2));
}
出演者が自然にフォローしてくれるわけです。
「みなさんお久しぶりです!5日ぶりのスタラジです。ちょっとお休みいただいてましたが復活しました!」
5. 日曜・祝日は休み、でも収集は止めない
// bin/is_broadcast_day.js
function getBroadcastMode(date = new Date()) {
const dayOfWeek = date.getDay();
const isHoliday = holidayJp.isHoliday(date);
if (dayOfWeek === 6) // 土曜日
return { broadcast: true, mode: 'weekly', reason: '土曜日: 増刊号' };
if (dayOfWeek === 0) // 日曜日
return { broadcast: false, mode: 'skip', reason: '日曜日: 配信なし' };
if (isHoliday)
return { broadcast: false, mode: 'skip', reason: `祝日: 配信なし` };
return { broadcast: true, mode: 'daily', reason: '平日: 日刊配信' };
}
RSS収集は毎日実行し、台本生成だけスキップ。翌営業日に溜まったニュースを自動で拾います。
6. フィードの自動スキップ
80以上のRSSフィードを毎日叩いていると、一部のフィードが落ちていることもあります。3回連続で失敗したフィードは自動でスキップし、パイプライン全体が止まることを防ぎます。
// src/db.js - 3回連続失敗で自動スキップ
async function getSkippedFeeds(db) {
return await db.all(
`SELECT url FROM feed_errors WHERE consecutive_failures >= 3`
);
}
成功したら即カウンターリセット。一時的な障害には強く、恒久的に壊れたフィードには手をかけない設計です。
7. 音声合成と「読み仮名」問題
台本をそのままTTSに渡すと「Bloomberg L.P.」を「ブルームバーグエルピー」と読み上げてしまいます。会話では誰もそうは言わない。かといって台本からソース表記を消すと、字幕やスライドで原文が失われる。
解決策はルビ記法です。台本に {原文|読み} 形式で記載し、用途に応じて片側だけ抽出します。
**ルイス**: {Bloomberg L.P.|ブルームバーグ}の記事で、
{Cursor|カーソル}が約500億ドル規模の評価額で…
// src/ruby_text.js - 1つの台本から2つのテキストを生成
function toSpeechText(text) { // TTS用:読み仮名
return text.replace(/\{[^|]+\|([^}]+)\}/g, '$1');
// → "ブルームバーグの記事で、カーソルが約500億ドル規模の評価額で…"
}
function toSubtitleText(text) { // 字幕用:原文表記
return text.replace(/\{([^|]+)\|[^}]+\}/g, '$1');
// → "Bloomberg L.P.の記事で、Cursorが約500億ドル規模の評価額で…"
}
台本ファイルは1つ。regex 1行で字幕と音声が分離できます。
8. キャラクターごとの音声
6人のキャラクターにはそれぞれ異なるGoogle Cloud TTS(Chirp3-HD)のボイスを割り当てています。
| キャラクター | 性別 | ボイスID | 性格 |
|---|---|---|---|
| グリーン | 男性 | Puck | 不満爆発・早口 |
| ベントン | 男性 | Fenrir | クール・ドライ |
| ハサウェイ | 女性 | Leda | ポジティブ |
| ロス | 男性 | Charon | 情熱・行動派 |
| カーター | 男性 | Sadachbia | 真面目・勉強家 |
| ルイス | 女性 | Achernar | バランス・仲裁 |
1回の配信(31セリフ・7.5分)の音声生成コストは約7円。月20回配信でも約150円です。
実際に生成された台本(抜粋)
**ルイス**: さて、今日もスタラジ始めていきましょう!
3月12日、記念すべき第1回配信です。今日のメンバーは?
**ロス**: ロスです!いやー初回からテンション上がるニュースが
多すぎてやばい!AI界隈がすごいことになってる!
**グリーン**: グリーンです。……初回だからって手加減しないから。
あ、でもCursor使ったらバグだらけだった話はあとで絶対する。
**ルイス**: まず最初のトピック。Bloombergの記事で、
Rebecca Torrenceさん、Rachel Metzさん、Natasha Mascarenhasさんの
共同執筆なんだけど、AIコーディングスタートアップの
Cursorが約500億ドルの評価額で資金調達を協議中だって。
**ロス**: 500億ドル!?日本円にしたら約8兆円だよ!?
ソースメディア名、著者名、記事タイトルが正確に入っているのがポイントです。
パイプラインの心臓部: podcast_runner.sh
全体の流れを制御するのがこのシェルスクリプトです。
#!/bin/bash
# 1. 配信日判定(日曜・祝日はスキップ)
BROADCAST_JSON=$(node bin/is_broadcast_day.js)
# 2. RSS収集(毎日実行)
node bin/fetch_rss.js
# 3. 配信スキップなら終了
if [ $BROADCAST_EXIT -ne 0 ]; then
echo "RSS収集のみ完了"; exit 0
fi
# 4. 空白期間の検出 & ペルソナ選出(6人→3人)
ABSENCE_MSG=$(node bin/days_since_last.js)
node bin/select_personas.js > "$PERSONA_FILE"
# 5. Claude Code 自律実行(20分タイムアウト)
timeout --signal=KILL 1200 bash -lc \
"claude -p - --dangerously-skip-permissions" < "$PROMPT_FILE"
# 6. DB格納 & バックアップ
node bin/save_script_to_db.js "${OUTPUT_FILE}"
# 7. スライドJSON生成(Claude Code 2回目、10分タイムアウト)
timeout --signal=KILL 600 bash -lc \
"claude -p - --dangerously-skip-permissions" < "$SLIDE_PROMPT_FILE"
# 8. Google Drive同期(台本 + スライド + DB + NotebookLM用MD)
rclone copyto "$OUTPUT_FILE" "gdrive:${GDRIVE_FOLDER}/${GDRIVE_FILENAME}"
rclone copyto news.db "gdrive:db/news.db"
claude -p - はClaude Code CLIの非対話モードで、標準入力からプロンプトを受け取ります。--dangerously-skip-permissions により、ファイル読み書きやコマンド実行を自律的に行います。Claude Code を1回の実行で 2回 呼び出しているのがポイント——1回目で台本生成、2回目でその台本とDBデータからスライドJSONを生成します。
Google Drive 同期とNotebookLM連携
生成された台本やデータは自動でGoogle Driveに同期されます。
rclone の OAuth 認証を使い、毎回の実行後に自動転送。サービスアカウントではGoogle Driveのストレージクォータが割り当てられないため、個人アカウントのOAuth方式を採用しました。
開発スピード: 2日で本番稼働
このプロジェクトは構想から本番稼働まで2日で完成しました。
| 日時 | マイルストーン |
|---|---|
| 3/11 14:41 | 最初のコミット。スクレイパーとペルソナ設定から開始 |
| 3/11 16:00 | SQLite統合、配信日判定、進捗表示 |
| 3/11 21:30 | RSS収集に全面切り替え。80+フィード、著者情報抽出 |
| 3/11 23:00 | 増刊号対応、6人ペルソナ完成、障害リカバリ、DBバックアップ |
| 3/12 00:15 | 台本パーサー、TTS音声マッピング、スライド生成、動画合成を一気に実装 |
| 3/12 09:54 | Pi上で初の本番稼働成功。第1回台本が自動生成される |
| 3/12 10:30 | Google Drive自動同期(台本・DB・NotebookLM用エクスポート) |
| 3/12 20:00 | スライドJSON自動生成、2ソース分離設計 |
| 3/12 22:20 | TTS音声生成、ルビ記法による読み仮名制御 |
初回コミットから最新コミットまで約32時間、46コミット。実際の開発時間はもっと短く、睡眠時間もありますし、日中は外出もしてます。別のプロダクトのPoC(結局断念)に寄り道したりしています。
この速度を支えたのはClaude Codeとの対話型TDDです。「テストを書く→実装する→リファクタする」のサイクルをClaude Codeと二人三脚で回すと、1モジュールが数分で完成します。人間がやっているのは方針の判断と品質の確認だけで、コーディングのほぼすべてをAIが担っています。
開発手法: AI×TDD
このプロジェクト自体もClaude Codeと対話しながらTDD(テスト駆動開発)で構築しています。
- RED: 先にテストを書いて失敗を確認
- GREEN: 最小限のコードでテストを通す
- REFACTOR: 命名・責務・重複・複雑度をチェック
現在 14テストスイート、108テスト がすべてパスしています。AIが書いたコードをAIがテストする——この開発サイクル自体がなかなか面白い体験です。
Test Suites: 14 passed, 14 total
Tests: 108 passed, 108 total
今後の展望: 動画パイプライン
台本と音声まではRaspberry Piで全自動生成できるようになりました。次のステップは「映像」です。
Piの役割は「テキスト+音声工場」。毎朝Driveに台本・スライドJSON・音声MP3が揃った状態になり、あとはスライド画像の生成と動画合成を残すのみです。キャラクターごとに異なる声(Chirp3-HD)が割り当てられていて、グリーンは早口の男性ボイス、ハサウェイは明るい女性ボイス、ベントンはクールな男性ボイス——毎朝、全自動でYouTubeにポッドキャスト動画がアップされる世界が見えてきました。
プロジェクト構成
auto-podcast-trader/
├── src/ # コアモジュール
│ ├── db.js # SQLite抽象化(記事・台本・フィードエラー)
│ ├── parse_script.js # 台本パーサー
│ ├── tts.js # Google Cloud TTS(6キャラ×Chirp3-HD)
│ ├── ruby_text.js # ルビ記法変換(音声用/字幕用)
│ ├── audio_concat.js # ffmpeg音声結合
│ ├── slide_generator.js # Playwright HTML→PNG
│ ├── video_composer.js # ffmpeg動画合成
│ └── news_sources.json # 80+ RSSフィード定義
├── bin/ # CLIスクリプト
│ ├── fetch_rss.js # RSS一括収集
│ ├── is_broadcast_day.js # 配信日判定
│ ├── select_personas.js # ランダムペルソナ選出
│ ├── get_unused_news.js # 未使用記事取得
│ ├── mark_news_used.js # 使用済みマーク
│ ├── save_script_to_db.js # 台本DB格納
│ ├── days_since_last.js # 空白期間検出
│ ├── generate_podcast_audio.js # TTS音声一括生成
│ └── export_db_for_notebooklm.js # MD変換
├── scripts/
│ └── podcast_runner.sh # パイプライン全体制御
├── prompts/
│ └── slide_spec_prompt.txt # スライドJSON生成プロンプト
├── personas.md # 6人のキャラクター設定
├── tests/ # 14スイート / 108テスト
├── outputs/ # 生成物(台本・スライドJSON)
└── backups/ # DBスナップショット(7日ローテ)
おわりに
Raspberry Pi 1台と Claude Code があれば、毎朝更新されるAIポッドキャストが作れます。
ニュースの収集、トレンド分析、台本執筆、出演者の選出、スライド仕様の生成、クラウド同期、障害リカバリ——すべてが自動化されていて、人間がやることは「朝起きて読む(聴く)」だけ。
もうすぐ、読むだけじゃなく「観る」こともできるようになる予定です。
このプロジェクトは AI(Claude Code)との共同開発で構築されました。テスト駆動開発のサイクルから、この記事の執筆まで——すべてが完全自動、Claude Codeとの対話の中で生まれています。