TL;DR
一行创意由 Gemma 4 31B 展开为10-beat结构,HiDream 生成11张2048²图像,LTX-2 A2V/I2V 生成11个片段,Irodori-TTS 生成台词和男声旁白,ffmpeg 自动烧录字幕和Hook标题覆盖。实测25-30分钟生成40秒竖屏 (512×768) 视频。全部在单张本地GPU (96 GB Blackwell) 上运行,API成本为零。
完成版 (已发布):
本文目标读者
面向希望在本地GPU上量产AI视频喜剧的个人开发者。本文关注点并非单个模型,而是 “将多个模型连接成一条流水线并投入运营的设计”。
做了什么
将面向Z世代的黑暗喜剧讽刺(以 No Means Yes? 这一性同意困境为素材的海外短视频格式)从一行创意自动化到40秒视频。
完成效果:
- Hook (0-5秒): 美女特写 + 旁白“回应了‘你是男孩子吧’的他,其命运——” + 大标题“No Means Yes?”
- 正片 (5-37秒): 电影院约会 → “可以亲你吗?”→ “不要…不行啦…”→ 沮丧 → “再主动一点嘛。你是男孩子吧?”→ 原来如此 → 接吻
- 结局 (37-40秒): 法庭“主文:被告人因不同意性交等罪,判处有期徒刑3年”+ 法槌“咚!”+ 牢房落泪
before / after:
| 传统制作方式 | 本流水线 | |
|---|---|---|
| 创意 → 发布视频 | 2-3天 (手动编辑) | 25-30分钟 (全自动) |
| API 成本 | DALL-E + 视频生成每条数百日元 | 0元 (仅电费) |
| 字幕 | 手动编写SRT | 按句号自动分割并烧录 |
| Hook | 单独拍摄 | 集成到流水线中 |
架构
[阶段 A] Gemma 4 31B (vllm, 端口 8894) → plan.json (10 beats + hook)
[阶段 B] HiDream-O1-Image (端口 8895) → 11 张 2048² 图像
+ Gemma 4 31B 多模态视觉评判 (--judge --max-retries 2)
[阶段 C] Irodori-TTS (端口 8880) + LTX-2 A2V (端口 8892) / I2V (端口 8891)
→ 11 个片段 + Hook 片段 → ffmpeg 拼接 → 字幕烧录
实现集中在 llm_server/storyboard/ 目录下 (pipeline.py / visual.py / judge.py / video.py / render.py / run.py)。
10-beat consent_dilemma 格式
在 prompts.py 的 CONSENT_DILEMMA_SYSTEM 中作为系统提示固定:
| # | 类型 | 说话者 | 渲染器 | 内容 |
|---|---|---|---|---|
| 1 | 挑衅 | b | LTX-2 A2V | 意味深长的邀请 |
| 2 | 询问 | a | LTX-2 A2V | 认真的同意确认 |
| 3 | 拒绝 | b | LTX-2 A2V | 含蓄的拒绝 (如“不要…不行啦…”的模糊形式) |
| 4 | 沮丧 | a (沉默) | LTX-2 I2V | 沮丧 |
| 5 | 煤气灯效应 | b | LTX-2 A2V | 矛盾的引导性发言 |
| 6 | 停顿 | a (沉默) | LTX-2 I2V | 短暂的醒悟 |
| 7 | 接吻 | a (沉默) | LTX-2 I2V | 男方亲吻的瞬间 |
| 8 | 判决 | 法官 | LTX-2 A2V | 快速宣判 |
| 9 | 法槌音效 | 法官 | LTX-2 I2V (保留音频) | 法槌 + AI 自动“咚!”音 |
| 10 | 牢房 | a (沉默) | LTX-2 I2V | 牢房落泪 |
关键在于 3 次反转:
- 拒绝不采用平淡的“不行”: 使用“不要…不行啦…”并拉长尾音,营造出表演性的“No that doesn't mean No”的微妙感。这是后续煤气灯效应矛盾成立的前提。
- 煤气灯效应后不立即接吻: 插入“停顿”(原来如此) 1.5秒。为了节奏和情感曲线。
- 判决与牢房的两段式结局: 仅判决会显得突兀。加上牢房哭泣的画面,能让观众切实感受到“他真的被判有罪了”。
Hook 的设计 (TikTok 前3秒问题)
竖屏短视频的前3秒决定了流失率。在正片10 beats之前放置Hook片段:
"hook": {
"title_overlay": "No Means Yes?",
"narrator_line": "回应了‘你是男孩子吧’的他,其命运——",
"image_prompt": "ultra close-up of beautiful Japanese woman, half-lidded eyes, ...",
"duration_sec": 3.5
}
实现中的两个陷阱:
陷阱 1: 旁白 TTS 时长超过 duration_sec 会导致音频被截断。“其命运”的“运”字处发生了音频截断。对策: 先生成 TTS → 用 ffprobe 实测时长 → 将 max(plan_duration, narrator + 0.6) 传递给 I2V 时长。
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像素) 将其置于最顶部。
字幕烧录 (适配静音观看)
为了应对在电车上关掉声音观看的用户,并提高跨平台可靠性,采用内嵌字幕。
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 = 底部居中。MarginV=60 确保与底部边缘保持距离。
长句分割: 单个 beat 内若有超过30个字符的行会与脸部重叠。使用 _split_subtitle 按句号 。.!? 分割句子 → 使用贪心算法分割成28字符以下的块 → 按 beat 时长均匀分配:
输入:
用语言确认什么的,一点也不浪漫嘛。喂,再主动一点啊。你是男孩子吧?
输出 (将一个8.9秒的 beat 分割为2个时间块):
| 时间 | 字幕 |
|---|---|
| 15.16-19.63秒 | 用语言确认什么的,一点也不浪漫嘛。 |
| 19.63-24.10秒 | 喂,再主动一点啊。你是男孩子吧? |
使用 LTX-2 I2V 生成音效 (法槌音效)
LTX-2 distilled 的 I2V 输出 mp4 会 自动包含 AI 生成的音频 (环境音/音效)。除非使用 ffmpeg -map 0:v:0 -map 1:a:0 明确丢弃,否则音频会随提示词一起生成。
将其用作音效生成器:
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 = 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 标志,创建了一条在 ffmpeg 重新编码时不丢弃音频的路径。
法槌音效的提示词:
"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."
使用法官的最后一帧 + 法槌提示词即可生成“咚!”声。如果失败,则设计为回退到《逆转裁判》等音效。
陷阱列表
开发过程中踩到的主要5个陷阱:
1. Codex CLI 在 vLLM 0.20.2 上挂起
使用 codex exec -p gemma4 发送系统提示 + 创意时,在 /v1/responses 的握手阶段以0% CPU 状态挂起超过20分钟。由于子进程输出通过 tail -200 管道传输,初始的 stderr 也被阻塞。
规避: 放弃使用 codex,直接使用 urllib.request 调用 /v1/chat/completions。使用 response_format={"type":"json_object"} 强制 JSON 输出。25秒内完成 plan.json 生成。
2. HiDream 无法消除电影屏幕
即使在 setting_prompt 中明确指定 "The movie screen is BEHIND the camera and NOT VISIBLE in frame",在2048/50步下屏幕仍会留在背景中。
规避: 使用 t2i 生成 scene_base → 将同一图像输入 I2I 编辑,并提示“将屏幕替换为暗墙,角色位置不变”→ 一次消除。采用低分辨率 → I2I 修复 → 提高分辨率并重新生成所有 beat 的两步法。
3. HiDream 将嘴唇对嘴唇的亲吻变成脸颊亲吻
在标准放大器下,HiDream 倾向于将亲吻解释为脸颊亲吻。必须使用 "CRITICAL: their LIPS meet directly — mouth-to-mouth contact at the CENTER of the frame. NOT a cheek kiss" 级别的强指令。在 _beat_edit_prompt 中为亲吻 beat 准备了专用的提前返回块。
4. CAST / CROP_BOX / SPEAKER_A2V_PROMPT 硬编码为两人
CAST 字典 / CROP_BOX 字典 / SPEAKER_A2V_PROMPT 字典仅知道 a (健太) 和 b (美咲),分布在3个位置。若要添加法官/旁白,需要同时更新3个字典 (通过 KeyError 发现)。对于具有 setting_override 的 beat,render_speech_beat_ltx_a2v 也添加了分支,使其从 beat 自身的图像而非 scene_base 进行肖像裁剪。
5. Gemma 4 多模态评判器误报过多
在 storyboard/judge.py 中实现了视觉评判器,将 beat 图像 + 预期表情发送给 Gemma 4 31B 进行 YES/NO 判定。确实能捕获“手指数量异常”、“张嘴对话姿势”、“场景几何不匹配”等 明显 失败,但在“微妙害羞表情”等模糊领域会频繁返回 FAIL。
实用策略: 设置 max-retries 为2,若连续3次 FAIL 则接受并继续。切换到前沿审查器 (Gemini 3.1 Pro) 的阈值尚未自动化。
VRAM 共存设计
96 GB Blackwell Max-Q 的分配:
| 进程 | 空闲 (GiB) | 峰值 (GiB) |
|---|---|---|
| Gemma 4 31B (NVFP4) | 38 | 38 |
| HiDream-O1-Image | 16 | 33 |
| TTS 服务器 | 3 | 3 |
| Ditto | 3 | 3 |
| LTX-2 A2V (冷启动 fp8-cast) | 1 | 24 |
| LTX-2 T2V/I2V (冷启动) | 1 | 8 |
全部峰值合计 109 GiB → 内存不足。运行流程:
- 阶段 A: Gemma 31B + HiDream 空闲,峰值约 62 GiB
- 带评判的阶段 B: Gemma 31B + HiDream 峰值约 73 GiB
- 定稿前
pkill -f "vllm.*gemma"杀死 Gemma → 释放 38 GiB - 阶段 B 定稿 (2048/50): HiDream 峰值约 33 GiB
- 阶段 C 前
lsof -ti tcp:8895 | xargs kill杀死 HiDream → 释放 16 GiB - 阶段 C: LTX-2 + TTS + Ditto 峰值约 32 GiB
只需在阶段切换时显式杀死进程,即可将所有步骤容纳在一张卡上。
迭代循环 (缓存策略)
部分重新生成而非“全部重做”是加速的关键:
# 仅重新生成1个 beat 的图像 (仅 HiDream)
python -m storyboard.visual --plan ... --out ... --only-beat 7 --steps 50 --resolution 2048
# 部分视频重新生成 (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_NN_*.mp4) — 当 beat 类型/说话者/台词变更时失效 - Hook 完成版 (
clip_00_hook.mp4) — 仅想更改标题位置时删除此文件即可 (可重用 LTX-2 I2V 的hook_silent.mp4) - 字幕 SRT — 每次重新生成 (10秒)
标题位置/字幕样式/Hook 内文案调整可在30秒内重新烧录。LTX-2 I2V 的100秒部分可直接复用。
在 Kotonia 中的运用
此流水线生成的视频面向社交媒体分发 (TikTok / YouTube Shorts / IG Reels),是 Kotonia (kotonia.ai) 获取注意力并引导付费的上游环节。
技术上,这是对 /studio/ (HiDream 图像生成) 相同栈向视频方向的扩展。未来计划作为 /video-studio/,通过 Web UI 一键调用相同的流水线 (目前仅 CLI)。
相关文章 / 给想尝试的人
- HiDream-O1-Image 的5种功能在1张GPU上常驻的配置 — Studio (
/studio/) 的后端设计 - 将 LTX-2 塞入95GB单张GPU的 fp8-cast 量化 — 阶段 C 的视频生成基础
- 用 Claude Code 复现语言学习短视频 — 6-beat “芒果事件”格式的先行实现
- 想尝试的人可以在 /studio/ 一键试用图像生成端 (视频化 CLI 目前仅限自托管)
