用 Node 与 ffmpeg 串起 AI 短视频流水线
教程将模型生成的场景计划、图片和配音拼接为竖屏视频,覆盖静态图缩放、语音字幕同步及片段合并。方案使用 Node 和原生 ffmpeg,并强调校验场景 JSON、处理句子截断及 API 失败。
AI 深度解读
AI 短视频真正费工夫的地方,是把图片、配音和字幕拼成一条节奏正常、不会截断旁白、失败后还能继续生成的视频。作者给出的方案是 Node 加原生 ffmpeg:LLM 负责规划,图片和语音服务负责素材,视频加工交给可验证的命令。
这套方案值得看,不在于用了多复杂的模型,而在于把容易被演示跳过的工程细节补齐了。不过,字幕同步只是近似估算,单机任务恢复也有适用边界,不能直接当成完整生产方案。
模型负责规划,成片节奏要听音频的
整条流水线从一个简短需求开始:LLM 输出场景计划,每个场景包含图片提示词、旁白和预计时长;随后生成图片与配音,逐场景渲染,最后拼接成 MP4。
计划采用普通 JSON,关键字段是 title、scenes,以及场景中的 imagePrompt、narration、durationSec。OpenAI-compatible APIs 可以设置 response_format: { type: "json_object" },但作者仍要求验证结果。
原因很实际:返回内容可能带代码围栏、夹着解释文字,把要求的 6 个场景扩成 14 个,甚至把时长写成 "3s"。处理流程包括清理围栏、JSON.parse、检查所有字段,并约束场景数量和时长,例如把时长限制在 2 到 12 秒。验证失败就抛出可安全重试的错误。
不过,计划通过验证,只能说明结构合格。
最终场景时长必须根据实际配音决定。
LLM 不知道 TTS 最终读得多快。作者先用 ffprobe -v error -show_entries format=duration -of csv=p=0 voice.mp3 测量音频,再计算片段长度。原文示例测得 3.112 秒。
下面这段代码解决旁白被提前截断的问题:
const speech = await probeDuration(audioPath); // ffprobe, as above
const dur = Math.max(scene.durationSec, speech + 0.25); // never shorter than the voice, plus a breath
要看的是第二行:配音时长额外加 0.25 秒,再与计划时长取较大值。这个余量给句尾留一点呼吸空间,但它是作者采用的经验值,并没有语速或内容类型的对照实验。
这里还有一个实现顺序问题:如果计划阶段把时长限制为 12 秒,测出旁白更长后,又强行压回 12 秒,仍然会截断。长度超标需要回到旁白生成环节处理,不能靠裁视频解决。
静态图片怎样变成自然的竖屏片段
作者选择 Ken Burns 效果,也就是让静态图片缓慢推近。它成本低,不依赖视频生成服务,但素材尺寸和裁剪顺序会直接影响观感。
原文强调两个细节:小尺寸输入直接执行 zoompan 容易抖动;横图必须先按目标比例裁剪,否则可能被拉变形。因此顺序是先等比例放大并裁剪,再执行缩放运动。
下面这条命令把图片和配音渲染成一个 720×1280、25 FPS、时长 3.4 秒的竖屏片段:
W=720; H=1280; FPS=25; DUR=3.4
FRAMES=85 # DUR × FPS
ffmpeg -y -loop 1 -i scene.png -i voice.mp3 -filter_complex "\
[0:v]scale=900:1600:force_original_aspect_ratio=increase,crop=900:1600,\
zoompan=z='min(zoom+0.0008,1.15)':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=$FRAMES:s=${W}x${H}:fps=$FPS,\
format=yuv420p[v]" \
-map "[v]" -map 1:a -af apad -t $DUR \
-c:v libx264 -preset veryfast -c:a aac -ar 44100 -ac 2 segment.mp4
关键在 scale、crop、zoompan 连续出现的那一行:先得到比输出大 1.25 倍的 900×1600 画布,再逐帧增加 0.0008 的缩放量,上限为 1.15,即最多放大 15%。x/y 表达式让画面围绕中心推进。
apad 在音频尾部补静音,-t 固定片段长度;yuv420p 用于手机和浏览器播放兼容。横屏 16:9 则改用 1600:900 的中间画布和 s=1280x720。
这套处理解决的是比例、运动和编码问题。它没有解决构图问题:主体靠边的图片经过中心裁剪后,仍可能丢掉重要内容。
字幕能跟上配音,但还不是逐词对齐
没有 TTS 词级时间戳时,作者把旁白切成短块,再按每块的字符长度分配显示时间。函数 captionCues 默认 maxWords = 4,遇到标点提前断开,权重使用 c.length + 2;toSrt 再把结果写成 SRT。
大白话解释,就是字多的字幕多显示一会儿。这比整句从头挂到尾更有节奏,但没有真正听懂音频里的停顿与重音。作者说这种方式实际观感不错,原文没有给出同步误差数据。
字幕通过 subtitles 滤镜烧录,依赖 libass。样式示例使用 FontName=DejaVu Sans、FontSize=10、Bold=1、Outline=1.6、Shadow=0.8、Alignment=2、MarginV=64。
容易误判的是字号:SRT 输入下,作者按 288 像素高的布局画布解释缩放,因此 FontSize=10 在高 1280 像素的视频中约为 44 px。若目标字号约为短边的 6%,计算式是 FontSize = 0.06 × min(W, H) × 288 / H。
另一个实用做法是把 ffmpeg 的工作目录设为素材目录,只传 SRT 文件名,减少冒号、反斜杠、引号和 Windows 盘符带来的转义问题。drawtext=textfile=... 也可以沿用这个办法。用户文本应写进文件,不要直接塞进滤镜字符串。
对国内开发者而言,这段代码还需要改造:它用空白拆分单词,不能直接拿来给中文逐句切字幕。
真正让流水线可用的是一致性和恢复能力
测试阶段不必每次调用付费配音。ffmpeg 编译时带有 libflite,就能使用 flite=textfile=line.txt:voice=slt,配合 loudnorm=I=-16、-ar 44100 生成配音。macOS 可用 say -o voice.aiff -f line.txt,Linux 和 Windows 可用 espeak-ng -w voice.wav -f line.txt。
作者用 VoiceProvider 接口统一它们,包含 name 和 speak(text, outPath): Promise<void>,通过环境变量切换本地或付费实现。
拼接阶段则要求所有片段统一分辨率、帧率、像素格式、编码器、音频采样率和声道。满足条件后,使用 concat demuxer,配合 -f concat -safe 0 -i list.txt -c copy -movflags +faststart 输出 final.mp4。-c copy 避免重新编码,+faststart 把索引移到文件前部,方便浏览器提前播放。作者特别提到,漏掉 -ac 2、混入单声道 TTS,是拼接异常的常见原因。
API 故障处理也有明确规则:429 和 5xx 使用带随机抖动的指数退避;400、401、403 按永久错误快速失败。验证后的计划保存在任务记录里,避免图片失败后重新付费调用 LLM。
并发要设上限。作者举例场景同时处理 3 个、任务同时处理 2 个,均可配置;对 40 个场景直接执行无界 Promise.all,很容易撞上限流。
单机先用每任务一个 job.json,原子写入,worker 轮询队列;启动时把遗留的 running 任务重新入队。扩展到多机后,再通过相同接口接入 Redis/BullMQ 或 Postgres SKIP LOCKED。
我的判断
我认可这套分工:让模型产出计划,让音频决定时长,让 ffmpeg 执行确定的加工步骤。它适合先把一条可检查、可复现的生成链路跑通。
但原文展示的是命令和工程经验,没有吞吐量、成本、字幕误差或失败恢复的对照数据。「字幕同步」「生产可用」都需要结合具体素材验证。
按字符分配字幕时间适合低成本草稿;要求精确卡词,就需要词级时间戳或音频对齐方案。单机文件队列也有价值,但原文没有交代多 worker 抢占、重复执行和中间产物清理,不能把重新入队等同于完整恢复机制。
能拿来做什么
- 用 Node 先串起单场景流程,记录实际配音时长、最终片段时长和 ffmpeg 参数,确认旁白完整后再增加场景数量。
- 做中文内容时,单独实现按标点和长度切分的字幕逻辑,并用真实配音检查停顿位置,避免直接沿用英文空白分词。
- 把编码参数集中管理,让所有片段统一使用同一份配置,重点检查
44100采样率和-ac 2,再执行无重编码拼接。 - 为任务保存已验证计划和完成状态,先限制场景与任务并发,再接入图片、TTS 等外部服务,降低失败重试的浪费。