FFmpeg で音声を抽出し、OpenAI Whisper で文字起こしして、生成した SRT を動画に焼き込む——この一連の処理を Python スクリプト 1 本で完結させる方法を解説する。手動の文字起こしは不要になる。
僕はチュートリアル動画に字幕を付ける作業に毎回うんざりしていた。手作業で文字を起こして、タイムコードを合わせて、ファイルを書き出して……。1 本ならまだしも、本数が増えると割に合わない。
この記事では、Whisper と FFmpeg を組み合わせて 字幕生成を全自動化するパイプライン を構築する。音声抽出 → 文字起こし → SRT 生成 → 字幕焼き込みまでを一本のスクリプトで処理する。
字幕作成の手間を消す
字幕作成のボトルネックはふつう「文字起こし」だ。話し言葉をテキストに変換して、どこで区切るかを決めて、タイムコードを付ける。これが全体の 9 割の時間を食う。
Whisper はこの問題をほぼ解決する。音声ファイルを渡すと、文字起こしとタイムコードの付与を自動でやってくれる。出力形式に SRT を選べば、そのまま FFmpeg に渡して字幕を焼き込める。
必要なものは以下の 2 つ。
- Python 3.8 以上 + Whisper
- FFmpeg (インストール済みであること)
FFmpeg の基本的な使い方は FFmpeg 入門チュートリアル にまとめてある。インストールがまだなら先にそちらを見てほしい。
Whisperのインストール(モデルサイズ選び)
Whisper は pip でインストールできる。
pip install openai-whisper
インストールが終わったら、モデルサイズを選ぶ。Whisper にはいくつかのサイズがある。
| モデル | VRAM | 速度 | 精度 |
|---|---|---|---|
| tiny | ~1GB | 最速 | 低 |
| base | ~1GB | 速い | 普通 |
| small | ~2GB | 普通 | 良い |
| medium | ~5GB | 遅い | 高い |
| large-v3 | ~10GB | 最遅 | 最高 |
| turbo | ~6GB | 速い | 最高に近い |
turbo モデルは 2024 年 9 月にリリースされた。large-v3 のデコーダ層を 32 → 4 に削減して約 8 倍高速化しつつ、精度は large-v3 に迫る。6GB 以上の VRAM がある GPU なら、ほとんどの用途で turbo が最適解だ。
FFmpegで音声を抽出する
Whisper は動画ファイルを直接渡すこともできるが、音声だけ先に抽出しておくほうが処理が速い。FFmpeg で音声を WAV に変換する。
ffmpeg -i input.mp4 -vn -acodec pcm_s16le -ar 16000 -ac 1 audio.wav
オプションの意味は以下のとおり。
-vn— 映像を無視して音声だけを処理する-acodec pcm_s16le— WAV 形式(16bit リニア PCM)で出力する-ar 16000— サンプリングレートを 16kHz に設定する(Whisper の推奨値)-ac 1— モノラルに変換する(Whisper はモノラルで精度が安定する)
出力の audio.wav を次のステップで Whisper に渡す。
Whisperで文字起こしとSRT生成
音声ファイルができたら Whisper に渡す。CLI と Python API の両方で使える。
CLI で使う場合
whisper audio.wav --model medium --language ja --output_format srt --output_dir ./subtitles
--language ja を指定しないと言語自動検出モードになる。日本語動画は明示的に指定したほうが精度が上がる。
./subtitles/audio.srt にファイルが生成される。中身はこんな形式だ。
1
00:00:00,000 --> 00:00:03,500
こんにちは、今日はFFmpegとWhisperの使い方を紹介します。
2
00:00:03,500 --> 00:00:07,200
まずはWhisperのインストールから始めましょう。
Python API で使う場合
細かい制御が必要なときは Python から呼ぶ。
import whisper
model = whisper.load_model("medium")
result = model.transcribe("audio.wav", language="ja")
# SRT形式で保存する
def format_timestamp(seconds: float) -> str:
ms = int((seconds % 1) * 1000)
s = int(seconds) % 60
m = int(seconds) // 60 % 60
h = int(seconds) // 3600
return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
with open("subtitles/audio.srt", "w", encoding="utf-8") as f:
for i, segment in enumerate(result["segments"], start=1):
start = format_timestamp(segment["start"])
end = format_timestamp(segment["end"])
text = segment["text"].strip()
f.write(f"{i}\n{start} --> {end}\n{text}\n\n")
print("SRT ファイルを生成しました")
result["segments"] の各要素に start・end(秒)・text が入っている。これを SRT 形式に整形して書き出す。
FFmpegで字幕を焼き込む
SRT ファイルができたら FFmpeg で動画に焼き込む。subtitles フィルターを使う。
ffmpeg -i input.mp4 -vf "subtitles=subtitles/audio.srt" -c:a copy output.mp4
フォントや文字サイズを調整したいときは force_style オプションを使う。
ffmpeg -i input.mp4 \
-vf "subtitles=subtitles/audio.srt:force_style='FontName=Noto Sans CJK JP,FontSize=24,PrimaryColour=&HFFFFFF&,OutlineColour=&H000000&,Outline=2'" \
-c:a copy output.mp4
force_style の値は ASS/SSA フォーマットのスタイル指定に従う。色指定は &HBBGGRR& 形式で、BGR 順(HTML の RGB とは逆)な点に注意。末尾の & も必要だ。日本語動画には Noto Sans CJK JP や Yu Gothic など日本語対応フォントを指定すると文字化けを防げる。
パイプラインを1つのスクリプトにまとめる
ここまでの手順を一本の Python スクリプトにまとめる。動画ファイルを渡すだけで字幕焼き込み済みの動画が出力される。
import subprocess
import sys
import os
import whisper
def extract_audio(input_video: str, output_audio: str) -> None:
"""動画から音声を抽出する"""
cmd = [
"ffmpeg", "-i", input_video,
"-vn", "-acodec", "pcm_s16le",
"-ar", "16000", "-ac", "1",
output_audio, "-y"
]
subprocess.run(cmd, check=True, capture_output=True)
print(f"音声を抽出しました: {output_audio}")
def transcribe_to_srt(audio_path: str, srt_path: str, model_name: str = "medium") -> None:
"""Whisper で文字起こしして SRT を生成する"""
model = whisper.load_model(model_name)
result = model.transcribe(audio_path, language="ja")
def format_timestamp(seconds: float) -> str:
ms = int((seconds % 1) * 1000)
s = int(seconds) % 60
m = int(seconds) // 60 % 60
h = int(seconds) // 3600
return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
with open(srt_path, "w", encoding="utf-8") as f:
for i, segment in enumerate(result["segments"], start=1):
start = format_timestamp(segment["start"])
end = format_timestamp(segment["end"])
text = segment["text"].strip()
f.write(f"{i}\n{start} --> {end}\n{text}\n\n")
print(f"SRT を生成しました: {srt_path}")
def burn_subtitles(input_video: str, srt_path: str, output_video: str) -> None:
"""字幕を動画に焼き込む"""
# パスに非ASCII文字が含まれないことを前提とする
vf = f"subtitles={srt_path}"
cmd = [
"ffmpeg", "-i", input_video,
"-vf", vf,
"-c:a", "copy",
output_video, "-y"
]
subprocess.run(cmd, check=True, capture_output=True)
print(f"字幕焼き込み完了: {output_video}")
def main(input_video: str) -> None:
base = os.path.splitext(input_video)[0]
audio_path = f"{base}_audio.wav"
srt_path = f"{base}_subtitles.srt"
output_video = f"{base}_subtitled.mp4"
print("=== 字幕自動生成パイプライン ===")
extract_audio(input_video, audio_path)
transcribe_to_srt(audio_path, srt_path)
burn_subtitles(input_video, srt_path, output_video)
# 中間ファイルを削除する
os.remove(audio_path)
print(f"\n完了: {output_video}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("使い方: python subtitle_pipeline.py <input_video>")
sys.exit(1)
main(sys.argv[1])
実行は 1 コマンドで済む。
python subtitle_pipeline.py input.mp4
バッチ処理でフォルダ内の動画を一括処理したい場合は、FFmpeg × Python バッチ自動化 の記事が参考になる。
Whisper の medium/large モデルは GPU がないと処理に時間がかかる。ローカル PC を占有せずに字幕生成を回したい場合は、VPS にエンコード専用サーバーを構築して SSH 経由でスクリプトを実行する方法もある。
faster-whisperで高速化する
動画の本数が多いと処理時間が気になってくる。faster-whisper は Whisper を CTranslate2(C++最適化推論エンジン)で再実装したもので、最大 4 倍速く動作し、メモリ消費も大幅に少ない。
pip install faster-whisper
from faster_whisper import WhisperModel
model = WhisperModel("turbo", compute_type="float16")
segments, info = model.transcribe("audio.wav", language="ja")
with open("subtitles/audio.srt", "w", encoding="utf-8") as f:
for i, segment in enumerate(segments, start=1):
start = format_timestamp(segment.start)
end = format_timestamp(segment.end)
f.write(f"{i}\n{start} --> {end}\n{segment.text.strip()}\n\n")
API が少し違う(segments がジェネレータ、属性はドットアクセス)が、出力結果は同じだ。僕は 32blog の動画パイプラインを faster-whisper に切り替えたところ、30 分の動画で処理時間が 12 分 → 3 分未満になった(RTX 3060 環境)。
よくある質問
Whisper は GPU なしでも動く?
動く。ただし CPU だと medium モデルで 10 分の動画に 5〜10 分かかる。GPU なら 30 秒程度。turbo モデルは CPU でも比較的速い。GPU がないなら さくらの VPS で処理を回す手もある。
ソフト字幕とハード字幕の違いは?
ハード字幕(焼き込み)は動画のピクセルに埋め込まれるので視聴者が切り替えられない。ソフト字幕はコンテナ内に別ストリームとして格納され、プレイヤー側でオン/オフできる。この記事は subtitles フィルターでのハード字幕を扱っている。ソフト字幕にしたい場合は ffmpeg -i input.mp4 -i subs.srt -c copy -c:s mov_text output.mp4 を使う。
1 つの動画に複数言語が混在している場合は?
Whisper の --language は動画全体に 1 言語を指定する。複数言語が混在する場合は --language を省略して自動検出に任せる。ただし精度は落ちるので、言語の切り替わりポイントで音声を分割してから処理するのが確実だ。
Whisper の精度は有料の文字起こしサービスと比べてどう?
クリアな日本語音声なら、large-v3 や turbo は商用サービス(Amazon Transcribe 等)と遜色ない精度が出る。ノイズが多い環境、話者が重なる場面、強い方言では精度が落ちる。業務利用なら焼き込み前に SRT を目視確認することを推奨する。
SRT ではなく VTT で出力できる?
できる。CLI なら --output_format vtt を指定するだけ。Python で書く場合はタイムスタンプの区切り文字を変える(SRT は HH:MM:SS,mmm でカンマ、VTT は HH:MM:SS.mmm でドット)。VTT は Web 動画の <track> 要素で使う標準フォーマットだ。
なぜ動画を直接 Whisper に渡さず音声を先に抽出する?
Whisper は動画を直接受け取れるが、内部で FFmpeg を呼んで音声抽出している。自分で先にやれば サンプリングレート(16kHz モノラル)を明示的に制御でき、モデルを変えてリトライする際に音声抽出を繰り返さずに済む。
Windows で subtitles フィルターが動かない
パスの問題が多い。Windows では C ランタイムの fopen が ANSI コードページを使うため、パスに日本語や全角文字が含まれると subtitles フィルターが失敗する。SRT ファイルを英数字のみのパスにコピーしてから実行しよう。
日本語の文字起こしに最適なモデルは?
medium か large-v3 を推奨する。tiny/base では固有名詞の誤認識が多発する。速度と精度のバランスを取るなら turbo がベスト——large-v3 に近い精度で約 8 倍速い。
まとめ
FFmpeg と Whisper を組み合わせると、字幕作成のほぼ全工程を自動化できる。
- 音声抽出 → Whisper 文字起こし → SRT 生成 → FFmpeg 焼き込みの 4 ステップ
- turbo モデルがほとんどの用途で最適解(速くて精度も高い)
- faster-whisper を使えば処理速度がさらに 4 倍に
- Windows では SRT ファイルのパスを英数字のみにする
- Python スクリプトにまとめれば
python subtitle_pipeline.py input.mp4の 1 コマンドで完結する
手作業の文字起こしに時間を使うより、Whisper に任せて編集に集中しよう。僕はこのパイプラインを導入してから、字幕作業にかける時間が大幅に減った。
関連記事: