GPT Proto
Home/Skills/youtube-clipper

youtube-clipper

It helps you download long videos from YouTube, analyzes the video to provide a "long-to-short" editing proposal, and automatically adds bilingual Chinese and English subtitles.

Download for Windows

TECHNICAL_NOTES.md

# 技术坑点记录

本文档记录 YouTube Clipper Skill 开发过程中遇到的关键技术问题和解决方案。

## 1. FFmpeg libass 支持问题

### 问题描述
标准 Homebrew FFmpeg 不包含 libass 库,导致无法使用 `subtitles` 滤镜烧录字幕。

### 错误信息
```
No such filter: 'subtitles'
```

或者在检查滤镜时:
```bash
$ ffmpeg -filters 2>&1 | grep subtitles
# 无输出
```

### 根本原因
- Homebrew 的标准 `ffmpeg` formula 为了减小包体积,不包含某些非核心库
- libass 是字幕渲染库,用于 `subtitles` 滤镜
- 没有 libass,FFmpeg 无法烧录字幕到视频

### 解决方案

#### macOS
使用 `ffmpeg-full` 替代标准 FFmpeg:

```bash
# 安装 ffmpeg-full
brew install ffmpeg-full

# 路径(Apple Silicon)
/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg

# 路径(Intel)
/usr/local/opt/ffmpeg-full/bin/ffmpeg

# 验证 libass 支持
/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg -filters 2>&1 | grep subtitles
```

#### 其他系统
从源码编译 FFmpeg,确保包含 libass:

```bash
# Ubuntu/Debian
sudo apt-get install libass-dev
./configure --enable-libass
make
sudo make install

# 验证
ffmpeg -filters 2>&1 | grep subtitles
```

### 检测逻辑
`burn_subtitles.py` 中实现的检测逻辑:

1. 优先检查 `ffmpeg-full` 路径(macOS)
2. 检查标准 `ffmpeg` 是否支持 libass
3. 如果都不满足,提示安装指南

```python
def detect_ffmpeg_variant():
    # 检查 ffmpeg-full(macOS)
    if platform.system() == 'Darwin':
        full_path = '/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg'
        if Path(full_path).exists():
            return {'type': 'full', 'path': full_path}

    # 检查标准 ffmpeg
    standard_path = shutil.which('ffmpeg')
    if standard_path:
        has_libass = check_libass_support(standard_path)
        return {'has_libass': has_libass}
```

---

## 2. 文件路径空格问题

### 问题描述
FFmpeg `subtitles` 滤镜无法正确处理包含空格的文件路径,即使使用引号或转义也无效。

### 错误信息
```
[Parsed_subtitles_0 @ 0x...] Unable to find '/path/with'
```

注意路径被截断在空格处(`/path/with spaces``/path/with`)。

### 示例
```bash
# 失败的尝试
ffmpeg -i video.mp4 -vf "subtitles='/path/with spaces/sub.srt'" output.mp4
ffmpeg -i video.mp4 -vf "subtitles=/path/with\ spaces/sub.srt" output.mp4
ffmpeg -i video.mp4 -vf subtitles="'/path/with spaces/sub.srt'" output.mp4

# 都会报错:Unable to find '/path/with'
```

### 根本原因
FFmpeg `subtitles` 滤镜的路径解析存在 bug,无法正确处理:
- 引号内的空格
- 转义的空格
- 混合引号

这是 FFmpeg 的已知限制。

### 解决方案:使用临时目录

核心思路:将文件复制到**无空格路径**的临时目录,处理后再移回。

```python
import tempfile
import shutil

def burn_subtitles(video_path, subtitle_path, output_path):
    # 1. 创建临时目录(路径保证无空格)
    temp_dir = tempfile.mkdtemp(prefix='youtube_clipper_')
    # 例如: /tmp/youtube_clipper_abc123

    try:
        # 2. 复制文件到临时目录
        temp_video = os.path.join(temp_dir, 'video.mp4')
        temp_subtitle = os.path.join(temp_dir, 'subtitle.srt')
        shutil.copy(video_path, temp_video)
        shutil.copy(subtitle_path, temp_subtitle)

        # 3. 执行 FFmpeg(路径无空格)
        cmd = [
            'ffmpeg',
            '-i', temp_video,
            '-vf', f'subtitles={temp_subtitle}',
            temp_output
        ]
        subprocess.run(cmd, check=True)

        # 4. 移动输出文件到目标位置
        shutil.move(temp_output, output_path)

    finally:
        # 5. 清理临时目录
        shutil.rmtree(temp_dir, ignore_errors=True)
```

### 为什么这样有效?
- `tempfile.mkdtemp()` 生成的路径不包含空格(通常是 `/tmp/xxx`
- FFmpeg 可以正确处理无空格的路径
- 对用户透明,输入输出可以有任意路径

### 其他尝试过但无效的方案
❌ 使用双引号:`subtitles="/path/with spaces/sub.srt"`
❌ 使用单引号:`subtitles='/path/with spaces/sub.srt'`
❌ 转义空格:`subtitles=/path/with\ spaces/sub.srt`
❌ 混合引号:`subtitles="'/path/with spaces/sub.srt'"`
❌ FFmpeg `-filter_complex`:仍然有同样问题

**唯一有效**:临时目录方案

---

## 3. VTT 转 SRT 格式转换

### 格式差异

| 项目 | VTT | SRT |
|------|-----|-----|
| 头部 | `WEBVTT` | 无 |
| 序号 | 可选 | 必需(从1开始) |
| 时间分隔符 | `.` (点) | `,` (逗号) |
| 样式信息 | 支持 | 不支持 |

### 时间戳格式

```
VTT:  00:00:00.000 --> 00:00:03.500
SRT:  00:00:00,000 --> 00:00:03,500
              ↑                  ↑
            逗号                逗号
```

### 转换实现

```python
def vtt_to_srt(vtt_path, srt_path):
    # 1. 移除 WEBVTT 头部
    content = content.replace('WEBVTT\n\n', '')

    # 2. 移除样式信息
    content = re.sub(r'STYLE.*?-->', '', content, flags=re.DOTALL)

    # 3. 转换时间戳分隔符
    # . → , (仅在时间戳中)
    content = re.sub(
        r'(\d{2}:\d{2}:\d{2})\.(\d{3})',
        r'\1,\2',
        content
    )

    # 4. 添加序号(如果没有)
    # ...
```

### 注意事项
- VTT 可能包含位置信息(`align:start position:0%`),需要移除
- VTT 可能有多行文本,转 SRT 时保持多行
- 时间戳格式严格:`HH:MM:SS,mmm`(必须有小时)

---

## 4. 字幕时间戳调整

### 问题描述
剪辑视频后,字幕时间戳需要相对于新的起始时间。

### 示例
原视频:
```
[00:02:00] 字幕1
[00:02:03] 字幕2
[00:02:06] 字幕3
```

剪辑 02:00-02:10 后,字幕应该变为:
```
[00:00:00] 字幕1
[00:00:03] 字幕2
[00:00:06] 字幕3
```

### 实现
```python
def adjust_subtitle_time(time_seconds, offset):
    """
    调整字幕时间戳

    Args:
        time_seconds: 原始时间(秒)
        offset: 偏移量(秒),即剪辑起始时间

    Returns:
        float: 调整后的时间
    """
    adjusted = time_seconds - offset
    return max(0.0, adjusted)  # 确保不为负数
```

### 边界情况处理
1. 字幕完全在时间范围内:保留
2. 字幕完全在时间范围外:丢弃
3. 字幕跨越边界:
   - 起始时间调整为 0(如果在范围前)
   - 结束时间调整为片段时长(如果在范围后)

---

## 5. 批量翻译优化

### 问题
逐条翻译字幕会产生大量 API 调用,速度慢且成本高。

### 数据
- 一个 30 分钟视频:约 600 条字幕
- 逐条翻译:600 次 API 调用
- 批量翻译(20条/批):30 次 API 调用
- **节省 95% API 调用**

### 实现策略

```python
def translate_batch(subtitles, batch_size=20):
    batches = []
    for i in range(0, len(subtitles), batch_size):
        batch = subtitles[i:i + batch_size]
        batches.append(batch)

    # 每批一起翻译
    for batch in batches:
        # 合并为单个文本
        batch_text = '\n'.join([sub['text'] for sub in batch])

        # 一次 API 调用翻译整批
        translations = translate_text(batch_text)

        # 分配翻译结果
        # ...
```

### 批量大小选择
- **20 条**是平衡点:
  - 小于 20:API 调用过多
  - 大于 30:单次输入过长,翻译质量下降
  - 20-25:最佳范围

### 翻译质量保证
批量翻译时需要:
1. 保持上下文连贯性
2. 每条字幕单独翻译(不要合并)
3. 返回 JSON 数组,顺序对应

---

## 6. yt-dlp 最佳实践

### 格式选择
```python
'format': 'bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4]/best'
```

解释:
- `bestvideo[height<=1080]`:视频最高 1080p
- `[ext=mp4]`:优先 mp4 格式(兼容性好)
- `+bestaudio[ext=m4a]`:合并最佳音频
- `/best[height<=1080][ext=mp4]`:备选方案
- `/best`:最终备选

### 为什么限制 1080p?
1. 文件大小:4K 视频太大(可能 5-10GB)
2. 处理速度:FFmpeg 处理时间长
3. 输出场景:短视频平台主要是 1080p 或更低
4. 存储空间:节省磁盘

### 字幕下载
```python
'writesubtitles': True,
'writeautomaticsub': True,  # 自动字幕作为备选
'subtitleslangs': ['en'],   # 英文字幕
'subtitlesformat': 'vtt',   # VTT 格式
```

优先级:
1. 人工字幕(如果有)
2. 自动字幕(YouTube 自动生成)

### 输出模板
```python
'outtmpl': '%(title)s [%(id)s].%(ext)s'
```

结果示例:
```
Anthropic's Amodei on AI [Ckt1cj0xjRM].mp4
Anthropic's Amodei on AI [Ckt1cj0xjRM].en.vtt
```

包含视频 ID 的好处:
- 唯一性:不会重复
- 可追溯:可以找到原视频

---

## 7. 双语字幕样式

### SRT 格式双语
```srt
1
00:00:00,000 --> 00:00:03,500
This is English subtitle
这是中文字幕

2
00:00:03,500 --> 00:00:07,000
Another English line
另一行中文
```

### FFmpeg 烧录样式
```bash
subtitles=subtitle.srt:force_style='FontSize=24,MarginV=30'
```

参数说明:
- `FontSize=24`:字体大小(适合 1080p)
- `MarginV=30`:底部边距(像素)
- 默认:白色文字 + 黑色描边

### 样式调整建议

| 视频分辨率 | FontSize | MarginV |
|-----------|----------|---------|
| 720p      | 20       | 20      |
| 1080p     | 24       | 30      |
| 4K        | 48       | 60      |

---

## 8. Python 依赖管理

### 必需依赖
```bash
pip install yt-dlp pysrt python-dotenv
```

- `yt-dlp`:YouTube 视频下载
- `pysrt`:SRT 字幕解析和操作
- `python-dotenv`:环境变量管理(可选)

### 导入错误处理
```python
try:
    import yt_dlp
except ImportError:
    print("❌ Error: yt-dlp not installed")
    print("Please install: pip install yt-dlp")
    sys.exit(1)
```

在每个脚本中检查依赖,给出清晰的安装指导。

---

## 9. 跨平台路径处理

### 使用 pathlib
```python
from pathlib import Path

# ✅ 推荐
video_path = Path('/path/to/video.mp4')
if video_path.exists():
    ...

# ❌ 避免
video_path = '/path/to/video.mp4'
if os.path.exists(video_path):
    ...
```

### 路径拼接
```python
# ✅ 推荐
output_path = output_dir / 'video.mp4'

# ❌ 避免
output_path = output_dir + '/video.mp4'  # 在 Windows 上失败
```

---

## 10. 错误处理最佳实践

### 详细错误信息
```python
try:
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"❌ Command failed:")
        print(f"   Command: {' '.join(cmd)}")
        print(f"   Return code: {result.returncode}")
        print(f"   Error output:")
        print(result.stderr)
        raise RuntimeError("Command failed")
except Exception as e:
    print(f"❌ Error: {str(e)}")
    import traceback
    traceback.print_exc()
    sys.exit(1)
```

### 用户友好的错误消息
```python
# ❌ 不好
raise Exception("FFmpeg failed")

# ✅ 好
raise RuntimeError(
    "FFmpeg does not support libass (subtitles filter). "
    "Please install ffmpeg-full: brew install ffmpeg-full"
)
```

---

## 总结

| 问题 | 解决方案 | 优先级 |
|------|---------|--------|
| FFmpeg libass 缺失 | 安装 ffmpeg-full | 🔴 必须 |
| 路径空格问题 | 使用临时目录 | 🔴 必须 |
| VTT → SRT | 转换时间分隔符 | 🟡 重要 |
| 字幕时间调整 | 减去起始时间 | 🟡 重要 |
| API 调用过多 | 批量翻译(20条/批)| 🟢 优化 |
| 文件过大 | 限制 1080p | 🟢 优化 |

所有关键问题都有经过验证的解决方案,可以直接使用。