ストーリー
田
田中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形式に変換 |
チェックリスト
推定所要時間: 30分