はじめに
前回の記事(vol.3)では、AIポッドキャスト「スタラジ」にプロデューサーを置き、リスナー投票型オーディションでキャストを選び直した話を書きました。
今回は、その先。キャストの顔を作り、口パクさせ、動画にし、毎朝8:30までに自動配信するところまで持っていった話です。
全部ラズパイ1台(Raspberry Pi 4 / 4GB RAM)で完結しています。
今回やったこと
- 12キャラ×2表情の画像をGeminiで生成 — VTuber風口パクアニメ用
- Remotionで口パク動画を自動生成 — Pi上でレンダリング
- 朝のパイプラインを再設計 — 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.jsonのentriesに「誰が何秒から何秒まで話しているか」が記録されているので、現在のフレームを秒に変換して話者を特定。話している人だけ口をパクパクさせます。
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」を爆誕させることです。