---
name: sports-news-video-production
description: 张哥的世界杯新闻视频制作流程——从下载比赛录像到输出中文配音新闻视频
version: 2.1
author: 星璇
tags: [news, video, sports, ffmpeg, world-cup, chinese]
---

# 体育新闻视频制作流程

## 适用场景
制作世界杯/体育比赛新闻视频，要求：
- 真实比赛画面（从中国可访问的网站下载）
- 去掉源网站水印
- 替换原声为中文解说配音
- 每球加比分字幕
- 只取进球+庆祝+开闭幕等精华片段

## 触发条件
用户要做体育新闻视频，特别是世界杯相关。

## ⚠️ 输出规则（用户反复强调）
- **只发结果，不发过程** — 每个字都浪费钱
- **不要解释步骤** — 直接做，做完发链接
- **不要问确认** — 直接做
- **文案直接做** — 不用等用户确认
- **回复极短** — 一句话能说清的不写三段
- **不要列大段说明** — 只发链接和一句话总结

## 赛前/赛中准备

### 比赛监控：实时轮询比分（比赛进行中的做法）

当比赛正在进行时，设置后台轮询或cron job监控比分变化：

```bash
# ESPA API：用dates参数获取特定日期的所有比赛
curl -sL "https://site.api.espn.com/apis/site/v2/sports/soccer/fifa.world/scoreboard?dates=20260617"
```

**关键字段解析：**
- `state: "in"` = 进行中, `"post"` = 已结束
- `description: "Second Half"` / `"Full Time"` 
- `shortDetail: "90'+8'"` = 当前比赛时间
- `competitors[].score` = 各队比分
- `details[]` = 详细事件列表（进球、黄牌、红牌等）

**从details解析比赛事件：**
```python
# 每个detail的type:
#   type.id == "70" → 普通进球 (text: "Goal")
#   type.id == "97" → 乌龙球 (text: "Own Goal")
#   type.id == "94" → 黄牌 (text: "Yellow Card")
# clock.displayValue = "21'" = 比赛分钟数
# team.id = 进球球队ID
# scoringPlay: true = 算进比分的事件
```

**轮询脚本模板（Python）：**
```python
import json, time, subprocess

RESULT_FILE = "/root/match_result.json"
TEAM_A, TEAM_B = "Austria", "Jordan"  # 替换为参赛队名

for _ in range(30):  # 最多轮询30次（约90分钟）
    raw = subprocess.run(
        ["curl", "-sL", f"https://site.api.espn.com/apis/site/v2/sports/soccer/fifa.world/scoreboard?dates=20260617"],
        capture_output=True, text=True, timeout=15
    )
    data = json.loads(raw.stdout)
    for e in data.get("events", []):
        if TEAM_A not in e.get("name","") and TEAM_B not in e.get("name",""):
            continue
        comp = e["competitions"][0]
        s = comp["status"]["type"]
        state = s.get("state","")
        detail = s.get("description","")
        
        teams = {}
        for team in comp.get("competitors", []):
            teams[team["team"]["displayName"]] = team.get("score","0")
        
        result = {
            "state": state, "detail": detail,
            "score": f'{teams.get(TEAM_A,"0")}-{teams.get(TEAM_B,"0")}',
            TEAM_A: teams.get(TEAM_A,"0"),
            TEAM_B: teams.get(TEAM_B,"0"),
            "updated": time.strftime("%Y-%m-%d %H:%M:%S")
        }
        with open(RESULT_FILE, "w") as f:
            json.dump(result, f, indent=2)
        
        if state == "post" or "Full Time" in detail:
            print(f"MATCH OVER: {result['score']}")
            print(json.dumps(result))
            sys.exit(0)
    time.sleep(180)  # 每3分钟查一次
```

### Cron Job定时制作（比赛刚结束时调用）

```bash
# 定时任务创建参数：
# schedule: 预估比赛结束时间+15分钟缓冲（如22:00开球→00:15定时）
# skills: ["sports-news-video-production"]
# prompt: 包含"检查match_result.json → 找比赛录像 → 下载 → 剪辑 → 配音 → 发布"的完整指令
# deliver: 发送到用户所在平台（如qqbot）
# 不设置 repeat（只执行一次）
```

注意：cron job启动后，先检查match_result.json是否存在且state为"post"/"Final"。如果不是，等待最多20分钟（每2分钟重试一次）。

### 详细事件与进球解析（获取每个进球详情）— ⚠️ 必须先推导比分顺序！

**这是整个制作过程中最容易出错的一步。2026-06-17因进球顺序错误被用户当场指出。**

从ESPN API获取details列表可以获得精确的进球时间和类型。但**必须自己计算每球后的比分**，不能假设最终的`score`就是顺序：乌龙球的比分含义与普通进球不同。

```python
# 解析competition的details — 正确推导进球顺序
details = comp.get("details", [])
# 找出所有进球事件（按API返回顺序，就是实际发生顺序）
goal_events = [d for d in details if d.get("scoringPlay") and d.get("scoreValue", 0) > 0]

# 追踪每一球后的比分
team_474_score = 0  # 通常是奥地利队
team_2917_score = 0  # 通常是约旦队
# ⚠️ 需要先查competitors数组确定哪个team ID对应哪个队名

goal_timeline = []

# 按API顺序逐个处理进球
for d in goal_events:
    clock = d["clock"]["displayValue"]  # 如 "21'"
    team_id = d["team"]["id"]           # 进球的球队ID
    is_own_goal = d.get("ownGoal", False)
    d_type = d["type"]["text"]           # "Goal" 或 "Own Goal"
    
    if is_own_goal:
        # ⚠️ 乌龙球：team_id是打入乌龙的球队，但分加给对手
        d_type = "Own Goal"
        # 给对面的队加分
        if team_id == team_474_id:
            team_2917_score += 1
            scorer = f"Team{team_id}(乌龙球送给对手)"
        else:
            team_474_score += 1
            scorer = f"Team{team_id}(乌龙球送给对手)"
    else:
        # 普通进球：team_id就是得分方
        if team_id == team_474_id:
            team_474_score += 1
        else:
            team_2917_score += 1
    
    goal_timeline.append({
        "minute": clock,
        "type": d_type,
        "score": f"{team_474_score}-{team_2917_score}"
    })

# 输出按时间排序的进球顺序
for g in goal_timeline:
    print(f"  {g['minute']} - {g['type']} → 比分 {g['score']}")
```

#### 验证进球顺序的正确性

输出进球顺序后，**必须验证最终比分是否与API返回的`score`一致**：

```python
# 从competitors拿最终比分
scores = {}
for team in comp.get("competitors", []):
    scores[team["team"]["displayName"]] = team.get("score", "0")
api_final = f'{scores.get("Austria", "0")}-{scores.get("Jordan", "0")}'

# 从自己推导的goal_timeline拿最后一条的比分
derived_final = goal_timeline[-1]["score"]

if api_final != derived_final:
    print(f"⚠️ 比分对不上！API: {api_final}, 推导: {derived_final}")
    print("→ 可能遗漏了进球事件，需要重新获取数据")
else:
    print(f"✅ 比分验证通过: {api_final}")
```

⚠️ **2026-06-17教训**：第一次做奥地利vs约旦时，错误地把乌龙球理解为奥地利丢分（实际乌龙球给对手加分），导致进球顺序全错。

## 制作流程

### 第一步：寻找比赛录像源
用Yahoo Japan搜索（不用Google/YouTube）：
```
https://search.yahoo.co.jp/search?p=<关键词>&ei=UTF-8
```

可用 `site:v.qq.com` 或 `site:dailymotion.com` 限定搜索范围。

优先尝试的网站：
1. **腾讯视频 (v.qq.com)** — yt-dlp可下载，16:9横屏，效果最好
2. **央视网 (worldcup.cctv.com)** — 官方集锦，但CDN有海外封锁（见陷阱1）
3. **Dailymotion (dailymotion.com)** — ✅ **yt-dlp可以直接下载，无需impersonation**（2026-06-15验证）
4. **FOX Sports (foxsports.com)** — ✅ **2026-06-17验证，可找到直接mp4下载链接！**
   通过Yahoo Japan搜索 `site:foxsports.com 球队A 球队B highlights 2026` 可找到比赛页面的watch链接（`https://www.foxsports.com/watch/fmc-XXXX`）。
   该页面的HTML中**直接包含mp4文件的下载URL**（无需鉴权）：
   ```bash
   # 步骤1：找到FOX Sports比赛的watch页面
   curl -sL "https://search.yahoo.co.jp/search?p=site:foxsports.com+Austria+Jordan+highlights+2026&ei=UTF-8" | grep -oP 'https?://www\\.foxsports\\.com/watch/[^"\\'<>\\s]+'

   # 步骤2：从watch页面提取mp4直链
   curl -sL "https://www.foxsports.com/watch/fmc-XXXX" | grep -oP 'https?://statics\\.foxsports\\.com/mediacloud/[^"]+\\.mp4' | sort -u

   # 步骤3：下载（需加Referer头）
   curl -sL -H "Referer: https://www.foxsports.com/" -o match.mp4 "mp4_url"
   ```
   **命名规律**：文件名包含队伍缩写、进球分钟数和事件类型（GOAL/OWN_GOAL/PENALTY等）。所有视频为1920x1080（16:9横屏），需要转9:16竖屏。下载速度稳定。。推荐两种搜索方式：
   - **Yahoo Japan site:dailymotion.com** 搜索（通用）
   - **Dailymotion直接搜索**（更准，2026-06-17验证有效）：
     ```bash
     # 搜索关键词
     curl -sL "https://www.dailymotion.com/search/KEYWORDS/videos" | grep -oP '/video/[a-z0-9]+' | sort -u
     
     # 检查视频标题确认内容
     for vid in ID1 ID2 ID3; do
       echo "=== \$vid ==="
       curl -sL "https://www.dailymotion.com/video/\$vid" | python3 -c "
       import sys, re; h = sys.stdin.read()
       t = re.search(r'<title[^>]*>(.*?)</title>', h)
       if t: print('Title:', t.group(1))
       d = re.search(r'\"description\"[^>]*content=\"([^\"]+)\"', h)
       if d: print('Desc:', d.group(1)[:200])
       "
     done
     
     # 确认后下载
     yt-dlp --no-check-certificates -o "/root/news_clips/match_raw.mp4" "https://www.dailymotion.com/video/ID"
     ```
   视频多为848x480或480x360竖屏/方形，不是真9:16。注意匹配的集锦视频可能很短（1-2分钟），不要误以为是完整比赛。

### 第二步：下载比赛录像（核心——要能从任何网站下）

按优先级依次尝试：

**1. yt-dlp（首选，支持上千个网站）**
```bash
yt-dlp "URL" -o "/tmp/match_raw.mp4"
```

**2. you-get（中国网站专用）**
```bash
you-get "URL" -o /tmp/
```

**3. ffmpeg直下（有mp4/m3u8直链时）**
```bash
ffmpeg -headers "Referer: https://来源网站" -i "视频URL" -c copy /tmp/download.mp4
```

**4. Python爬页面（小网站/博客）**
```bash
curl -sL "URL" | python3 -c "
import sys, re
html = sys.stdin.read()
for v in re.findall(r'(https?://[^\"\\\'<>]+\.(?:mp4|m3u8|flv)[^\"\\\'<>]*)', html): print(v)
for v in re.findall(r'<video[^>]*src=\"([^\"]+)\"', html): print(v)
"
```

**5. YouTube（仅在用户提供cookies时可用）**
   - 本服务器IP被YouTube识别为bot，yt-dlp报"Sign in to confirm"（需cookies文件）
   - Invidious/Piped实例大多已关停，不可靠
   - **不要浪费时间去试YouTube**，除非用户提供了cookies文件
   - 可通过Yahoo Japan搜索发现YouTube视频的存在
   - **可用Jina AI Reader解析YouTube页面标题和时长**（无需下载，纯文本）：
     ```bash
     curl -sL "https://r.jina.ai/https://www.youtube.com/watch?v=VIDEO_ID" \
       -H "User-Agent: Mozilla/5.0" | head -5
     # 输出标题、URL Source、Markdown Content（文本版页面内容）
     # 可用于确认视频是否匹配比赛，而无需实际下载
     ```
   - `curl_cffi` (`pip install curl_cffi`) 可以绕过YouTube的初始bot检测（返回200），但视频播放仍需要登录(playabilityStatus: LOGIN_REQUIRED)。不可用于实际下载视频。
浏览器打开 → Network面板 → 筛选Media → 找视频请求URL

**小网站/博客视频下载技巧：**
- 个人博客视频常是mp4直链，curl/ffmpeg直接下
- iframe外链看src里的真实链接
- 社交媒体（Twitter/X, Instagram等）用yt-dlp

#### 下载失败排查
```bash
curl -sI "URL" | head -5                           # 网站可访问？
yt-dlp --force-generic-extractor -v "URL"           # 强通用模式
```

### 第三步：分析视频结构
```bash
ffmpeg -i input.mp4 -filter:v "select='gt(scene,0.2)',showinfo" -f null - 2>&1 | grep pts_time
```

### 第四步：裁水印 + 截取精华片段
```bash
# 先提取一帧检查水印位置
ffmpeg -y -ss 5 -i input.mp4 -vframes 1 frame.jpg
# 裁剪并截取
ffmpeg -y -ss <开始秒> -i input.mp4 -t <时长> \
  -vf "crop=<宽>:<高>:<x偏移>:<y偏移>" \
  -c:v libx264 -preset fast -crf 22 -c:a aac -b:a 96k \
  /tmp/clips/seg_X.mp4
```

典型7段结构：
1. 开场（5-8s）→ 2. 第1球（10-15s）→ 3. 第2球（10-15s）
4. 对手进球（10-15s）→ 5. 第3球（10-15s）→ 6. 第4球（10-15s）
7. 最终比分/结束（5-8s）

### 第五步：拼接所有片段
```bash
for i in 0 1 2 3 4 5 6; do echo "file '/tmp/clips/seg_${i}.mp4'"; done > /tmp/clips/list.txt
ffmpeg -y -f concat -safe 0 -i /tmp/clips/list.txt -c copy /tmp/combined.mp4
```

### 第五步B：集锦视频的时长匹配策略（2026-06-17新增）
当下载的视频是**已剪辑好的短集锦**（1-2分钟，非完整比赛），而非整场比赛时，直接用它做素材但需做时长匹配：

1. **先测TTS配音时长** — 用edge-tts生成配音后立即测量：
   ```bash
   edge-tts --voice zh-CN-YunxiNeural --rate=+5% -f /root/news_clips/commentary.txt --write-media /tmp/commentary_test.mp3
   ffprobe -i /tmp/commentary_test.mp3 -show_entries format=duration -v quiet -of csv="p=0"
   ```

2. **场景检测找关键片段** — 用较低阈值识别集锦中的每个短场景：
   ```bash
   ffmpeg -i /root/news_clips/match_raw.mp4 -filter:v "select='gt(scene,0.3)',showinfo" -f null - 2>&1 | grep pts_time
   ```

3. **按场景切割并选择** — 根据场景变化时间点切成5-7段，总时长接近配音时长：
   - seg_0: 0-第一个场景（开场，~10s）
   - seg_1~seg_3: 各进球片段（每个8-18s）
   - seg_4: 结束片段
   - 目标总时长 ≈ 配音时长 ± 5s

4. **concat后检查** — 拼接后测量总时长，必要时微调segments

### 第六步：写中文新闻稿文案

结构：开场问候 + 比赛信息 → 每个进球过程 + 比分 → 最终比分 + 总结

**张哥要求的叙事格式（2026-06-17明确）：**

1. **开场** — "裁判吹哨，比赛开始"（模拟真实的比赛转播开头）
2. **每个进球** — 按时间顺序描述：
   - 第X分钟，谁进的球，怎么进的（头球、远射、点球等）
   - 球员庆祝特写（拍胸口、滑跪、拥抱队友等）
   - 球迷庆祝画面（欢呼、挥舞旗帜、跳跃等）
3. **高潮搭配** — 每个进球配上该进球最精彩的庆祝画面：如果第2球的球迷欢呼最精彩，就把那一段的球迷画面配到第2球的解说里
4. **结尾** — 最终比分 + 比赛意义总结

**文案示例（奥地利3-1约旦，修正后）：**
```
各位观众大家好，欢迎收看世界杯新闻。
裁判吹哨，比赛正式开始！

比赛第21分钟，奥地利队率先打破僵局，
一脚精彩的射门洞穿了约旦队的大门，
奥地利球员兴奋地拥抱在一起庆祝，
看台上的奥地利球迷欢呼雀跃！

第50分钟，约旦队抓住机会扳平比分，
两队战成1比1。

第76分钟，约旦队后卫不慎打入乌龙球，
奥地利队2比1再次领先。

比赛最后时刻，第90加12分钟，
奥地利队获得点球并稳稳罚进，
将比分锁定为3比1。

最终，奥地利队3比1战胜约旦队，
取得了一场关键的胜利。感谢收看。
```

**注意：** 用户习惯是"先写文案，直接做，不用等我确认"。

### 第七步：生成TTS中文配音
```bash
edge-tts --voice zh-CN-YunxiNeural --rate=+5% --text "<文案>" --write-media /tmp/commentary.mp3
```

### 第八步：加字幕 + 混音输出 + 竖屏转换

**⚠️ 重要：concat后如果某些段无音频流（如title card用color滤镜生成），concat输出的视频也没有音频流。** 此时`[0:a]`会报"matches no streams"错误。必须先加静音音频轨道再混音：

```bash
# 方案1：concat时用-f lavfi -i anullsrc添加静音轨道
ffmpeg -y -f concat -safe 0 -i segments.txt \
  -f lavfi -i anullsrc=r=24000:cl=mono \
  -shortest -c:v copy -c:a aac -b:a 128k \
  /tmp/naked_with_audio.mp4

# 然后混音
ffmpeg -y -i /tmp/naked_with_audio.mp4 -i /tmp/tts.mp3 \
  -filter_complex "[0:a]volume=0.25[a0];[1:a]adelay=500|500[a1];[a0][a1]amix=inputs=2:duration=first[a]" \
  -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 128k \
  -t $(ffprobe -i /tmp/tts.mp3 -show_entries format=duration -v quiet -of csv="p=0") \
  /tmp/final_news.mp4

# 方案2：每段单独加音频再concat（确保各段都有音频流，不会丢失）
```
```bash
ffmpeg -y -i /tmp/combined.mp4 -i /tmp/commentary.mp3 \
  -filter_complex "
    [0:v]drawtext=text='标题':fontcolor=#FFD700:fontsize=36:box=1:boxcolor=black@0.6:x=(w-text_w)/2:y=40:enable='between(t,0,5)',
           drawtext=text='1-0':fontcolor=#00FF00:fontsize=48:box=1:boxcolor=black@0.7:x=(w-text_w)/2:y=(h/2)-50:enable='between(t,<开始>,<结束>)'[v];
    [0:a]volume=0.1[a1];[1:a]adelay=500|500[a2];[a1][a2]amix=inputs=2:duration=first:dropout_transition=2[a]
  " -map "[v]" -map "[a]" -c:v libx264 -preset fast -crf 23 -c:a aac -b:a 128k /tmp/tech_video.mp4
```

### 输出方向：9:16竖屏（手机端）为默认选项

**张哥的所有视频最终都发在视频号/手机上，9:16竖屏是默认输出格式且不可协商。** 2026-06-17再次强调：不要做16:9横屏。

**每一球的剪辑结构（用户2026-06-17明确要求）：**
每个进球片段必须包含以下三种镜头的组合，缺一不可：
1. **进球射门瞬间** — 球怎么进的（射手、射门方式）
2. **球员庆祝特写** — 进球球员的庆祝动作（拥抱、滑跪、呐喊等）
3. **球迷庆祝镜头** — 看台上球迷的反应（欢呼、挥舞旗帜等）

如果某个进球的球迷庆祝画面特别精彩，就优先把那个画面配到那段解说里。

```bash
# 竖屏裁剪（从横屏源中间裁竖条）
ffmpeg -y -i input.mp4 \
  -vf "crop=ih*9/16:ih,scale=1080:1920" \
  -c:v libx264 -preset ultrafast -crf 28 \
  final_news.mp4
```

```bash
# 输出9:16竖屏（1080x1920）
ffmpeg -y -i input.mp4 \
  -vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:color=black" \
  -c:v libx264 -preset ultrafast -crf 28 -c:a aac -b:a 96k \
  final_news.mp4
```

如果源视频是横屏（16:9），从中间裁竖条：
```bash
ffmpeg -y -i input.mp4 \
  -vf "crop=ih*9/16:ih,scale=1080:1920" \
  -c:v libx264 -preset ultrafast -crf 28 \
  final_news.mp4
```

如果是纯文字/图文版（无视频素材时），直接用`color=c=色值:s=1080x1920`做9:16底色。

### 第九步：发布

1. 检查已有HTTP服务：
   ```bash
   ss -tlnp | grep -E ':80 |:9000 |:8899 '
   ```
2. 如果80端口无服务，检查其他端口：
   - 8899端口：可能是`python3 -m http.server 8899`在运行
   - 8888端口：可能是Hermes webchat服务器（有密码保护，不要用这个端口给用户发文件！）
3. 确认服务目录：
   ```bash
   ls -l /proc/$(pgrep -f "http.server" | head -1)/cwd 2>/dev/null
   ```
   确保文件在HTTP服务器的工作目录下
4. 如果没有HTTP服务，启动Python简易服务器：
   ```bash
   python3 -m http.server 80 --directory /root &
   # 或用background模式
   ```
5. 服务健康检查：
   ```bash
   curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:80/final_news.mp4
   # 如果返回000 → 服务没起来
   # 如果返回200 → 可以给用户发链接
   ```
6. 访问 http://<服务器IP>:端口/final_news.mp4

### 特殊：用户在中国访问印度服务器
- 服务器IP: 157.173.212.215 (印度Hostinger)
- 用户在中国，GFW可能拦截HTTP请求
- 如果用户说403，先查服务器状态确认不是服务端问题
- 如果服务端正常但用户仍403，建议用户挂VPN访问

## 注意事项
1. **不用Google/YouTube** — 只用Yahoo Japan搜索
2. **必须去水印** — crop滤镜裁剪掉源网站logo。Coverr视频水印可能在画面正中间（占70%画面），需要裁掉中间部分只留上下边缘
3. **替换原声** — 原声保留10-15%作为背景
4. **文案先写直接做** — 不要等用户确认
5. **只取进球片段** — 不要用整个集锦
6. **每球加比分字幕**
7. **多源验证比分** — 用ESPN或Yahoo Sports确认最终结果
8. **只发结果不发过程** — 张哥极度在意token消耗，不要描述操作步骤，只发链接和简短说明
9. **回复必须极短** — 一句话能说清的不写三段

## 已知陷阱

### 陷阱1：CCTV CDN海外封锁
CCTV的网宿CDN(WSSEA认证)对海外IP返回403。
- 换源到腾讯视频或Dailymotion
- 或通过国内代理中转
参考文件：`references/cctv-cdn-geoblocking-and-apis.md`

### 陷阱5：Dailymotion视频可能竖屏
Dailymotion的足球集锦有时是480x848竖屏。用ffmpeg加黑边补成16:9：
```bash
ffmpeg -i input.mp4 -vf "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2:black" output.mp4
```

### 陷阱6：雨燕直播等中文直播站可达性复杂
雨燕直播系(yuyanzb4.net等)用Cloudflare CDN。**注意：该站是国内打不开、国外能打开**（与直觉相反）。但本服务器(印度Hostinger)的IP仍被Cloudflare封锁，无法访问。详见 `references/chinese-streaming-sites-2026-06-13.md`

### 陷阱8：Coverr视频水印在画面中央
Coverr视频的水印可能在画面正中间，占70%高度。不能用简单的边缘裁剪。
需要裁掉中间部分，只保留上下各15%画面，然后拼接。
详见 `references/coverr-download-and-watermark.md`

### 陷阱9：Mixkit部分类别无法下载
Mixkit的news/nature类别URL返回HTTP 200但0字节。只有funny类别能正常下载。

### 陷阱12：竖屏转9:16格式（2026-06-15需求）

张哥要的9:16竖屏（1080x1920）手机视频，免费素材网站很少提供真正的9:16源。
解决方案：用ffmpeg把低分辨率竖屏视频（如848x480、640x360）放大到1080x1920：

```bash
# 方法1：竖屏视频→9:16（两边加黑边）
ffmpeg -i input.mp4 \
  -vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:color=black" \
  -c:v libx264 -preset ultrafast -crf 30 -c:a aac -b:a 96k \
  output.mp4

# 方法2：只在片头30秒
ffmpeg -y -i input.mp4 -t 30 \
  -vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2" \
  -c:v libx264 -preset ultrafast -crf 35 -c:a aac -b:a 64k \
  output.mp4
```

**参数说明：**
- `scale=1080:1920:force_original_aspect_ratio=decrease`：按比例缩放不超限
- `pad=1080:1920:(ow-iw)/2:(oh-ih)/2`：不足部分加黑边居中
- `-preset ultrafast`：最快编码（比fast快5倍，质量略降）
- `-crf 30~35`：低码率，搞笑视频不需要高清

**Dailymotion视频批量下载：**
```bash
# 从搜索页提取视频ID
curl -sL "https://www.dailymotion.com/search/funny/videos" | grep -oP '/video/[^"\\s]+' | sort -u

# 批量下载（下载到/root/news_clips/而非/tmp）
for vid in x9aiij2 x9a5in6 x4glnb1 x7eufu4 x8ydzcq; do
  yt-dlp --no-check-certificates \
    -o "/root/news_clips/dm_${vid}.mp4" \
    "https://www.dailymotion.com/video/$vid"
done
```

**Mixkit视频ID枚举下载：**
```bash
# funny分类IDs可枚举
for id in 3346 3351 3373 4103 44877 4606 4627 4640 4647 4688 4872 4886 49058; do
  curl -sL "https://assets.mixkit.co/videos/${id}/${id}-360.mp4" \
    -o "/root/news_clips/mixkit_${id}.mp4"
done
```

### 陷阱10：/tmp磁盘空间不足
/tmp是2GB tmpfs，视频文件多了会满（No space left on device）。
视频文件应放在/root/news_clips/，HTTP服务器从/root启动。

### 陷阱11：QQ语音消息无法保存
QQ语音消息通过临时URL(.amr)传递，有短时效(rkey验证)。
网关只做语音→文字转录，不保存音频文件。
需要用户以**文件附件**形式重新发送才能保存音频。

## 声音克隆与混音（2026-06-14新增需求）

张哥想克隆雨燕直播主播的声音，并与李永乐老师的声音混合，用于视频配音。

### 当前限制
- **本服务器无GPU**，无法运行本地声音克隆模型
- **QQ语音消息不可存** — 网关只转录文字，不保存音频文件。需用户以文件形式发送
- 声音克隆的可行路径详见 `references/voice-cloning-approach.md`

### 获取主播声音的方法
1. 让用户以**文件附件**发送（不是QQ语音消息）
2. 从用户PC本地录音文件获取
3. 从公开视频中提取（如果主播在Dailymotion/B站等平台有视频）

### 声音混合思路
- 提取两人声音的频谱特征，按比例混合
- 输出介于两者之间的中间态声音
- 工具：librosa + World声码器（服务器）或 RVC（用户PC）
- 详见 `references/voice-cloning-approach.md`

### 陷阱14：Getty Images照片直链被封锁（2026-06-17验证）

Getty Images的媒体CDN(`media.gettyimages.com`) 对直接curl下载返回0字节文件。即使添加User-Agent和Referer头也无法下载。

替代方案：
1. **使用其他新闻图片源**：从AP Images、Reuters或比赛新闻文章中提取图片
2. **图文+TTS视频**（见下文备选方案）

### 陷阱15：中国网络无法访问印度服务器HTTP端口（2026-06-17验证）

服务器在印度(Hostinger印度机房)，用户在中国。直连HTTP下载可能被：
- GFW拦截（返回虚假的403/超时页面）
- ISP屏蔽非标准端口

症状：服务器本地curl返回HTTP 200，但用户始终收到403或无法连接。

应对策略：
1. 尝试多种端口（80、443、8080、8888、8899、9000）
2. 确认Hermes webchat服务器（端口8888）有密码保护 — 用户可能得到的是它返回的403
3. 最后手段：通过QQ发文件（用户可能不想要，因为流量费）
4. 目前无完美解决方案。最可靠的路径是用户挂香港VPN访问。

### ⚠️ 找不到比赛录像时怎么办（2026-06-17关键教训）

**核心原则：用户要的是真实比赛视频，不是照片也不是图文！** 照片+TTS的图文版对用户来说等同于"没有画面"。

**正确流程：**
1. **搜索阶段**：用Yahoo Japan至少尝试3种不同关键词组合，每种换不同语言：
   - 中文：`球队A 球队B 集锦 视频 2026`
   - 英文：`TeamA vs TeamB 2026 highlights`
   - 日文：`チームA チームB ハイライト`
   - 加上 `site:v.qq.com` / `site:dailymotion.com` / `site:foxsports.com` 限定
   
2. **多平台尝试**：
   - Dailymotion直接搜索：`https://www.dailymotion.com/search/KEYWORDS/videos`
   - FOX Sports watch页面（见来源4）
   - 新闻文章提取嵌入视频URL（Guardian、ESPN、FIFA官网等）

3. **不要做任何无视频画面的版本**：
   - ❌ 纯文字+背景色+TTS → 被用户说"连画面都没有"
   - ❌ 比赛照片+TTS → 被用户说"不是照片，要是视频"
   - ✅ 用户明确要的是真实比赛视频画面，含进球+球员庆祝+球迷欢呼

4. **仍找不到 → 直接告诉用户"没找到这场比赛录像"**，并询问用户有没有链接可以提供。

5. **如果用户给了链接**，拿到链接后立刻重做带真实比赛画面的版本。

```bash
# 1. 生成TTS
edge-tts --voice zh-CN-YunxiNeural --rate=+5% --text "解说词" --write-media /tmp/tts.mp3

# 2. 测时长
dur=$(ffprobe -i /tmp/tts.mp3 -show_entries format=duration -v quiet -of csv="p=0")

# 3. 制作各段：用color滤镜+drawtext多行字幕
# 每个分段用不同颜色背景，文字居中
# 关键：drawtext不支持\n换行，每行用一个drawtext实例
ffmpeg -y -f lavfi -i "color=c=#1a1a2e:s=1280x720:d=8" \
  -vf "drawtext=text='2026世界杯':fontcolor=white:fontsize=48:box=1:boxcolor=black@0.5:boxborderw=20:x=(w-text_w)/2:y=200:fontfile=/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc,drawtext=text='奥地利3-1约旦':fontcolor=#FFD700:fontsize=36:x=(w-text_w)/2:y=280" \
  -c:v libx264 -preset ultrafast -crf 28 /tmp/seg_0.mp4

# 4. 拼接+混音
ffmpeg -y -f concat -safe 0 -i segments.txt -c copy /tmp/naked.mp4
ffmpeg -y -i /tmp/naked.mp4 -i /tmp/tts.mp3 -c:v copy -c:a aac -shortest /tmp/final_news.mp4
```

⚠️ **注意**：这是最后手段，用户明确想要真实比赛画面。使用时要跟用户说明没找到录像源。

### 陷阱2：垂直视频处理
Dailymotion等来源可能是竖屏(480x848)。
```bash
ffmpeg -i input.mp4 -vf "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2:black" output.mp4
```

### 陷阱3：比分不确定
先查权威来源：
```bash
curl -sL "https://site.api.espn.com/apis/site/v2/sports/soccer/fifa.world/scoreboard"
```

### 音频混合关键陷阱：concat后视频无音频流（2026-06-17验证）

当concat的段落中包含**color滤镜生成的title card等无音频流的视频**时，concat -c copy的输出视频会丢失音频流。此时`[0:a]`会报错"Stream specifier ':a' matches no streams"。

**❌ 错误做法：**
```bash
# 如果concat后的视频无音频流，这句会失败
ffmpeg -i naked.mp4 -i tts.mp3 -filter_complex "[0:a]volume=0.25[a0];..." -map 0:v -map "[a]" ...
# 错误：Stream specifier ':a' in filtergraph matches no streams
```

**✅ 正确做法（两步法）：**
```bash
# Step 1: 加静音音频轨道
ffmpeg -y -i naked.mp4 -f lavfi -i anullsrc=r=24000:cl=mono \
  -shortest -c:v copy -c:a aac -b:a 128k \
  naked_with_audio.mp4

# Step 2: 混音（用 -t 显式指定TTS时长，不用 -shortest）
TTS_DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 tts.mp3)
ffmpeg -y -i naked_with_audio.mp4 -i tts.mp3 \
  -filter_complex "[0:a]volume=0.25[a0];[1:a]adelay=500|500[a1];[a0][a1]amix=inputs=2:duration=first[a]" \
  -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 128k \
  -t $TTS_DUR final_news.mp4
```

**为什么不能用`-shortest`？** 因为加了静音轨道后视频有了一个与视频等长的音频流。TTS配音（39秒）比视频（8分钟）短，但`-shortest`取的是输入流的最小长度，而静音轨道和视频一样长（8分钟），所以不起作用。必须用`-t`显式截断。

### 短集锦视频的segmentation策略（2026-06-17）

当下载的视频是**已剪辑好的短集锦**而非整场比赛时，不要试图剪出多个片段。直接：

1. 用场景检测找关键切换点
2. 按场景切成5-7段（每段约8-20秒）
3. 总时长匹配TTS配音时长（±5s）
4. 每段加比分/事件字幕

```python
# Python示例：集锦场景切割
import subprocess, re

# 场景检测
r = subprocess.run([
    "ffmpeg", "-i", "input.mp4",
    "-filter:v", "select='gt(scene,0.3)',showinfo",
    "-f", "null", "-"
], capture_output=True, text=True, timeout=120)

scene_times = [float(x) for x in re.findall(r'pts_time:([\d.]+)', r.stderr + r.stdout)]
# 用scene_times做切割点
```

### 快速制作脚本结构（2026-06-17验证）

视频制作脚本的推荐结构（不要把所有逻辑放在一个复杂的bash命令链中）：

```python
#!/usr/bin/env python3
"""结构模板"""
import subprocess, os

# 1. 生成TTS → 测时长
# 2. 准备各个片段（每个片段都是独立ffmpeg调用）
#    - title card（color滤镜+drawtext）
#    - 进球片段（trim+转9:16+字幕）
#    - B-roll片段（trim+转9:16）
# 3. concat所有片段（-c copy）
# 4. 加静音音频轨道（anullsrc）
# 5. 混音TTS（-t 指定TTS时长）
# 6. 复制到/root/ + HTTP验证
```

**关键参数选择：**
- `-preset ultrafast`（比fast快5x）— 视频制作不需要高压缩
- `-crf 28`（比23质量略低但速度快很多）
- 字幕字体：`/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc`

### 使用Jina AI Reader识别YouTube页面内容（无需下载）

当Yahoo Japan搜索结果包含YouTube链接时，可以用Jina AI Reader获取视频标题和描述，无需实际下载或登录：

```bash
curl -sL "https://r.jina.ai/https://www.youtube.com/watch?v=VIDEO_ID" \
  -H "User-Agent: Mozilla/5.0" | head -5
# 输出包含：Title, URL Source, Markdown Content
```

这可以帮助确认YouTube视频是否匹配目标比赛。标记为"LIVE"或"Watchalong"的视频是直播流，通常不含比赛完整画面。

### Scrapling/curl_cffi对YouTube的效果（2026-06-17验证）

`scrapling`的HTTP Fetcher（底层使用`curl_cffi`）可以通过浏览器指纹模拟绕过YouTube的初始bot检测（返回HTTP 200），**但无法绕过视频播放的登录要求**（`playabilityStatus: LOGIN_REQUIRED`）。无法用于实际下载视频。

StealthyFetcher需要Playwright，而Playwright与Ubuntu 26.04不兼容（scrapling install失败）。**不要在本服务器上尝试使用scrapling的浏览器功能下载YouTube视频。**

### 陷阱13：Cron Job轮询比赛状态的流程（2026-06-17验证）

当作为cron job运行时，需自动等待比赛结束再制作视频：

```bash
# 1. 检查 match_result.json
cat /root/match_result.json

# 2. 如果state不是"post"/"Final"，等2分钟后重试（最多20分钟）
while true; do
  state=$(python3 -c "import json; print(json.load(open('/root/match_result.json'))['state'])")
  [ "$state" = "post" ] && break
  sleep 120
done

# 3. 读取最终比分
score=$(python3 -c "
import json
d = json.load(open('/root/match_result.json'))
print(f\\\"{d['france']}-{d['senegal']}\\\")  # 根据实际字段调整
")

# 4. 制作视频（用最终比分信息写文案和字幕）
```

#### Cron Job创建范例（实时比赛→自动产出视频）
```python
# 关键参数：
# schedule: 预估结束时间（90分钟+15分钟+10分钟缓冲）
#   例：22:00开球 → schedule="2026-06-17T00:15:00"（次日0:15）
# skills: ["sports-news-video-production"]
# deliver: 用户平台（如"qqbot"）
# prompt: 完整指令，包含：
#   1. 读取/root/match_result.json
#   2. 搜索比赛录像（Yahoo Japan）
#   3. 下载→剪辑→配音→发布
#   4. 只发链接，不发过程
```

注意：match_result.json的事件列表可能只包含部分时间点的比分快照，不一定是每次进球都记录。文案写作需要用比分变化推断进球顺序。更准确的进球信息可以从ESPN API的`details`字段获取（见上文"详细事件与进球解析"）。

### 比赛时间推算（用于cron job排期）

```python
# 从ESPN API获取比赛时间
for e in data.get("events", []):
    date = e.get("date", "")  # "2026-06-17T04:00:00Z"
    # 开球时间 = date（UTC）
    # 预计结束 = date + 1小时45分钟（90分钟+15分钟中场）
    # cron job排期 = 预计结束 + 15分钟缓冲
```

### 同时监控多场比赛

ESPN API的`scoreboard`接口返回当天所有比赛。用循环遍历events列表，匹配不同队名即可监控多场比赛。

```bash
# 单次API调用可获取当天全部比赛
curl -sL "https://site.api.espn.com/apis/site/v2/sports/soccer/fifa.world/scoreboard?dates=20260617"

# 筛选特定比赛
for e in data.get("events", []):
    name = e.get("name", "")
    if "Austria" in name or "Jordan" in name:
        # 处理奥地利vs约旦
    if "France" in name or "Senegal" in name:
        # 处理法国vs塞内加尔
```

