mcpField
V
video-subtitle-cutter

video-subtitle-cutter

github.com/different-aiクリエイティブ・メディア25592 回インストール30 週間前に更新
リポジトリ

video-subtitle-cutter について

video-subtitle-cutterは、Whisperでの文字起こしから、テキストに基づくカット選定、FFmpeg編集、字幕生成までを案内するスキルです。発話の不要部分を除き、再エンコードで滑らかな映像にしたい制作者向けです。

主な機能

  1. 01Whisperによるタイムスタンプ付き文字起こし
  2. 02AIによるカット箇所の特定
  3. 03FFmpegでの動画切り出しと結合
  4. 04滑らかなカットのための再エンコード案内
  5. 05SRT字幕の生成

活用シーン

  1. 01フィラーや言い直しの削除
  2. 02ポッドキャストやインタビューの編集
  3. 03編集後の動画向け字幕作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/different-ai/agent-bank --skill video-subtitle-cutter

インストール後、Claude Code で/video-subtitle-cutterを入力して読み込まれたか確認します。

詳細

このツールでできること

以下の方法で動画編集を自動化します。

  1. 動画をタイムスタンプ付きの字幕に文字起こしします(Whisper)
  2. AI で文字起こし結果を分析し、カット箇所(フィラー語、無音、言い間違い)を特定します
  3. クリーンなセグメントをカットして結合する FFmpeg コマンドを生成します
  4. 完成した動画の字幕(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
大きなファイルの処理に時間がかかりすぎる
  1. -preset fast または -preset veryfast を使用します(速度を優先する代わりに品質が下がります)
  2. 文字起こしの前に音声を抽出します(ファイルサイズを大幅に小さくできます)
  3. ローカルモデルの代わりに Whisper API を使用します
  4. 並列処理します(複数のセグメントを同時に処理します)
# 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 でこのスキルを使用する場合:

  1. 音声を抽出する(文字起こしを高速化します):

    ffmpeg -i video.mp4 -vn -acodec libmp3lame -q:a 2 temp_audio.mp3 -y
    
  2. Whisper で文字起こしする:

    whisper temp_audio.mp3 --model medium --output_format json --output_dir ./
    
  3. transcript.json を読み込み、セグメントを分析します

  4. 次の基準に基づいて、残すセグメントを特定します:

    • フィラーワード(um、uh、like、you know)を削除する
    • 長い無音(1.5 秒を超える間隔)を削除する
    • 言い直しや繰り返しを削除する
    • 「ショート動画スタイル」の場合は、フック+要点+CTA のみを残す
  5. 再エンコードして結合する(再エンコードは必須です。-c copy は使用しないでください):

    # Use the Python script above with crf=18 for quality
    
  6. 最終動画の字幕を生成する:

    whisper output.mp4 --model medium --output_format srt
    
  7. 編集前後の統計情報を含めて、結果を報告する

品質設定の参考
用途CRFプリセット備考
アーカイブ/マスター15-17slowほぼロスレス、大容量ファイル
YouTube/Vimeo18-20medium高画質、推奨
ソーシャルメディア21-23fast良好な画質、ファイルサイズ小
プレビュー/ドラフト24-28veryfast素早くレンダリング
避けるべきパターン(実行しないでください)
# 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

コメント