1 行アイデアを 40 秒コメディ動画にする 10-beat パイプライン
TL;DR
1 行のアイデアを Gemma 4 31B が 10-beat 構造に展開、HiDream で 2048² 画像 11 枚、LTX-2 A2V/I2V で 11 clip、Irodori-TTS で台詞と男性ナレーター、ffmpeg で字幕焼き込みと Hook タイトル overlay まで全自動。実測 25-30 分で 40 秒の縦長 (512×768) 動画。すべてローカル GPU 1 枚 (96 GB Blackwell)、API コストゼロ。
完成版 (公開済み):
この記事の対象読者
ローカル GPU で AI 動画コメディを量産したい個人開発者向け。各モデル単体ではなく 「複数モデルを 1 本のパイプラインに繋いで運用する設計」 に関心がある人。
何をしたか
Z 世代向けのダーク・コメディ風刺(No Means Yes? という性的同意ジレンマをネタにする海外ショート動画フォーマット)を、1 行のアイデア → 40 秒動画まで自動化した。
完成イメージ:
- Hook (0-5s): 美しい女性のドアップ + ナレーター「『男の子でしょ』に応えた、彼の運命——」+ 大型タイトル「No Means Yes?」
- 本編 (5-37s): 映画館デート → 「キスしていい?」→ 「やだ…ダメだよぉ…」→ 落胆 → 「もっと積極的になってよ。男の子でしょ?」→ なるほどね → キス
- オチ (37-40s): 裁判所「主文 被告人を不同意性交等罪により懲役 3 年に処する」+ ハンマー「コン!」+ 牢屋で涙
before / after:
| 従来の作り方 | この pipeline | |
|---|---|---|
| アイデア → 公開動画 | 2-3 日 (手動編集) | 25-30 分 (全自動) |
| API コスト | DALL-E + 動画生成で 1 本数百円 | 0 円 (電気代のみ) |
| 字幕 | 手動で SRT 書く | 句読点で自動分割焼き込み |
| Hook | 別撮り | パイプラインに統合 |
アーキテクチャ
[Stage A] Gemma 4 31B (vllm, port 8894) → plan.json (10 beats + hook)
[Stage B] HiDream-O1-Image (port 8895) → 11 枚の 2048² 画像
+ Gemma 4 31B multimodal で視覚 judge (--judge --max-retries 2)
[Stage C] Irodori-TTS (port 8880) + LTX-2 A2V (port 8892) / I2V (port 8891)
→ 11 clip + Hook clip → ffmpeg concat → 字幕焼き込み
実装は llm_server/storyboard/ 配下に集約 (pipeline.py / visual.py / judge.py / video.py / render.py / run.py)。
10-beat consent_dilemma フォーマット
prompts.py の CONSENT_DILEMMA_SYSTEM で system prompt として固定:
| # | type | speaker | renderer | 内容 |
|---|---|---|---|---|
| 1 | provocation | b | LTX-2 A2V | 意味深な誘い |
| 2 | ask | a | LTX-2 A2V | 真面目な同意確認 |
| 3 | refusal | b | LTX-2 A2V | 控えめな拒否 (「やだ…ダメだよぉ…」のような曖昧形) |
| 4 | dejection | a (silent) | LTX-2 I2V | 落胆 |
| 5 | gaslight | b | LTX-2 A2V | 矛盾した誘導発言 |
| 6 | pause | a (silent) | LTX-2 I2V | 短い realization |
| 7 | kiss | a (silent) | LTX-2 I2V | 男からのキス瞬間 |
| 8 | verdict | judge | LTX-2 A2V | 早口判決 |
| 9 | gavel_se | judge | LTX-2 I2V (keep_audio) | ハンマー + AI 自動「コン!」音 |
| 10 | jail | a (silent) | LTX-2 I2V | 牢屋で涙 |
ポイントは 3 段階のひっくり返し:
- refusal を flat な「ダメ」にしない: 「やだ…ダメだよぉ…」と語尾を伸ばし、performative な「No that doesn't mean No」 のニュアンスを出す。これが後の gaslight の矛盾を成立させる前提。
- gaslight 直後に kiss を置かない: 「pause」(なるほどね) で 1.5 秒挟む。テンポと感情曲線のため。
- verdict と jail の二段オチ: 判決だけだと唐突。牢屋で泣く絵があると「彼は本当に有罪になった」が腑に落ちる。
Hook の設計 (TikTok の最初 3 秒問題)
縦長ショートは最初の 3 秒で離脱率が決まる。本編 10 beats の前に Hook segment を前置:
"hook": {
"title_overlay": "No Means Yes?",
"narrator_line": "「男の子でしょ」に応えた、彼の運命——",
"image_prompt": "ultra close-up of beautiful Japanese woman, half-lidded eyes, ...",
"duration_sec": 3.5
}
実装上の罠 2 つ:
罠 1: narrator TTS 長が duration_sec を超えると音声が切れる。「彼の運命」の「か」で audio cut-off が起きた。対策: TTS を先に生成 → ffprobe で実測 → max(plan_duration, narrator + 0.6) を I2V duration に渡す。
narrator_dur = _ffprobe_duration(narrator_wav)
duration = max(float(hook.get("duration_sec", 0.0)), narrator_dur + 0.6)
ltx_i2v_clip(portrait, i2v_prompt, duration, silent_video, keep_audio=False)
罠 2: drawtext の y 位置。y=h*0.30 (画面 1/3 高) だと顔被り。y=20 (絶対 20 px) で最上部に。
字幕焼き込み (無音視聴対応)
電車で音消して見るユーザー対応 + プラットフォーム横断信頼性のため、burned-in subtitles。
style = (
"FontName=Noto Sans CJK JP,FontSize=18,PrimaryColour=&H00FFFFFF,"
"OutlineColour=&H00000000,Outline=2,Shadow=0,BorderStyle=1,"
"Alignment=2,MarginV=60,Bold=1"
)
# ffmpeg -i raw.mp4 -vf "subtitles=subs.srt:force_style='..."
Alignment=2 = bottom center。MarginV=60 で最下端から余裕。
長文分割: 1 つの beat 内に 30 字以上の line があると顔被り。_split_subtitle で句点 。.!? で文を分割 → 貪欲法で 28 字以下 chunk → beat duration を均等割で配分:
入力:
言葉で確認するのなんてロマンチックじゃないよね。ねえ、もっと積極的になってよ。男の子でしょ?
出力 (1 つの 8.9s beat を 2 chunk で時間分割):
| 時間 | 字幕 |
|---|---|
| 15.16-19.63s | 言葉で確認するのなんてロマンチックじゃないよね。 |
| 19.63-24.10s | ねえ、もっと積極的になってよ。男の子でしょ? |
LTX-2 I2V で効果音を生成する (gavel_se)
LTX-2 distilled は I2V 出力 mp4 に AI 生成音声 (環境音 / 効果音) を自動で含める。ffmpeg -map 0:v:0 -map 1:a:0 で明示 drop しない限り、prompt から音が付いてくる。
これを SE 生成器として転用する:
def render_se_tail_beat(sb_dir, beat, prior_clip, work_dir):
# 1. 前 beat の最終フレーム抽出
extract_last_frame(prior_clip, last_frame_png)
# 2. その画像を I2V に投入、prompt で SE 要求
prompt = build_gavel_se_prompt(beat)
return ltx_i2v_clip(last_frame_png, prompt, duration, clip_path, keep_audio=True)
ltx_i2v_clip に keep_audio=True flag を追加し、ffmpeg 再エンコード時に音声を drop しない経路を作った。
gavel_se 用 prompt:
"Single decisive arm motion of the judge bringing the gavel down sharply "
"onto the wooden bench. Loud sharp wood-on-wood thwack impact sound. "
"Brief, contained, no other motion in the frame."
裁判官の最終フレーム + ハンマー prompt で「コン!」音が出る。外したら逆転裁判 SE などで fallback、という設計。
罠リスト
開発中に踏んだ主要な罠 5 個:
1. Codex CLI が vLLM 0.20.2 で hang
codex exec -p gemma4 で system prompt + idea を投げると、/v1/responses の handshake で 0% CPU のまま 20 分以上 hang する。subprocess 出力を tail -200 でパイプしていたせいで初期 stderr も封じられた。
回避: codex 経由は諦めて urllib.request で /v1/chat/completions 直叩き。response_format={"type":"json_object"} で JSON 強制。25 秒で plan.json 生成完了。
2. HiDream が cinema screen を消してくれない
setting_prompt で "The movie screen is BEHIND the camera and NOT VISIBLE in frame" と明示しても、2048/50 steps でも screen が背景に残り続ける。
回避: t2i で scene_base 生成 → 同じ画像を I2I edit に投入し「screen を dark wall に置換、キャラ位置同一」と prompt → 1 発で消える。低解像 → I2I fix → 解像度上げて全 beat 再生成、の 2 段。
3. HiDream が lips-on-lips キスを cheek kiss にする
standard amplifier では HiDream はキスを cheek kiss に解釈しがち。"CRITICAL: their LIPS meet directly — mouth-to-mouth contact at the CENTER of the frame. NOT a cheek kiss" レベルの強い directive 必須。kiss beat 専用の早期 return ブロックを _beat_edit_prompt で用意。
4. CAST / CROP_BOX / SPEAKER_A2V_PROMPT が 2 人ハードコード
a (健太) と b (美咲) しか知らない CAST dict / CROP_BOX dict / SPEAKER_A2V_PROMPT dict が 3 箇所に。judge / narrator を追加するなら 3 dict 同時更新が必要 (KeyError で気付く)。setting_override 持ち beat は scene_base ではなく beat 自身の画像から portrait crop するよう render_speech_beat_ltx_a2v も分岐追加。
5. Gemma 4 multimodal judge は false-positive 過剰
storyboard/judge.py で Gemma 4 31B に beat 画像 + 期待表情を投げて YES/NO 判定させる視覚 judge を実装。確かに「指本数異常」「open-mouth conversational pose」「scene geometry mismatch」のような 明らかな 失敗は拾うが、「subtle shy expression」のような微妙な領域では FAIL を連発する。
実用: max-retries 2 で 3 連続 FAIL なら受容して進める。frontier reviewer (Gemini 3.1 Pro) へ切替する閾値は未自動化。
VRAM 同居設計
96 GB Blackwell Max-Q での内訳:
| プロセス | idle (GiB) | peak (GiB) |
|---|---|---|
| Gemma 4 31B (NVFP4) | 38 | 38 |
| HiDream-O1-Image | 16 | 33 |
| TTS server | 3 | 3 |
| Ditto | 3 | 3 |
| LTX-2 A2V (cold-start fp8-cast) | 1 | 24 |
| LTX-2 T2V/I2V (cold-start) | 1 | 8 |
全部 peak で 109 GiB → OOM。運用フロー:
- Stage A: Gemma 31B + HiDream idle で peak ~62 GiB
- Stage B with judge: Gemma 31B + HiDream peak で ~73 GiB
- 清書前に
pkill -f "vllm.*gemma"で Gemma kill → 38 GiB 解放 - Stage B 清書 (2048/50): HiDream peak ~33 GiB
- Stage C 前に
lsof -ti tcp:8895 | xargs killで HiDream kill → 16 GiB 解放 - Stage C: LTX-2 + TTS + Ditto で peak ~32 GiB
Stage 切替時に明示 kill するだけで全工程が 1 枚に乗る。
イテレーションループ (cache 戦略)
「全部刷り直し」ではなく 部分再生成 が高速化の鍵:
# 1 beat だけ画像 regen (HiDream のみ)
python -m storyboard.visual --plan ... --out ... --only-beat 7 --steps 50 --resolution 2048
# 部分 video regen (TTS + LTX-2)
python -m storyboard.video --dir ... --regen-beats 5,6,7 --skip-review
# 字幕や Hook タイトル位置だけ調整
rm _video_work/clip_00_hook.mp4 _video_work/subs_irodori.srt
python -m storyboard.video --dir ... --regen-beats none --skip-review # ~30 秒
キャッシュ階層:
- HiDream beat 画像 (
beat_NN_<type>.png) —--only-beatで個別 80 秒 - A2V / I2V clip (
clip_NN_*.mp4) — beat type / speaker / line 変更で invalid - Hook 完成版 (
clip_00_hook.mp4) — タイトル位置だけ変えたい時はこれだけ削除 (LTX-2 I2V の重いhook_silent.mp4は再利用) - 字幕 SRT — 毎回再生成 (10 秒)
タイトル位置 / 字幕 style / Hook 内文言調整は 30 秒で焼き直せる。LTX-2 I2V の 100 秒部分はそのまま使える。
Kotonia でどう活きているか
このパイプラインで生成した動画は SNS distribution (TikTok / YouTube Shorts / IG Reels) 向けで、Kotonia (kotonia.ai) のアテンション獲得 → 課金導線の上流を担う。
技術的には、/studio/ (HiDream の画像生成) と同じスタックを動画方向に拡張したもの。将来的には /video-studio/ として Web UI から 1-click で同じパイプラインを呼べる構成を想定 (現状は CLI のみ)。
関連記事 / 試したい人へ
- HiDream-O1-Image の 5 機能を 1 GPU で常駐させる構成 — Studio (
/studio/) のバックエンド設計 - LTX-2 を 95GB GPU 1 枚に詰める fp8-cast 量子化 — Stage C の動画生成基盤
- 言語学習ショート動画を Claude Code で再現してみた — 6-beat 「マンゴー事件」フォーマットの先行実装
- 試したい人は /studio/ で画像生成側を 1-click で試せる (動画化 CLI は現状 self-host のみ)