活用シーンとソリューション

AI で動画を多言語吹き替え:Whisper で文字起こし、LLM で翻訳、Kokoro で音声合成

5 ステップ 14 分で読めます 59 回閲覧 0
目次

中国語で収録した商品デモ、チュートリアル、ブランド動画も、現地の言語で話せるようになれば、ほかの市場の購入者に届けられます。しかし、プロによる吹き替えは費用も時間もかかります。この記事では、自社サーバー上に自動パイプラインを構築します。faster-whisper が元の音声をタイムスタンプ付きで文字起こしし、ローカルの言語モデルが 1 行ずつ翻訳し、Kokoro が翻訳した各行を読み上げ、スクリプトが各行を元のタイミングの枠に収めます。吹き替え済みの MP4、翻訳字幕ファイル、そして最終実行の前に修正できる編集可能な翻訳文が得られます。

吹き替えるのは、自分が所有している動画か、翻訳の許可を得た動画だけにしてください。機械翻訳や合成音声には誤りがつきものです。重要な動画はネイティブスピーカーに確認してもらい、AI で生成した音声は各プラットフォームのルールに従って表示してください。

ステップ1:コンポーネントをインストール

/opt/tts は、Kokoro による AI ナレーションの手順に従って用意します。日本語が必要な場合は、日本語用の追加パッケージも入れてください。翻訳は Ollama で実行します(Ollama でローカル LLM を動かす方法を参照)。続いて、音声認識と API クライアントを追加します。

/opt/tts/bin/pip install faster-whisper "av<19" openai     # av<19: PyAV 19 is not yet compatible with faster-whisper 1.2
ollama pull qwen2.5:7b

Whisper は元の言語として約 100 言語を認識できますが、翻訳先の言語は Kokoro が話せる言語である必要があります(ナレーションの記事のボイス一覧表を参照)。それ以外の言語に翻訳する場合は、字幕のみを翻訳してください。

ステップ2:吹き替えスクリプトを作成

cat > /opt/tts/dub.py <<'EOF2'
import os, subprocess, sys
import numpy as np
import soundfile as sf
from faster_whisper import WhisperModel
from kokoro import KPipeline
from openai import OpenAI

TARGET = os.environ.get("TARGET", "English")             # language name for the translator
LANG = os.environ.get("TTS_LANG", "a")                   # Kokoro language code of the target language
VOICE = os.environ.get("TTS_VOICE", "af_heart")
MAX_SPEED = float(os.environ.get("MAX_SPEED", "1.3"))    # never speak faster than this to fit a line in
ORIGINAL = os.environ.get("ORIGINAL_VOLUME", "0.15")     # keep the original audio quietly underneath (0 = remove)
RATE = 24000
client = OpenAI(base_url=os.environ.get("AI_BASE_URL", "http://127.0.0.1:11434/v1"),
                api_key=os.environ.get("AI_API_KEY", "ollama"))
MODEL = os.environ.get("AI_MODEL", "qwen2.5:7b")

def ts(t):
    ms = int(round(t * 1000))
    h, ms = divmod(ms, 3600000)
    m, ms = divmod(ms, 60000)
    s, ms = divmod(ms, 1000)
    return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"

def translate(text, previous):
    r = client.chat.completions.create(model=MODEL, temperature=0.2, messages=[
        {"role": "system", "content": f"Translate the line into natural spoken {TARGET} for a video voiceover. "
         "Keep it about as short as the original and keep product names unchanged. Return only the translation."},
        {"role": "user", "content": (f"Previous line, for context: {previous}\n" if previous else "") + f"Line: {text}"}])
    return r.choices[0].message.content.strip()

src = sys.argv[1]
base = os.path.splitext(src)[0] + "." + TARGET.lower()
cache = base + ".lines.txt"                              # translations, one per line: edit and run again
fixed = open(cache, encoding="utf-8").read().splitlines() if os.path.exists(cache) else []
whisper = WhisperModel(os.environ.get("WHISPER_MODEL", "small"), device="cpu", compute_type="int8")
segments, info = whisper.transcribe(src, vad_filter=True)
segs = [(s.start, s.end, s.text.strip()) for s in segments]
pipeline = KPipeline(lang_code=LANG, repo_id="hexgrad/Kokoro-82M")

def speak(text, speed):
    chunks = [np.asarray(a, dtype=np.float32) for _, _, a in pipeline(text, voice=VOICE, speed=speed)]
    return np.concatenate(chunks) if chunks else np.zeros(0, dtype=np.float32)

track = np.zeros(int((info.duration + 1) * RATE), dtype=np.float32)
srt, lines, cursor = [], [], 0.0
for i, (start, end, text) in enumerate(segs):
    line = fixed[i] if i < len(fixed) and fixed[i].strip() else translate(text, segs[i - 1][2] if i else "")
    lines.append(line)
    slot = (segs[i + 1][0] if i + 1 < len(segs) else info.duration) - start
    audio = speak(line, 1.0)
    if len(audio) / RATE > slot > 0:                       # too long for its place: speak a little faster
        audio = speak(line, min(MAX_SPEED, len(audio) / RATE / slot))
    at = max(start, cursor)                                # never overlap the previous line
    a = int(at * RATE)
    if a + len(audio) > len(track):
        track = np.concatenate([track, np.zeros(a + len(audio) - len(track), dtype=np.float32)])
    track[a:a + len(audio)] += audio
    cursor = at + len(audio) / RATE
    srt.append(f"{i + 1}\n{ts(at)} --> {ts(cursor)}\n{line}\n")
    print(f"{ts(start)} {text} -> {line}", flush=True)

sf.write(base + ".wav", track[:int(max(info.duration, cursor) * RATE)], RATE)
with open(cache, "w", encoding="utf-8") as f:
    f.write("\n".join(lines) + "\n")
with open(base + ".srt", "w", encoding="utf-8") as f:
    f.write("\n".join(srt))
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", src, "-i", base + ".wav", "-filter_complex",
                f"[0:a]volume={ORIGINAL}[o];[1:a][o]amix=inputs=2:duration=longest,loudnorm=I=-14:TP=-1.5[a]",
                "-map", "0:v", "-map", "[a]", "-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-ar", "48000", "-ac", "2",
                base + ".mp4"], check=True)
print("done:", base + ".mp4", base + ".srt")
EOF2

翻訳した各行は、元の行が始まった時刻から再生されます。行が枠に収まらないほど長い場合は最大 30% 速く読み上げ、それでも長すぎる場合は次の行の開始を少し遅らせるため、行同士が重なることはありません。元の音声は小さな音量で下に残るので、音楽や環境音も保たれます。

ステップ3:動画を吹き替える

cd /data/dub
TARGET=English TTS_LANG=a TTS_VOICE=af_heart /opt/tts/bin/python /opt/tts/dub.py promo-zh.mp4
TARGET=Japanese TTS_LANG=j TTS_VOICE=jf_alpha /opt/tts/bin/python /opt/tts/dub.py promo-zh.mp4

翻訳先の言語ごとに、動画と同じ場所に 4 つのファイルが作成されます。例えば promo-zh.english.mp4(吹き替え済み動画)、.srt(翻訳字幕)、.wav(音声トラックのみ)、.lines.txt(翻訳文。元のセグメント 1 つにつき 1 行)です。

ステップ4:翻訳を確認して修正

  1. 吹き替え済みの動画を、promo-zh.english.lines.txt と照らし合わせながら視聴します。
  2. 誤っている行や長すぎる行を、1 セグメント 1 行の形を保ったまま、このファイル上で直接修正します。
  3. 同じコマンドをもう一度実行します。スクリプトは再翻訳せずに修正後の行を使い、音声、字幕、動画を作り直します。

便利な設定:ORIGINAL_VOLUME=0 で元の音声を完全に消せます。MAX_SPEED=1.2 にすると話し方が落ち着き、WHISPER_MODEL=medium にすると処理時間は長くなりますが、元の音声をより正確に認識できます。

ステップ5:仕上げ

# Burn the translated subtitles into the dubbed video
ffmpeg -i promo-zh.english.mp4 -vf "subtitles=promo-zh.english.srt" -c:a copy promo-zh.english-sub.mp4
  • BGM を残したまま元の声だけを消したい場合は、先に Demucs などの音源分離ツールでボーカルを分離します。CPU でも動作しますが、時間がかかります。
  • 翻訳文は元の文より長くなりがちです。翻訳プロンプトで短い表現を指示するか、確認の際に行を短くしてください。
  • 字幕は自動音声認識で作成されるため、元の音声に含まれる人名や型番が誤認識されることがあります。確認の際にチェックしてください。

よくある質問

口の動きに合わせられますか?

いいえ。これはボイスオーバー方式の吹き替えで、商品デモ、画面録画、ナレーション付きの動画に適しています。リップシンクには別のモデルが必要で、通常は GPU も必要です。また、実在の人物が映っている場合は、本人の同意という問題が生じます。

翻訳にクラウドの AI を使えますか?

使えます。OpenAI 互換の API であれば、AI_BASE_URL、AI_API_KEY、AI_MODEL を設定してください。その場合、文字起こしの内容はサーバーの外に送信されるため、プロバイダーがサービスを提供している地域で、利用規約の範囲内で利用してください。

1 本の動画にどのくらい時間がかかりますか?

文字起こし、翻訳、音声生成はすべて CPU で実行されます。Whisper の small モデルと 7B の翻訳モデルを使う場合、1 分の動画で数分程度を見込んでください。通常、最も時間がかかるのは翻訳です。

上記の手順で解決しない場合は、サポートチケットを送信して IMIDC の 24 時間 365 日対応テクニカルサポートへご連絡ください。サーバー IP、OS と Python のバージョン、実行したコマンド、エラー全文を添えていただくと、より迅速に調査できます。

この回答はお役に立ちましたか?

関連チュートリアル