Skip to content
Skillv1.0.0

vlog-auto-edit

AI Agent自动剪辑旅行Vlog的完整工作流。从原始素材到成品视频,系统级只需ffmpeg,其余在Python venv内完成。by nyx研究所 (GitHub @znyupup · B站/小红书 @nyx研究所)

by znyupup(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from znyupup/ai-video-editing-skill (SKILL.md). Install upstream with npx skills add znyupup/ai-video-editing-skill. Copyright stays with the author.

AI Agent 自动剪辑旅行 Vlog

Author: nyx研究所 · GitHub · B站 @nyx研究所 · 小红书 @nyx研究所 · X @znyupup_music

把一堆手机拍的旅行素材,用AI自动剪成一个完整vlog。 最小依赖:ffmpeg + Python(whisper或FunASR+Pillow) + 视觉API。系统级只装ffmpeg,其余pip装。

触发条件

  • 用户有一批旅行/日常视频素材,想自动剪成vlog
  • 用户说"帮我剪个视频"、"自动剪辑"、"vlog剪辑"

前置要求

1. 系统工具

工具 用途 安装
ffmpeg 视频裁剪/编码/拼接/抽帧/音量检测 brew install ffmpeg (macOS) / apt install ffmpeg (Linux)
Python 3.9+ 脚本胶水 macOS/Linux 系统自带

2. Python依赖(先检测,已有则跳过)

先检测系统是否已安装所需包,不要无脑创建 venv:

# 检测 whisper
python3 -c "import whisper; print('✅ whisper已安装:', whisper.__file__)"

# 检测 Pillow
python3 -c "from PIL import Image; print('✅ Pillow已安装')"

⚠ 重要:不要用 2>/dev/null 吃掉错误!要看到实际报错才能判断是真没装还是PATH问题。

⚠ macOS 注意: 检查系统已安装的包再决定是否需要 venv。如果系统 python3 已经能 import whisperfrom PIL import Image,直接用就行,不要盲目创建 venv 导致找不到已有的包。

仅在检测不通过时才安装:

# 方案A: 直接装到用户环境(推荐)
pip install openai-whisper Pillow

# 方案B: 如果用户环境有冲突,再用 venv
python3 -m venv .venv && source .venv/bin/activate
pip install openai-whisper Pillow

⚠ 不要每个项目都新建 venv 重装一遍!whisper模型文件1.4GB,pip install也要几分钟。

⚠ ASR引擎二选一:Whisper 或 FunASR

本工作流支持两个ASR引擎,优先用 whisper,whisper 装不上就用 FunASR:

Whisper (OpenAI) FunASR (阿里达摩院)
安装 pip install openai-whisper pip install funasr modelscope torchaudio
模型 medium (1.4GB, 从GitHub下载) paraformer-zh (1.05GB, 从ModelScope下载)
附加模型 VAD模型 + 标点模型 (首次运行自动下载, 共~50MB)
中文效果 好(与whisper medium相当,细节略多)
速度 14s音频 ≈ 5-10s (CPU) 14s音频 ≈ 1.9s (CPU, RTF=0.132) ⚡
时间戳粒度 句级别 (每句话一个start/end) 字级别 (每个字一个时间戳)
模型下载源 GitHub (国内不稳定,容易失败) ModelScope (国内极快,~15MB/s)
隐性依赖 torch torch + torchaudio (⚠ 必须额外装)

检测已安装的ASR引擎:

python3 -c "import whisper; print('✅ whisper可用')" 2>/dev/null \
  || python3 -c "from funasr import AutoModel; print('✅ funasr可用')" 2>/dev/null \
  || echo "❌ 没有可用的ASR引擎,需要安装一个"

⚠ FunASR 安装注意事项:

  • 必须装 torchaudio!pip install funasr modelscope 不会自动装 torchaudio,import 时会报 No module named 'torchaudio'
  • 正确安装命令:pip install funasr modelscope torchaudio
  • 首次运行会自动从 ModelScope 下载3个模型(ASR + VAD + 标点),总共约1.1GB,国内速度快
  • 模型缓存在 ~/.cache/modelscope/hub/models/iic/ 目录下
  • import 时会有 urllib3 的 SSL 警告(NotOpenSSLWarning),可忽略,不影响功能

标题卡方案自动选择:

ffmpeg -filters 2>&1 | grep drawtext
# 有drawtext → 直接用ffmpeg,不需要Pillow
# 没有 → 用Pillow生成透明PNG再overlay(macOS brew ffmpeg通常没编freetype)

可选(参考分析阶段用):

which yt-dlp && echo "✅ yt-dlp已安装" || pip install yt-dlp
python3 -c "import scenedetect" 2>/dev/null && echo "✅ scenedetect已安装" || pip install scenedetect

3. 视觉理解模型(API)

需要一个能理解图像内容的视觉模型API,用于分析素材画面。

要求:

  • 支持 OpenAI Chat Completions 格式(messages + image_url)
  • 支持 base64 图片输入(本地素材抽帧后编码上传)
  • 中文理解能力(需要描述画面内容、标注镜头类型等)

推荐模型:

模型 费用 说明
智谱 GLM-4.6V-Flash 免费 注册 https://open.bigmodel.cn 即用,中文理解好
GPT-4o 付费 效果最好
Qwen-VL 付费 阿里云,中文好

调用示例:

import base64, json, urllib.request

API_URL = 'YOUR_VISION_API_ENDPOINT'   # 替换为你的视觉模型端点
API_KEY='***'               # 替换为你的API Key
MODEL = 'YOUR_MODEL_NAME'             # 替换为你的模型名

with open('frame.jpg', 'rb') as f:
    img_b64 = base64.b64encode(f.read()).decode()

payload = {
    'model': MODEL,
    'messages': [{'role': 'user', 'content': [
        {'type': 'image_url', 'image_url': {'url': f'data:image/jpeg;base64,{img_b64}'}},
        {'type': 'text', 'text': '简洁描述画面内容,标注:镜头类型(远/全/中/近/特写)、拍摄手法(固定/手持/移动)、画面氛围。格式:内容|类型|手法|氛围'}
    ]}],
    'max_tokens': 200
}

req = urllib.request.Request(API_URL, json.dumps(payload).encode(),
    {'Content-Type': 'application/json', 'Authorization': f'Bearer {API_KEY}'})
with urllib.request.urlopen(req, timeout=30) as resp:
    result = json.loads(resp.read())
    print(result['choices'][0]['message']['content'])

⚠ 视觉API调用注意事项:

  • 输入格式:ffmpeg抽帧为JPEG → base64编码 → data:image/jpeg;base64,{b64} 放入 image_url
  • 抽帧尺寸:不需要原始分辨率,缩到 720p 即可(节省上传带宽和token)
  • 端点区分:有些平台视觉模型和文本模型端点不同,确认用对
  • 限流:高峰时段容易被限流,加 retry + sleep

不需要的(踩过的坑)

  • ❌ 剪映/CapCut — v10.4+ 项目文件加密,无法程序化操作
  • ❌ moviepy — ffmpeg命令行已够用,多一层抽象反而不灵活
  • ❌ capcut-cli (GitHub) — 基于明文JSON,对加密后的新版无效
  • ❌ ImageMagick — Pillow已够用,不需要再装一个图像工具

工作流(7个阶段)

阶段1: 素材盘点

目标:了解你有什么素材。

# 批量获取素材信息
for f in footage/*.{MOV,mp4,MP4}; do
  echo "=== $f ==="
  ffprobe -v quiet -print_format json -show_format -show_streams "$f" \
    | python3 -c "import json,sys; d=json.load(sys.stdin); \
      s=d['streams'][0]; f=d['format']; \
      print(f\"  时长: {float(f['duration']):.1f}s\"); \
      print(f\"  分辨率: {s['width']}x{s['height']}\"); \
      print(f\"  编码: {s['codec_name']}\")"
done

输出一份素材清单:数量、总时长、分辨率分布、拍摄时间范围。

阶段2: 参考研究(可选但强烈建议)

目标:建立"好vlog长什么样"的认知。不做这步直接剪,效果会很差。

步骤:

  1. 找2-3个同类型优质vlog(B站/YouTube)
  2. yt-dlp 下载720p视频 + 音频
  3. whisper 转录旁白(提取叙事结构)
  4. scenedetect 检测镜头切换(统计节奏)
  5. ffmpeg 抽关键帧 + 视觉API分析(理解画面构成)

已验证的规律:

  • 镜头节奏: 平均3-4秒/镜头是黄金节奏
  • 内容配比: 美食40-45% + 风景30% + 人物15% + 日常15%
  • 镜头类型: 近景/中景为主(60-65%),全景/远景穿插(35-40%)
  • 叙事结构: 精彩片头 → 出发/到达 → 按地点串联 → 情感升华收尾
  • 长短交替: 短镜头(<2s)做蒙太奇,长镜头(>8s)做叙事

剪辑教程要点:

  1. 找侧重点,不要流水账
  2. 三幕式结构: 25%悬念 + 50%发展 + 25%收尾
  3. 精彩放片头
  4. 情感占51%(Walter Murch六法则)

阶段3: 素材三维分析

目标:让AI理解每条素材的内容。每条素材做三维分析:

a) 音频分析 — ASR转录(Whisper 或 FunASR)

# 提取音频(16kHz单声道WAV,两个引擎通用的最佳输入格式)
ffmpeg -i footage/INPUT.MOV -vn -acodec pcm_s16le -ar 16000 -ac 1 /tmp/audio.wav

方案1: Whisper转录(优先使用)

import whisper, json
model = whisper.load_model('medium')
result = model.transcribe('/tmp/audio.wav', language='zh')
segments = [{"start": s['start'], "end": s['end'], "text": s['text']} for s in result['segments']]
# 输出: [{"start": 0.5, "end": 2.3, "text": "我们现在在中央大街"}, ...]

方案2: FunASR转录(Whisper装不上时的替代)

import warnings
warnings.filterwarnings("ignore")
from funasr import AutoModel

model = AutoModel(
    model="paraformer-zh",      # Paraformer-large,中文ASR主模型(1.05GB)
    vad_model="fsmn-vad",       # 语音活动检测,自动切分静音段
    punc_model="ct-punc",       # 自动加标点
    log_level="ERROR",
    disable_update=True         # 跳过版本检查,加快启动
)

result = model.generate(input="/tmp/audio.wav")
text = result[0]["text"]        # 完整识别文本(已加标点)
timestamps = result[0].get("timestamp", [])  # 字级别时间戳 [[start_ms, end_ms], ...]

# ⚠ FunASR时间戳是字级别(毫秒),需要聚合成句级别才能和whisper格式统一
# 聚合策略:按标点符号(。!?,)切分成句子
segments = []
if timestamps and text:
    puncs = set("。!?,,.!?")
    current_start = timestamps[0][0] / 1000.0
    current_text = ""
    chars = list(text)
    for i, (char, ts) in enumerate(zip(chars, timestamps)):
        current_text += char
        if char in puncs or i == len(chars) - 1:
            segments.append({
                "start": round(current_start, 1),
                "end": round(ts[1] / 1000.0, 1),
                "text": current_text.strip()
            })
            if i < len(chars) - 1:
                current_start = timestamps[i + 1][0] / 1000.0
            current_text = ""
# 输出格式与whisper一致: [{"start": 0.6, "end": 2.9, "text": "我们现在在中央大街的入口,"}, ...]

⚠ FunASR 输出差异注意:

  • FunASR 返回的是完整文本 + 字级别时间戳,不像 whisper 直接给句级别 segments
  • 上面的聚合代码把字级别时间戳按标点切分成句子,输出格式与 whisper 对齐
  • 如果素材没有语音,FunASR 返回空文本 "",timestamps 为空 []
  • FunASR 速度比 whisper 快 3-5x(14s音频只需1.9s),批量处理时优势明显

b) 音量检测 — ffmpeg

ffmpeg -i footage/INPUT.MOV -af volumedetect -f null /dev/null 2>&1 | grep volume
# mean_volume: 平均音量(dB)  max_volume: 峰值音量(dB)
# mean < -40dB 基本无声  mean > -20dB 有明显声音/语音

c) 视觉分析 — ffmpeg抽帧 + 视觉理解模型

# 抽帧策略(按时长分段)
# ≤20s → 3帧(首/中/尾)
# 20-60s → 5帧
# >60s → 每15s一帧

# 抽首/中/尾三帧示例(缩到720p节省带宽)
ffmpeg -i footage/INPUT.MOV -vf "select=eq(n\,0),scale=1280:-1" -frames:v 1 -q:v 2 frame_start.jpg
ffmpeg -i footage/INPUT.MOV -vf "select=eq(n\,MIDDLE),scale=1280:-1" -frames:v 1 -q:v 2 frame_mid.jpg
ffmpeg -i footage/INPUT.MOV -vf "reverse,scale=1280:-1" -frames:v 1 -q:v 2 frame_end.jpg

d) 输出格式(每条素材一个JSON)

{
  "filename": "clip_001.MOV",
  "duration": 15.3,
  "resolution": "1920x1080",
  "visual": [
    {"time": "0:00", "description": "...", "shot_type": "近景", "camera": "手持", "mood": "温馨"}
  ],
  "audio": {
    "has_speech": true,
    "mean_volume_db": -20.5,
    "max_volume_db": -3.2,
    "transcript": [{"start": 0.5, "end": 2.3, "text": "..."}]
  }
}

阶段3.5: 素材精修预处理

目标:在给LLM之前,自动标注每条素材的"推荐有效区间",让LLM专注叙事编排。

为什么需要这步: 原始手机素材普遍存在:开头录制口令、同一句话重复多遍、前1-2s举手机晃动、说完话后拖很长的无意义画面。如果把这些原始数据直接给LLM,LLM输出的plan还需要逐条修补。

预处理做四件事:

a) 录制口令检测

RECORDING_CUES = [
    "开始了", "好了开始", "开始录了", "好了 开始",
    "走了", "好了", "来了",  # 仅在前3s内出现时算口令
]

def detect_cues(transcript_segments, duration):
    """检测录制口令,返回skip zones"""
    skip_zones = []
    for seg in transcript_segments:
        text = seg['text'].strip()
        if seg['start'] < 3.0 and any(text.startswith(c) or text == c for c in RECORDING_CUES):
            skip_zones.append({
                'type': 'recording_cue',
                'range': [seg['start'], seg['end']],
                'text': text,
                'action': 'skip'
            })
        if seg['end'] > duration - 3.0 and text in ["好了", "走了", "好了 走"]:
            skip_zones.append({
                'type': 'recording_cue',
                'range': [seg['start'], seg['end']],
                'text': text,
                'action': 'skip'
            })
    return skip_zones

b) 重复语音检测

from difflib import SequenceMatcher

def detect_repeats(segments, threshold=0.6):
    """检测重复语音,标记建议跳过的重复段"""
    repeats = []
    for i, a in enumerate(segments):
        for j, b in enumerate(segments):
            if j <= i: continue
            ratio = SequenceMatcher(None, a['text'], b['text']).ratio()
            if ratio >= threshold and len(a['text']) > 4:
                skip = a if len(a['text']) <= len(b['text']) else b
                repeats.append({
                    'type': 'repeat',
                    'range': [skip['start'], skip['end']],
                    'text': skip['text'],
                    'kept': b['text'] if skip == a else a['text'],
                    'action': 'suggest_skip'
                })
    return repeats

c) 起手晃动 / 结尾拖拽检测

def detect_trim_points(segments, duration, mean_volume_db):
    """基于语音+音量推荐起止点"""
    suggested_start = 0.0
    suggested_end = duration

    if segments:
        first_speech = segments[0]['start']
        last_speech_end = segments[-1]['end']
        if first_speech > 2.0:
            suggested_start = max(0, first_speech - 0.5)
        if duration - last_speech_end > 3.0:
            suggested_end = last_speech_end + 1.5
    else:
        if duration > 5.0:
            suggested_start = 1.0

    return suggested_start, suggested_end

d) 纯画面时长限制

模式 纯画面上限 口令处理 重复处理 起手晃动
strict 8-10s 全部去除 只留1遍 去1.5s
normal 12-15s 去除明确口令 去明显重复 去1.0s
loose 20-25s 仅去"开始了" 保留大部分 去0.5s

e) 输出格式

预处理结果追加到素材分析数据里:

━━━ clip_012.mp4 | 时长25.8s | 1920x1080 ━━━
  【画面】...
  【语音】...
  【精修建议】模式: normal
    原始区间: [0.0 - 25.8]
    推荐区间: [2.4 - 25.8]
    ⊘ [0.0-2.4] 跳过: 录制口令 "开始了"
    有效语音: [2.62-25.8]

⚠ 重要原则:预处理只做标注和建议,不做硬裁剪。LLM保留最终决定权。

阶段4: LLM叙事编排

目标:把预处理后的素材数据交给LLM,生成剪辑方案。

输入:素材分析 + 精修建议(推荐区间) + 参考研究结论 输出:剪辑方案JSON(结构见下方schema)

edit_plan JSON Schema

{
  "title": "视频标题",
  "structure": [
    {
      "section": "段落名 — 副标题",
      "description": "本段落内容概述",
      "clips": [
        {
          "file": "素材文件名.mp4",
          "start": 0.0,
          "end": 12.0,
          "note": "画面内容简述",
          "subtitle": "保留的语音文字"
        }
      ]
    }
  ],
  "bgm_suggestion": "BGM风格建议(含genre/mood/instruments/tempo/bpm)",
  "editing_notes": "整体剪辑说明"
}

约束:

  • 同一素材可出现在多个clip中(不同时间区间)
  • start/end 不能截断语音中间(阶段4.5会校验)
  • 第一个section建议是"开篇"类引入段落
  • 最后一个section建议是"收尾"类段落

详细prompt模板见 templates/edit_plan_prompt.md

阶段4+ : 分镜可视化 + Dashboard(浏览器预览)

目标:把LLM输出的剪辑方案做成图文并茂的网页,方便用户直观Review。

Dashboard 包含两个面板:

  1. 📋 素材总览 — 素材缩略图网格 + 时长/语音/音量标注 + 筛选(全部/已用/未用/含语音) + 点击查看详情
  2. ✅ 成品质量检查 — 成品视频定时抽帧(每30秒) + 段落定位 + 点击放大

使用 gen_dashboard.py 脚本:

# 最小用法(只生成素材面板)
python3 scripts/gen_dashboard.py \
  --analysis clip_analysis.json \
  --plan edit_plan.json \
  --footage footage/ \
  --out output/

# 完整两面板(含成品QC帧)
python3 scripts/gen_dashboard.py \
  --analysis clip_analysis.json \
  --plan edit_plan.json \
  --footage footage/ \
  --video output/final.mp4 \
  --out output/

支持两种分析数据格式(自动检测):

  • 标准格式 clip_analysis.json: {filename, duration, visual: [...], audio: {...}}
  • 紧凑格式 clips_compact.json: {file, dur, visual: "string", speech: [...]}

用户确认分镜方案后,再进入渲染阶段。

阶段4.5: 语音截断校验

目标:自动校验LLM输出的plan,确保没有把任何语音从中间截断。

def validate_speech(plan_clips, speech_data):
    """检查每个clip的start/end是否截断了语音"""
    issues = []
    for clip in plan_clips:
        f = clip['file']
        cs, ce = clip['start'], clip['end']
        if f not in speech_data: continue
        for ss, se, txt in speech_data[f]:
            if ss < ce and se > cs:  # 语音与clip有交集
                if ss < cs - 0.3:    # 语音开始在clip之前
                    issues.append(f"截断开头: {f} clip从{cs}开始,但语音\"{txt}\"{ss}开始")
                if se > ce + 0.5:    # 语音结束在clip之后
                    issues.append(f"截断结尾: {f} clip在{ce}结束,但语音\"{txt}\"{se}结束")
    return issues

校验不通过时:自动修复start/end,不需要回LLM重新编排。

自动修复逻辑

def fix_speech_cuts(plan, speech_data, margin=0.3):
    """
    自动修复语音截断问题。直接修改plan中的start/end。
    
    参数:
        plan: edit_plan dict (会被原地修改)
        speech_data: dict, {filename: [(start, end, text), ...]}
        margin: float, 语音边界容差(秒)
    
    返回:
        fixes: list of str, 修复日志
    """
    fixes = []
    
    for sec in plan["structure"]:
        for clip in sec["clips"]:
            f = clip["file"]
            cs = float(clip["start"])
            ce = float(clip["end"])
            
            if f not in speech_data:
                continue
            
            for ss, se, txt in speech_data[f]:
                if ss >= ce or se <= cs:
                    continue
                
                # 开头截断:clip.start > speech.start + margin
                if ss < cs - margin and se > cs:
                    old_start = cs
                    new_start = max(0, ss - 0.1)
                    clip["start"] = round(new_start, 1)
                    fixes.append(
                        f"  修复开头: {f} [{old_start}{new_start}] "
                        f"语音\"{txt[:20]}\"{ss}s开始"
                    )
                    cs = new_start
                
                # 结尾截断:clip.end < speech.end - margin
                if se > ce + margin and ss < ce:
                    old_end = ce
                    new_end = se + 0.2
                    clip["end"] = round(new_end, 1)
                    fixes.append(
                        f"  修复结尾: {f} [{old_end}{new_end}] "
                        f"语音\"{txt[:20]}\"{se}s结束"
                    )
                    ce = new_end
    
    return fixes


def validate_and_fix(plan, speech_data):
    """
    端到端校验+修复入口。
    
    用法:
        import json
        # 加载plan
        plan = json.load(open('edit_plan.json'))
        # 加载语音数据 (格式: {filename: [{start, end, text}, ...]})
        raw = json.load(open('clip_analysis.json'))
        speech_data = {}
        for clip_info in raw:
            fname = clip_info['filename']
            if clip_info['audio']['has_speech']:
                speech_data[fname] = [
                    (s['start'], s['end'], s['text'])
                    for s in clip_info['audio']['transcript']
                ]
        
        # 校验
        all_clips = [c for sec in plan['structure'] for c in sec['clips']]
        issues = validate_speech(all_clips, speech_data)
        print(f'发现 {len(issues)} 个截断问题')
        
        # 修复
        fixes = fix_speech_cuts(plan, speech_data)
        print(f'自动修复 {len(fixes)} 处')
        
        # 二次校验确认
        all_clips2 = [c for sec in plan['structure'] for c in sec['clips']]
        issues2 = validate_speech(all_clips2, speech_data)
        assert len(issues2) == 0, f'仍有 {len(issues2)} 个问题未修复!'
        print('✅ 二次校验通过')
        
        # 保存修复后的plan
        json.dump(plan, open('edit_plan_fixed.json', 'w', ensure_ascii=False), indent=2)
    """
    all_clips = [c for sec in plan['structure'] for c in sec['clips']]
    issues = validate_speech(all_clips, speech_data)
    fixes = fix_speech_cuts(plan, speech_data)
    return issues, fixes

修复策略:

截断类型 检测条件 修复方式
开头截断 clip.start > speech.start + margin clip.start → speech.start - 0.1s
结尾截断 clip.end < speech.end - margin clip.end → speech.end + 0.2s

边界容差 (margin=0.3s): Whisper/FunASR 时间戳有±0.2-0.3s误差。修复后必须跑二次校验。

阶段5: ffmpeg自动执行

目标:把剪辑方案转成ffmpeg命令并执行。

渲染策略:逐片段裁剪 → 拼接

# 单个片段裁剪+编码
ffmpeg -y -ss 00:00:02.400 -i footage/INPUT.mp4 -t 00:00:23.400 \
  -vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,setsar=1" \
  -r 30 -c:v libx264 -preset medium -crf 18 -pix_fmt yuv420p \
  -c:a aac -b:a 128k -ar 44100 -ac 2 \
  -movflags +faststart segments/seg_001.mp4

# 拼接成品
ffmpeg -y -f concat -safe 0 -i concat.txt -c copy -movflags +faststart output.mp4

变速

# 2倍速(视频+音频同步)
ffmpeg -i input.mp4 -filter:v "setpts=0.5*PTS" -filter:a "atempo=2.0" out.mp4

片头高光蒙太奇

从各段落选最有视觉冲击力的镜头,快切拼接放在视频最前面。

选片原则:

  • 每个主要段落选1个精彩画面
  • 每个镜头0.5-1秒,快切节奏
  • 总共6-10个镜头,总时长5-8秒
  • 排除竖屏素材
  • 不加音频(-an),最终由BGM覆盖
import subprocess, os

highlights = [
    ('footage/clip_a.mp4',   1.5, 2.2),
    ('footage/clip_b.mp4',   2.0, 2.7),
    # ... 每个0.7秒左右
]

for i, (f, start, end) in enumerate(highlights, 1):
    dur = end - start
    cmd = f'ffmpeg -y -ss {start} -i "{f}" -t {dur} ' \
          f'-vf "scale=1920:1080:force_original_aspect_ratio=decrease,' \
          f'pad=1920:1080:(ow-iw)/2:(oh-ih)/2" ' \
          f'-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p ' \
          f'-an highlights/hl_{i}.mp4'
    subprocess.run(cmd, shell=True)

# concat
with open('highlights/concat.txt', 'w') as f:
    for i in range(1, len(highlights)+1):
        f.write(f"file 'hl_{i}.mp4'\n")

subprocess.run('ffmpeg -y -f concat -safe 0 -i highlights/concat.txt '
               '-c copy highlights/montage.mp4', shell=True)

段落标题叠加

用 Pillow 生成透明RGBA的PNG,ffmpeg overlay到视频画面上。不要用黑底标题卡

from PIL import Image, ImageDraw, ImageFont, ImageFilter

W, H = 1920, 1080
title = "段落标题"

img = Image.new("RGBA", (W, H), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)

# 字体:macOS用冬青黑体,Linux用Noto Sans CJK
# font = ImageFont.truetype("/System/Library/Fonts/Hiragino Sans GB.ttc", 60)  # macOS
# font = ImageFont.truetype("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", 60)  # Linux
font = ImageFont.truetype("YOUR_FONT_PATH", 60)

bbox = draw.textbbox((0, 0), title, font=font)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
x, y = (W - tw) // 2, (H - th) // 2

# 柔和阴影
shadow = Image.new("RGBA", (W, H), (0, 0, 0, 0))
sd = ImageDraw.Draw(shadow)
sd.text((x+2, y+2), title, fill=(0, 0, 0, 180), font=font)
shadow = shadow.filter(ImageFilter.GaussianBlur(radius=6))

img = Image.alpha_composite(img, shadow)
draw = ImageDraw.Draw(img)
draw.text((x, y), title, fill=(255, 255, 255, 240), font=font)
img.save("title_overlay.png")

overlay到视频上:

# 标题显示前3秒
ffmpeg -y -i section.mp4 -i title.png \
  -filter_complex "overlay=0:0:enable='between(t,0,3)'" \
  -c:v libx264 -preset medium -crf 18 -pix_fmt yuv420p \
  -c:a aac -b:a 128k -movflags +faststart \
  section_titled.mp4

⚠ overlay关键Pitfall:

  • ❌ 不要加 shortest=1 — PNG只有1帧,会导致视频流立刻截断
  • ✅ 直接用 overlay=0:0:enable='between(t,0,3)' 最简单可靠
  • ✅ 先用短片段(5-8s)验证overlay效果

段落拼接(推荐方案)

✅ 正确方案:逐段编码 → concat copy

echo "file 'final_sec_01.mp4'" > concat.txt
echo "file 'final_sec_02.mp4'" >> concat.txt
ffmpeg -y -f concat -safe 0 -i concat.txt -c copy -movflags +faststart output.mp4

❌ 不要用xfade链式合并: 链式xfade会导致帧数累积丢失,后半段全黑。

段落间过渡:段落内硬切即可。如需渐变,每段首尾加fade比xfade更可靠。

BGM生成与混音(可选)

BGM 可以手动添加,也可以用 AI 音乐生成工具(如 MiniMax music、Suno 等)。

a) AI 生成 BGM

风格映射参考(vlog常用):

Vlog 风格 genre mood instruments tempo
轻松日常 indie pop cheerful, carefree ukulele, acoustic guitar moderate
文艺治愈 folk, acoustic warm, gentle acoustic guitar, piano, strings slow
美食探店 jazz, bossa nova cozy, playful piano, upright bass moderate
冰雪/冬季 cinematic, ambient serene, majestic piano, celesta, strings slow
热带/海岛 tropical house sunny, relaxed steel drums, marimba upbeat
城市探索 lo-fi hip hop chill, urban keys, vinyl crackle moderate
b) 混合到视频
# BGM 音量 10-15%(有语音的vlog要压低BGM)
ffmpeg -y -i video_no_bgm.mp4 -i bgm.m4a \
  -filter_complex "[0:a]volume=1.0[orig];[1:a]volume=0.12[bgm];[orig][bgm]amix=inputs=2:duration=first:dropout_transition=2[aout]" \
  -map 0:v -map "[aout]" -c:v copy -c:a aac -b:a 192k \
  -movflags +faststart output/final.mp4

BGM 音量建议:

  • 有大量语音/旁白 → 8-12%
  • 语音较少,空镜为主 → 15-25%
  • 纯空镜蒙太奇段落 → 30-40%

变速

ffmpeg -i input.mp4 -filter:v "setpts=0.5*PTS" -filter:a "atempo=2.0" out.mp4

Pitfalls

  1. 剪映不可用 — v10.4+ 对 draft_info.json 加密,不要浪费时间
  2. 视觉API端点要区分 — 有些平台视觉模型和文本模型端点不同,确认用对
  3. Whisper模型选择 — base中文太差,large太慢,medium是性价比最优
  4. 不要跳过参考研究 — 没有"好vlog标准"的AI会剪出流水账
  5. ffmpeg编码兼容 — 拼接时注意素材编码一致性,先逐片段统一编码再concat
  6. 视觉API限流 — 高峰时段容易限流,加retry和sleep
  7. 预处理在LLM之前做 — 口令/重复/晃动检测在阶段3.5处理
  8. 预处理是建议不是硬裁剪 — LLM保留覆盖权,某些口令可能有叙事价值
  9. 语音截断必须自动校验 — LLM输出的plan可能在语音中间切断
  10. Whisper幻觉 — 纯音乐/环境音段落whisper会编造文字,需用音量阈值(-40dB)过滤
  11. 竖屏混横屏 — 素材分析阶段就要检测,竖屏(9:16)混在横屏里很突兀,建议去掉
  12. overlay不要加shortest=1 — PNG只有1帧,shortest=1会让视频流截断
  13. 标题卡风格 — 白字+柔和阴影叠在视频画面上,不要黑底标题卡
  14. ffmpeg drawtext不可用 — macOS Homebrew ffmpeg默认没编译freetype,用Pillow生成PNG overlay代替
  15. HEVC警告可忽略 — "Error constructing the frame RPS" 是HEVC解码警告,不影响输出
  16. Non-monotonic DTS — concat拼接时常见的时间戳警告,播放不受影响
  17. 空镜不宜过长 — 无语音的纯画面>6s偏长,建议压缩到5-8s
  18. xfade链式合并不可靠 — 链式xfade会导致帧数累积丢失,后半段全黑,用concat代替
  19. 先用短片段验证 — 每次改渲染方案都先用5-8秒短片段验证效果
  20. 渲染完必须验证帧数ffmpeg -i output.mp4 -f null - 检查实际frame数
  21. BGM别太大声 — 有语音段BGM控制在8-12%,先试听再定
  22. 多段BGM衔接 — 保持相近的key和tempo(bpm差值<20)
  23. 混音后必须试听 — 不同素材原始音量差异大
  24. 不要重复安装依赖 — whisper/Pillow等先检测再装,不要每次都新建venv
  25. FunASR必须装torchaudiopip install funasr modelscope 不够,必须 pip install funasr modelscope torchaudio,否则 import 报 No module named 'torchaudio'
  26. FunASR时间戳是字级别 — 不像whisper直接给句级别segments,FunASR返回每个字的[start_ms, end_ms],需要按标点聚合成句子才能喂给后续的语音截断校验(阶段4.5)
  27. FunASR幻觉比whisper轻 — 纯环境音/音乐时FunASR通常返回空文本,不像whisper会编造内容。但仍需用音量阈值(-40dB)做二次确认

编码与拼接类(视频教程实战新增)

  1. ⚠️ atempo加速后音视频时长不一致 — atempo处理后音频比视频短0.4-0.8秒/段。8段累积差好几秒,后半段音画完全对不上。每段剪辑后必须立即验证音视频时长差<0.05s,超过则用apad补齐:
# 验证
ffprobe -show_entries stream=duration -select_streams v -of csv=p=0 seg.mp4  # 视频时长
ffprobe -show_entries stream=duration -select_streams a -of csv=p=0 seg.mp4  # 音频时长
# 补齐
ffmpeg -y -i seg.mp4 -af "apad" -c:v copy -c:a aac -shortest seg_fixed.mp4
  1. ⚠️ concat copy音频时间戳不连续 — 多段concat copy后音频咔咔响/剪映音画偏移。正确方案:音频单独提取wav+apad补齐+concat filter拼接,视频单独concat copy,最后合并
# 每段提取音频(apad补齐到视频时长)
ffmpeg -y -i seg.mp4 -vn -af "apad" -ar 48000 -ac 2 -c:a pcm_s16le -t {视频时长} seg.wav
# concat filter拼接音频(不是demuxer)
ffmpeg -y -i a.wav -i b.wav -filter_complex "[0:a][1:a]concat=n=2:v=0:a=1[aout]" -map "[aout]" -c:a aac audio.m4a
# 合并
ffmpeg -y -i video.mp4 -i audio.m4a -c:v copy -c:a copy output.mp4
  1. 剪映兼容编码 — 导入剪映音画不同步的修复:-r 30 -vsync cfr(固定帧率) + -x264-params "bframes=0"(无B帧) + -ar 48000 -ac 2(统一音频)

  2. crf值对录屏类内容要低 — 录屏文字密集,crf 22码率不够会糊。录屏用-crf 18-b:v 3M。一般vlog素材crf 22即可

  3. 多次重编码画质累积损失 — 剪辑→拼接→加背景→叠进度条,每次编码都损失画质。尽量一次filter链搞定,减少编码次数

  4. 后台ffmpeg进程泄漏 — kill后台任务时可能只杀shell不杀ffmpeg子进程。每次kill后ps aux | grep ffmpeg确认,残留用kill -9 PID

  5. 开工前第一件事确认输出分辨率 — 全流程统一分辨率不要中途改。录屏类用1080p(文字密集对分辨率敏感),vlog素材用原始分辨率或1080p

  6. ⚠️ overlay视频时音频被覆盖/丢失 — 用filter_complex overlay叠加进度条/水印等视频时,ffmpeg可能自动选取overlay源(静音)的音频而不是原视频的音频。必须用-map明确指定音频来源

# ❌ 不指定map,音频可能来自overlay源(静音)
ffmpeg -i main.mp4 -i overlay.mp4 -filter_complex "overlay=0:0" output.mp4

# ✅ 明确map:视频取filter输出,音频取原视频
ffmpeg -i main.mp4 -i overlay.mp4 \
  -filter_complex "[0:v][1:v]overlay=0:0[vout]" \
  -map "[vout]" -map 0:a \
  -c:v libx264 -c:a copy output.mp4

如果还不行(音频编解码问题),分步做更可靠

# Step1: 只处理视频(-an去掉音频)
ffmpeg -i main.mp4 -i overlay.mp4 -filter_complex "overlay" -map "[vout]" -an video_only.mp4
# Step2: 把原始音频合回来
ffmpeg -i video_only.mp4 -i main.mp4 -map 0:v -map 1:a -c:v copy -c:a copy final.mp4
  1. ⚠️ -shortest会截断音频 — 视频和音频时长不完全一致时,-shortest会丢掉最后几秒的音频。优先用-t {精确时长}替代,或者确保音视频等长后不加-shortest

端到端示例

项目文件结构

my-vlog-project/
├── footage/                    # 原始素材(用户提供)
│   ├── clip_001.MOV
│   └── ...
│
├── analysis/                   # 阶段3输出
│   └── clip_analysis.json
│
├── edit_plan.json              # 阶段4输出
├── edit_plan_fixed.json        # 阶段4.5输出
│
├── output/
│   ├── dashboard.html          # Dashboard网页
│   ├── thumbnails/             # 素材缩略图
│   ├── qc_frames/              # 成品QC帧
│   ├── segments/               # 裁剪后的片段
│   ├── highlights/             # 片头高光蒙太奇
│   ├── titles/                 # 段落标题PNG
│   ├── sections/               # 带标题的段落视频
│   ├── bgm/                    # BGM文件
│   ├── video_no_bgm.mp4        # 无BGM的完整视频
│   └── final.mp4               # 🎬 成品视频
│
└── reference/                  # 阶段2输出(可选)

中间产物示例

examples/ 目录下的示例文件。

执行流程速查

素材文件夹 footage/
     │
     ▼
 阶段1: 素材盘点 → ffprobe批量扫描 → 清单
     │
     ▼
 阶段2: 参考研究 → 下载优质vlog → 分析节奏(可选)
     │
     ▼
 阶段3: 素材三维分析 → whisper/FunASR+音量+视觉 → clip_analysis.json
     │
     ▼
 阶段3.5: 精修预处理 → 口令/重复/晃动/纯画面标注
     │
     ▼
 阶段4: LLM叙事编排 → edit_plan.json
     │
     ▼
 阶段4+: Dashboard可视化 → 浏览器预览确认
     │
     ▼
 阶段4.5: 语音截断校验 → 自动修复
     │
     ▼
 阶段5: ffmpeg渲染 → 裁剪→标题→拼接→BGM → final.mp4 🎬

验证

  • 素材盘点完成,生成清单
  • 参考研究完成,提取规律
  • 素材三维分析完成(视觉+音频+音量)
  • 素材精修预处理完成
  • LLM剪辑方案生成
  • 语音截断校验通过
  • 每段片段音视频时长差<0.05s
  • ffmpeg执行成功,输出成品视频
  • 成品音频完整(不被-shortest截断)
  • 拖到视频后半段检查音画同步
  • 成品视频质量人工确认

Author: nyx研究所 · GitHub · B站 · 小红书 @nyx研究所 License: MIT

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/znyupup-ai-video-editing-skill-vlog-auto-edit/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

znyupup-ai-video-editing-skill-vlog-auto-edit.ocm.jsonjson
{
  "ocm": "1",
  "id": "znyupup-ai-video-editing-skill-vlog-auto-edit",
  "kind": "skill",
  "name": "vlog-auto-edit",
  "description": "AI Agent自动剪辑旅行Vlog的完整工作流。从原始素材到成品视频,系统级只需ffmpeg,其余在Python venv内完成。by nyx研究所 (GitHub @znyupup · B站/小红书 @nyx研究所)",
  "publisher": "znyupup",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "general"
    ],
    "tags": [
      "skill-md",
      "video-editing",
      "vlog",
      "ffmpeg",
      "whisper",
      "funasr",
      "automation",
      "github"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "AI Agent自动剪辑旅行Vlog的完整工作流。从原始素材到成品视频,系统级只需ffmpeg,其余在Python venv内完成。by nyx研究所 (GitHub @znyupup · B站/小红书 @nyx研究所)"
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "github",
      "repository": "https://github.com/znyupup/ai-video-editing-skill",
      "path": "SKILL.md",
      "ref": "b6429ab550a64c595dc68d42c47cf2b489a50619",
      "url": "https://github.com/znyupup/ai-video-editing-skill/blob/b6429ab550a64c595dc68d42c47cf2b489a50619/SKILL.md",
      "key": "znyupup/ai-video-editing-skill/SKILL.md"
    }
  },
  "instructions": "# AI Agent 自动剪辑旅行 Vlog\n\n**Author:** nyx研究所 · [GitHub](https://github.com/znyupup) · [B站 @nyx研究所](https://space.bilibili.com/4330525) · 小红书 @nyx研究所 · [X @znyupup_music](https://x.com/znyupup_music)\n\n> 把一堆手机拍的旅行素材,用AI自动剪成一个完整vlog。\n> 最小依赖:ffmpeg + Python(whisper或FunASR+Pillow) + 视觉API。系统级只装ffmpeg,其余pip装。\n\n## 触发条件\n\n- 用户有一批旅行/日常视频素材,想自动剪成vlog\n- 用户说\"帮我剪个视频\"、\"自动剪辑\"、\"vlog剪辑\"\n\n## 前置要求\n\n### 1. 系统工具\n\n| 工具 | 用途 | 安装 |\n|------|------|------|\n| ffmpeg | 视频裁剪/编码/拼接/抽帧/音量检测 | `brew install ffmpeg` (macOS) / `apt install ffmpeg` (Linux) |\n| Python 3.9+ | 脚本胶水 | macOS/Linux 系统自带 |\n\n### 2. Python依赖(先检测,已有则跳过)\n\n*",
  "cost": {
    "context_tokens": 6704
  }
}

Fetch it by URL: GET /api/v1/registry/znyupup-ai-video-editing-skill-vlog-auto-edit/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.