1 行アイデアを 40 秒コメディ動画にする 10-beat パイプライン

著者 #ai#動画生成#ffmpeg#ltx#hidream

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 コストゼロ。

完成版 (公開済み):

@youtube

この記事の対象読者

ローカル 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.pyCONSENT_DILEMMA_SYSTEM で system prompt として固定:

#typespeakerrenderer内容
1provocationbLTX-2 A2V意味深な誘い
2askaLTX-2 A2V真面目な同意確認
3refusalbLTX-2 A2V控えめな拒否 (「やだ…ダメだよぉ…」のような曖昧形)
4dejectiona (silent)LTX-2 I2V落胆
5gaslightbLTX-2 A2V矛盾した誘導発言
6pausea (silent)LTX-2 I2V短い realization
7kissa (silent)LTX-2 I2V男からのキス瞬間
8verdictjudgeLTX-2 A2V早口判決
9gavel_sejudgeLTX-2 I2V (keep_audio)ハンマー + AI 自動「コン!」音
10jaila (silent)LTX-2 I2V牢屋で涙

ポイントは 3 段階のひっくり返し:

  1. refusal を flat な「ダメ」にしない: 「やだ…ダメだよぉ…」と語尾を伸ばし、performative な「No that doesn't mean No」 のニュアンスを出す。これが後の gaslight の矛盾を成立させる前提。
  2. gaslight 直後に kiss を置かない: 「pause」(なるほどね) で 1.5 秒挟む。テンポと感情曲線のため。
  3. 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_clipkeep_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)3838
HiDream-O1-Image1633
TTS server33
Ditto33
LTX-2 A2V (cold-start fp8-cast)124
LTX-2 T2V/I2V (cold-start)18

全部 peak で 109 GiB → OOM。運用フロー:

  1. Stage A: Gemma 31B + HiDream idle で peak ~62 GiB
  2. Stage B with judge: Gemma 31B + HiDream peak で ~73 GiB
  3. 清書前に pkill -f "vllm.*gemma" で Gemma kill → 38 GiB 解放
  4. Stage B 清書 (2048/50): HiDream peak ~33 GiB
  5. Stage C 前に lsof -ti tcp:8895 | xargs kill で HiDream kill → 16 GiB 解放
  6. 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 のみ)。

関連記事 / 試したい人へ