关于 video-subtitle-cutter
video-subtitle-cutter 是用于 AI 辅助视频清理的技能,涵盖 Whisper 转录、依据文本选择剪辑点、使用 FFmpeg 编辑和生成字幕。它面向需要删除口语瑕疵并通过重新编码保持剪辑流畅的创作者。
主要功能
- 01使用 Whisper 生成时间戳转录
- 02借助 AI 识别剪辑片段
- 03用 FFmpeg 剪切并拼接视频
- 04指导重新编码以保持剪辑流畅
- 05生成 SRT 字幕
适用场景
- 01删除填充词和说错重来的片段
- 02精简播客或访谈视频
- 03为剪辑后的视频制作字幕
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ different-ai/ agent-bank --skill video-subtitle-cutter安装后在 Claude Code 中输入/video-subtitle-cutter确认已加载。
详细介绍
我的工作
通过以下方式自动化视频剪辑:
- 将视频转录为带时间戳的字幕(Whisper)
- 使用 AI 分析转录内容,识别需要剪切的部分(填充词、停顿、错误)
- 生成 FFmpeg 命令,剪切并拼接干净的片段
- 为最终视频生成字幕(SRT)
重要:始终重新编码(切勿使用 -c copy)
最常见的错误是使用 -c copy 剪切。 这会导致:
- 剪切点出现画面冻结(冻结 1–8 秒)
- 音视频不同步
- 播放卡顿
原因是什么? H.264 视频每隔 2–10 秒会使用关键帧(I 帧)。-c copy 只能在关键帧处剪切,因此 FFmpeg 会包含额外的帧,导致画面冻结。
解决方案: 始终使用质量设置重新编码片段:
# WRONG - causes freeze frames
ffmpeg -ss 10 -i video.mp4 -t 5 -c copy segment.mp4
# CORRECT - smooth cuts at any timestamp
ffmpeg -ss 10 -i video.mp4 -t 5 \
-c:v libx264 -preset fast -crf 18 \
-c:a aac -b:a 192k \
-avoid_negative_ts make_zero \
segment.mp4
质量预设(CRF = 恒定速率因子):
crf 15-17= 接近无损(文件较大)crf 18-20= 高质量(推荐)crf 21-23= 良好质量(文件较小)crf 24-28= 中等质量(文件小得多)
前提条件
# Install Whisper (choose one)
pip install openai-whisper # Local (requires Python 3.9+)
# OR use OpenAI API (no local install needed)
# Install FFmpeg
brew install ffmpeg # macOS
sudo apt install ffmpeg # Linux
快速开始
第 1 步:转录视频
选项 A:本地 Whisper(免费,速度较慢)
whisper video.mp4 --model medium --output_format json --output_dir ./
选项 B:OpenAI Whisper API(速度快,付费)
curl https://api.openai.com/v1/audio/transcriptions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F file="@video.mp4" \
-F model="whisper-1" \
-F response_format="verbose_json" \
-F timestamp_granularities[]="segment" \
> transcript.json
选项 C:先使用 ffmpeg 提取音频(适用于大型文件)
# Extract audio (much smaller file to upload)
ffmpeg -i video.mp4 -vn -acodec libmp3lame -q:a 2 audio.mp3
# Then transcribe the audio
whisper audio.mp3 --model medium --output_format json
第 2 步:分析转录内容以确定剪切位置
使用以下提示词将转录内容提供给 AI:
Analyze this video transcript and identify segments to CUT (remove).
TRANSCRIPT:
{paste transcript.json segments here}
Identify these issues:
1. FILLER WORDS: "um", "uh", "like", "you know", "basically", "actually", "so", "right"
2. FALSE STARTS: Incomplete sentences that restart ("I think— actually, let me...")
3. LONG PAUSES: Gaps > 1.5 seconds between segments
4. REPETITIONS: Same word/phrase repeated ("really really really")
5. CORRECTIONS: "Wait, I meant...", "Sorry, let me rephrase..."
6. TANGENTS: Off-topic rambling (use judgment)
Return a JSON array of segments to KEEP (not cut):
[
{"start": 0.0, "end": 2.5, "text": "Welcome to this video"},
{"start": 3.1, "end": 8.4, "text": "Today we're going to cover..."},
...
]
Rules:
- Merge adjacent keep segments if gap < 0.3s
- Ensure cuts don't happen mid-word (check word boundaries)
- Preserve natural speech rhythm (don't over-cut)
- When in doubt, keep the segment
第 3 步:生成 FFmpeg 命令(高质量)
确定要保留的片段后,使用此 Python 脚本实现平滑剪切:
import json
import subprocess
import os
VIDEO_INPUT = "video.mp4"
VIDEO_OUTPUT = "video_clean.mp4"
SEGMENTS_FILE = "keep_segments.json"
with open(SEGMENTS_FILE) as f:
segments = json.load(f)
segment_files = []
for i, seg in enumerate(segments):
outfile = f"temp_seg_{i:04d}.mp4"
segment_files.append(outfile)
# MUST re-encode for smooth cuts (no -c copy!)
cmd = [
'ffmpeg', '-y',
'-ss', str(seg['start']), # Seek BEFORE input (fast)
'-i', VIDEO_INPUT,
'-t', str(seg['end'] - seg['start']), # Duration
'-c:v', 'libx264',
'-preset', 'fast', # fast/medium/slow
'-crf', '18', # Quality (lower = better, 15-23 recommended)
'-c:a', 'aac',
'-b:a', '192k',
'-avoid_negative_ts', 'make_zero', # Fix timestamp issues
'-async', '1', # Sync audio
outfile
]
subprocess.run(cmd, capture_output=True)
print(f"✓ Segment {i+1}/{len(segments)}")
# Create concat file
with open('temp_concat.txt', 'w') as f:
for sf in segment_files:
f.write(f"file '{sf}'\n")
# Concatenate (can use -c copy here since all segments match)
subprocess.run([
'ffmpeg', '-y', '-f', 'concat', '-safe', '0',
'-i', 'temp_concat.txt',
'-c', 'copy',
VIDEO_OUTPUT
])
# Cleanup
for sf in segment_files:
os.remove(sf)
os.remove('temp_concat.txt')
print(f"✓ Created: {VIDEO_OUTPUT}")
关键标志说明:
-ss放在-i前:快速定位(无需解码整个视频)-t:片段时长(不是结束时间)-crf 18:高质量编码-avoid_negative_ts make_zero:修复 concat 时间戳问题-async 1:保持音频同步
第 4 步:生成字幕
创建最终视频后,使用 Whisper 重新生成字幕:
# Generate SRT subtitles for the cleaned video
whisper video_clean.mp4 --model medium --output_format srt --output_dir ./
# For higher accuracy (slower):
whisper video_clean.mp4 --model large --output_format srt --language en
# Output: video_clean.srt
将字幕烧录到视频中(可选):
# Embed subtitles permanently
ffmpeg -i video_clean.mp4 -vf "subtitles=video_clean.srt:force_style='FontSize=24,FontName=Arial,PrimaryColour=&HFFFFFF,OutlineColour=&H000000,Outline=2'" -c:a copy video_with_subs.mp4
字幕样式选项:
FontSize=24- 文字大小FontName=Arial- 字体PrimaryColour=&HFFFFFF- 白色文字(BGR 格式)OutlineColour=&H000000- 黑色描边Outline=2- 描边粗细MarginV=50- 与底部的距离
完整工作流脚本(高质量)
#!/usr/bin/env python3
"""
video_clean.py - Clean up video by removing filler words/pauses
Uses re-encoding for smooth cuts (no freeze frames)
"""
import json
import subprocess
import os
import sys
def get_duration(filepath):
"""Get video duration in seconds"""
result = subprocess.run([
'ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', filepath
], capture_output=True, text=True)
return float(json.loads(result.stdout)['format']['duration'])
def extract_segment(input_file, start, end, output_file, crf=18, preset='fast'):
"""Extract a segment with re-encoding for smooth cuts"""
cmd = [
'ffmpeg', '-y',
'-ss', str(start),
'-i', input_file,
'-t', str(end - start),
'-c:v', 'libx264',
'-preset', preset,
'-crf', str(crf),
'-c:a', 'aac',
'-b:a', '192k',
'-avoid_negative_ts', 'make_zero',
'-async', '1',
output_file
]
return subprocess.run(cmd, capture_output=True, text=True)
def concatenate_segments(segment_files, output_file):
"""Concatenate segments into final video"""
with open('temp_concat.txt', 'w') as f:
for sf in segment_files:
f.write(f"file '{sf}'\n")
subprocess.run([
'ffmpeg', '-y', '-f', 'concat', '-safe', '0',
'-i', 'temp_concat.txt',
'-c', 'copy',
output_file
], capture_output=True)
os.remove('temp_concat.txt')
def generate_subtitles(video_file, model='medium'):
"""Generate SRT subtitles using Whisper"""
subprocess.run([
'whisper', video_file,
'--model', model,
'--output_format', 'srt',
'--output_dir', './'
])
def main(video_input, segments, output_name, crf=18):
"""Main workflow"""
segment_files = []
print(f"\n{'='*50}")
print(f"Processing: {video_input}")
print(f"Quality: CRF {crf} (lower=better, 15-23 recommended)")
print(f"{'='*50}\n")
# Extract segments with re-encoding
for i, seg in enumerate(segments):
outfile = f"temp_seg_{i:04d}.mp4"
segment_files.append(outfile)
result = extract_segment(video_input, seg['start'], seg['end'], outfile, crf)
if result.returncode == 0:
duration = seg['end'] - seg['start']
print(f"✓ Segment {i+1}/{len(segments)}: {duration:.1f}s")
else:
print(f"✗ Error on segment {i+1}")
print(result.stderr[-500:])
# Concatenate
print("\nConcatenating segments...")
concatenate_segments(segment_files, output_name)
# Cleanup temp segments
for sf in segment_files:
os.remove(sf)
# Generate subtitles
print("\nGenerating subtitles...")
generate_subtitles(output_name)
# Stats
orig_duration = get_duration(video_input)
new_duration = get_duration(output_name)
orig_size = os.path.getsize(video_input) / (1024*1024)
new_size = os.path.getsize(output_name) / (1024*1024)
print(f"\n{'='*50}")
print(f"COMPLETE")
print(f"{'='*50}")
print(f"Original: {orig_duration:.0f}s | {orig_size:.1f} MB")
print(f"Output: {new_duration:.0f}s | {new_size:.1f} MB")
print(f"Removed: {orig_duration - new_duration:.0f}s ({((orig_duration - new_duration)/orig_duration)*100:.0f}%)")
print(f"Video: {output_name}")
print(f"Subtitles: {output_name.replace('.mp4', '.srt')}")
if __name__ == '__main__':
# Example usage
VIDEO = "input.mp4"
SEGMENTS = [
{"start": 0.0, "end": 10.5},
{"start": 12.3, "end": 25.0},
# ... add your segments
]
main(VIDEO, SEGMENTS, "output_clean.mp4", crf=18)
AI 分析提示词模板
基础清理(仅去除填充词)
Remove filler words from this transcript. Return segments to KEEP.
Filler words to remove: um, uh, like, you know, basically, actually, so, right, I mean
TRANSCRIPT SEGMENTS:
{segments}
Return JSON: [{"start": float, "end": float, "text": "cleaned text"}, ...]
深度清理(播客/访谈)
Clean this podcast transcript for a tight, professional edit.
REMOVE:
- All filler words (um, uh, like, you know, basically, so, right)
- False starts and restarts
- Pauses longer than 1 second
- Repetitions
- Off-topic tangents
- "That's a great question" type filler responses
- Excessive laughter/reactions (keep some for naturalness)
KEEP:
- Core content and insights
- Natural transitions
- Important reactions that add context
TRANSCRIPT:
{segments}
Return JSON array of segments to KEEP with cleaned text.
轻度清理(保留自然感)
Lightly clean this transcript while preserving natural speech patterns.
ONLY REMOVE:
- "Um" and "uh" when standalone (not part of thinking pause)
- Obvious mistakes followed by corrections
- Technical issues (coughs, phone rings, etc.)
PRESERVE:
- Natural "like" and "you know" that add personality
- Thinking pauses that feel authentic
- Personality quirks
TRANSCRIPT:
{segments}
Return JSON array of segments to KEEP.
转录格式参考
Whisper JSON 输出
{
"text": "Full transcript text...",
"segments": [
{
"id": 0,
"start": 0.0,
"end": 2.5,
"text": " Welcome to this video.",
"tokens": [50364, 5765, ...],
"temperature": 0.0,
"avg_logprob": -0.25,
"compression_ratio": 1.2,
"no_speech_prob": 0.01
},
{
"id": 1,
"start": 2.5,
"end": 5.8,
"text": " Um, so today we're going to...",
...
}
],
"language": "en"
}
保留片段格式(用于 FFmpeg)
[
{ "start": 0.0, "end": 2.5, "text": "Welcome to this video." },
{ "start": 3.2, "end": 5.8, "text": "Today we're going to..." }
]
高级:词级时间戳
要精确去除填充词,请使用词级时间戳:
# Whisper with word timestamps
whisper video.mp4 --model medium --word_timestamps True --output_format json
这会提供:
{
"segments": [
{
"start": 0.0,
"end": 2.5,
"text": "Um welcome to this video",
"words": [
{ "word": "Um", "start": 0.0, "end": 0.3 },
{ "word": "welcome", "start": 0.5, "end": 0.9 },
{ "word": "to", "start": 0.9, "end": 1.0 },
{ "word": "this", "start": 1.0, "end": 1.2 },
{ "word": "video", "start": 1.2, "end": 1.6 }
]
}
]
}
现在,你可以精准地剪掉 “Um”(0.0–0.3),并保留 “welcome to this video”(0.5–1.6)。
故障排除
剪切点画面冻结(最常见)
原因: 使用了 -c copy,它只能在关键帧处剪切。
解决方案: 始终使用 -c:v libx264 -crf 18 重新编码(参见上面的示例)。
音视频不同步
提取片段时添加以下标志:
ffmpeg -ss 10 -i video.mp4 -t 5 \
-c:v libx264 -crf 18 \
-c:a aac -b:a 192k \
-avoid_negative_ts make_zero \ # Fix negative timestamps
-async 1 \ # Sync audio to video
segment.mp4
剪切处听起来很突兀
为每个片段添加音频淡入/淡出:
ffmpeg -ss 10 -i video.mp4 -t 5 \
-c:v libx264 -crf 18 \
-af "afade=t=in:st=0:d=0.05,afade=t=out:st=4.95:d=0.05" \
-c:a aac segment.mp4
大型文件处理耗时过长
- 使用
-preset fast或-preset veryfast(以画质换取速度) - 先提取音频再转录(文件会小得多)
- 使用 Whisper API 替代本地模型
- 并行处理(同时处理多个片段)
# Faster encoding (slightly lower quality)
ffmpeg ... -preset veryfast -crf 20 ...
# Even faster for previews
ffmpeg ... -preset ultrafast -crf 23 ...
Whisper 漏掉词语
- 使用
--model large以提高准确度 - 使用
--language en强制使用英语 - 先标准化音频:
ffmpeg -i video.mp4 -af "loudnorm=I=-16:TP=-1.5:LRA=11" -c:v copy normalized.mp4
重新编码后文件过大
增大 CRF 值(数值越高 = 文件越小,画质越低):
# Original quality (large)
-crf 18
# Good quality (medium)
-crf 22
# Acceptable quality (small)
-crf 26
与 OpenCode 集成
在 OpenCode 中使用此技能时:
-
提取音频(加快转录速度):
ffmpeg -i video.mp4 -vn -acodec libmp3lame -q:a 2 temp_audio.mp3 -y -
使用 Whisper 转录:
whisper temp_audio.mp3 --model medium --output_format json --output_dir ./ -
读取 transcript.json 并分析片段
-
根据以下标准确定要保留的片段:
- 删除填充词(um、uh、like、you know)
- 删除较长的停顿(间隔 >1.5s)
- 删除说错后重说的片段和重复内容
- 对于“短视频风格”:仅保留吸引开场 + 关键要点 + 行动号召
-
重新编码并拼接(必须重新编码,绝不能使用 -c copy):
# Use the Python script above with crf=18 for quality -
为最终视频生成字幕:
whisper output.mp4 --model medium --output_format srt -
报告结果,附上处理前后的统计数据
质量设置参考
| 用途 | CRF | 预设 | 备注 |
|---|---|---|---|
| 归档/母版 | 15-17 | slow | 接近无损,文件较大 |
| YouTube/Vimeo | 18-20 | medium | 高质量,推荐 |
| 社交媒体 | 21-23 | fast | 画质良好,文件较小 |
| 预览/草稿 | 24-28 | veryfast | 快速渲染 |
反面做法(切勿采用)
# WRONG: -c copy causes freeze frames
ffmpeg -ss 10 -i video.mp4 -t 5 -c copy segment.mp4
# WRONG: -to instead of -t with -ss before -i
ffmpeg -ss 10 -i video.mp4 -to 15 ... # -to is absolute, not relative
# WRONG: Missing timestamp fix flags
ffmpeg ... -c:v libx264 ... # Missing -avoid_negative_ts