Skip to content

Starsoup Script-to-Video Pipeline

搭建說明書 v1.0

適用服務器:Starsoup Server A(Ubuntu + Docker + Cloudflare Tunnel)
預計搭建時間:3~4 小時
難度:中等(需要基本 Docker 和命令行操作能力)


目錄

  1. 前置準備
  2. Phase 1 — 部署 n8n
  3. Phase 2 — Python 環境搭建
  4. Phase 3 — API Key 申請
  5. Phase 4 — 測試各個節點
  6. Phase 5 — 建立 n8n Workflow
  7. Phase 6 — Cloudflare Tunnel 暴露
  8. 常見問題
  9. 目錄結構總覽

1. 前置準備

1.1 確認服務器環境

登入服務器,確認以下依賴已安裝:

bash
docker --version        # 需要 20.x 以上
docker compose version  # 需要 v2.x(注意:是 docker compose 不是 docker-compose)
python3 --version       # 需要 3.9 以上
pip3 --version

如果 Docker Compose v2 未安裝:

bash
sudo apt update
sudo apt install docker-compose-plugin

1.2 建立項目目錄

bash
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 tmp

1.3 確認 Cloudflare Tunnel 已在運行

bash
# 確認 cloudflared 服務狀態
systemctl status cloudflared
# 或者查看 Docker 容器
docker ps | grep cloudflared

2. Phase 1 — 部署 n8n

2.1 建立 docker-compose.yml

bash
cd ~/starsoup-infrastructure/services/script-to-video
nano docker-compose.yml

貼入以下內容:

yaml
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 文件

bash
nano .env
env
# 數據庫
DB_USER=starsoup
DB_PASSWORD=請替換為強密碼
DB_NAME=n8n

# n8n 訪問
N8N_HOST=n8n.你的域名.com       # 替換為你的 Cloudflare 域名
N8N_USER=admin
N8N_PASSWORD=請替換為強密碼
bash
# 建立 .env.example(供 Git 追蹤)
cp .env .env.example
# 把 .env.example 的密碼全部改為佔位符
nano .env.example

# 確保 .env 不進入 Git
echo ".env" >> .gitignore

2.3 啟動 n8n

bash
docker compose up -d

# 查看啟動日誌
docker compose logs -f n8n

啟動成功後,你會看到:

n8n ready on 0.0.0.0, port 5678

2.4 本地驗證

在服務器上測試(Cloudflare 配置好之前先用本地訪問):

bash
curl http://localhost:5678
# 應該返回 n8n 的 HTML 頁面

3. Phase 2 — Python 環境搭建

這些 Python 腳本跑在服務器上,由 n8n 通過 Execute Command 節點觸發。

3.1 安裝系統依賴

bash
# FFmpeg(視頻合成必須)
sudo apt update
sudo apt install -y ffmpeg

# 確認安裝成功
ffmpeg -version

3.2 安裝 Python 依賴

bash
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.txt

3.3 建立合成腳本

bash
nano ~/starsoup-infrastructure/services/script-to-video/scripts/compose_video.py
python
#!/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])
bash
chmod +x scripts/compose_video.py

3.4 建立語音生成腳本

bash
nano scripts/generate_tts.py
python
#!/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 快速測試

bash
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.mp3

4. Phase 3 — API Key 申請

4.1 Pexels API Key(免費)

  1. 前往 https://www.pexels.com/api/
  2. 登錄(或免費注冊)
  3. 點擊 "Your API Key" → 複製 Key
  4. 限制:每小時 200 次請求,每月無上限

4.2 Anthropic API Key

你已有 Anthropic API Key,直接使用即可。

模型選擇 claude-haiku-4-5-20251001(最便宜,場景解析完全夠用)。

4.3 飛書 Webhook(可選)

  1. 在飛書群中添加「自定義機器人」
  2. 獲取 Webhook URL(格式:https://open.feishu.cn/open-apis/bot/v2/hook/xxx

5. Phase 4 — 測試各個節點

在開始建 n8n Workflow 之前,先獨立測試每個環節。

5.1 測試 Pexels API

bash
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

成功返回示例:

json
{
  "videos": [
    {
      "id": 1234567,
      "video_files": [
        { "quality": "hd", "file_type": "video/mp4", "link": "https://..." }
      ]
    }
  ]
}

5.2 測試 Claude Haiku 場景解析

bash
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.tool

5.3 測試完整 Python 合成腳本

bash
# 手動準備一個測試 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 界面左側選 CredentialsNew

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
javascript
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-01
    • content-type: application/json
  • Body (JSON):
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
javascript
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:
bash
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: 3
    • orientation: landscape
text
={{ $json.keywords[0] }}

節點 8:下載視頻片段

  • Node Type: Code
javascript
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
bash
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
bash
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
javascript
// 收集所有批次的結果
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
bash
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(視頻合成可能需要幾分鐘)
bash
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
text
{{ $json.stdout }}

成功分支 → 節點 15:飛書通知成功

  • Node Type: HTTP Request
  • Method: POST
  • URL: 你的飛書 Webhook URL
  • Body:
json
{
  "msg_type": "text",
  "content": {
    "text": "✅ 視頻合成完成!\nJob ID: {{ $json.jobId }}\n文件路徑:/opt/starsoup/outputs/{{ $json.jobId }}.mp4"
  }
}

失敗分支 → 節點 16:飛書通知失敗

json
{
  "msg_type": "text",
  "content": {
    "text": "❌ 視頻合成失敗\nJob ID: {{ $json.jobId }}\n錯誤:{{ $json.stderr }}"
  }
}

6.3 激活 Workflow

節點全部連接完成後:

  1. 點擊右上角 Save
  2. 點擊 Activate 開關(變成綠色)
  3. Workflow 開始監聽 Webhook

7. Phase 6 — Cloudflare Tunnel 暴露

7.1 在 Cloudflare Zero Trust 添加 n8n 服務

登入 Cloudflare Zero Trust Dashboard

Networks → Tunnels → 選擇你的 Tunnel → Edit

添加 Public Hostname:

字段
Subdomainn8n
Domain你的域名.com
Service TypeHTTP
URLlocalhost:5678

7.2 測試 Webhook 訪問

bash
curl -X POST https://n8n.你的域名.com/webhook/script-to-video \
  -H "Content-Type: application/json" \
  -u "webhook用戶名:webhook密碼" \
  -d '{"script": "黃昏,城市街道。主角緩緩走過人群。\n主角:今天又是這樣的一天。"}'

成功後你應該很快收到飛書通知。


8. 常見問題

n8n 容器啟動失敗

bash
# 查看詳細錯誤
docker compose logs n8n

# 最常見原因:postgres 還沒 ready
# 解決:等待 postgres healthcheck 通過再重試
docker compose restart n8n

MoviePy 合成報錯 ImageMagick not found

bash
sudo apt install imagemagick
# 如果有字幕/文字疊加需求才需要這個

Execute Command 節點找不到 Python

n8n 容器內沒有 Python,所以腳本必須跑在宿主機上。解決方案:

bash
# 在 docker-compose.yml 的 n8n 服務中,把命令改為:
# 通過 ssh 或 docker exec 在宿主機執行
# 或者:直接用宿主機的 cron/API 代替 Execute Command

更好的方案:建立一個輕量的 Python API 容器,n8n 通過 HTTP 調用它:

yaml
# 在 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

視頻合成很慢

bash
# 查看服務器 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