Starsoup Script-to-Video Pipeline
搭建說明書 v1.0
適用服務器:Starsoup Server A(Ubuntu + Docker + Cloudflare Tunnel)
預計搭建時間:3~4 小時
難度:中等(需要基本 Docker 和命令行操作能力)
目錄
- 前置準備
- Phase 1 — 部署 n8n
- Phase 2 — Python 環境搭建
- Phase 3 — API Key 申請
- Phase 4 — 測試各個節點
- Phase 5 — 建立 n8n Workflow
- Phase 6 — Cloudflare Tunnel 暴露
- 常見問題
- 目錄結構總覽
1. 前置準備
1.1 確認服務器環境
登入服務器,確認以下依賴已安裝:
docker --version # 需要 20.x 以上
docker compose version # 需要 v2.x(注意:是 docker compose 不是 docker-compose)
python3 --version # 需要 3.9 以上
pip3 --version如果 Docker Compose v2 未安裝:
sudo apt update
sudo apt install docker-compose-plugin1.2 建立項目目錄
mkdir -p ~/starsoup-infrastructure/services/script-to-video
cd ~/starsoup-infrastructure/services/script-to-video
# 建立子目錄
mkdir -p n8n/workflows
mkdir -p scripts
mkdir -p outputs
mkdir -p tmp1.3 確認 Cloudflare Tunnel 已在運行
# 確認 cloudflared 服務狀態
systemctl status cloudflared
# 或者查看 Docker 容器
docker ps | grep cloudflared2. Phase 1 — 部署 n8n
2.1 建立 docker-compose.yml
cd ~/starsoup-infrastructure/services/script-to-video
nano docker-compose.yml貼入以下內容:
version: "3.8"
services:
postgres:
image: postgres:15-alpine
container_name: starsoup-n8n-db
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: n8nio/n8n:latest
container_name: starsoup-n8n
restart: unless-stopped
ports:
- "5678:5678"
environment:
# 基本配置
- N8N_HOST=${N8N_HOST}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://${N8N_HOST}/
# 數據庫
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${DB_NAME}
- DB_POSTGRESDB_USER=${DB_USER}
- DB_POSTGRESDB_PASSWORD=${DB_PASSWORD}
# 安全
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=${N8N_USER}
- N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD}
# 執行設置
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=336 # 保留 14 天執行記錄
# 時區
- GENERIC_TIMEZONE=Asia/Tokyo
volumes:
- n8n_data:/home/node/.n8n
# 把服務器上的腳本目錄掛載進 n8n 容器
- ./scripts:/opt/starsoup/scripts
- ./outputs:/opt/starsoup/outputs
- /tmp/starsoup-pipeline:/tmp/starsoup-pipeline
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
n8n_data:2.2 建立 .env 文件
nano .env# 數據庫
DB_USER=starsoup
DB_PASSWORD=請替換為強密碼
DB_NAME=n8n
# n8n 訪問
N8N_HOST=n8n.你的域名.com # 替換為你的 Cloudflare 域名
N8N_USER=admin
N8N_PASSWORD=請替換為強密碼# 建立 .env.example(供 Git 追蹤)
cp .env .env.example
# 把 .env.example 的密碼全部改為佔位符
nano .env.example
# 確保 .env 不進入 Git
echo ".env" >> .gitignore2.3 啟動 n8n
docker compose up -d
# 查看啟動日誌
docker compose logs -f n8n啟動成功後,你會看到:
n8n ready on 0.0.0.0, port 56782.4 本地驗證
在服務器上測試(Cloudflare 配置好之前先用本地訪問):
curl http://localhost:5678
# 應該返回 n8n 的 HTML 頁面3. Phase 2 — Python 環境搭建
這些 Python 腳本跑在服務器上,由 n8n 通過 Execute Command 節點觸發。
3.1 安裝系統依賴
# FFmpeg(視頻合成必須)
sudo apt update
sudo apt install -y ffmpeg
# 確認安裝成功
ffmpeg -version3.2 安裝 Python 依賴
cd ~/starsoup-infrastructure/services/script-to-video/scripts
# 建立虛擬環境(推薦,避免污染系統環境)
python3 -m venv venv
source venv/bin/activate
# 建立 requirements.txt
cat > requirements.txt << 'EOF'
moviepy==1.0.3
edge-tts==6.1.9
requests==2.31.0
pillow==10.0.0
numpy==1.24.0
EOF
pip install -r requirements.txt3.3 建立合成腳本
nano ~/starsoup-infrastructure/services/script-to-video/scripts/compose_video.py#!/usr/bin/env python3
"""
Starsoup Script-to-Video Pipeline
視頻合成腳本 — 由 n8n Execute Command 節點觸發
用法:
python3 compose_video.py <job_id>
輸入:
/tmp/starsoup-pipeline/<job_id>/scenes.json
輸出:
/opt/starsoup/outputs/<job_id>.mp4
"""
import json
import sys
import os
from moviepy.editor import (
VideoFileClip,
AudioFileClip,
concatenate_videoclips,
concatenate_audioclips,
ColorClip,
TextClip,
CompositeVideoClip
)
def load_scenes(job_id: str) -> dict:
path = f"/tmp/starsoup-pipeline/{job_id}/scenes.json"
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def build_fallback_clip(duration: float, text: str) -> VideoFileClip:
"""當素材搜尋失敗時,生成黑底白字的 fallback 片段"""
bg = ColorClip(size=(1280, 720), color=(0, 0, 0), duration=duration)
txt = TextClip(text, fontsize=40, color="white", size=(1200, None),
method="caption").set_duration(duration)
return CompositeVideoClip([bg, txt.set_position("center")])
def compose(job_id: str):
meta = load_scenes(job_id)
scenes = meta["scenes"]
video_clips = []
audio_clips = []
for scene in scenes:
duration = float(scene["duration"])
# 視頻片段
clip_path = scene.get("clip_path")
if clip_path and os.path.exists(clip_path):
try:
clip = VideoFileClip(clip_path).subclip(0, duration)
# 統一解析度為 1280x720
clip = clip.resize((1280, 720))
except Exception as e:
print(f"[WARN] 場景 {scene['id']} 視頻加載失敗: {e},使用 fallback")
clip = build_fallback_clip(duration, scene.get("description", ""))
else:
print(f"[WARN] 場景 {scene['id']} 無素材,使用 fallback")
clip = build_fallback_clip(duration, scene.get("description", ""))
video_clips.append(clip)
# 音頻片段
audio_path = scene.get("audio_path")
if audio_path and os.path.exists(audio_path):
audio = AudioFileClip(audio_path)
# 如果音頻比視頻長,延伸視頻;如果短,填充靜音
if audio.duration > duration:
clip = clip.set_duration(audio.duration)
video_clips[-1] = clip
audio_clips.append(audio)
else:
# 靜音佔位
from moviepy.audio.AudioClip import AudioClip
silence = AudioClip(lambda t: 0, duration=duration, fps=44100)
audio_clips.append(silence)
# 拼接
print("[INFO] 開始拼接視頻...")
final_video = concatenate_videoclips(video_clips, method="compose")
final_audio = concatenate_audioclips(audio_clips)
final = final_video.set_audio(final_audio)
# 輸出
output_path = f"/opt/starsoup/outputs/{job_id}.mp4"
os.makedirs(os.path.dirname(output_path), exist_ok=True)
print(f"[INFO] 輸出到 {output_path}")
final.write_videofile(
output_path,
fps=24,
codec="libx264",
audio_codec="aac",
threads=4,
logger=None
)
# 清理臨時文件
import shutil
shutil.rmtree(f"/tmp/starsoup-pipeline/{job_id}", ignore_errors=True)
print(f"[SUCCESS] 合成完成:{output_path}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法:python3 compose_video.py <job_id>")
sys.exit(1)
compose(sys.argv[1])chmod +x scripts/compose_video.py3.4 建立語音生成腳本
nano scripts/generate_tts.py#!/usr/bin/env python3
"""
語音生成腳本(Edge TTS)
用法:python3 generate_tts.py <job_id> <scene_id> <text> [voice]
"""
import asyncio
import sys
import os
import edge_tts
async def generate(job_id: str, scene_id: str, text: str, voice: str):
output_dir = f"/tmp/starsoup-pipeline/{job_id}/audio"
os.makedirs(output_dir, exist_ok=True)
output_path = f"{output_dir}/{scene_id}.mp3"
communicate = edge_tts.Communicate(text, voice)
await communicate.save(output_path)
print(f"[SUCCESS] {output_path}")
if __name__ == "__main__":
job_id = sys.argv[1]
scene_id = sys.argv[2]
text = sys.argv[3]
voice = sys.argv[4] if len(sys.argv) > 4 else "zh-CN-XiaoxiaoNeural"
asyncio.run(generate(job_id, scene_id, text, voice))3.5 快速測試
cd ~/starsoup-infrastructure/services/script-to-video/scripts
source venv/bin/activate
# 測試 TTS
python3 generate_tts.py test001 scene_1 "你好,這是一段測試語音。"
ls /tmp/starsoup-pipeline/test001/audio/
# 應該出現 scene_1.mp3
# 播放確認(服務器上需要 sox)
# sudo apt install sox
# play /tmp/starsoup-pipeline/test001/audio/scene_1.mp34. Phase 3 — API Key 申請
4.1 Pexels API Key(免費)
- 前往 https://www.pexels.com/api/
- 登錄(或免費注冊)
- 點擊 "Your API Key" → 複製 Key
- 限制:每小時 200 次請求,每月無上限
4.2 Anthropic API Key
你已有 Anthropic API Key,直接使用即可。
模型選擇 claude-haiku-4-5-20251001(最便宜,場景解析完全夠用)。
4.3 飛書 Webhook(可選)
- 在飛書群中添加「自定義機器人」
- 獲取 Webhook URL(格式:
https://open.feishu.cn/open-apis/bot/v2/hook/xxx)
5. Phase 4 — 測試各個節點
在開始建 n8n Workflow 之前,先獨立測試每個環節。
5.1 測試 Pexels API
export PEXELS_API_KEY="你的Key"
curl -H "Authorization: $PEXELS_API_KEY" \
"https://api.pexels.com/videos/search?query=sunset+city&per_page=1" | python3 -m json.tool成功返回示例:
{
"videos": [
{
"id": 1234567,
"video_files": [
{ "quality": "hd", "file_type": "video/mp4", "link": "https://..." }
]
}
]
}5.2 測試 Claude Haiku 場景解析
export ANTHROPIC_API_KEY="你的Key"
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-haiku-4-5-20251001",
"max_tokens": 1024,
"messages": [{
"role": "user",
"content": "你是一個視頻場景分析師。給定以下腳本,拆分場景並生成關鍵詞。\n\n腳本:\n黃昏,城市街道。主角緩緩走過人群。\n主角:「今天又是這樣的一天。」\n\n僅輸出JSON,格式:{\"scenes\":[{\"id\":1,\"description\":\"...\",\"keywords\":[\"...\"],\"duration\":5,\"dialogue\":\"...\"}]}"
}]
}' | python3 -m json.tool5.3 測試完整 Python 合成腳本
# 手動準備一個測試 scenes.json
mkdir -p /tmp/starsoup-pipeline/test_job_001
mkdir -p /tmp/starsoup-pipeline/test_job_001/audio
# 先下載一個測試視頻(替換為上一步 Pexels 返回的真實 URL)
wget -O /tmp/starsoup-pipeline/test_job_001/clip_1.mp4 "Pexels視頻URL"
# 生成測試語音
cd ~/starsoup-infrastructure/services/script-to-video/scripts
source venv/bin/activate
python3 generate_tts.py test_job_001 scene_1 "今天又是這樣的一天。"
# 建立 scenes.json
cat > /tmp/starsoup-pipeline/test_job_001/scenes.json << 'EOF'
{
"scenes": [
{
"id": 1,
"description": "黃昏城市街道",
"duration": 5,
"clip_path": "/tmp/starsoup-pipeline/test_job_001/clip_1.mp4",
"audio_path": "/tmp/starsoup-pipeline/test_job_001/audio/scene_1.mp3"
}
]
}
EOF
# 執行合成
python3 compose_video.py test_job_001
# 確認輸出
ls -lh ~/starsoup-infrastructure/services/script-to-video/outputs/6. Phase 5 — 建立 n8n Workflow
所有節點測試通過後,進入 n8n 界面搭建 Workflow。
訪問:http://localhost:5678(本地測試)或 https://n8n.你的域名.com
6.1 設置 Credentials
在 n8n 界面左側選 Credentials → New:
Anthropic API
- Type: Header Auth
- Name:
x-api-key - Value:
你的 Anthropic API Key
Pexels API
- Type: Header Auth
- Name:
Authorization - Value:
你的 Pexels API Key
6.2 Workflow 節點配置
新建 Workflow,按以下順序添加節點:
節點 1:Webhook(觸發器)
- Node Type: Webhook
- HTTP Method:
POST - Path:
script-to-video - Authentication: Basic Auth(設置用戶名密碼)
- Response Mode:
Respond Immediately
測試 URL(建好後):
POST https://n8n.你的域名.com/webhook/script-to-video
Body: { "script": "腳本內容..." }節點 2:生成 Job ID + 準備目錄
- Node Type: Code
const jobId = `job_${Date.now()}`;
const script = $input.first().json.body.script;
// 在服務器上建立臨時目錄(通過後續 Execute Command 節點完成)
return [{
json: {
jobId,
script
}
}];節點 3:Claude Haiku 場景解析
- Node Type: HTTP Request
- Method:
POST - URL:
https://api.anthropic.com/v1/messages - Authentication:
Predefined Credential Type→ 選 Anthropic Credential - Headers:
anthropic-version:2023-06-01content-type:application/json
- Body (JSON):
{
"model": "claude-haiku-4-5-20251001",
"max_tokens": 2048,
"messages": [{
"role": "user",
"content": "你是一個視頻場景分析師。給定以下戲劇腳本,請:\n1. 切分為若干場景(每個場景5-15秒)\n2. 每個場景給3個英文搜索關鍵詞\n3. 估算場景時長(中文每秒3字)\n4. 提取台詞\n\n僅輸出純JSON,不要任何解釋。格式:\n{\"scenes\":[{\"id\":1,\"description\":\"場景描述\",\"keywords\":[\"kw1\",\"kw2\",\"kw3\"],\"duration\":6,\"dialogue\":\"台詞\"}]}\n\n腳本:\n={{ $json.script }}"
}]
}節點 4:解析 Claude 返回 JSON
- Node Type: Code
const response = $input.first().json;
const content = response.content[0].text;
// 清理可能的 markdown 代碼塊
const cleaned = content.replace(/```json|```/g, "").trim();
const parsed = JSON.parse(cleaned);
return [{
json: {
jobId: $("生成 Job ID + 準備目錄").first().json.jobId,
scenes: parsed.scenes
}
}];節點 5:建立臨時目錄
- Node Type: Execute Command
- Command:
JOB_ID={{ $json.jobId }}
mkdir -p /tmp/starsoup-pipeline/$JOB_ID/clips
mkdir -p /tmp/starsoup-pipeline/$JOB_ID/audio
echo "目錄建立完成:/tmp/starsoup-pipeline/$JOB_ID"節點 6:Split In Batches(每個場景獨立處理)
- Node Type: Split In Batches
- Batch Size:
1 - 輸入:
scenes數組(需先用 Code 節點把數組 flatten)
節點 7:Pexels 搜索視頻
- Node Type: HTTP Request
- Method:
GET - URL:
https://api.pexels.com/videos/search - Authentication: Pexels Credential
- Query Parameters:
query: 使用下方 n8n 表達式per_page:3orientation:landscape
={{ $json.keywords[0] }}節點 8:下載視頻片段
- Node Type: Code
const scene = $input.first().json;
const pexelsData = $("Pexels搜索視頻").first().json;
// 找到最合適的視頻(HD 品質,時長 >= 場景時長)
let videoUrl = null;
if (pexelsData.videos && pexelsData.videos.length > 0) {
for (const video of pexelsData.videos) {
if (video.duration >= scene.duration) {
// 找 HD 或最高品質
const hdFile = video.video_files.find(f => f.quality === "hd");
const anyFile = video.video_files[0];
videoUrl = hdFile ? hdFile.link : anyFile.link;
break;
}
}
// 如果沒有足夠長的,就取第一個
if (!videoUrl) {
videoUrl = pexelsData.videos[0].video_files[0].link;
}
}
return [{
json: {
...scene,
videoUrl: videoUrl,
jobId: $("解析 Claude 返回 JSON").first().json.jobId
}
}];節點 9:下載視頻到服務器
- Node Type: Execute Command
JOB_ID={{ $json.jobId }}
SCENE_ID={{ $json.id }}
VIDEO_URL="{{ $json.videoUrl }}"
OUTPUT="/tmp/starsoup-pipeline/$JOB_ID/clips/scene_$SCENE_ID.mp4"
if [ -n "$VIDEO_URL" ]; then
wget -q -O "$OUTPUT" "$VIDEO_URL"
echo "下載完成:$OUTPUT"
else
echo "無視頻URL,將使用fallback"
fi節點 10:生成語音
- Node Type: Execute Command
JOB_ID={{ $json.jobId }}
SCENE_ID={{ $json.id }}
DIALOGUE="{{ $json.dialogue }}"
cd /opt/starsoup/scripts
source venv/bin/activate
python3 generate_tts.py "$JOB_ID" "scene_$SCENE_ID" "$DIALOGUE"節點 11:合併所有場景數據 + 生成 scenes.json
所有場景循環完成後:
- Node Type: Code
// 收集所有批次的結果
const allScenes = $input.all().map(item => {
const s = item.json;
return {
id: s.id,
description: s.description,
duration: s.duration,
clip_path: `/tmp/starsoup-pipeline/${s.jobId}/clips/scene_${s.id}.mp4`,
audio_path: `/tmp/starsoup-pipeline/${s.jobId}/audio/scene_${s.id}.mp3`
};
});
const jobId = $input.first().json.jobId;
const scenesJson = JSON.stringify({ scenes: allScenes }, null, 2);
return [{
json: { jobId, scenesJson }
}];節點 12:寫入 scenes.json
- Node Type: Execute Command
JOB_ID={{ $json.jobId }}
cat > /tmp/starsoup-pipeline/$JOB_ID/scenes.json << 'JSONEOF'
{{ $json.scenesJson }}
JSONEOF
echo "scenes.json 寫入完成"節點 13:執行視頻合成
- Node Type: Execute Command
- Timeout:
600(視頻合成可能需要幾分鐘)
JOB_ID={{ $json.jobId }}
cd /opt/starsoup/scripts
source venv/bin/activate
python3 compose_video.py "$JOB_ID"
echo "EXIT_CODE:$?"節點 14:If 判斷是否成功
- Node Type: If
- Condition: 讀取下方 n8n 表達式輸出,並判斷是否包含
SUCCESS
{{ $json.stdout }}成功分支 → 節點 15:飛書通知成功
- Node Type: HTTP Request
- Method:
POST - URL:
你的飛書 Webhook URL - Body:
{
"msg_type": "text",
"content": {
"text": "✅ 視頻合成完成!\nJob ID: {{ $json.jobId }}\n文件路徑:/opt/starsoup/outputs/{{ $json.jobId }}.mp4"
}
}失敗分支 → 節點 16:飛書通知失敗
{
"msg_type": "text",
"content": {
"text": "❌ 視頻合成失敗\nJob ID: {{ $json.jobId }}\n錯誤:{{ $json.stderr }}"
}
}6.3 激活 Workflow
節點全部連接完成後:
- 點擊右上角 Save
- 點擊 Activate 開關(變成綠色)
- Workflow 開始監聽 Webhook
7. Phase 6 — Cloudflare Tunnel 暴露
7.1 在 Cloudflare Zero Trust 添加 n8n 服務
登入 Cloudflare Zero Trust Dashboard
Networks → Tunnels → 選擇你的 Tunnel → Edit
添加 Public Hostname:
| 字段 | 值 |
|---|---|
| Subdomain | n8n |
| Domain | 你的域名.com |
| Service Type | HTTP |
| URL | localhost:5678 |
7.2 測試 Webhook 訪問
curl -X POST https://n8n.你的域名.com/webhook/script-to-video \
-H "Content-Type: application/json" \
-u "webhook用戶名:webhook密碼" \
-d '{"script": "黃昏,城市街道。主角緩緩走過人群。\n主角:今天又是這樣的一天。"}'成功後你應該很快收到飛書通知。
8. 常見問題
n8n 容器啟動失敗
# 查看詳細錯誤
docker compose logs n8n
# 最常見原因:postgres 還沒 ready
# 解決:等待 postgres healthcheck 通過再重試
docker compose restart n8nMoviePy 合成報錯 ImageMagick not found
sudo apt install imagemagick
# 如果有字幕/文字疊加需求才需要這個Execute Command 節點找不到 Python
n8n 容器內沒有 Python,所以腳本必須跑在宿主機上。解決方案:
# 在 docker-compose.yml 的 n8n 服務中,把命令改為:
# 通過 ssh 或 docker exec 在宿主機執行
# 或者:直接用宿主機的 cron/API 代替 Execute Command更好的方案:建立一個輕量的 Python API 容器,n8n 通過 HTTP 調用它:
# 在 docker-compose.yml 添加
python-runner:
build: ./python-runner
container_name: starsoup-python-runner
ports:
- "8888:8888"
volumes:
- ./scripts:/app/scripts
- ./outputs:/opt/starsoup/outputs
- /tmp/starsoup-pipeline:/tmp/starsoup-pipeline視頻合成很慢
# 查看服務器 CPU 核心數
nproc
# 在 compose_video.py 中調整 threads 參數
final.write_videofile(..., threads=4) # 改為你的核心數9. 目錄結構總覽
~/starsoup-infrastructure/services/script-to-video/
├── docker-compose.yml # n8n + PostgreSQL 容器配置
├── .env # 環境變量(不入 Git)
├── .env.example # 環境變量模板(入 Git)
├── .gitignore
├── AGENTS.md # AI Agent 上下文導航
│
├── scripts/
│ ├── venv/ # Python 虛擬環境(不入 Git)
│ ├── requirements.txt
│ ├── compose_video.py # 視頻合成主腳本
│ └── generate_tts.py # 語音生成腳本
│
├── n8n/
│ └── workflows/
│ └── script-to-video.json # n8n Workflow 導出備份
│
├── outputs/ # 最終視頻輸出(掛載進容器)
└── tmp/ # 說明文件(實際臨時文件在 /tmp)快速參考:每個視頻的成本估算
| 環節 | 費用 |
|---|---|
| Claude Haiku 場景解析(~500 tokens) | ~$0.003 |
| Pexels 素材下載 | $0 |
| Edge TTS 語音 | $0 |
| FFmpeg + MoviePy 合成 | $0(服務器算力) |
| 每個5分鐘視頻合計 | < $0.01 |
Starsoup Internal Documentation — 2026-03-19