在单台 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 里没有 shapkapt-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 替代了旧的 fadeinwith_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 会做:

  1. n8n 编排:在 n8n 里用 HTTP Request 节点调用视频服务
  2. 接入 LLM:用 One API 生成视频脚本,驱动图片选择
  3. 加配音和字幕:用 TTS 和 FFmpeg
  4. YouTube 发布:配置 YouTube OAuth2,自动上传

整个链路打通后,你会得到一个”输入题目 → 输出可发布视频”的自动化工作流。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注