前阵子我搭了一条能自动出片的短视频生产线,核心思路就是用 LLM 把脚本、分镜、字幕、配音、剪辑这些环节串成一条“可编程的管线”。这篇文章就是来完整拆这套方案的:思路是什么、结构怎么设计、代码骨架长什么样、以及我在实际运行中踩过的一堆坑。目标是让有 Python 基础、想用大模型做视频自动化的人,看完能照着自己搭一套。
先说清楚它到底能干什么。这套管线接收一个很粗糙的输入,比如“帮我做一条讲 AI 绘画入门的短视频”,然后自动完成以下工作:生成解说词、把解说词拆成分镜列表、为每个分镜匹配视频素材、生成配音和字幕、最后拼接出一条带背景音乐和转场的成片。整个过程完全由代码驱动,LLM 只负责中间那些需要“语义理解”的部分,视频切拼这种程序逻辑则交给脚本处理。听起来很玄,但落地之后你会发现,真正卡人的地方其实就那么几个点,这篇我都会讲到。
如果你已经试过用 ChatGPT 写脚本、再用剪映手动剪片,那这套管线的价值就很好理解了。它本质上是在做“无人工干预的内容生产流水线”,适合做自媒体矩阵、批量科普视频、或者有大量库存素材想快速消化成视频的人。下面我按自己的实现顺序来拆解。
1. 项目整体设计与思路拆解
1.1 先想清楚:为什么是“可编程”,而不是“问AI要一条视频”
很多人听到“用 LLM 做短视频”,第一反应是让 AI 直接生成视频成片。但现阶段真正稳定好用的方式,不是让 LLM 吐出视频文件,而是让它生成“对视频制作过程的完整描述”,再由程序去执行。理解这一点,是整个项目成立的前提。
我把 LLM 定位成一个“编导”,它输出的是分镜脚本、画面描述、配音文案和字幕文本这些结构化的中间产品。真正把画面剪出来、音频合上去的,是 MoviePy、FFmpeg 这类视频处理工具。为什么这么设计?因为 LLM 擅长的是语言和语义,视频素材的切割、拼接、缩放、编码是确定性的算法逻辑,混在一起只会让两边都做不好。
“可编程”在这个项目里有两层意思。第一层是流程可编排,管线里的每一步都是独立函数,你可以只跑脚本生成、不跑渲染,也可以在渲染之前手动替换某个分镜素材。第二层是 LLM 的决策逻辑可控,通过 Prompt 和参数约束让它的输出严格符合预定 schema,而不是自由发挥。这两层控制住之后,整个系统才谈得上稳定。
1.2 管线整体架构:五个模块,一条数据流
我最终跑通的架构是五个模块串联:
- 输入模块:接收一个主题词、一篇文章链接、甚至一篇纯文本稿件,统一转换成“选题描述”。
- 脚本模块:LLM 根据选题生成解说词,这是整条视频的“语言层”。
- 分镜模块:LLM 把解说词切成 N 个分镜,每个分镜里包含旁白文本、字幕文本、画面描述、时长建议、转场类型。
- 素材匹配模块:根据画面描述在本地素材库里检索视频片段和图片,没有合适素材时用占位背景兜底。
- 渲染模块:把每个分镜的素材按时间轴拼起来,叠加字幕、配音、背景音乐,导出成 MP4。
每个模块的输入输出都是标准 Python 数据结构,主要是 dict 和 list,中间结果我会落一份 JSON 到磁盘。这么做的原因很朴素:方便调试、方便断点续跑,调用 LLM 是有成本的,如果渲染阶段挂了,前面生成的东西不应该浪费。
这套架构最大的优势是“可插拔”。想换 TTS 服务、想换素材免费站、想改成数字人驱动,都只影响对应的一个模块,管线主体不用动。这也是我坚持不把逻辑写成一坨的原因。
2. 核心细节解析与实操要点
2.1 脚本生成不是写作文,是出结构化数据
很多第一次做这类项目的人会在第一步就翻车,因为他们让 LLM“生成一段脚本”。结果模型输出一篇带标题、分段、加粗文字的 Markdown,程序根本没法直接消费。
我的做法是:让 LLM 直接输出 JSON。系统提示词里写清楚“你是短视频编导,你的输出必须是一个符合指定 JSON Schema 的分镜数组,禁止输出任何解释性文字”。然后给一个 few-shot 示例,比如:
[ { "scene_id": 1, "narration": "很多人觉得 AI 绘画很难,其实入门只需要三样东西。", "subtitle": "AI 绘画入门只需要三样东西", "visual_prompt": "深夜书桌上放着数位屏和手绘笔,屏幕亮着绘画软件界面", "duration_sec": 4, "transition": "dissolve" } ]关键在于,所有字段都必须在系统提示词里提前定义好,并且给出取值范围。比如transition我限定成cut、dissolve、slide_left三种,因为渲染模块只实现了这三种转场,LLM 一旦输出一个fade_zoom,后面就报错了。宁可减少自由度,也不要让下游不可控。
温度参数这里我实测下来:生成脚本创意部分用temperature=0.9,拆分子镜时用temperature=0.3。原因是分镜拆解是结构化任务,越冷越稳定;脚本创意需要发散,但一旦发散过头,分镜阶段又会很痛苦。这个问题我在第 4 节还会专门讲。
2.2 分镜指令设计:让每个场景可执行
分镜是整个管线的核心数据结构,它必须同时被后期环节理解。我建议字段不要贪多,够用就行,以下这几个字段是经过实际验证的最小集合:
| 字段 | 类型 | 说明 |
|---|---|---|
scene_id | int | 分镜序号,渲染时按此排序 |
narration | string | 该段旁白全文,用于 TTS 配音 |
subtitle | string | 屏幕上显示的字幕,通常比旁白短 |
visual_prompt | string | 画面描述,素材匹配模块的查询词 |
duration_sec | float | 该分镜时长,配音长度不够时自动延长 |
transition | string | 上一镜到本镜的转场方式 |
设计这些字段时的核心原则是“下游能直接执行”。narration可以直接交给 TTS,duration_sec可以作为画面长度的参考值,visual_prompt是素材检索的 query。字段之间要有明确的对应关系,不要让渲染模块再去猜语义。
我当时踩过一个坑:字幕字段让 LLM 自由编写,它经常输出和旁白一模一样的长句,结果屏幕上全是密密麻麻的字。后来我在示例里专门写了一条“字幕是旁白的提炼,单行最多 12 个汉字”,并在校验逻辑里强制长度超过就截断,问题才解决。这类细节不亲身跑一遍真的很难意识到。
2.3 素材匹配:LLM 和素材库之间的“对齐层”
这是整个项目里最容易被低估的模块。一开始我天真地以为,给 LLM 一段画面描述,然后拿这段文字去 Pexels 搜素材就行了。结果搜出来的素材和文案气质经常对不上,尤其是一些抽象描述,比如“希望感”或者“数据流动”,基本搜不到匹配内容。
后来我换了个思路,不再让 LLM 自由发挥画面描述,而是在 Prompt 里注入素材库的真实标签列表。比如素材库里有哪些镜头、什么场景、什么样的风格,LLM 只能在这些候选标签里做组合和选择。这一步把“凭空想象画面”变成了“基于可用素材做编排”,匹配成功率显著提升。
具体实现上,我用了一个很轻量级的方案:素材入库时人工打标签,存成一个 JSON 映射,文件名对应标签数组。匹配函数就是简单的标签交集排序,LLM 输出的visual_prompt会先做分词,再和素材标签做匹配。如果你有更多工程量,可以换成 CLIP 向量检索,但我个人体验是,对于几百条素材的小库,标签匹配已经够用,而且可解释性强。这个取舍后面还会再聊。
3. 实操过程与核心环节实现
3.1 管线代码骨架:能跑通的最简版本
完整代码太长了,这里给一个浓缩版骨架,基本能反映各个模块之间的数据流转。我平时用的调用后端是 OpenAI 兼容接口,本地部署的 Ollama 也能用这套代码,只需要把base_url和model换一下。
import json import subprocess from pathlib import Path CACHE_DIR = Path("./cache") CACHE_DIR.mkdir(exist_ok=True) def call_llm(user_prompt: str, system_prompt: str, temperature: float = 0.7) -> str: # 这里以 OpenAI 接口为例,接入本地模型同理 from openai import OpenAI client = OpenAI( base_url="http://localhost:11434/v1", # 本地 Ollama 示例,换成其他服务也可 api_key="EMPTY", ) resp = client.chat.completions.create( model="qwen2.5:14b", # 或 gpt-4o-mini 等 messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], temperature=temperature, response_format={"type": "json_object"}, ) return resp.choices[0].message.content def generate_storyboard(topic: str) -> list[dict]: system = ( "你是短视频编导。将用户给出的主题拆解为 5-8 个分镜。" "必须输出 JSON 对象,格式为 {\"scenes\": [...]}。" "每个分镜包含 scene_id, narration, subtitle, visual_prompt, duration_sec, transition。" "transition 只能取 cut/dissolve/slide_left。" ) user = f"主题:{topic}\n请输出分镜 JSON。" raw = call_llm(user, system, temperature=0.3) data = json.loads(raw) return data["scenes"] def generate_script(topic: str) -> str: system = "你是短视频文案写手,写一段 200 字左右的口播脚本,语气自然,不要 Markdown 标题。" raw = call_llm(topic, system, temperature=0.9) return raw.strip() def match_material(visual_prompt: str) -> str: # 素材标签匹配:返回素材文件路径,简化版实现 return find_best_material_by_tags(visual_prompt) def render_video(scenes: list[dict], output_path: str): # 用 MoviePy 或 FFmpeg 按 scenes 渲染 ...这个骨架其实只体现了两个 LLM 调用点:一个是把选题扩展成完整口播稿,一个是把口播稿拆成分镜 JSON。很多人以为重点在渲染,其实渲染反而是最成熟的部分,MoviePy 文档里写得很清楚,真正需要动脑子的地方是 LLM 输出的可靠性。我做了三层防护:JSON 格式校验、字段缺失兜底、类型强制转换。比如duration_sec如果模型输出了字符串,我会直接用float()强转,转不了就改成默认值 4。
3.2 一个案例跟跑:从输入“AI 绘画入门”到成片
我拿一个真实跑过的例子来走一遍流程。输入主题是“AI 绘画入门”,第一步generate_script先产出了一段口播稿,大意是“很多人觉得 AI 绘画门槛高,其实只要掌握三个步骤:选工具、写提示词、调参数”。这段文案大概 180 字。
第二步generate_storyboard把这段文案切成了 6 个分镜。这里注意,分镜化不是按字数机械切分,LLM 会结合语义断点,比如“选工具”是一镜,“写提示词”是另一镜。每个分镜的narration加在一起应该等于前面生成的整段口播稿,这是我在校验逻辑里写死的一个约束:如果旁白拼接后语义对不上,就重新调用分镜模块。
第三步素材匹配,我给每一个visual_prompt去素材库里找镜头。比如“选工具”对应的是一个电脑屏幕上打开绘画软件的画面,“写提示词”对应键盘打字特写。素材库找不到的就用封面图 + 缓慢缩放的特效补足,这招可以避免因为缺素材导致整个渲染流程中断。
第四步渲染。这里我选择用 MoviePy 逐镜加载素材、按duration_sec裁剪、拼接转场,再统一叠加字幕轨和 TTS 音轨。TTS 我用的是本地部署的 edge-tts,免费且延迟低,一小段旁白几秒钟就能生成。整个渲染过程大约 2 分钟出一条约 40 秒的视频。第一条成片的效果明显有“AI 廉价感”,但胜在稳定,内容逻辑通顺、字幕定位准确,这一步自动化就已经产生了价值。
3.3 渲染参数配置的几个经验值
视频分辨率我固定成 1080x1920 竖屏,这是短视频平台的主流画幅。帧率用 30,码率靠 MoviePy 默认参数,实测在手机上看完全够用。字幕我压在视频下方三分之一处,白字黑边,字体用系统中文字体,尺寸大概是高度 4% 左右,太大会遮挡画面,太小手机上看不清。
背景音乐的处理有个细节:不要直接整段铺上去,而是用set_audio混合时把音乐音量压低到旁白音量的 0.15 倍,并且用audio_fadeout做结尾淡出。如果不压音量,人声和音乐一旦同时出现,会互相干扰到根本听不清。这个 0.15 倍是我多次试出来的平衡点,不同音乐类型可能还要微调。
4. 常见问题与排查技巧实录
4.1 LLM 输出不稳定,JSON 解析崩了怎么办
这是跑管线最普遍的问题,没有之一。即使我规定了response_format是json_object,仍然会遇到模型输出内容里夹杂着解释文字、字段名被改写、JSON 里多了一个尾逗号等问题。
我的解决方案不是追求模型永远正确,而是做一套“解析容错 + 自动修复”。解析容错指的是先把返回内容里多余的解释文字清理掉,提取第一个{到最后一个}之间的子串再json.loads。自动修复则是对常见错误做规则替换,比如把单引号替换成双引号、去掉尾逗号。如果都失败,就让 LLM 自己在提示词里看到报错信息然后重新生成,最多重试三次。
后来我还加了一层缓存:每个 Prompt 的响应都会存在本地,同一请求直接读缓存,不二次调 LLM。这在调试阶段省下的时限不是一点半点,因为你经常会改渲染模块的代码,改完重新跑管线,如果 LLM 步骤缓存住,整个过程只需要几十秒就能看到渲染结果。
4.2 素材匹配不准,画面和解说对不上
画面和文案脱节是视频观感最致命的问题。解说词在讲“提示词怎么写”,画面上却在播放风景空镜,这种视频一眼假。我的排查发现,问题往往出在素材匹配阶段而不是 LLM 生成阶段,因为visual_prompt写得太抽象,素材库又没有对应标签,匹配函数只能返回一个“最像但其实不像”的结果。
解决办法是双管齐下。第一,在 Prompt 里限制visual_prompt只能从素材库标签集中选择,宁可降低画面多样性,也要保证可匹配。第二,给素材库做了一定程度的冗余,同一个场景有多角度、多时长的片段,这样即使标签相同,拼接出来的画面也不至于重复到让人看腻。
另外一个小经验是:素材检索不要只看命中数量,还要考虑时长。如果一个分镜要求 6 秒,素材只有 2 秒,拉伸到 6 秒观感会明显变慢。我的做法是对素材时长和duration_sec做比例判断,差距超过 1.5 倍就换素材,而不是硬拉。
4.3 渲染阶段内存爆了怎么办
MoviePy 处理视频时会把帧读进内存,我一开始所有素材直接VideoFileClip()全部加载,剪到第 10 个片段的时候内存占用已经将近 10GB,跑长视频直接卡死。
解决思路是分块处理和流式拼接。我先把每个分镜单独导出成一小段 MP4,再用 FFmpeg 的 concat demuxer 把所有片段无损合到一起。这样任何时刻都只存在两个待处理文件,内存占用从 10GB 降到了 500MB 以内。
如果你像我一样用 MoviePy 合成字幕和音轨,也可以考虑只对“需要复杂特效的那一段”用 MoviePy,最终输出交给 FFmpeg。这两个工具混用才是真正的工程实践,纯用 MoviePy 处理长视频就是这个项目的隐形大坑。
4.4 批量出片越出越像,怎么破
管线跑通后,下一步自然是批量生产视频。这里很快就碰到一个新问题:连着生成 10 条视频,选题、语气、叙事结构高度同质化,看一条还行,看三条就腻了。
这个问题的根源不完全在 LLM,而在于 Prompt 太固定。想解决就需要在脚本阶段引入随机化和风格轮换。我在系统提示词里加了一个style参数,每次从一组预设风格里随机取一个,比如“极简干货风”“悬疑开头风”“故事讲述风”。结构上也做一些变化,有时候用“痛点开头”,有时候用“场景反衬开头”,让 LLM 在指定的框架内还有发挥余地。
同理,分镜数量和时长也不要固定。原来我固定成“5-8 个分镜,每个 4 秒”,后来改成“根据脚本长度动态决定,时长在 3-6 秒之间浮动”,成片的节奏感明显好很多,也更能防止鬼打墙式的相似感。
最后再分享一个经验:别追求一开始就全自动。我建议先把“脚本生成 + 分镜拆解”自动化,配音字幕渲染还用原来的人工流程,跑通之后再逐步把素材匹配、渲染模块接进来。这样每一步的稳定性都能得到验证,也不会因为系统太复杂导致出了问题无从下手。这条管线的价值不在于彻底不用人,而在于把人从重复劳动里解放出来,把时间和精力真正放到选题和风格打磨这些更有创造力的事情上。