動画編集 -- AI編集ツール
AIを活用した動画編集の自動化技術を、自動字幕生成・シーン検出・オブジェクト除去・音声分離まで実践的に解説し、編集ワークフローの効率を劇的に向上させる手法を提示する
動画編集 -- AI編集ツール
AIを活用した動画編集の自動化技術を、自動字幕生成・シーン検出・オブジェクト除去・音声分離まで実践的に解説し、編集ワークフローの効率を劇的に向上させる手法を提示する
この章で学ぶこと
- AI 動画編集の基本機能 -- 自動字幕生成、シーン検出、無音部分カット、オブジェクト追跡
- 主要ツールの比較 -- Runway、Descript、CapCut、DaVinci Resolve の AI 機能と使い分け
- プロダクションワークフロー -- 素材取込から公開までの AI 活用パイプライン
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- 動画生成 — Sora、Runway、Pika の内容を理解していること
1. AI 動画編集の全体像
1.1 ワークフロー
AI 動画編集パイプライン
素材取込 粗編集 仕上げ 公開
+----------+ +----------+ +----------+ +----------+
| 自動文字 | | AI シーン | | AI カラー| | AI サムネ|
| 起こし | -> | 検出・分割| --> | グレーディ| -> | イル生成 |
| (Whisper) | | 無音カット| | ング | | リサイズ |
+----------+ +----------+ +----------+ +----------+
| AI 話者 | | AI オブジェ| | AI 音声 | | AI 字幕 |
| 分離 | | クト除去 | | ノイズ除去| | 翻訳 |
+----------+ +----------+ +----------+ +----------+
1.2 技術マップ
AI 動画編集 技術スタック
音声処理
├── Whisper (OpenAI) --- 音声→テキスト変換
├── Demucs (Meta) --- 音声分離 (BGM/ボーカル)
└── RVC --- 音声変換・クローニング
映像処理
├── SAM (Meta) --- 自動セグメンテーション
├── RIFE --- フレーム補間 (スローモーション)
├── Real-ESRGAN --- 超解像 (アップスケール)
└── ProPainter --- オブジェクト除去・修復
テキスト処理
├── GPT-4 --- スクリプト生成・要約
├── DeepL/Google --- 字幕翻訳
└── ElevenLabs --- AI ナレーション生成
1.3 AI 動画編集の進化タイムライン
2019 基礎技術の成熟
│ └─ 自動字幕(YouTube 自動字幕の精度向上)
│
2020 Descript 登場
│ └─ テキストベース動画編集の革新
│
2021 Whisper (OpenAI)
│ └─ 多言語高精度文字起こしの民主化
│
2022 Runway Gen-1 / SAM
│ └─ AI 映像効果とセグメンテーション
│
2023 AI 動画編集の統合化
│ └─ CapCut AI、DaVinci Resolve AI 機能拡充
│
2024 エンドツーエンド AI 編集
│ └─ プロンプトベースの編集指示
│ ProPainter / Track Anything
│ └─ 動画内オブジェクト除去の高品質化
│
2025 マルチモーダル編集
└─ テキスト指示で動画全体を自動編集
2. 自動字幕生成
2.1 Whisper による実装
# OpenAI Whisper で自動字幕生成
import whisper
import json
model = whisper.load_model("large-v3")
# 音声ファイルから文字起こし + タイムスタンプ
result = model.transcribe(
"video.mp4",
language="ja",
task="transcribe",
word_timestamps=True, # 単語レベルのタイムスタンプ
verbose=False,
)
# SRT 形式で出力
def to_srt(segments):
srt_lines = []
for i, seg in enumerate(segments, 1):
start = format_timestamp(seg['start'])
end = format_timestamp(seg['end'])
text = seg['text'].strip()
srt_lines.append(f"{i}\n{start} --> {end}\n{text}\n")
return "\n".join(srt_lines)
def format_timestamp(seconds):
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
ms = int((seconds % 1) * 1000)
return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
srt_content = to_srt(result['segments'])
with open("subtitles.srt", "w", encoding="utf-8") as f:
f.write(srt_content)
print(f"字幕生成完了: {len(result['segments'])} セグメント")2.2 話者分離 (Speaker Diarization)
# pyannote.audio で話者分離
from pyannote.audio import Pipeline
import whisper
# 話者分離モデル
diarization = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.1",
use_auth_token="your-hf-token"
)
# 話者セグメントの取得
dia_result = diarization("interview.wav")
for turn, _, speaker in dia_result.itertracks(yield_label=True):
print(f"[{turn.start:.1f}s - {turn.end:.1f}s] {speaker}")
# [0.5s - 12.3s] SPEAKER_00
# [12.8s - 25.1s] SPEAKER_012.3 Whisper + 話者分離の統合パイプライン
# 話者分離付き文字起こしの完全パイプライン
import whisper
from pyannote.audio import Pipeline
from dataclasses import dataclass
@dataclass
class TranscriptionSegment:
start: float
end: float
text: str
speaker: str
class SpeakerAwareTranscriber:
"""話者情報付き文字起こし"""
def __init__(self, whisper_model: str = "large-v3", hf_token: str = ""):
self.whisper = whisper.load_model(whisper_model)
self.diarization = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.1",
use_auth_token=hf_token,
)
def transcribe_with_speakers(
self,
audio_path: str,
language: str = "ja",
max_speakers: int = None,
) -> list[TranscriptionSegment]:
"""話者分離付き文字起こし"""
# 1. Whisper で文字起こし
whisper_result = self.whisper.transcribe(
audio_path,
language=language,
word_timestamps=True,
)
# 2. pyannote で話者分離
dia_params = {}
if max_speakers:
dia_params["max_speakers"] = max_speakers
dia_result = self.diarization(audio_path, **dia_params)
# 3. 話者情報と文字起こしを統合
segments = []
for seg in whisper_result["segments"]:
# セグメントの中間時刻で話者を判定
mid_time = (seg["start"] + seg["end"]) / 2
speaker = self._get_speaker_at_time(dia_result, mid_time)
segments.append(TranscriptionSegment(
start=seg["start"],
end=seg["end"],
text=seg["text"].strip(),
speaker=speaker,
))
return segments
def _get_speaker_at_time(self, dia_result, time: float) -> str:
"""指定時刻の話者を取得"""
for turn, _, speaker in dia_result.itertracks(yield_label=True):
if turn.start <= time <= turn.end:
return speaker
return "UNKNOWN"
def export_to_srt(
self,
segments: list[TranscriptionSegment],
output_path: str,
include_speaker: bool = True,
):
"""SRT 形式でエクスポート"""
lines = []
for i, seg in enumerate(segments, 1):
start = self._format_time(seg.start)
end = self._format_time(seg.end)
text = f"[{seg.speaker}] {seg.text}" if include_speaker else seg.text
lines.append(f"{i}\n{start} --> {end}\n{text}\n")
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
def _format_time(self, seconds: float) -> str:
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
ms = int((seconds % 1) * 1000)
return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
# 使用例
transcriber = SpeakerAwareTranscriber(
whisper_model="large-v3",
hf_token="your-hf-token",
)
segments = transcriber.transcribe_with_speakers(
"interview.wav",
language="ja",
max_speakers=2,
)
transcriber.export_to_srt(segments, "interview_subtitles.srt")
# 出力例:
# 1
# 00:00:01,200 --> 00:00:05,800
# [SPEAKER_00] 今日はAI動画編集についてお話しします
#
# 2
# 00:00:06,100 --> 00:00:10,500
# [SPEAKER_01] はい、よろしくお願いします2.4 字幕のスタイリングと自動翻訳
# 字幕の自動翻訳とスタイリング
from dataclasses import dataclass
from typing import Optional
@dataclass
class SubtitleStyle:
"""字幕のスタイル設定"""
font_family: str = "Noto Sans JP"
font_size: int = 48
font_color: str = "#FFFFFF"
outline_color: str = "#000000"
outline_width: int = 3
background_color: Optional[str] = None # None=透明
position: str = "bottom_center" # top, bottom, center
max_chars_per_line: int = 20
class SubtitleProcessor:
"""字幕の後処理・翻訳"""
def auto_split_long_lines(
self, text: str, max_chars: int = 20
) -> str:
"""長い字幕テキストを改行で分割"""
if len(text) <= max_chars:
return text
words = text.split()
lines = []
current_line = ""
for word in words:
if len(current_line + word) > max_chars and current_line:
lines.append(current_line.strip())
current_line = word + " "
else:
current_line += word + " "
if current_line.strip():
lines.append(current_line.strip())
return "\n".join(lines)
def translate_subtitles(
self,
segments: list,
target_lang: str = "en",
service: str = "deepl",
) -> list:
"""字幕を翻訳"""
if service == "deepl":
return self._translate_with_deepl(segments, target_lang)
elif service == "gpt4":
return self._translate_with_gpt4(segments, target_lang)
return segments
def _translate_with_gpt4(self, segments: list, target_lang: str) -> list:
"""GPT-4 を使った高品質翻訳(文脈考慮)"""
from openai import OpenAI
client = OpenAI()
# 全文を一括翻訳(文脈を維持するため)
all_texts = [seg.text for seg in segments]
numbered_texts = "\n".join(
f"{i}: {text}" for i, text in enumerate(all_texts)
)
response = client.chat.completions.create(
model="gpt-4",
messages=[
{
"role": "system",
"content": f"Translate the following numbered lines to {target_lang}. "
f"Keep the numbering. Maintain natural conversation flow.",
},
{"role": "user", "content": numbered_texts},
],
)
# 翻訳結果をパース
translated_lines = response.choices[0].message.content.strip().split("\n")
for i, seg in enumerate(segments):
if i < len(translated_lines):
# "0: translated text" → "translated text"
parts = translated_lines[i].split(": ", 1)
seg.text = parts[1] if len(parts) > 1 else parts[0]
return segments
def generate_ass_file(
self,
segments: list,
style: SubtitleStyle,
output_path: str,
):
"""ASS (Advanced SubStation Alpha) 形式で出力"""
header = f"""[Script Info]
Title: AI Generated Subtitles
ScriptType: v4.00+
PlayResX: 1920
PlayResY: 1080
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV
Style: Default,{style.font_family},{style.font_size},&H00FFFFFF,&H00000000,&H00000000,0,0,1,{style.outline_width},0,2,10,10,40
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
"""
events = []
for seg in segments:
start = self._to_ass_time(seg.start)
end = self._to_ass_time(seg.end)
text = seg.text.replace("\n", "\\N")
events.append(
f"Dialogue: 0,{start},{end},Default,,0,0,0,,{text}"
)
with open(output_path, "w", encoding="utf-8") as f:
f.write(header + "\n".join(events))
def _to_ass_time(self, seconds: float) -> str:
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
cs = int((seconds % 1) * 100)
return f"{h}:{m:02d}:{s:02d}.{cs:02d}"3. シーン検出と自動カット
# PySceneDetect でシーン検出
from scenedetect import detect, ContentDetector, split_video_ffmpeg
# シーン検出(コンテンツの変化を検出)
scene_list = detect("raw_footage.mp4", ContentDetector(threshold=27.0))
print(f"検出されたシーン: {len(scene_list)} 個")
for i, scene in enumerate(scene_list):
print(f" シーン {i+1}: {scene[0].get_timecode()} - {scene[1].get_timecode()}")
# シーンごとに動画を分割
split_video_ffmpeg("raw_footage.mp4", scene_list, output_dir="scenes/")# FFmpeg + Whisper で無音部分の自動カット
import subprocess
import json
def detect_silence(input_file, threshold=-30, duration=1.0):
"""無音区間を検出"""
cmd = [
'ffmpeg', '-i', input_file,
'-af', f'silencedetect=noise={threshold}dB:d={duration}',
'-f', 'null', '-'
]
result = subprocess.run(cmd, capture_output=True, text=True)
# stderr からサイレンス区間を解析
silences = parse_silence_output(result.stderr)
return silences
def remove_silence(input_file, output_file, silences):
"""無音区間を除去した動画を生成"""
# 音声がある区間のみを抽出して結合
filter_complex = build_trim_filter(silences, get_duration(input_file))
cmd = ['ffmpeg', '-i', input_file, '-filter_complex', filter_complex, output_file]
subprocess.run(cmd)
# 使用例
silences = detect_silence("lecture.mp4", threshold=-35, duration=0.8)
remove_silence("lecture.mp4", "lecture_trimmed.mp4", silences)3.1 高度なシーン検出パイプライン
# 複数の検出手法を組み合わせた高精度シーン検出
from scenedetect import SceneManager, open_video
from scenedetect.detectors import ContentDetector, ThresholdDetector, AdaptiveDetector
class AdvancedSceneDetector:
"""複数手法によるシーン検出"""
def __init__(self):
self.detectors = {
"content": ContentDetector(threshold=27.0),
"adaptive": AdaptiveDetector(
adaptive_threshold=3.0,
min_scene_len=15, # 最低15フレーム
),
}
def detect_scenes(
self,
video_path: str,
method: str = "adaptive",
min_scene_duration: float = 1.0,
) -> list:
"""シーン検出を実行"""
video = open_video(video_path)
scene_manager = SceneManager()
scene_manager.add_detector(self.detectors[method])
scene_manager.detect_scenes(video)
scene_list = scene_manager.get_scene_list()
# 最小シーン長でフィルタリング
fps = video.frame_rate
min_frames = int(min_scene_duration * fps)
filtered = [
scene for scene in scene_list
if (scene[1] - scene[0]).get_frames() >= min_frames
]
return filtered
def classify_scenes(self, video_path: str, scenes: list) -> list:
"""
各シーンを分類
分類カテゴリ:
- dialogue: 人物の会話シーン
- action: 動きの多いシーン
- transition: トランジション
- static: 静的なシーン(スライド、テロップ等)
"""
classified = []
for scene in scenes:
# フレームの動き量を計算
motion = self._calculate_motion(video_path, scene)
# 音声解析
has_speech = self._detect_speech(video_path, scene)
if has_speech and motion < 0.3:
category = "dialogue"
elif motion > 0.7:
category = "action"
elif motion < 0.1:
category = "static"
else:
category = "general"
classified.append({
"start": scene[0].get_timecode(),
"end": scene[1].get_timecode(),
"category": category,
"motion_score": motion,
"has_speech": has_speech,
})
return classified
def auto_highlight(
self,
video_path: str,
target_duration: float = 60.0,
priority: list = None,
) -> list:
"""
自動ハイライト生成
動きの多いシーンと会話シーンを優先的に選択し、
指定時間に収まるようにシーンを選択する
"""
scenes = self.detect_scenes(video_path)
classified = self.classify_scenes(video_path, scenes)
if priority is None:
priority = ["action", "dialogue", "general", "static"]
# 優先度に基づいてソート
scored = []
for scene in classified:
priority_score = (
len(priority) - priority.index(scene["category"])
if scene["category"] in priority else 0
)
scored.append((priority_score, scene))
scored.sort(reverse=True, key=lambda x: x[0])
# 目標時間に収まるようにシーンを選択
selected = []
total_duration = 0
for _, scene in scored:
# シーン長を計算(簡易)
scene_duration = 3.0 # 仮の値
if total_duration + scene_duration <= target_duration:
selected.append(scene)
total_duration += scene_duration
return selected
def _calculate_motion(self, video_path, scene):
"""シーンの動き量を計算"""
return 0.5 # 実際にはフレーム差分で計算
def _detect_speech(self, video_path, scene):
"""シーンに音声があるか検出"""
return True # 実際にはVADで検出4. AI 映像処理
4.1 超解像(アップスケール)
# Real-ESRGAN で動画アップスケール
# コマンドライン実行
# realesrgan-ncnn-vulkan -i input.mp4 -o output.mp4 -n realesrgan-x4plus -s 4
# Python API
from realesrgan import RealESRGANer
from basicsr.archs.rrdbnet_arch import RRDBNet
model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=4)
upsampler = RealESRGANer(scale=4, model_path='weights/RealESRGAN_x4plus.pth', model=model)
# フレームごとに超解像
output, _ = upsampler.enhance(input_frame, outscale=4)4.2 フレーム補間(スローモーション)
# RIFE でフレーム補間 (30fps → 120fps)
python inference_video.py \
--video input_30fps.mp4 \
--output output_120fps.mp4 \
--exp 2 \ # 2^2 = 4倍のフレーム数
--model rife-v4.64.3 オブジェクト除去(ProPainter)
# ProPainter による動画内オブジェクト除去
import cv2
import numpy as np
from pathlib import Path
class VideoObjectRemover:
"""動画からオブジェクトを除去"""
def __init__(self, model_path: str = "propainter_weights"):
self.model = self._load_model(model_path)
def remove_object(
self,
video_path: str,
mask_dir: str,
output_path: str,
flow_completion: bool = True,
):
"""
動画からオブジェクトを除去
video_path: 入力動画
mask_dir: フレームごとのマスク画像ディレクトリ
白=除去対象、黒=保持
output_path: 出力動画
flow_completion: オプティカルフロー補完を使用
"""
# 1. 動画をフレームに分解
frames = self._extract_frames(video_path)
masks = self._load_masks(mask_dir, len(frames))
# 2. オプティカルフローの計算
if flow_completion:
flows_forward = self._compute_flow(frames, direction="forward")
flows_backward = self._compute_flow(frames, direction="backward")
# マスク領域のフロー補完
flows_forward = self._complete_flow(flows_forward, masks)
flows_backward = self._complete_flow(flows_backward, masks)
# 3. 時空間注意機構によるインペインティング
inpainted_frames = self.model.inpaint(
frames=frames,
masks=masks,
flows_f=flows_forward if flow_completion else None,
flows_b=flows_backward if flow_completion else None,
)
# 4. 動画として書き出し
self._write_video(inpainted_frames, output_path, fps=30)
def track_and_remove(
self,
video_path: str,
initial_mask: str,
output_path: str,
):
"""
最初のフレームのマスクからオブジェクトを追跡して除去
SAM + Track Anything を組み合わせ
"""
# 1. SAM でセグメンテーション
# 2. 後続フレームで追跡
# 3. 全フレームのマスクを生成
# 4. ProPainter で除去
pass
def _extract_frames(self, video_path):
frames = []
cap = cv2.VideoCapture(video_path)
while True:
ret, frame = cap.read()
if not ret:
break
frames.append(frame)
cap.release()
return frames
def _load_masks(self, mask_dir, num_frames):
masks = []
for i in range(num_frames):
mask_path = Path(mask_dir) / f"mask_{i:04d}.png"
if mask_path.exists():
mask = cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE)
masks.append(mask)
else:
masks.append(np.zeros_like(masks[0]) if masks else None)
return masks
def _write_video(self, frames, output_path, fps=30):
h, w = frames[0].shape[:2]
writer = cv2.VideoWriter(
output_path,
cv2.VideoWriter_fourcc(*'mp4v'),
fps, (w, h)
)
for frame in frames:
writer.write(frame)
writer.release()
def _compute_flow(self, frames, direction):
"""RAFT によるオプティカルフロー計算"""
pass
def _complete_flow(self, flows, masks):
"""マスク領域のフロー補完"""
pass
def _load_model(self, model_path):
"""ProPainter モデルのロード"""
pass4.4 AI 音声処理
# Demucs による音声分離
class AudioSeparator:
"""AI による音声分離"""
def separate(
self,
audio_path: str,
output_dir: str,
model: str = "htdemucs_ft",
) -> dict:
"""
音声をステムに分離
出力ステム:
- vocals: ボーカル/音声
- drums: ドラム
- bass: ベース
- other: その他の楽器
"""
import subprocess
cmd = [
"python", "-m", "demucs",
"--model", model,
"--out", output_dir,
audio_path,
]
subprocess.run(cmd, check=True)
stem_dir = Path(output_dir) / model / Path(audio_path).stem
return {
"vocals": str(stem_dir / "vocals.wav"),
"drums": str(stem_dir / "drums.wav"),
"bass": str(stem_dir / "bass.wav"),
"other": str(stem_dir / "other.wav"),
}
def remove_background_music(
self,
video_path: str,
output_path: str,
):
"""動画から BGM を除去し、音声のみ残す"""
import subprocess
# 1. 音声を抽出
subprocess.run([
"ffmpeg", "-i", video_path,
"-vn", "-acodec", "pcm_s16le",
"temp_audio.wav"
])
# 2. 音声分離
stems = self.separate("temp_audio.wav", "./separated")
# 3. ボーカルのみで動画を再構成
subprocess.run([
"ffmpeg",
"-i", video_path,
"-i", stems["vocals"],
"-c:v", "copy",
"-map", "0:v:0",
"-map", "1:a:0",
output_path,
])
def enhance_voice(
self,
audio_path: str,
output_path: str,
):
"""音声のノイズ除去と品質向上"""
# 1. 音声分離でボーカルを抽出
# 2. ノイズ除去(spectral gating)
# 3. EQ調整(音声帯域を強調)
# 4. ラウドネス正規化
pass5. 自動編集ワークフロー
5.1 YouTube 動画の自動編集パイプライン
# YouTube 動画の自動編集パイプライン
class YouTubeAutoEditor:
"""YouTube 動画の自動編集"""
def __init__(self):
self.whisper_model = whisper.load_model("large-v3")
self.scene_detector = AdvancedSceneDetector()
def auto_edit(
self,
raw_video: str,
output_video: str,
config: dict = None,
):
"""
自動編集フルパイプライン
config:
silence_threshold: 無音カットの閾値 (dB)
silence_duration: 無音判定の最小長 (秒)
padding: カット前後の余白 (秒)
target_duration: 目標動画長 (秒, None=制限なし)
add_subtitles: 字幕を追加するか
subtitle_lang: 字幕言語
"""
if config is None:
config = {
"silence_threshold": -35,
"silence_duration": 0.8,
"padding": 0.1,
"target_duration": None,
"add_subtitles": True,
"subtitle_lang": "ja",
}
print("Step 1: 文字起こし...")
transcription = self.whisper_model.transcribe(
raw_video,
language=config["subtitle_lang"],
word_timestamps=True,
)
print("Step 2: 無音区間検出...")
silences = detect_silence(
raw_video,
threshold=config["silence_threshold"],
duration=config["silence_duration"],
)
print("Step 3: シーン検出...")
scenes = self.scene_detector.detect_scenes(raw_video)
print("Step 4: 編集ポイントの決定...")
edit_points = self._merge_edit_points(
silences=silences,
scenes=scenes,
transcription=transcription,
padding=config["padding"],
)
print("Step 5: 動画のカット & 結合...")
self._apply_edits(raw_video, edit_points, output_video)
if config["add_subtitles"]:
print("Step 6: 字幕の追加...")
srt_path = output_video.replace(".mp4", ".srt")
self._generate_subtitles(transcription, srt_path)
print(f"自動編集完了: {output_video}")
def _merge_edit_points(self, silences, scenes, transcription, padding):
"""無音カット、シーン検出、文字起こしを統合して編集ポイントを決定"""
# 音声がある区間を保持
keep_regions = []
# 無音区間の逆(音声がある区間)を計算
# シーン境界で自然なカットポイントを選択
# 言葉の途中でのカットを避ける
return keep_regions
def _apply_edits(self, input_video, edit_points, output_video):
"""FFmpeg で編集を適用"""
pass
def _generate_subtitles(self, transcription, output_path):
"""字幕ファイルを生成"""
srt = to_srt(transcription["segments"])
with open(output_path, "w", encoding="utf-8") as f:
f.write(srt)5.2 バッチ処理パイプライン
# 複数動画の一括処理
import concurrent.futures
from pathlib import Path
class BatchVideoProcessor:
"""複数動画のバッチ処理"""
def __init__(self, max_workers: int = 4):
self.max_workers = max_workers
def process_batch(
self,
input_dir: str,
output_dir: str,
operations: list,
):
"""
ディレクトリ内の全動画を処理
operations: 適用する処理のリスト
例: ["transcribe", "remove_silence", "upscale", "subtitle"]
"""
Path(output_dir).mkdir(parents=True, exist_ok=True)
videos = list(Path(input_dir).glob("*.mp4"))
print(f"処理対象: {len(videos)} 本の動画")
results = {}
with concurrent.futures.ProcessPoolExecutor(
max_workers=self.max_workers
) as executor:
futures = {}
for video in videos:
output = Path(output_dir) / video.name
future = executor.submit(
self._process_single, str(video), str(output), operations
)
futures[future] = video.name
for future in concurrent.futures.as_completed(futures):
name = futures[future]
try:
result = future.result()
results[name] = {"status": "success", **result}
print(f" 完了: {name}")
except Exception as e:
results[name] = {"status": "error", "error": str(e)}
print(f" 失敗: {name} - {e}")
return results
def _process_single(self, input_path, output_path, operations):
"""単一動画の処理"""
result = {}
for op in operations:
if op == "transcribe":
result["transcription"] = self._transcribe(input_path)
elif op == "remove_silence":
input_path = self._remove_silence(input_path)
elif op == "upscale":
input_path = self._upscale(input_path)
elif op == "subtitle":
self._add_subtitle(input_path, output_path)
return result
def _transcribe(self, path):
pass
def _remove_silence(self, path):
return path
def _upscale(self, path):
return path
def _add_subtitle(self, input_path, output_path):
pass6. 主要ツール比較
| 機能 | Runway | Descript | CapCut | DaVinci Resolve |
|---|---|---|---|---|
| AI 字幕生成 | 対応 | 対応 (高精度) | 対応 | 対応 |
| テキストベース編集 | -- | 対応 (核心機能) | -- | -- |
| AI 背景除去 | Green Screen AI | -- | AI カットアウト | Magic Mask |
| オブジェクト除去 | Inpainting | -- | -- | Object Removal |
| AI カラー補正 | -- | -- | -- | AI Color Match |
| 音声ノイズ除去 | -- | Studio Sound | ノイズ除去 | Voice Isolation |
| 料金 | $12-76/月 | $24/月 | 無料/Pro $10/月 | 無料/Studio $295 |
| 対象 | クリエイター | ポッドキャスト・YouTube | SNS動画 | プロ映像制作 |
| ユースケース | 推奨ツール | 理由 |
|---|---|---|
| YouTube 動画 | Descript | テキストベース編集で高速 |
| SNS ショート動画 | CapCut | 無料、テンプレート豊富 |
| 映画・CM 品質 | DaVinci Resolve | プロ仕様のカラー・音声ツール |
| 実験的 VFX | Runway | 最先端の AI 映像生成 |
| ポッドキャスト | Descript | 音声編集 + 動画化 |
| 教育コンテンツ | Descript + CapCut | 字幕重視、コスト効率 |
| 企業プレゼン | CapCut / Canva | テンプレート活用、簡単操作 |
ツール別 AI 機能詳細比較
| AI 機能 | 品質 | 速度 | コスト | 推奨ツール |
|---|---|---|---|---|
| 自動字幕 (日本語) | ★★★★★ | 高速 | 低 | Whisper (ローカル) |
| 自動字幕 (多言語) | ★★★★☆ | 高速 | 中 | Descript |
| テキストベース編集 | ★★★★★ | 即時 | 中 | Descript |
| オブジェクト除去 | ★★★★☆ | 中速 | 高 | Runway |
| 背景除去 | ★★★★☆ | 高速 | 低 | CapCut |
| 音声分離 | ★★★★★ | 中速 | 低 | Demucs (ローカル) |
| カラーグレーディング | ★★★★★ | 即時 | 中 | DaVinci Resolve |
| フレーム補間 | ★★★★☆ | 低速 | 低 | RIFE (ローカル) |
| 超解像 | ★★★★☆ | 低速 | 低 | Real-ESRGAN (ローカル) |
7. アンチパターン
アンチパターン 1: AI 字幕を無校正で公開
BAD:
Whisper で字幕生成 → そのまま公開
→ 固有名詞の誤認識、句読点の不適切な位置
→ 視聴者の信頼を損なう
GOOD:
1. Whisper で字幕ドラフト生成
2. 固有名詞・専門用語を辞書登録
3. 人間が最終校正(特に数字・固有名詞)
4. タイミング調整(読みやすい表示時間)
アンチパターン 2: 過度な AI 効果の適用
BAD:
AI 超解像 + AI カラー + AI ノイズ除去 + AI 手ブレ補正
→ アーティファクト(AI の痕跡)が蓄積
→ 不自然な映像になる
GOOD:
- 最も効果の高い1-2種類の処理に絞る
- 元素材の品質を活かす
- AI 処理前後の比較を必ず確認
- エクスポート前にフル解像度でプレビュー
アンチパターン 3: 無音カットの過剰適用
BAD:
全ての無音を機械的にカット
→ 話の「間」が失われ、聞き取りにくい
→ 息つく暇のない不自然なテンポ
GOOD:
- 意図的な「間」は残す(2秒以下の無音は保持)
- カット前後にパディング(0.1-0.2秒)を追加
- 話者の交代時は少し長めの間を残す
- テーマ転換時のポーズを尊重
アンチパターン 4: Whisper のモデルサイズを考慮しない
BAD:
全ての文字起こしに large-v3 を使用
→ 短い動画でも処理に時間がかかる
→ GPU リソースの無駄遣い
GOOD:
- ドラフト段階: base / medium で高速処理
- 最終版: large-v3 で高精度処理
- リアルタイム用: tiny / base(CPU動作可能)
- 言語が明確: language パラメータを指定して精度向上
8. パフォーマンス最適化ガイド
8.1 処理速度の最適化
処理パイプラインの最適化テクニック:
1. プロキシ編集
- 元素材: 4K / 60fps
- 編集用プロキシ: 720p / 30fps
- 最終出力時に元素材で再レンダリング
- → 編集速度が 4-8 倍高速化
2. GPU パイプライン
- Whisper: GPU で 10 倍高速化
- Real-ESRGAN: GPU 必須、タイル処理で VRAM 節約
- RIFE: GPU で リアルタイム処理可能
- → CPU 比較で全体 5-10 倍高速化
3. 並列処理
- 音声処理と映像処理を並列実行
- フレーム単位の処理はマルチプロセス化
- 複数動画のバッチ処理
- → スループット 2-4 倍向上
4. キャッシュ戦略
- 文字起こし結果をキャッシュ
- シーン検出結果をキャッシュ
- 中間フレームを一時ファイルに保存
- → 再処理時間を 90% 削減
8.2 品質管理チェックリスト
動画品質チェックリスト:
映像:
□ 解像度が出力要件を満たしている
□ フレームレートが一定(ドロップフレームなし)
□ カラーグレーディングが統一されている
□ AI アーティファクトが目立たない
□ トランジションが自然
音声:
□ 音量レベルが統一されている(LUFS 基準)
□ ノイズが除去されている
□ BGM と音声のバランスが適切
□ 話者の声がクリア
字幕:
□ 誤字・脱字がない
□ タイミングが音声と一致している
□ 固有名詞が正しい
□ 改行位置が適切
□ フォントサイズと色が読みやすい
実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
トラブルシューティング
よくあるエラーと解決策
| エラー | 原因 | 解決策 |
|---|---|---|
| 初期化エラー | 設定ファイルの不備 | 設定ファイルのパスと形式を確認 |
| タイムアウト | ネットワーク遅延/リソース不足 | タイムアウト値の調整、リトライ処理の追加 |
| メモリ不足 | データ量の増大 | バッチ処理の導入、ページネーションの実装 |
| 権限エラー | アクセス権限の不足 | 実行ユーザーの権限確認、設定の見直し |
| データ不整合 | 並行処理の競合 | ロック機構の導入、トランザクション管理 |
デバッグの手順
- エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
- 再現手順の確立: 最小限のコードでエラーを再現する
- 仮説の立案: 考えられる原因をリストアップする
- 段階的な検証: ログ出力やデバッガを使って仮説を検証する
- 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
# ロガーの設定
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
def debug_decorator(func):
"""関数の入出力をログ出力するデコレータ"""
@wraps(func)
def wrapper(*args, **kwargs):
logger.debug(f"呼び出し: {func.__name__}(args={args}, kwargs={kwargs})")
try:
result = func(*args, **kwargs)
logger.debug(f"戻り値: {func.__name__} -> {result}")
return result
except Exception as e:
logger.error(f"例外発生: {func.__name__}: {e}")
logger.error(traceback.format_exc())
raise
return wrapper
@debug_decorator
def process_data(items):
"""データ処理(デバッグ対象)"""
if not items:
raise ValueError("空のデータ")
return [item * 2 for item in items]パフォーマンス問題の診断
パフォーマンス問題が発生した場合の診断手順:
- ボトルネックの特定: プロファイリングツールで計測
- メモリ使用量の確認: メモリリークの有無をチェック
- I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
- 同時接続数の確認: コネクションプールの状態を確認
| 問題の種類 | 診断ツール | 対策 |
|---|---|---|
| CPU負荷 | cProfile, py-spy | アルゴリズム改善、並列化 |
| メモリリーク | tracemalloc, objgraph | 参照の適切な解放 |
| I/Oボトルネック | strace, iostat | 非同期I/O、キャッシュ |
| DB遅延 | EXPLAIN, slow query log | インデックス、クエリ最適化 |
設計判断ガイド
選択基準マトリクス
技術選択を行う際の判断基準を以下にまとめます。
| 判断基準 | 重視する場合 | 妥協できる場合 |
|---|---|---|
| パフォーマンス | リアルタイム処理、大規模データ | 管理画面、バッチ処理 |
| 保守性 | 長期運用、チーム開発 | プロトタイプ、短期プロジェクト |
| スケーラビリティ | 成長が見込まれるサービス | 社内ツール、固定ユーザー |
| セキュリティ | 個人情報、金融データ | 公開データ、社内利用 |
| 開発速度 | MVP、市場投入スピード | 品質重視、ミッションクリティカル |
アーキテクチャパターンの選択
| アーキテクチャ選択フロー |
|---|
| ① チーム規模は? |
| ├─ 小規模(1-5人)→ モノリス |
| └─ 大規模(10人+)→ ②へ |
| ② デプロイ頻度は? |
| ├─ 週1回以下 → モノリス + モジュール分割 |
| └─ 毎日/複数回 → ③へ |
| ③ チーム間の独立性は? |
| ├─ 高い → マイクロサービス |
| └─ 中程度 → モジュラーモノリス |
トレードオフの分析
技術的な判断には必ずトレードオフが伴います。以下の観点で分析を行いましょう:
1. 短期 vs 長期のコスト
- 短期的に速い方法が長期的には技術的負債になることがある
- 逆に、過剰な設計は短期的なコストが高く、プロジェクトの遅延を招く
2. 一貫性 vs 柔軟性
- 統一された技術スタックは学習コストが低い
- 多様な技術の採用は適材適所が可能だが、運用コストが増加
3. 抽象化のレベル
- 高い抽象化は再利用性が高いが、デバッグが困難になる場合がある
- 低い抽象化は直感的だが、コードの重複が発生しやすい
# 設計判断の記録テンプレート
class ArchitectureDecisionRecord:
"""ADR (Architecture Decision Record) の作成"""
def __init__(self, title: str):
self.title = title
self.context = ""
self.decision = ""
self.consequences = []
self.alternatives = []
def set_context(self, context: str):
"""背景と課題の記述"""
self.context = context
return self
def set_decision(self, decision: str):
"""決定内容の記述"""
self.decision = decision
return self
def add_consequence(self, consequence: str, positive: bool = True):
"""結果の追加"""
self.consequences.append({
'description': consequence,
'type': 'positive' if positive else 'negative'
})
return self
def add_alternative(self, name: str, reason_rejected: str):
"""却下した代替案の追加"""
self.alternatives.append({
'name': name,
'reason_rejected': reason_rejected
})
return self
def to_markdown(self) -> str:
"""Markdown形式で出力"""
md = f"# ADR: {self.title}\n\n"
md += f"## 背景\n{self.context}\n\n"
md += f"## 決定\n{self.decision}\n\n"
md += "## 結果\n"
for c in self.consequences:
icon = "✅" if c['type'] == 'positive' else "⚠️"
md += f"- {icon} {c['description']}\n"
md += "\n## 却下した代替案\n"
for a in self.alternatives:
md += f"- **{a['name']}**: {a['reason_rejected']}\n"
return md9. FAQ
Q1. Whisper の精度を向上させるには?
A. (1) large-v3 モデルを使用する(精度最高だが処理速度は遅い)。(2) 言語を明示的に指定する(language="ja")。(3) 初期プロンプト(initial_prompt)で専門用語やコンテキストを与える。(4) ノイズの多い音声は事前に音声分離(Demucs)でボーカルを抽出してから処理する。
Q2. 長尺動画の編集を効率化するには?
A. (1) Whisper で文字起こし → テキストベースで不要部分を特定。(2) Descript のテキストベース編集で、テキストの削除 = 動画の該当部分カット。(3) PySceneDetect でシーン分割 → 不要シーンを除外。(4) 無音部分の自動カット。これらの組み合わせで10時間の素材を1-2時間で粗編集できる。
Q3. AI 動画編集の処理を高速化するには?
A. (1) GPU を活用する(NVIDIA CUDA 対応の GPU で Whisper は10倍高速化)。(2) プロキシ編集(低解像度で編集→最終書き出しで高解像度に切替)。(3) バッチ処理(複数動画を並列処理)。(4) クラウド GPU(Google Colab、Runway Cloud)を活用。ローカル処理にこだわらず、クラウドとのハイブリッド運用が現実的。
Q4. Descript のテキストベース編集の仕組みは?
A. Descript は動画の音声を文字起こしし、テキストとタイムラインを完全に同期させる。テキストエディタで文章を削除すると、対応する動画の区間も自動的にカットされる。逆に、テキストの並べ替えで動画の構成を変更できる。「um」「えーと」といったフィラーワードの自動検出・削除機能もある。テキストベースで大まかな編集を行い、タイムラインで微調整する2段階ワークフローが効率的。
Q5. AI で動画の BGM を自動生成できるか?
A. 可能。(1) Suno AI: テキストプロンプトから楽曲を生成。動画の雰囲気に合わせた BGM を作成できる。(2) Udio: 高品質な音楽生成。(3) Stable Audio: Stability AI の音楽生成モデル。(4) Mubert: API 連携可能な BGM 生成サービス。いずれも商用利用にはライセンス確認が必要。動画の長さに合わせた尺調整や、シーン転換に合わせた曲調変化は、現時点では人間の調整が必要。
FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 項目 | ポイント |
|---|---|
| 自動字幕 | Whisper で生成 → 人間が校正 → SRT/VTT 出力 |
| シーン検出 | PySceneDetect で自動分割、無音カットで効率化 |
| 超解像 | Real-ESRGAN で低解像度素材を4K化 |
| 音声処理 | Demucs で音声分離、ノイズ除去 |
| ツール選定 | YouTube=Descript、SNS=CapCut、プロ=DaVinci Resolve |
| 品質管理 | AI 処理は最小限に、過度な適用はアーティファクトの原因 |
| 自動化 | 無音カット + 字幕生成の自動パイプラインが最も投資効果高い |
| 最適化 | GPU 活用 + プロキシ編集 + バッチ処理で大幅高速化 |
次に読むべきガイド
参考文献
- Whisper (OpenAI) -- https://github.com/openai/whisper -- 音声認識モデル
- Runway ML Documentation -- https://docs.runwayml.com/ -- AI 映像編集ツール
- DaVinci Resolve Training -- https://www.blackmagicdesign.com/products/davinciresolve/training
- ProPainter -- Zhou et al. (ICCV 2023) -- 動画インペインティング
- Demucs -- Rouard et al. (2023) -- 音声分離モデル
- PySceneDetect -- https://www.scenedetect.com/ -- シーン検出ライブラリ