LESSON 30分

ストーリー

田中VPoE
STTとTTSを学んだ。次は動画分析だ。動画は画像と音声の両方を含む最もリッチなモダリティと言える
あなた
動画全体をAIに渡して分析するのは難しそうですね。データ量が大きいですし
田中VPoE
その通り。動画処理の鍵は「いかに効率的に情報を抽出するか」だ。フレーム抽出、シーン検出、音声分離を組み合わせて、必要な情報だけを効率的にAIに渡す
あなた
NetShop社では研修動画の字幕生成や、商品紹介動画の要約に使えそうですね
田中VPoE
まさにその通り。動画コンテンツの活用度を大幅に引き上げる技術だ

動画分析のアプローチ

基本戦略

動画をそのままAIモデルに入力するのではなく、分解してから分析します。

動画
 ├── 映像トラック → [フレーム抽出] → 画像のリスト → [VLM分析]
 ├── 音声トラック → [音声分離]   → 音声ファイル  → [STT]
 └── メタデータ  → 長さ、解像度、FPS等

これらの分析結果を統合 → 動画の内容理解

処理コストの見積もり

手法コスト精度用途
全フレーム分析非常に高最高非推奨(コスト過大)
等間隔フレーム抽出一般的な動画分析
シーン変化検出低〜中効率的な要約
音声のみ分析会議録画など音声主体
サムネイル + 音声コスト重視の概要把握

フレーム抽出とシーン検出

等間隔フレーム抽出

import cv2
import base64
from typing import Generator

def extract_frames_interval(
    video_path: str,
    interval_sec: float = 5.0,
    max_frames: int = 60
) -> list[dict]:
    """等間隔でフレームを抽出"""
    cap = cv2.VideoCapture(video_path)
    fps = cap.get(cv2.CAP_PROP_FPS)
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    duration = total_frames / fps
    interval_frames = int(fps * interval_sec)

    frames = []
    frame_idx = 0

    while cap.isOpened() and len(frames) < max_frames:
        ret, frame = cap.read()
        if not ret:
            break

        if frame_idx % interval_frames == 0:
            _, buffer = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
            frames.append({
                "index": frame_idx,
                "timestamp": frame_idx / fps,
                "image_base64": base64.b64encode(buffer).decode()
            })

        frame_idx += 1

    cap.release()
    return frames

シーン変化検出

import numpy as np

def detect_scene_changes(
    video_path: str,
    threshold: float = 30.0,
    min_scene_duration: float = 2.0
) -> list[dict]:
    """ヒストグラム差分でシーン変化を検出"""
    cap = cv2.VideoCapture(video_path)
    fps = cap.get(cv2.CAP_PROP_FPS)
    min_frames = int(fps * min_scene_duration)

    prev_hist = None
    scenes = []
    frame_idx = 0
    last_scene_frame = 0

    while cap.isOpened():
        ret, frame = cap.read()
        if not ret:
            break

        # グレースケールヒストグラムを計算
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        hist = cv2.calcHist([gray], [0], None, [256], [0, 256])
        hist = cv2.normalize(hist, hist).flatten()

        if prev_hist is not None:
            # ヒストグラム差分
            diff = cv2.compareHist(prev_hist, hist, cv2.HISTCMP_CHISQR)

            if diff > threshold and (frame_idx - last_scene_frame) > min_frames:
                _, buffer = cv2.imencode(".jpg", frame)
                scenes.append({
                    "frame_index": frame_idx,
                    "timestamp": frame_idx / fps,
                    "diff_score": diff,
                    "image_base64": base64.b64encode(buffer).decode()
                })
                last_scene_frame = frame_idx

        prev_hist = hist
        frame_idx += 1

    cap.release()
    return scenes

動画要約の実装

VLMによるフレーム分析と要約

import anthropic

def summarize_video(
    video_path: str,
    interval_sec: float = 10.0,
    max_frames: int = 30
) -> dict:
    """動画を要約する"""
    client = anthropic.Anthropic()

    # Step 1: フレーム抽出
    frames = extract_frames_interval(video_path, interval_sec, max_frames)

    # Step 2: 音声文字起こし
    audio_path = extract_audio_track(video_path)
    transcription = transcribe_audio(audio_path)

    # Step 3: VLMでフレームを分析
    frame_contents = []
    for frame in frames:
        content = [
            {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": frame["image_base64"]}},
            {"type": "text", "text": f"タイムスタンプ: {frame['timestamp']:.1f}秒"}
        ]
        frame_contents.extend(content)

    # Step 4: 統合要約
    frame_contents.append({
        "type": "text",
        "text": f"""この動画のフレームと音声書き起こしから、以下を作成してください:

音声書き起こし:
{transcription['text']}

出力:
1. 動画全体の要約(3〜5文)
2. 主要なシーン一覧(タイムスタンプ付き)
3. キーポイント(箇条書き)
4. アクションアイテム(該当する場合)

JSON形式で回答してください。"""
    })

    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=2048,
        messages=[{"role": "user", "content": frame_contents}]
    )

    return response.content[0].text


def extract_audio_track(video_path: str) -> str:
    """動画から音声トラックを抽出"""
    from moviepy.editor import VideoFileClip
    audio_path = video_path.rsplit(".", 1)[0] + ".wav"
    video = VideoFileClip(video_path)
    video.audio.write_audiofile(audio_path)
    video.close()
    return audio_path

字幕生成

SRT形式の字幕ファイル生成

def generate_subtitles(
    audio_path: str,
    language: str = "ja",
    max_chars_per_line: int = 40
) -> str:
    """SRT形式の字幕ファイルを生成"""
    transcription = transcribe_audio(audio_path, language)

    srt_content = ""
    for i, segment in enumerate(transcription["segments"], 1):
        start_time = format_srt_time(segment["start"])
        end_time = format_srt_time(segment["end"])
        text = segment["text"].strip()

        # 長い行は改行
        if len(text) > max_chars_per_line:
            mid = len(text) // 2
            space_pos = text.rfind(" ", 0, mid) or text.rfind("、", 0, mid) or mid
            text = text[:space_pos] + "\n" + text[space_pos:].strip()

        srt_content += f"{i}\n{start_time} --> {end_time}\n{text}\n\n"

    return srt_content


def format_srt_time(seconds: float) -> str:
    """秒数をSRT形式のタイムスタンプに変換"""
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    secs = int(seconds % 60)
    millis = int((seconds % 1) * 1000)
    return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"

業務活用パターン

パターン入力出力用途
動画要約研修動画要約テキスト + キーフレームナレッジベース化
字幕生成任意の動画SRT/VTTファイルアクセシビリティ
手順書生成操作デモ動画ステップバイステップの手順書マニュアル自動生成
ハイライト抽出長時間動画重要シーンのクリップレビュー効率化

まとめ

項目内容
基本戦略動画を映像・音声・メタデータに分解して分析
フレーム抽出等間隔抽出 + シーン変化検出
動画要約フレーム分析(VLM) + 音声分析(STT)の統合
字幕生成STTのタイムスタンプ付き出力をSRT形式に変換

チェックリスト

  • 動画分析の基本戦略(分解→分析→統合)を理解した
  • フレーム抽出とシーン変化検出の実装方法を把握した
  • VLM + STTによる動画要約の仕組みを理解した
  • SRT形式の字幕ファイル生成方法を説明できる

推定所要時間: 30分