この記事は以下の続編です👇
https://qiita.com/sumin_engineer1223/items/26d5698d74d31864ba65
背景
Unityの認定資格を取るために購入した学習バンドルの動画が、
すべて英語音声かつ字幕なしというハードモードでした。
前回の記事では、Whisperを使って英語字幕を自動生成するツールを作りました。
今回はその発展として、自動で日本語字幕をつけるツールをPythonで作ってみました。
完成イメージ
英語の .mp4 ファイルを入力すると:
- 音声から文字起こし(Whisper)
- 文ごとに分割し、Google翻訳で日本語に
- 字幕(SRT)を生成し、動画に焼き込み(FFmpeg)
という一連の処理ができます。
使用ライブラリ
- openai-whisper:音声からテキストへの文字起こし
- googletrans:Google翻訳の非公式API
- moviepy: 動画の音声抽出
- ffmpeg:動画への字幕焼き付け
- nltk:英語の文分割(自然な翻訳のため)
環境構築
まずは仮想環境の作成と必要パッケージのインストールです。
# 仮想環境を作成
python -m venv venv
source venv/bin/activate # Windowsは venv\Scripts\activate
# パッケージをインストール
pip install moviepy==1.0.3 openai-whisper ffmpeg-python googletrans==4.0.0-rc1 nltk
# punkt モデルのダウンロード(文分割用)
python -c "import nltk; nltk.download('punkt')"
# ffmpeg をインストール(macOS)
brew install ffmpeg
ディレクトリ構成
TranslateMovie/
├── input.mp4
├── jp_subtitled_video_generator.py
├── output/
│ ├── audio.wav
│ ├── subtitles.srt
│ └── output_with_subs.mp4
コア処理のポイント
Whisperで文字起こし+文分割
model = whisper.load_model("base")
result = model.transcribe("audio.wav", task="transcribe")
full_text = " ".join([seg["text"].strip() for seg in result["segments"]])
# 文単位で分割(nltk)
from nltk.tokenize.punkt import PunktSentenceTokenizer, PunktParameters
tokenizer = PunktSentenceTokenizer(PunktParameters())
sentences = tokenizer.tokenize(full_text)
Google翻訳で自然な文単位に翻訳
from googletrans import Translator
translator = Translator()
translated = [translator.translate(s, src="en", dest="ja").text for s in sentences]
字幕(SRT形式)として保存
字幕の時間は文ごとに均等に割り当てています。
with open("output/subtitles.srt", "w", encoding="utf-8") as f:
for i, (en, ja) in enumerate(zip(sentences, translated), start=1):
# タイムスタンプ省略
f.write(f"{i}\n{start} --> {end}\n{ja}\n\n")
字幕を動画に焼き付ける(FFmpeg)
ffmpeg -i input.mp4 -vf "subtitles=output/subtitles.srt:force_style='FontName=NotoSansCJKjp'" -c:a copy output/output_with_subs.mp4
実行スクリプト全体
以下が全体のスクリプトです。
import os
import whisper
import subprocess
from moviepy.editor import VideoFileClip
from googletrans import Translator
from nltk.tokenize.punkt import PunktSentenceTokenizer, PunktParameters
# ファイル設定
input_video_path = "input.mp4"
extracted_audio_path = "output/audio.wav"
output_srt_path = "output/subtitles.srt"
output_video_path = "output/output_with_subs.mp4"
# 削除処理
for path in [extracted_audio_path, output_srt_path, output_video_path]:
if os.path.exists(path):
os.remove(path)
print(f"🧹 削除しました: {path}")
# タイムスタンプのフォーマット
def format_timestamp(seconds: float) -> str:
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
ms = int((seconds - int(seconds)) * 1000)
return f"{h:02}:{m:02}:{s:02},{ms:03}"
# ステップ1:mp4 → wav
print("🎞️ MP4 → WAV に変換中...")
video = VideoFileClip(input_video_path)
video.audio.write_audiofile(extracted_audio_path)
# ステップ2:Whisperで全文文字起こし
print("🗣 Whisperで文字起こし中...")
model = whisper.load_model("base")
result = model.transcribe(extracted_audio_path, task="transcribe")
full_text = " ".join([seg["text"].strip() for seg in result["segments"]])
# ステップ3:文ごとに分割して翻訳
print("🌐 Google翻訳中...")
punkt_params = PunktParameters()
tokenizer = PunktSentenceTokenizer(punkt_params)
sentences = tokenizer.tokenize(full_text)
translator = Translator()
# ステップ4:SRT形式で出力
print("💬 SRT字幕を生成中...")
duration_per_sentence = (result["segments"][-1]["end"] - result["segments"][0]["start"]) / len(sentences)
current_time = result["segments"][0]["start"]
with open(output_srt_path, "w", encoding="utf-8") as f:
for i, en_sentence in enumerate(sentences, start=1):
ja_sentence = translator.translate(en_sentence, src="en", dest="ja").text
start = current_time
end = start + duration_per_sentence
f.write(f"{i}\n")
f.write(f"{format_timestamp(start)} --> {format_timestamp(end)}\n")
f.write(f"{ja_sentence}\n\n")
current_time = end
# ステップ5:ffmpegで字幕焼き込み
print("📽 字幕を動画に焼き込み中...")
subprocess.run([
"ffmpeg",
"-i", input_video_path,
"-vf", f"subtitles={output_srt_path}:force_style='FontName=NotoSansCJKjp'",
"-c:a", "copy",
output_video_path
])
print(f"✅ 完了!字幕付き動画を生成しました → {output_video_path}")
動画が処理されると、output_with_subs.mp4
が output/
に生成され、日本語字幕付きで視聴できるようになります。
感想
日本語字幕付きで視聴できるものの、意味で分けて時間単位で均等に割り振ると、英語で話しているタイミングと英語で話しているタイミングがずれてちくはぐになってしまいました。
個人的には先に作った英語字幕のほうが見やすかったです。