# 已验证的ffmpeg视频制作流程（2026-06-12）

本记录来自成功制作世界杯集锦视频和科技科普视频的经验。

## 核心原则

**永远不要一次性做全部步骤。** 分步执行，每一步独立验证。

## 步骤1：下载素材

用curl从Mixkit/Coverr下载即可。TikTok India可以用yt-dlp（无需登录）。

```bash
curl -sL -o output.mp4 "https://assets.mixkit.co/videos/XXXX/XXXX-720.mp4"
```

## 步骤2：生成TTS

用Python脚本（edge_tts）一次性生成所有TTS音频：

```python
import asyncio, edge_tts
async def gen(text, out):
    c = edge_tts.Communicate(text, "zh-CN-YunxiNeural", rate="+5%")
    await c.save(out)
asyncio.run(gen("文本内容", "tts_00.mp3"))
```

可用语音：
- `zh-CN-XiaoxiaoNeural` — 女声，之前成功
- `zh-CN-YunxiNeural` — 男声，本次成功（优选）
- rate参数："+5%"或"+10%"

## 步骤3：给视频加音频（关键）

**用 `-c:v copy` 跳过视频重编码，只加音频流：**

```python
import subprocess
cmd = [
    "ffmpeg", "-y",
    "-i", "input.mp4",   # 已有视频（无音频）
    "-i", "tts_00.mp3",  # 新音频
    "-c:v", "copy",      # 不重编码视频（最快，最可靠）
    "-c:a", "aac", "-b:a", "128k",
    "-map", "0:v:0",     # 视频从第一个input取
    "-map", "1:a:0",     # 音频从第二个input取
    "-shortest",         # 裁剪到最短流的长度
    "-movflags", "+faststart",
    "output.mp4"
]
subprocess.run(cmd)
```

**注意：** `-c:v copy` 是关键。之前用 `-vf scale=...｀ 的调试图踩到了filter参数中逗号被Python解析的问题（因为subprocess的cmd是list, 逗号在fiter字符串内是合法的，但`-vf`本身必须是list中的一个元素，不能带空格拆分）。

## 步骤4：拼接所有片段

```python
with open("concat.txt", "w") as f:
    for c in combined:
        f.write(f"file '{c}'\n")

# 用-c copy（不重编码，秒完成）
subprocess.run([
    "ffmpeg", "-y", "-f", "concat", "-safe", "0",
    "-i", "concat.txt",
    "-c", "copy",                # 不重编码！
    "-movflags", "+faststart",
    "merged.mp4"
])
```

## 步骤5：提供HTTP访问

```python
# 复制到/tmp
import subprocess
subprocess.run(["cp", "merged.mp4", "/tmp/final.mp4"])

# 外部地址
# http://157.173.212.215:8080/final.mp4
```

## 完整Python脚本模板（execute_code内使用）

```python
import subprocess, os, asyncio, edge_tts

os.chdir("/root/videos")

clips = [
    ("素材文件.mp4", "标题", "解说词内容..."),
    # ...更多片段
]

async def gen_tts(text, out):
    c = edge_tts.Communicate(text, "zh-CN-YunxiNeural", rate="+5%")
    await c.save(out)

async def main():
    # Step 1: TTS
    tts_files = []
    for i, (clip, title, desc) in enumerate(clips):
        out = f"tts_{i:02d}.mp3"
        await gen_tts(desc, out)
        tts_files.append(out)
        print(f"  {out}")

    # Step 2: 加音频 (-c:v copy)
    combined = []
    for i, (clip, title, desc) in enumerate(clips):
        tts = tts_files[i]
        out = f"audio_{i:02d}.mp4"
        cmd = ["ffmpeg", "-y", "-i", clip, "-i", tts,
            "-c:v", "copy", "-c:a", "aac", "-b:a", "128k",
            "-map", "0:v:0", "-map", "1:a:0", "-shortest",
            "-movflags", "+faststart", out]
        subprocess.run(cmd)
        combined.append(out)

    # Step 3: 拼接 (-c copy)
    with open("concat.txt", "w") as f:
        for c in combined:
            f.write(f"file '{c}'\n")
    subprocess.run(["ffmpeg", "-y", "-f", "concat", "-safe", "0",
        "-i", "concat.txt", "-c", "copy", "-movflags", "+faststart",
        "merged.mp4"])

    # Step 4: 提供访问
    subprocess.run(["cp", "merged.mp4", "/tmp/merged.mp4"])

asyncio.run(main())
```

## 已知问题

1. **ffmpeg filter内的逗号**：`-vf "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-hh)/2"` 中的逗号是合法的filter分隔符，但在Python的subprocess.run(cmd_list)模式下，如果cmd是list，每个元素必须正确拆分。如果filter字符串被意外拆分，ffmpeg会收不到完整的filter参数。**解决方案：** 用 `-c:v copy` 完全跳过filter。
2. **TTS生成错误**：如果edge-tts的voice name不合法（带特殊字符），会静默失败生成0字节文件。**解决方案：** 检查文件大小，如果为0则重试。
3. **HTTP服务器路径问题**：端口8080的SimpleHTTP根目录是执行命令时的工作目录。用 `cd /tmp && python3 -m http.server 8080` 确保根目录是/tmp。
