在单台 VPS 上搭建 AI 视频生成工作流(一):从题目到 MP4
背景
之前在这台 VPS 上跑通了 RAG,体验了”用 AI 处理文档”的流程。现在想再往前一步:让 AI 根据一个题目自动生成视频。
VPS 的现状很苛刻:
- 总共 5.8Gi 内存,可用不到 2Gi
- 没有 GPU,只有 CPU
- 磁盘剩余 13G
- 上面跑着 K3s、WordPress、n8n、One API
这意味着不能本地运行视频生成模型(LTX-Video 要 8GB 显存,HunyuanVideo 要 14GB,Wan 2.2 要 24GB)。所以走轻量路线:用 Python + MoviePy 做视频合成,AI 图片从外部 API 获取。
架构设计
整个视频工作流拆成三层:
text
n8n(编排层)
↓ HTTP
视频合成服务(Flask + MoviePy)
↓
图片素材(外部 API 或本地静态图)
为什么拆成独立服务,而不是塞进 n8n 容器?
试过在 n8n 的 Dockerfile 里装 Python,结果发现 n8nio/n8n:latest 是一个极简镜像——没有 sh、没有 cat、没有 apk、没有 apt-get。往里面装系统包这条路走不通。
独立服务的优势:
- 不污染 n8n 镜像
- 可以独立升级和调试
- 用宿主机已有的 Python venv,环境可控
- n8n 只负责编排,逻辑清晰
第一步:准备静态图片
真实的 AI 图片生成 API 在国内 VPS 上往往不通(试过 Pollinations 返回 401,NVIDIA 的 ai.api.nvidia.com 超时)。所以先用静态占位图跑通流程,之后再替换成真实生成。
bash
mkdir -p /opt/rag-nvidia/static_images cd /opt/rag-nvidia/static_images curl -L -o img1.jpg "https://picsum.photos/1080/1920?random=1" curl -L -o img2.jpg "https://picsum.photos/1080/1920?random=2" curl -L -o img3.jpg "https://picsum.photos/1080/1920?random=3"
picsum.photos 提供随机图片,支持指定尺寸(1080/1920 是竖版 9:16)。3 张图分别约 100KB、255KB、359KB。
第二步:写视频合成服务
创建 /opt/rag-nvidia/video_service.py:
python
import time
from pathlib import Path
from flask import Flask, request, jsonify
from moviepy import ImageClip, concatenate_videoclips, vfx
app = Flask(__name__)
OUTPUT_DIR = Path("/opt/rag-nvidia/videos")
OUTPUT_DIR.mkdir(exist_ok=True)
STATIC_IMAGES = [
"/opt/rag-nvidia/static_images/img1.jpg",
"/opt/rag-nvidia/static_images/img2.jpg",
"/opt/rag-nvidia/static_images/img3.jpg",
]
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok"})
@app.route("/generate", methods=["POST"])
def generate():
data = request.get_json() or {}
topic = data.get("topic", "AI")
job_id = f"video_{int(time.time())}"
workdir = OUTPUT_DIR / job_id
workdir.mkdir(exist_ok=True)
clips = []
for img in STATIC_IMAGES:
clip = ImageClip(img).with_duration(4).with_effects(
[vfx.FadeIn(0.5), vfx.FadeOut(0.5)]
)
clips.append(clip)
video = concatenate_videoclips(clips, method="compose")
out_path = workdir / "output.mp4"
video.write_videofile(
str(out_path), fps=24, codec="libx264", audio=False, logger=None,
)
video.close()
return jsonify({
"job_id": job_id,
"topic": topic,
"video_path": str(out_path),
"images_used": len(STATIC_IMAGES),
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001)
设计要点:
- 每张图片显示 4 秒,3 张共 12 秒
- 用
FadeIn/FadeOut做淡入淡出转场 audio=False暂时不加配音- 返回 JSON 包含视频路径,方便 n8n 后续处理
logger=None关掉 MoviePy 的进度条输出,避免日志污染
第三步:安装依赖
bash
cd /opt/rag-nvidia source venv/bin/activate pip install flask requests imageio-ffmpeg "moviepy>=2.0"
关键坑:MoviePy 版本
MoviePy 2.x 和 1.x 的 API 完全不同。2.x 用 from moviepy import ImageClip,1.x 用 from moviepy.editor import ImageClip。脚本用的是 2.x API,必须装 2.x。
另外 imageio-ffmpeg 是必需的——MoviePy 通过它调用 FFmpeg,首次运行时会自动下载 FFmpeg 二进制(约 30MB)。
第四步:启动服务
bash
cd /opt/rag-nvidia source venv/bin/activate nohup python video_service.py > /var/log/video_service.log 2>&1 & sleep 3 curl http://localhost:5001/health
输出 {"status":"ok"} 说明服务起来了。
第五步:生成第一个视频
bash
curl -X POST http://localhost:5001/generate \
-H "Content-Type: application/json" \
-d '{"topic": "AI改变教育"}' | python3 -m json.tool
首次运行耗时约 2.5 分钟,因为:
- MoviePy 首次初始化开销
- FFmpeg 二进制首次下载
- 3 张 1080×1920 图片的逐帧渲染
返回结果:
json
{
"images_used": 3,
"job_id": "video_1789369419",
"topic": "AI改变教育",
"video_path": "/opt/rag-nvidia/videos/video_1789369419/output.mp4"
}
确认文件:
bash
ls -lh /opt/rag-nvidia/videos/*/output.mp4
踩坑记录
1. n8n 镜像极简,装不了系统包
n8nio/n8n:latest 里没有 sh、apk、apt-get。试图用 Dockerfile 加 Python 环境失败。最终改成独立 Flask 服务。
2. Pollinations 图片 API 要求认证
gen.pollinations.ai/image/ 现在返回 401,匿名请求不再可用。免费 Key 需要去 enter.pollinations.ai 注册。
3. NVIDIA 图片生成端点网络不通
integrate.api.nvidia.com(文本/嵌入)通,但 ai.api.nvidia.com(图片生成)超时。这是域名级别的网络问题,不是 Key 的问题。
4. venv 挂载进容器不可用
宿主机的 venv 里 bin/python 是符号链接,指向宿主机的 Python 路径。容器里没有那个路径,所以报 no such file or directory。容器和宿主机必须各装各的依赖。
5. MoviePy 2.x 的 API 变化
vfx.FadeIn 替代了旧的 fadein,with_duration 替代了 set_duration。网上很多教程还是 1.x 的写法,容易踩坑。
当前状态
| 组件 | 状态 |
|---|---|
| Flask 服务 | ✅ 运行在 http://0.0.0.0:5001 |
/health | ✅ 返回 {"status":"ok"} |
/generate | ✅ 输入题目,输出 12 秒 MP4 |
| 静态图片 | ✅ 3 张 1080×1920 |
| n8n 容器 | ✅ 运行中,my-n8n:latest 自定义镜像 |
下一步
这一篇完成了”题目 → MP4″的核心能力。Part 2 会做:
- n8n 编排:在 n8n 里用 HTTP Request 节点调用视频服务
- 接入 LLM:用 One API 生成视频脚本,驱动图片选择
- 加配音和字幕:用 TTS 和 FFmpeg
- YouTube 发布:配置 YouTube OAuth2,自动上传
整个链路打通后,你会得到一个”输入题目 → 输出可发布视频”的自动化工作流。
