0
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.4 ── ラズパイ1台で毎朝「VTuber口パク動画ポッドキャスト」を全自動配信する

0
Posted at

はじめに

前回の記事(vol.3)では、AIポッドキャスト「スタラジ」にプロデューサーを置き、リスナー投票型オーディションでキャストを選び直した話を書きました。

今回は、その先。キャストの顔を作り、口パクさせ、動画にし、毎朝8:30までに自動配信するところまで持っていった話です。

全部ラズパイ1台(Raspberry Pi 4 / 4GB RAM)で完結しています。


今回やったこと

  1. 12キャラ×2表情の画像をGeminiで生成 — VTuber風口パクアニメ用
  2. Remotionで口パク動画を自動生成 — Pi上でレンダリング
  3. 朝のパイプラインを再設計 — cron 07:15開始、08:30配信完了

この記事に付属する動画

スタラジ春分の日スペシャル「あなたが選んだ声」(約13分)を、この記事の成果物として公開しています。

プロデューサー鈴木一生が初めてマイクの前に座り、ER組の卒業と新キャストの紹介を行ったオーディション特別版です。

※動画リンクは記事末尾


VTuber風キャラクター画像の量産

課題:口パクには「同じポーズで口だけ違う」2枚が必要

Remotionで口パク動画を作るには、各キャラに「口閉じ(closed)」「口開き(open)」の2枚のPNG画像が必要です。

ここで重要なのは、2枚の差分が口(と目)だけであること。体・腕・手・髪の位置が変わると、パラパラ切り替えたときに体がガクガクして見える。

失敗例:差分が大きすぎた

最初に生成した画像は、closedとopenでポーズが全く違いました。

  • closed: 腕組みで不敵な笑み
  • open: 腕をほどいて手を前に出し、口を大きく開けて叫ぶ

1枚ずつ見るとカッコいいのですが、パラパラ切り替えると全身がガクガク動いてホラーになります。

解決:JSONプロンプトの設計

キャラごとにJSONで画像生成プロンプトを定義し、closed/openで手のポーズを統一しました。

// assets/characters/prompts/reiji_closed.json(抜粋)
{
  "expression": {
    "face": "口は閉じている。片方の口角がやや上がった余裕の微笑み",
    "hands": "腕を組んでいる。力強く。ジャケットの上から"
  }
}

// assets/characters/prompts/reiji_open.json(抜粋)
{
  "expression": {
    "face": "口は自然に開いて話している。目はやや見開いて生き生き",
    "hands": "腕を組んでいる。力強く。ジャケットの上から"
  }
}

hands が完全に同じ。faceだけが異なる。これで切り替えても体は動かず、口だけがパクパクする。

共通設定でスタイルを統一

全キャラ共通のスタイルルールをJSONで定義し、各プロンプトに含めました。

{
  "common_settings": {
    "background": { "color": "#00B140" },
    "framing": { "type": "bust_up", "output_size": "512x768px" },
    "art_style": {
      "genre": "日本のVTuber風アニメイラスト",
      "rendering": "セル画調、フラットシェーディング",
      "head_body_ratio": "6-7頭身"
    },
    "ng_rules": [
      "複数人描画禁止。必ず1人だけを描くこと",
      "同一キャラの複製・重複描画禁止"
    ]
  }
}

ng_rulesの「1人だけ」指定は重要でした。指定しないとGeminiが同じキャラを2-3人並べて出力してきます。

後処理:グリーンバック除去

Geminiにグリーンバック(#00B140)背景で出力するよう指示し、Pythonで透過処理+リサイズ。

# bin/process_character_images.py(核心部分)
def remove_green_screen(img_array, tolerance=30, dilate=1):
    r, g, b = img_array[:,:,0], img_array[:,:,1], img_array[:,:,2]
    green_mask = (g > 80) & (g > r + tolerance) & (g > b + tolerance)
    if dilate > 0:
        green_mask = ndimage.binary_dilation(green_mask, iterations=dilate)
    img_array[green_mask, 3] = 0
    return img_array

横長で出力された画像は中央クロップ→縦長に変換してから処理。最終サイズは200x300pxの透過PNG。

完成:12キャラ24枚

キャラ 役割 テーマカラー
みのりん 月MC #E891B7
もえちゃん 火MC #FF6B35
レイナ姐さん 水MC #9B30FF
ホリケイ 木MC #27AE60
ヨースケ 金MC #FF6B35
りっくん アシスタントA #87CEEB
そらちゃん アシスタントB #1B2A4A
ホムラ 月ゲスト #FF4500
レイジ 火ゲスト #2D2D2D
ひなちゃん 水ゲスト #FF6B9D
あやちゃん 木ゲスト #E74C3C
まほ 金ゲスト #FF85A2

+プロデューサー鈴木一生(特別版のみ出演。目が見えない謎の男)。


Remotion動画生成:Pi上で口パク動画を焼く

アーキテクチャ

口パクの仕組み

Remotionのコンポーネントで、フレーム単位で話者を検出し、画像を切り替えます。

// CHAR_IMAGE_ID: キャラ名 → 画像ファイルID のマッピング
const CHAR_IMAGE_ID = {
  "みのりん": "minori", "もえちゃん": "moe",
  "レイナ姐さん": "reina", "ホリケイ": "horikei", // ...
};

const CharacterImage = ({ name, speaking, x }) => {
  const imageId = CHAR_IMAGE_ID[name];
  const frame = useCurrentFrame();
  // 8フレーム周期で口を開閉(30fpsなら約4回/秒のパクパク)
  const mouthOpen = speaking && frame % 8 < 4;
  const variant = mouthOpen ? "open" : "closed";
  const src = staticFile(`characters/${imageId}_${variant}.png`);

  return <Img src={src} style={{ width: 120 }} />;
};

timeline.jsonentriesに「誰が何秒から何秒まで話しているか」が記録されているので、現在のフレームを秒に変換して話者を特定。話している人だけ口をパクパクさせます。

Pi 4GB でのレンダリング性能

動画長 レンダリング時間 実測比
5秒テスト 30秒 6倍
7.9分(日刊・棒人間版) 38分 4.8倍
13.7分(特別版・VTuber版) 約80分 5.8倍

--concurrency=1 で安定動作。メモリ4GBでもOOM killerは発動しない。


朝のパイプライン再設計

目標:8:30に動画配信完了

9時始業のビジネスマンが通勤中に視聴できるよう、8:30までに動画をGoogle Driveに配信したい。

逆算タイムライン

時刻 処理 所要時間
07:15 RSS収集 3分
07:18 Claude台本生成 12分
07:30 スライドJSON生成 5分
07:35 TTS音声生成 8分
07:43 タイムライン構築 5秒
07:43 Remotionレンダリング 35分
08:18 Drive転送 2分
08:20 完了(10分バッファ)

cron開始を07:45→07:15に前倒し。現在はスライドJSONと音声を直列実行していますが、将来的に並列化すればさらに5分短縮できます。最悪ケース(台本15分+レンダリング40分)でも08:30に収まる設計です。

将来の最適化: 並列化

スライドJSON生成と音声生成は互いに独立しています。

  • スライドJSON: Claude Code CLIが台本+DBからスライド仕様を生成
  • 音声生成: Google Cloud TTSが台本の各セリフを音声化

現在は安定性を優先して直列実行していますが、バックグラウンドで並列実行すれば5分短縮できます。


学んだこと

1. 口パクは「差分の最小化」が全て

2枚の画像の差分が口だけなら自然な口パクになる。体が動くとホラーになる。画像生成AIに指示するとき、handsフィールドをclosed/openで完全に同じにするだけで解決した。

2. 画像生成AIには「やるな」を明示する

Geminiに「1人だけ描いて」と言っても2-3人描いてくる。ng_rulesに「複数人描画禁止」「同一キャラの複製禁止」を明記してようやく1人になった。AIへの指示は「やってほしいこと」より**「やるなということ」**の方が重要。

3. Pi 4GBでRemotionは動く

Raspberry Pi 4GBでRemotionのSSRレンダリングは普通に動く。VTuber画像版では10分の動画を約50分で焼ける。速くはないが、朝のバッチ処理なら十分実用的。concurrency=1がメモリ的に安全。

4. パイプラインは逆算で設計する

「何時に配信したいか」から逆算して各工程の開始時刻を決める。余裕を持たせたいなら並列化と前倒しで対応。「動けばいい」ではなく「何時に届くか」が運用の設計。


数字で見る今回の進捗

項目 Before After
キャラ画像 なし(棒人間) 12キャラ×2表情=24枚
動画生成 PoC(棒人間版) VTuber口パク動画(本番)
cron開始 07:45 07:15
配信完了目標 08:09(台本+スライドのみ) 08:20(動画含む)
パイプライン 台本+スライドで終了 音声+動画+Drive転送まで全自動

今後の展望

  • YouTube自動投稿: Google Drive→YouTube Data API v3で自動アップロード
  • EC2レンダリング: Pi→EC2にジョブを投げて動画生成を3-5分に短縮
  • リスナー反応の反映: 曜日ごとの再生数からゲスト選出確率を動的に調整

ラズパイ1台で、ニュース収集→AI台本→音声合成→動画レンダリング→クラウド配信。毎朝8:30に届くVTuberポッドキャスト。全部自動。

次の目標は、YouTubeに自動投稿して「チャンネル登録者0人のAI VTuber」を爆誕させることです。

0
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
0
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?