从零搭建 AI 自动视频流水线:Remotion + n8n + OneAPI + Tailscale 完整实录

这套系统能做什么

给它一个题目(比如”京都秋天的三日游”),它会自动完成:LLM 生成视频标题和分镜字幕 → 调用本地 Remotion 模板渲染 → 输出 mp4。调度在 VPS 上的 n8n,渲染在本机 Mac,两者通过 Tailscale 内网互通,不暴露公网。

本文记录从零搭建的完整过程,包括每个工具的定位、每一步的具体命令,以及实际踩到的坑和解决办法。

工具说明:每个工具在链路里的角色

Codex CLI —— 写代码的 AI 终端代理。不是视频工具,而是帮你生成和修改 Remotion 模板代码的编码助手。在项目目录里启动,用自然语言描述需求,它直接读写文件、跑命令。Remotion 官方把 Codex 列为推荐的 Coding Agent,配合 npx remotion skills add 安装的技能文件能让它更懂 Remotion 语法。

Remotion —— 基于 React 的编程式视频框架。你定义组件,它按帧渲染成 mp4。核心概念是 <Composition>(注册一个可渲染的视频)和 props(外部传入的数据)。它负责把 LLM 生成的 title 和 clips 数据组装成画面,本身不做智能剪辑。

n8n —— 可视化 workflow 调度器,跑在 VPS 上。负责串起整条链路:接收题目 → 调 LLM → 清洗 JSON → 调 Mac 渲染服务。节点(Node)是单个处理步骤,用连线串成 workflow,不需要写调度代码。

OneAPI —— 开源 API 管理分发网关,把 OpenAI、DeepSeek 等各种模型统一成 OpenAI 兼容格式。n8n 不直接调 DeepSeek,而是调 OneAPI 转发。换模型时只改 OneAPI 渠道配置,n8n 里的 URL 和 token 不用动。默认端口 3000。

Tailscale —— 内网穿透。把 VPS 和 Mac 放进同一个虚拟私有网络,每台设备分配 100.x.x.x 的 IP。n8n 在 VPS、渲染服务在 Mac,Tailscale 让 VPS 直接通过内网 IP 访问 Mac 的 8787 端口,不需要公网暴露或端口转发。

render-server.mjs / render.mjs —— 渲染的 HTTP 入口。render-server.mjs 监听 Mac 的 0.0.0.0:8787,接收 n8n 发来的 JSON,调用 render.mjs 执行实际渲染。render.mjs 用 Remotion 的 Node API 把 props 变成 mp4。这两个脚本是 n8n 和 Remotion 之间的胶水层。

架构总览

你给题目 → n8n(VPS)→ OneAPI(VPS)→ DeepSeek 生成 JSON
         → n8n 清洗 JSON
         → n8n HTTP 请求(Tailscale)→ render-server(Mac)→ render.mjs → Remotion → mp4
Codex 只在搭建阶段出现,帮你写 Remotion 模板和渲染脚本。运行时链路里没有 Codex:n8n 是指挥,Remotion 是执行者,OneAPI 提供大脑,Tailscale 提供通道。

环境要求

Mac(Intel 或 Apple Silicon),macOS 15+,Node.js 20+,ffmpeg

VPS(Linux),Docker,Nginx,ufw

Tailscale:Mac 和 VPS 加入同一个 tailnet

Codex CLI

第一部分:安装 Codex CLI

curl -fsSL https://chatgpt.com/codex/install.sh | sh
安装后需要新开一个终端窗口才能使用 codex 命令。也可以用 npm 或 Homebrew:
npm install -g @openai/codex
# 或
brew install --cask codex
首次运行 codex 时按提示登录。

第二部分:在 Mac 上搭建 Remotion 模板

1. 创建项目

npx create-video@latest --yes --blank my-vlog-template
cd my-vlog-template
npm i
npx remotion skills add
npx remotion skills add 会把 Remotion 官方最佳实践安装到 .agents/skills/,Codex 写代码时会自动参考。

2. 让 Codex 生成模板

在项目目录启动 codex,给它这段提示词:

修改当前 Remotion 项目的 Vlog 模板,接受 props:title: string,clips: Array<{ src: string; caption: string; durationInFrames: number }>。在 src/Root.tsx 的 <Composition> 上设置 defaultProps 填入假数据。主组件用 <Series> 顺序渲染每个 clip,每个 clip 用 <OffthreadVideo> 播放,底部叠加 caption 字幕,开头用 2 秒显示 title。Composition 的总时长要随 clips 自动计算。

Codex 会生成 src/Composition.tsx 和 src/Root.tsx。记下 Composition 的 id(假设是 MyComp),后面渲染要用。

3. 准备占位素材和 props

cd public
ffmpeg -y -f lavfi -i color=c=blue:s=1280x720:d=3 -pix_fmt yuv420p clip1.mp4
ffmpeg -y -f lavfi -i color=c=green:s=1280x720:d=3 -pix_fmt yuv420p clip2.mp4
cd ..
创建 props.json:
{
  "title": "测试 Vlog",
  "clips": [
    { "src": "clip1.mp4", "caption": "第一段画面", "durationInFrames": 90 },
    { "src": "clip2.mp4", "caption": "第二段画面", "durationInFrames": 120 }
  ]
}
4. 命令行渲染验证
bash
npx remotion render MyComp out/test-vlog.mp4 --props=./props.json
open out/test-vlog.mp4
看到标题 + 两段画面 + 字幕,说明模板通了。

第三部分:把渲染封装成 HTTP 服务

1. 让 Codex 写 render.mjs

提示词:

创建 render.mjs:从命令行参数读 JSON 文件路径,调用 @remotion/renderer 的 selectComposition 和 renderMedia,compositionId 固定为 MyComp,输出到 out/render-<时间戳>.mp4,完成后打印绝对路径,出错时 process.exit(1)。

2. 让 Codex 写 render-server.mjs

提示词:

创建 render-server.mjs:用 Node 原生 http 模块,监听 0.0.0.0:8787。只接受 POST /render,请求体是 { title, clips }。收到后写临时 JSON 文件,用 execFile 调 render.mjs,等待完成后返回 { ok: true, output: “<mp4绝对路径>” }。渲染失败返回 500。

3. 启动并验证

node render-server.mjs
另开终端:
curl -s -X POST http://127.0.0.1:8787/render \
  -H "Content-Type: application/json" \
  -d @props.json
期望返回 {"ok":true,"output":"/Users/.../render-xxx.mp4"}。

第四部分:配置 Tailscale 互通

1. 确认两端 IP

VPS 上执行:

tailscale status
记下 Mac 的 Tailscale IP(比如 100.98.125.115)和 VPS 自己的 IP(比如 100.99.4.120)。

2. 从 VPS 测试到 Mac 的连通

ping -c 3 <Mac的Tailscale_IP>

curl -s -X POST http://<Mac的Tailscale_IP>:8787/render \

-H “Content-Type: application/json” \

-d ‘{“title”:”远程测试”,”clips”:[{“src”:”clip1.mp4″,”caption”:”远程”,”durationInFrames”:90}]}’

返回 ok: true 说明 VPS 能直接调 Mac 的渲染服务。如果连不上,检查 macOS 防火墙是否放行 node 的入站连接。

第五部分:VPS 上确认 OneAPI

1. 确认 OneAPI 在跑

docker ps | grep one-api
curl -s http://127.0.0.1:3000/api/status
2. 关键:n8n 容器里访问 OneAPI 的地址
n8n 和 OneAPI 都在 Docker 里,但 n8n 容器内的 127.0.0.1 打不到宿主机。必须用 Docker 网关 IP:
docker exec n8n wget -qO- http://172.17.0.1:3000/api/status
返回 JSON 就对了。如果 172.17.0.1 不通,用下面的命令查实际网关:
docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'
3. 测试 LLM 调用
bash
curl -s http://127.0.0.1:3000/v1/chat/completions \
  -H "Authorization: Bearer sk-你的token" \
  -H "Content-Type: application/json" \
  -d '{"model":"你的模型名","messages":[{"role":"user","content":"只回复两个字:你好"}]}'
能返回 "content":"你好" 就说明 OneAPI 侧完全可用。

第六部分:n8n 导入 workflow

1. workflow 结构

Set Topic → HTTP Request(调 OneAPI)→ Code(清洗 JSON)→ HTTP Request(调 Mac 渲染)。

关键配置:

LLM 节点 URL 用 http://172.17.0.1:3000/v1/chat/completions(n8n 在容器里)

渲染节点 URL 用 http://<Mac的Tailscale_IP>:8787/render

Code 节点要剥掉 LLM 可能输出的 markdown 代码块,JSON.parse 后校验 title 和 clips 结构,把 src 兜底到合法文件名

2. 导入

把 workflow JSON 写到 /tmp/vlog-workflow.json,然后:

docker cp /tmp/vlog-workflow.json n8n:/tmp/vlog-workflow.json
docker exec n8n n8n import:workflow --input=/tmp/vlog-workflow.json
3. 访问 n8n 界面
如果 VPS 的 5678 端口外部打不开,用 SSH 隧道:
ssh -N -L 5678:127.0.0.1:5678 root@<VPS_IP>
浏览器访问 http://127.0.0.1:5678。

4. 执行并验证

打开 workflow,改 Set Topic 里的题目,点 Execute。最后一个节点输出 {“ok”:true,”output”:”/Users/…/render-xxx.mp4″} 就成功了。

实际踩到的坑与解决办法

坑 1:zsh 的 ! 历史扩展导致脚本无法粘贴执行

直接粘贴含 #!/bin/bash 的脚本,zsh 报 zsh: event not found: /bin/bash。原因是 zsh 把 ! 当历史引用。

解决:用 heredoc 存成文件再执行,EOF 必须带引号:

cat > ~/script.sh << 'EOF'
#!/bin/bash
...
EOF
chmod +x ~/script.sh && ~/script.sh
坑 2:Remotion Studio 占用端口导致渲染失败
npm run dev 的 Studio 还在跑时执行 npx remotion render,报 getStaticCompositions is undefined,因为渲染器连到了不完整的 Studio 页面。

解决:先停 Studio,或渲染时指定独立端口:

npx remotion render MyComp out/test.mp4 --props=./props.json --port=3340 --concurrency=1
坑 3:n8n 容器里的 127.0.0.1 打不到 OneAPI
n8n 和 OneAPI 在同一 VPS,但 n8n 是 bridge 网络,容器内的 127.0.0.1 是容器自己的 loopback,不是宿主机。

验证:

docker exec n8n wget -qO- http://127.0.0.1:3000/api/status
# Connection refused

docker exec n8n wget -qO- http://172.17.0.1:3000/api/status

# 正常返回 JSON

解决:n8n 里所有调 OneAPI 的 URL 用 http://172.17.0.1:3000。

坑 4:XServer パケットフィルター拦截外部端口

ufw 已放行 5678,VPS 内部 curl 127.0.0.1:5678 返回 200,但外部浏览器打不开,nc -zv 返回 Connection refused。

原因:XServer 在系统防火墙之外还有一层平台级包过滤(パケットフィルター),默认白名单模式,只有明确放行的端口才能从外部访问。

解决:登录 XServer 面板,在「パケットフィルター設定」里选「手動で設定」,添加 TCP + 端口号 + 允许所有来源,等 1–2 分钟生效。

临时绕过:用 SSH 本地转发,不经过外部端口:

ssh -N -L 5678:127.0.0.1:5678 root@<VPS_IP>
浏览器访问 http://127.0.0.1:5678。

坑 5:n8n workflow 导入报 NOT NULL constraint failed: workflow_entity.id

导入的 workflow JSON 顶层缺少 id 字段,n8n 的 SQLite 插入时该字段为 NULL,触发约束错误。

解决:在 JSON 顶层加一个唯一字符串 ID:

{
  "id": "vlog-test-001",
  "name": "Vlog Render Test",
  ...
}
坑 6:n8n execute CLI 命令报端口冲突
在容器运行中用 docker exec n8n n8n execute --id xxx,报 Task Broker's port 5679 is already in use。因为 CLI 会尝试启动完整新进程占用 5679,和正在运行的主实例冲突。

解决:不要在容器运行时用 CLI 执行。用浏览器界面(走 SSH 隧道)或 Webhook 触发。

后续可扩展方向

素材管理:把 public/ 里的占位视频换成真实素材,改 Code 节点里的 allowed 数组和 LLM 提示词里的文件名列表,让模型从实际素材中选。素材量大时可给 render-server 加 POST /upload 端点。

Webhook 触发:把 Manual Trigger 换成 Webhook,从手机或任何地方发 {“topic”:”…”} 触发整条流水线。

借鉴参考视频效果:用 mimic-reels-mcp 分析参考视频的剪辑节奏、转场、字幕样式,把量化后的”配方”应用到 Remotion 模板,让模板从”能出片”变成”好看”。

总结

这套系统的核心思路是职责分离:n8n 只做调度,OneAPI 只管模型,Remotion 只负责渲染,Tailscale 只提供通道。每个组件都可以独立替换或升级,不会牵一发动全身。Codex 在搭建阶段大幅降低了写 Remotion 代码的门槛,但运行时链路完全不依赖它,保证了生产环境的确定性。

发表回复

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