前言
起因是某天在 X 上刷到了 Pingo(语言学习 AI 应用)的短视频。一位学习日语的西方女性想说“我吃了芒果”,但因为浊点缺失,发音变成了“我吃了玛◯科”,AI 用面无表情的语气接话,女性陷入绝望。特定的音韵事故 + AI 的预期落空 + reaction shot 的反差完美地发挥了作用,我认为这可以作为“喜剧视频自动生成流水线”的一个很好的基准。
需求如下:
- 从一行创意文本生成竖屏喜剧
- 迭代周期以分钟计
- 成本几乎只有电费,即 API 调用要有限
- 质量达到可发布水平,即可以直接上传到 YouTube Shorts
结论是,我做到了。完成版在这里(已发布):
在开发过程中发现,“将视频审查等多模态编辑判断交给前沿模型,将高计算量任务放在本地”的混合架构,性价比极高。本文介绍该架构以及途中遇到的具体 bug 记录。
成品架构
[1 行创意文本]
↓
Gemini 3.1 Pro Preview (编排器)
↓ system prompt 强制 4-6 场景 + 2 角色固定阵容 + 竖屏 9:16
plan.json {scenes: [{speaker, script, tts_language, ltx_prompt, renderer}, ...]}
↓
XTTS (本地, port 8880) 生成每个场景的音频
↓ scene_NN.wav
渲染器分配:
├─ Ditto-TalkingHead (本地, port 8881): 普通对话 ~1-2s/场景
└─ LTX-2 A2V (本地, port 8892): 仅 reaction_only 场景 ~100s
↓ scene_NN.mp4
ffmpeg concat (libx264 + aac, 512x768 竖屏) → final.mp4
↓
Gemini 3.1 Pro Preview (审查器)
↓ 多模态评估视频 + plan 摘要
review.md (技术 / 完整性 / 质量 / 改进点)
要点:
- 繁重计算全部本地 — TTS / A2V 渲染器 / 轻量推理在本地 GPU (RTX PRO 6000 Blackwell) 上完成
- 判断类任务交给 Gemini — 只有编排器(场景设计 + 剧本)和审查器(视频编辑评估)使用前沿模型
- 本地 LLM (Gemma 4 E4B) 保留作为每个场景的技术预筛选 — 仅过滤“明显损坏”的廉价过滤器
VRAM 使用:本地 LLM (Gemma 4 E4B + 31B) 原本通过其他路径占用约 60GB,但将审查器/编排器委托给 Gemini 后,即使停止这些模型也能运行,VRAM 大幅释放。
为什么仅靠本地 LLM 无法达到目标
最初全部使用本地模型(Gemma 4 31B NVFP4 作为编排器,Gemma 4 E4B 多模态作为审查器)。可以跑通,结构看起来像那么回事,但达不到可发布质量。有两个原因。
(1) Gemma 4 31B (Google 系) 的安全微调模糊了笑点
参考的短视频喜剧核心在于“AI 明确指出事故并用面无表情的语气接话”这个节拍。具体来说,就是 AI 角色冷静地说“你刚才说了 X,我喜欢那个 X”。这个效果之所以有效,是因为它打破了“和蔼导师”的预期,一旦模糊化就全完了。
将相同的 system prompt + idea 输入本地 Gemma 4 31B,每次都会变成这样:
"不错。我饿的时候也喜欢那个。"
“饿的时候喜欢”这个节拍还在,但**“你刚才说了 X”这个明确指出的节拍(= 最具越界性的节拍)**消失了。Google 模型似乎经过了强烈训练,会避免在“不安全”的上下文中具体指名,通过调整提示词可以稍微引出一些,但无法稳定。
将相同的 system prompt + idea 输入 Gemini 3.1 Pro Preview(并且使用 safetySettings: BLOCK_NONE 最小化安全防护):
"原来如此。我是 AI 所以不能吃玛科,但我支持你。"
事故的指名 + 以 AI 自身立场面无表情地接话,两个节拍都具备了。
即使是同一 Google 模型系列,前沿模型的护栏也稍薄一些,这一点在 X 等平台上也有讨论。至少对于像这次这样“在喜剧语境中必要的越界”来说,Gemini 写得更直接。
(2) Gemma 4 E4B (4B 级, 多模态) 作为审查器不够敏感
审查器方面的问题更严重。E4B 可以对每个场景给出“OK / NG”的二元判断,但对所有场景都 rubber-stamp 判定为 OK。明显唇形同步损坏的场景也判“OK”,音频中途中断的场景也判“OK”。
让 Gemini 3.1 Pro Preview 审查同一个最终视频,它会返回这样的编辑级指摘:
Critical failure. The TTS/pipeline clearly censored the output, cutting off at "I ate p-" and entirely dropping the intended transgressive punchline. This destroys the "deadpan AI saying unhinged things" comedic archetype.
Top 3 fixes:
- Bypass TTS censorship: Force the pipeline to render the full intended script for Scene 5 ...
- Adjust comedic timing: Add a 0.5-second pause between Scene 4 and Scene 5 ...
- Verify Voice/Visual Match ...
它能指出“punchline 被截断”“需要 0.5 秒的停顿”“语音与视觉的一致性”等节奏/演出粒度的问题。这就是编辑信号分辨率的差异。
踩坑记:三次将 Gemini 的“截断”指摘误判为幻觉
接下来是丢人的事。Gemini 审查器多次指出“scene 5 中途被截断,在 'I ate p-' 处中断”。我(实现者)用 Whisper 单独对音频文件进行了转写验证:
$ whisper scene_04.wav --language en
"Wait, ha ha ha, you just said manco-o-tabeta. That literally means I ate
pussy honestly when I'm hungry, same."
全文存在。我判定“Gemini 产生了幻觉”,连续三次驳回了 Gemini 的指摘。
结果第三次 Gemini 仍然固执地指出“still truncated at 'I ate p-'”,于是我尝试用 ffprobe 检查最终的 mp4:
scene_04.mp4:
video duration = 8.000000s
audio duration = 7.979000s ← 原始 WAV 应该是 10.30s
音频在 8 秒处被截断了。
原因:流水线中的 MAX_DURATION_PER_SCENE = 8.0 这个隐式上限限制了 ditto 渲染器的 num_frames 为 8s,ffmpeg 的 -shortest 参数将视频对齐到 8s,音频也随之被截断。Whisper 只查看了截断前的 WAV 文件,无法检测到问题,而 Gemini 观看了最终的 mp4,所以能准确检测到。
前沿模型审查器“看似幻觉的指摘”最好养成老老实实验证的习惯,这是本次最大的教训。信号不是猜测。
修复很简单,删除 MAX_DURATION_PER_SCENE,直接使用音频原始长度。这样 scene 5 的 punchline 得以完整呈现,Gemini 评价为“The transgressive bite is perfect”,首次达到了可发布状态。
作为子代理的前沿模型 — Token 经济
这种模式之所以有效,是因为子代理端 (Gemini) 使用全新的上下文运行。具体来说:
- 主代理 (Claude Code) 的上下文:包含整个开发过程的日志、命令历史、工具输出、过去的试错等。容易膨胀到数十万 token
- 子代理 (Gemini) 的上下文:仅包含一个视频 (2-3MB base64) + plan 摘要 (约 1500 token) + 评估指令 (约 500 token)。每次都是全新的
这样做的好处是,子代理的工作不会累积到主代理的 token 消耗中。即使对一个视频迭代 10 次,主代理的上下文中也只包含“调用了 Gemini”这个事实及其简洁的返回值。实际观看和评估视频的负载封闭在 Gemini API 端。
成本估算 (Gemini 3.1 Pro Preview 费率, 截至 2026-05):
| 项目 | Token | 单价 | 合计 |
|---|---|---|---|
| 输入 (视频 + plan + 指令) | ~2500 | $1.25/M | $0.0031 |
| 输出 (审查 markdown) | ~450 | $10/M | $0.0045 |
| 1 次审查 | $0.0076 |
每个视频首次 1 次审查 + 差异迭代 3-5 次 ≈ $0.03-0.05。即使每天制作 5-10 个,每月也控制在 $10-20。对于为了制作视频而使用前沿模型来说,这个阈值非常低。
编排器端也类似(没有视频输入,只有文本,所以更便宜)。
差异迭代 — --regen-scenes
要达到可发布质量,需要高速循环“看视频 → 只修复问题部分 → 再看”。一次就出完成版是不可能的。
因此,在流水线端添加了只重新运行特定场景的 TTS + 渲染的路径。
# 普通生成
pipeline_multi.py --idea "..." --out outputs/run1
# 只重做 scene 6 (先直接编辑 plan.json 替换 script)
pipeline_multi.py --out outputs/run1 --regen-scenes 5
# 批量重新生成 scene 0, 2, 5
pipeline_multi.py --out outputs/run1 --regen-scenes 0,2,5
# 仅重新拼接现有 scene_NN.mp4 (用于 cherry-pick 后的重组)
pipeline_multi.py --out outputs/run1 --concat-only
--regen-scenes 指定的场景以外的 scene_NN.mp4 直接复用,仅重新生成指定索引的场景,然后重新拼接和审查。完全重新生成 60 秒 → 差异迭代 30 秒完成一轮。
Gemini 审查器的指摘 → 精确定位编辑 plan.json 中对应场景的 script 或 ltx_prompt → 等待 30 秒 → 查看结果,这个循环可以以分钟为单位运行,让我能够专注于文本编辑和视频质量改进的认知负荷。
代码片段
Gemini Pro API 调用 (多模态视频审查)
import httpx, base64
GEMINI_MODEL = "gemini-3.1-pro-preview"
GEMINI_API = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent"
def review_final(final_path, plan):
vid_b64 = base64.b64encode(final_path.read_bytes()).decode()
scene_summary = "\n".join(
f" scene {i+1}: speaker={s['speaker']}, lang={s.get('tts_language','ja')}, "
f"script={s['script']!r}"
for i, s in enumerate(plan["scenes"])
)
payload = {
"contents": [{"parts": [
{"inline_data": {"mime_type": "video/mp4", "data": vid_b64}},
{"text": REVIEW_PROMPT + f"\n\nScene plan:\n{scene_summary}"},
]}],
"generationConfig": {
"temperature": 0.3,
"maxOutputTokens": 8192,
# 3.x Pro 是思考模型:maxOutputTokens 包含思考部分
# 为确保输出空间,明确指定 thinking budget
"thinkingConfig": {"thinkingBudget": 1024},
},
# 喜剧语境,最小化安全过滤器
"safetySettings": [
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
],
}
r = httpx.post(
GEMINI_API,
headers={"x-goog-api-key": GOOGLE_API_KEY, "Content-Type": "application/json"},
json=payload,
timeout=120.0,
)
return r.json()["candidates"][0]["content"]["parts"][0]["text"]
如果不设置 thinkingConfig.thinkingBudget,Gemini 3.x Pro 的内部思考会消耗输出空间,导致响应在大约 40 token 处中断。使用 Gemini 3.x Pro 系列时的必要设置。
TTS 输出的质量检查 (STT 相似度 + 静音间隔重试)
XTTS 内部使用采样,因此即使脚本相同,每次执行的结果也会不同。有时会在中间插入较长的静音区间,或者发音失败。在 TTS 完成后使用 Whisper 进行转写,检查与预期脚本的相似度,如果不合格则重试:
import difflib
def _norm(s):
return re.sub(r"[\s。、,.!?「」'\"…—–\-:;()()]", "", s).lower()
def _script_similarity(expected, actual):
return difflib.SequenceMatcher(None, _norm(expected), _norm(actual)).ratio()
def synthesize_scene(scene, out_dir, idx, fallback_language):
lang = scene.get("tts_language", fallback_language)
expected = scene["script"]
best = None
for attempt in range(1, TTS_MAX_RETRIES + 1):
audio, sr = _xtts_once(scene, fallback_language)
gap = _longest_internal_gap_sec(audio, sr)
transcript = _stt(audio, sr, lang)
sim = _script_similarity(expected, transcript)
if best is None or _score(gap, sim) > _score(best[2], best[3]):
best = (audio, sr, gap, sim, transcript)
if gap <= 0.9 and sim >= 0.5:
break
print(f"⚠ gap={gap:.2f}s sim={sim:.2f}, retrying ({attempt})")
# 即使 3 次重试后仍未达到阈值,采用“最佳”重采样
audio, sr, gap, sim, transcript = best
sf.write(out_dir / f"scene_{idx:02d}.wav", audio, sr, subtype="PCM_16")
仅此一项,就能大幅减少 XTTS 的非确定性质量波动被带入视频的情况。
发展可能性
这种模式 — “将判断密集型部分作为子代理交给前沿模型,繁重计算放在本地” — 不仅适用于视频流水线:
- 大规模搜索排名:将 100 个网站的搜索结果交给前沿模型进行编辑评估,只将前 10 名返回给主代理。避免主代理的上下文被搜索结果的噪声污染
- 长文编辑审查:让前沿模型在编辑层面阅读 PR / 设计文档 / 规格书,而不是主代理。主代理只获取摘要
- 多语言 QA:为每种语言分配最优模型作为子代理,主代理只保留所有语言通用的判断逻辑
共同的思想是有意识地划分“应该放在上下文中”还是“应该通过 API 调用完成”。前沿模型的编辑信号在成本方面具有压倒性的性价比。
视频流水线方面,接下来将进行喜剧格式的通用化(分屏、3+ 角色、其他类型)和量产验证。
总结
- 通过本地 GPU + Gemini 3.1 Pro Preview 的混合架构,构建了从一行创意文本在 60 秒内生成可发布级喜剧视频的基础设施
- 仅靠本地模型在 (1) 安全微调模糊笑点和 (2) 审查器无法提供编辑信号这两点上无法达到可发布质量。通过将前沿模型作为子代理解决了这两个问题
- 要老老实实接受前沿模型审查器的指摘。仅用 Whisper 单独检查 WAV 文件会错过最终 mp4 的音频截断问题
- 子代理的 token 经济不会膨胀主代理的上下文,每个视频仅需 $0.03-0.05
- 通过差异迭代 (
--regen-scenes) 实现 30 秒循环,Gemini 指摘 → 修正 → 重新评估的周期可以以分钟为单位运行
完成的视频 (再次发布):
本地实现在 llm_server/pipeline_multi.py 中。作为内部资料,开发过程中的详细发现已另行积累在 docs/MULTI_SCENE_COMEDY_FINDINGS_2026-05-12.md 中。
