video-subtitle-cutter について
video-subtitle-cutterは、Whisperでの文字起こしから、テキストに基づくカット選定、FFmpeg編集、字幕生成までを案内するスキルです。発話の不要部分を除き、再エンコードで滑らかな映像にしたい制作者向けです。
主な機能
- 01Whisperによるタイムスタンプ付き文字起こし
- 02AIによるカット箇所の特定
- 03FFmpegでの動画切り出しと結合
- 04滑らかなカットのための再エンコード案内
- 05SRT字幕の生成
活用シーン
- 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 = Constant Rate Factor):
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:結合時のタイムスタンプの問題を修正します-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.5 秒を超える間隔)を削除する
- 言い直しや繰り返しを削除する
- 「ショート動画スタイル」の場合は、フック+要点+CTA のみを残す
-
再エンコードして結合する(再エンコードは必須です。
-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