1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIポッドキャスト「スタラジ」開発記 vol.2 ── スライド動的分割と音声パイプラインの進化

1
Posted at

タグ:RaspberryPi, ClaudeCode, Node.js, 個人開発, 自動化

AIポッドキャスト「スタラジ」開発記 vol.2 ── スライド動的分割と音声パイプラインの進化

はじめに

前回の記事で紹介した、Raspberry Pi × Claude Code による自動ポッドキャスト「スタラジ」。毎朝07:45に台本・スライドJSON・音声が全自動生成される仕組みを構築しました。

稼働開始から3日。実際に出力を聴き、見て、触った結果「ここがイケてない」が次々と見つかりました。今回はその改善の記録です。


課題一覧:稼働してみて分かったこと

# 課題 深刻度
1 スライドが1テーマ=1枚で情報スカスカ 致命的
2 スライドのソース情報が台本経由で不正確 致命的
3 話題が突然切り替わる(遷移セリフなし) 聞き心地悪い
4 遷移セリフが「さて」「ところで」ばかり 単調
5 1セリフが長すぎてTTSがエラー停止 バグ
6 音声がゆっくりすぎて眠くなる 聞き心地悪い
7 セクション間に「間」がなく唐突 聞き心地悪い

3日前に「動いた!」と喜んでいたシステムが、実際に使うと改善点だらけ。でも気づいたらすぐ直す。


改善1: スライドの動的分割(9枚→23枚)

Before: 1テーマ=1スライド

旧プロンプトには「1つのニューストピックにつき必ず1枚」と書いていました。結果、台本のセグメント数=スライド数になり、6テーマで6枚のニューススライド。7-8分のポッドキャストに対して情報がスカスカでした。

After: 情報量ベースで動的に決定

■ スライド枚数の決定ルール(最重要)
台本のセグメント構造にスライド枚数を合わせてはならない。
DBのニュース記事数と情報密度に基づいてスライド枚数を動的に決定すること。

手順:
1. get_unused_news.js --used-today の出力からニュース記事を全て取得
2. 台本のセクションごとに、関連するニュース記事を紐づける
3. 各セクション内の記事を「サブトピック」に分解する
4. 1スライド = 1サブトピック(箇条書き2〜3項目)でスライドを生成

さらに section_divider タイプを新設し、セクション間の遷移を視覚的にも分かりやすくしました。

1枚あたりの箇条書きを4〜6項目から2〜3項目に減らし、その分スライド枚数を増やす。情報密度を下げて視認性を上げる設計です。


改善2: DB主入力化 ── ソース情報の正確性

Before: 台本が主、DBが補助

入力1(主): 台本 → スライドのcontent, title, news_source
入力2(補助): DB → 事実の補足

台本経由の二次情報では、ソースメディア名や記事タイトルが丸められたり、不正確になる問題がありました。

After: DBが主、台本はspeaker_notesのみ

入力1(主): DB[is_used=1の記事] → content, title, news_source
入力2(副): 台本 → speaker_notesのみ

これにより news_source フィールドがDB値の直接転記になり、ソースメディア名と記事タイトルが正確に記載されるようになりました。

なお、音声読み上げ時に「Bloomberg L.P.」を「ブルームバーグ エルピー」と読んでしまう問題は、ルビ記法 {Bloomberg L.P.|ブルームバーグ} で対処。テキスト上は正式名称、TTS音声ではリスナーに通じる通称で読み上げます。


改善3: 話題遷移の自然化

Before: 無言ジャンプ

**ベントン**: ...実際、生産性向上の観点では画期的だよね。

## 日本のClaude実装事例

**ロス**: しかもさー、日本のエンジニアたちもClaude使いまくってるよ!

## セクション名 はパーサーがメタデータとして読むだけで、音声には含まれません。つまり聴いている人にはベントンの発言の直後にロスが全く別の話を始めたように聞こえます。

After: キャラが自然に橋渡し

台本生成プロンプトに以下を追加しました。

【話題の遷移】セクションが切り替わるときは、キャラクターが自然に
話題を転換するセリフを必ず入れてください。ただし「さて」「ところで」
のような定型句の繰り返しは禁止。毎回異なるパターンで遷移すること。

結果:

セクション境界 遷移パターン
→AI投資 ロス: 「前の話題から繋げると、評価額の話もやばいんだよ!」
→レイオフ ハサウェイ: 「でもさ、そんなに便利になったら人間いらなくなっちゃわない?」
→ハードウェア ハサウェイ: 「暗い話ばっかりで気分が沈むから、新製品の話しようよ!」
→エンタメ ベントン: 「ハードウェアはいいけど、現実世界がやばいことになってる」

全セクション境界で異なるパターン。「さて」「ところで」はゼロ。


改善4: セリフ長の制限と同一キャラ連続発言

問題: 1セリフに記事3本分を詰め込む

**ロス**: でもさ、逆にこういう時代だからこそ、新しいスキルを身につける
チャンスでもあるんだよ!Qiitaでもkenimo49さんが「Claude Codeが採用した
『git worktree』を図解する」って実践的な記事書いてるし、0h-n0さんの
「Claude Code Skillsで開発ワークフローを自動化する実践ガイド」とか、
axioradevさんの「Claude MCPで日本企業の財務分析をやってみた」とか、
日本のエンジニアも積極的にAIツール活用してる!

この1行が229文字。GCP TTSの文節長制限に引っかかり INVALID_ARGUMENT エラーで停止しました。

対策: 2段構え

1. 台本プロンプトで制限:

【1セリフの長さ制限】1つの **キャラ名**: セリフ 行は最大150文字程度。
長くなる場合は同一キャラの連続発言として複数行に分けてください。
**ロス**: まずBloombergの記事見た?Cursorが500億ドルの評価額で交渉中!
**ロス**: しかも前回ラウンドが26億ドルだったから約19倍だよ、19倍!

2. TTS側で自動分割(フォールバック):

// src/tts.js - 句点→読点の2段階分割
function splitLongText(text) {
    if (text.length <= 100) return [text];

    // Step 1: 句点・感嘆符・疑問符で分割
    const sentences = text.split(/(?<=[。!?!?])\s*/);
    // ... 100文字以内にマージ ...

    // Step 2: まだ長いチャンクは読点(、)で再分割
    for (const chunk of merged) {
        if (chunk.length <= 100) { chunks.push(chunk); continue; }
        const parts = chunk.split(/(?<=、)/);
        // ... 100文字以内にマージ ...
    }
    return chunks;
}

分割された各チャンクを個別にTTS生成し、ffmpegで結合。プロンプト側が効けばこのフォールバックは不要ですが、LLMの出力は不確定なので防御層として残しています。


改善5: 音声テンポの最適化

問題: デフォルト速度が遅い

GCP TTS Chirp3-HDのデフォルト speakingRate: 1.0 で生成すると、6.8文字/秒。日本語の自然な会話速度(8-10文字/秒)より明らかに遅く、ポッドキャストとしてダルい。

検証: 5段階の速度テスト

同じセリフで速度を変えて生成・試聴しました。

speakingRate duration 聴感
1.0 7.37s 遅い。眠くなる
1.1 7.30s 自然な人間ベース
1.2 6.84s やや速い
1.3 5.62s ちょっと倍速再生感
1.5 4.92s 倍速再生感

結論: speakingRate: 1.1 をシステムデフォルトに。 1.3以上はYouTubeの再生速度で対応可能。

audioConfig: { audioEncoding: 'MP3', speakingRate: 1.1 },

改善6: セグメント間ポーズ

話題が切り替わるとき、音声にも「間」が必要です。

// bin/generate_podcast_audio.js
const SEGMENT_PAUSE_SEC = 1.5;

// セグメント変更検知時に無音を挿入
if (segment !== currentSegment && currentSegment !== '') {
    generateSilence(silenceFile, SEGMENT_PAUSE_SEC);
    audioFiles.push(silenceFile);
}

スライドJSON側にも pause_sec フィールドを追加。動画合成エンジン(StreamCast Engine)がsection_dividerスライドを表示する時間を制御します。

{
  "type": "section_divider",
  "title": "AI業界",
  "subtitle": "コーディングAIの評価額競争",
  "pause_sec": 2
}

Before / After 比較

指標 Before (Day 1) After (Day 3)
スライド枚数 9枚 23枚
newsスライド 5枚 (各5項目) 15枚 (各2-3項目)
section_divider なし 5枚
news_sourceの正確性 台本経由(曖昧) DB直接転記
話題遷移 無言ジャンプ キャラが自然に橋渡し
セグメント間の間 なし 1.5s無音
TTS長文エラー 停止 2段階分割で回避
音声速度 1.0x (遅い) 1.1x (自然)
セリフ数 31 65
月間TTSコスト 約140円 約416円

コスト試算

GCP Cloud TTS Chirp3-HDは文字数課金($16 / 100万文字)。speakingRate を変えてもコストは変わりません。

項目 回数 1回コスト 月額
日刊 20回/月 約13円 約260円
増刊号(土曜) 4回/月 約39円 約156円
合計 約416円/月

スタバのコールドブリュー1杯(¥451)より安い月額で、毎日数百件のニュースを収集・分析し、AIが台本を書き、キャラクターが語るポッドキャスト音声を自動生成するシステムが丸ごと動きます。


学んだこと

1. 「動いた」と「使える」は違う

Day 1で「毎朝台本が自動生成される!」と感動しましたが、実際に聴いてみると話題転換が唐突、スライドがスカスカ、音声が遅い。稼働させてからが本当の開発の始まりでした。

2. LLMの出力は防御的に扱う

セリフが150文字を超えないようプロンプトで指示しても、LLMは守らないことがある。TTS側での自動分割(フォールバック)を入れておくことで、プロンプト違反があってもパイプラインは止まりません。プロンプト+バリデータ+フォールバックの3層防御が安定稼働の鍵です。

3. 主従関係を明確にする

スライド生成で「台本が主、DBが補助」にしていたら情報が不正確ではないが、明らかに伝えてくる情報が軽くなりました。「DBが主、台本は副」に逆転させたことで、ソース名が正確になった。入力データの主従関係はプロンプトで明示的に宣言すべきです。


今後の展望

  • StreamCast Engine連携: GAS+Canvasで動画自動生成。pause_secsection_divider に対応予定
  • YouTube自動投稿: 毎朝Drive上に台本・スライドJSON・音声MP3が揃う状態まで来た
  • PPTX生成: スライドJSONからPowerPointファイルを直接生成(stash退避中、TDD再開予定)

Day 1 は「自動化できた!」という興奮。Day 3 は「ちゃんと使えるものにする」という地道な改善。AIと一緒に作るプロダクトは、動かしてからが面白い。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?