在单台 VPS 上搭建 AI 视频生成工作流(二):接入 LLM 和 TTS,实现全自动

从 Part 1 结束的地方开始

Part 1 完成后,你有:

  • 一个 Flask 视频合成服务(video_service.py),输入题目,输出 12 秒的 MP4
  • 3 张静态占位图
  • n8n 里一个能触发视频生成的工作流

但视频有两个明显问题:

  1. 没有配音 —— 只是图片幻灯片,观众听不到内容
  2. 脚本是硬编码的 —— 每次要手动改题目,没有真正的 AI 生成

这篇解决这两个问题。

架构升级

Part 1 的流程:

text

n8n → 视频服务 → MP4

Part 2 的流程:

text

n8n (Manual Trigger)
  → HTTP Request: 调 One API → NVIDIA LLM 生成脚本
  → HTTP Request: 调视频服务(传脚本 + 题目)
  → 视频服务: edge-tts 生成配音 + MoviePy 合成
  → 带配音的 MP4

多了一个”生成脚本”的环节,视频服务也多了”生成配音”的能力。

第一步:接入 LLM 生成脚本

你的 VPS 上已经有一个 Docker 版的 One API,统一管理 LLM 渠道。关键是把 One API 的地址暴露给 n8n 容器。

打通 n8n 到 One API 的网络

n8n 是 Docker 容器,访问宿主机的服务需要走 Docker 网桥。之前已经遇到过这个问题:n8n 访问 172.17.0.1(Docker 网桥)被 iptables 拦截。

解决方案是放行 Docker 网段到宿主机的流量:

bash

iptables -I INPUT 1 -s 172.17.0.0/16 -p tcp --dport 3000 -j ACCEPT
iptables -I INPUT 1 -s 172.18.0.0/16 -p tcp --dport 3000 -j ACCEPT
iptables -I INPUT 1 -s 172.19.0.0/16 -p tcp --dport 3000 -j ACCEPT
iptables -I INPUT 1 -s 172.20.0.0/16 -p tcp --dport 3000 -j ACCEPT

同样的规则也要给视频服务的 5001 端口。保存规则:

bash

apt install -y iptables-persistent
netfilter-persistent save

选择 LLM 模型

NVIDIA 免费层提供了多个模型。测试下来,nvidia/nemotron-3-ultra-550b-a55b 效果最好,但它是推理模型——会先生成一段”思考过程”(reasoning_content),然后才输出正式回答(content)。

踩过的坑

  • max_tokens=1024 不够 —— 推理过程吃掉大部分 token,content 被截断成十几个字符
  • 解决:把 max_tokens 提到 4096
  • 即使不显式传 chat_template_kwargs,模型默认仍然会推理

n8n 节点配置

节点 2:HTTP Request

  • Method: POST
  • URL: http://172.17.0.1:3000/v1/chat/completions
  • Headers: Authorization: Bearer sk-你的OneAPI令牌
  • Options → Timeout: 300000
  • Body (JSON):

json

{
  "model": "nvidia/nemotron-3-ultra-550b-a55b",
  "messages": [
    {
      "role": "user",
      "content": "为主题「AI改变教育」写一段 30 秒 YouTube Shorts 脚本,包含开场钩子、正文要点、结尾 CTA。总字数控制在 130 字以内。只输出脚本内容。"
    }
  ],
  "max_tokens": 4096,
  "temperature": 0.7,
  "stream": false
}

关键点

  • max_tokens: 4096 —— 给推理模型留足预算
  • stream: false —— 关闭流式,避免 n8n 的超时问题
  • Timeout 300000 —— 推理模型响应约 37 秒,默认 18 秒会超时

第二步:加 TTS 配音

用 edge-tts,完全免费、无需 API Key、中文效果自然。

安装

bash

cd /opt/rag-nvidia
source venv/bin/activate
pip install edge-tts

中文音色

音色 ID名称特点
zh-CN-YunxiNeural云希男声,阳光活力,适合短视频
zh-CN-XiaoxiaoNeural晓晓女声,温暖自然
zh-CN-YunjianNeural云健男声,激情有力
zh-CN-YunyangNeural云扬男声,专业可靠

推荐 zh-CN-YunxiNeural

更新 video_service.py

关键改动是:视频时长由音频决定,而不是固定的 12 秒。

python

import edge_tts
import asyncio
from moviepy import AudioFileClip

async def generate_tts(text, output_path, voice="zh-CN-YunxiNeural"):
    communicate = edge_tts.Communicate(text=text, voice=voice)
    await communicate.save(output_path)


@app.route("/generate", methods=["POST"])
def generate():
    data = request.get_json() or {}
    topic = data.get("topic", "AI")
    script = data.get("script", "")
    job_id = f"video_{int(time.time())}"
    workdir = OUTPUT_DIR / job_id
    workdir.mkdir(exist_ok=True)

    # 1. TTS 配音
    audio_path = workdir / "voice.mp3"
    tts_text = script if script else f"今天我们来聊聊{topic}"
    asyncio.run(generate_tts(tts_text, str(audio_path)))
    audio_clip = AudioFileClip(str(audio_path))
    audio_duration = audio_clip.duration

    # 2. 每张图片的时长 = 音频总时长 / 图片数量
    per_image = audio_duration / len(STATIC_IMAGES)

    clips = []
    for img in STATIC_IMAGES:
        clip = ImageClip(img).with_duration(per_image).with_effects(
            [vfx.FadeIn(0.5), vfx.FadeOut(0.5)]
        )
        clips.append(clip)
    video = concatenate_videoclips(clips, method="compose")

    # 3. 合并音频
    final = video.with_audio(audio_clip)
    out_path = workdir / "output.mp4"
    final.write_videofile(
        str(out_path), fps=24, codec="libx264",
        audio_codec="aac", logger=None,
    )
    video.close()
    audio_clip.close()

    return jsonify({
        "job_id": job_id,
        "topic": topic,
        "video_path": str(out_path),
        "script_length": len(script),
        "video_duration": audio_duration,
    })

一个容易踩的坑:视频和音频时长不匹配

之前的版本是:

python

if audio_clip.duration > video.duration:
    audio_clip = audio_clip.subclipped(0, video.duration)  # 音频被裁剪
else:
    video = video.with_duration(audio_clip.duration)  # 视频拉长

问题在于:当脚本很长(音频 90 秒),而视频固定 12 秒时,走了第一条分支——音频被砍到 12 秒,后面 78 秒内容全丢。

修复:让图片的时长动态计算,per_image = audio_duration / len(STATIC_IMAGES),这样视频长度总是匹配音频。

第三步:n8n 传递脚本

节点 3(视频生成)的 Body 要接收节点 2 的输出。

不要用”Using JSON”手写 —— 节点 2 的脚本里有换行符,直接插入 JSON 字符串会报 Bad control character in string literal

用”Using Fields Below”

  • topicAI改变教育
  • script: 点 fx 按钮,选节点 2 的输出字段

表达式语法:

text

{{ $('oneapi LLM').item.json.choices[0].message.content }}

注意 $('oneapi LLM') 里的名字要和 n8n 画布上节点 2 显示的名字完全一致

完整工作流

text

Manual Trigger
    ↓
HTTP Request (oneapi LLM)
    - 调 One API,生成脚本
    - 约 37 秒
    ↓
HTTP Request (视频生成)
    - 传 topic + script
    - Timeout 300000
    - 约 1-2 分钟
    ↓
/opt/rag-nvidia/videos/video_xxx/output.mp4

实测结果

输入:题目 AI改变教育

节点 2 输出(97 字脚本):

text

开场:作业还在愁?AI秒批、个性化推题!正文:1.智能批改释放老师时间;2.自适应路径让每个孩子按节奏进步;3.虚拟导师24h陪伴,打破时空限制。结尾:关注我,获取更多AI教育黑科技!

节点 3 输出

json

{
  "job_id": "video_1789380608",
  "script_length": 97,
  "topic": "AI改变教育",
  "video_duration": 23.02,
  "video_path": "/opt/rag-nvidia/videos/video_1789380608/output.mp4"
}

视频文件

text

voice.mp3   135K   edge-tts 生成的 23 秒配音
output.mp4  2.2M   带配音的完整视频

ffprobe:
  codec_name=h264, codec_type=video, duration=23.00s
  codec_name=aac,  codec_type=audio, duration=23.02s

从点击到拿到视频,全程约 2-3 分钟,完全自动。

踩坑记录

1. 推理模型的 token 预算
nemotron-3-ultra 是推理模型,reasoning_content 会消耗大量 token。max_tokens=1024 时 content 被截断成十几个字符。必须给到 4096。

2. n8n 的默认超时是 18 秒
不是 180 秒。改成 300000(300 秒 = 5 分钟),单位是毫秒。

3. Docker 容器访问宿主机的 iptables 问题
K3s 和 UFW 都在管理 iptables,INPUT 链 policy 是 DROP。Docker 容器到宿主机的流量被拦。解决:iptables -I INPUT 1 -s 172.17.0.0/16 -p tcp --dport <端口> -j ACCEPT

4. n8n 的 JSON Body 不接受换行符
用”Using Fields Below”代替”Using JSON”,让 n8n 自己处理转义。

5. 视频和音频时长不匹配
如果音频比视频长,且代码直接裁剪音频,长脚本会被截断。正确做法是让图片时长动态计算,匹配音频。

6. n8n 的 Execute Workflow 会自动跑整条链
前提是节点之间有连接。如果只跑了一个节点,检查节点间的箭头是否连上。

当前状态

组件状态
n8n 编排✅ 5678
One API 网关✅ 172.17.0.1:3000
NVIDIA LLM✅ nemotron-3-ultra-550b
edge-tts 配音✅ 云希男声
视频合成✅ MoviePy + FFmpeg
音视频同步✅ 时长自动匹配
输出✅ 23 秒带配音 MP4

下一步

Part 2 完成了”题目 → 带配音视频”的全自动链路。Part 3 会做:

  1. Webhook 触发 —— 用 URL 传题目,实现真正的”输入即生成”
  2. 加字幕 —— 用 edge-tts 的时间戳把文字烧录到画面
  3. YouTube 上传 —— 配置 OAuth2,自动发布
  4. 动态图片 —— 用脚本驱动图片生成,替换静态占位图

发表回复

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