應用場景與解決方案

AI 影片翻譯配音教程:Whisper 語音識別、大模型逐句翻譯、Kokoro 重新配音

5 個步驟 13 分鐘閱讀 62 次閱讀 0
本文目錄

用中文錄製的產品演示、教程或品牌影片,只要換成當地語言,就能觸達其他市場的買家。專業配音價格高、週期長。本文在你自己的伺服器上搭建一套自動化流程:faster-whisper 識別原影片語音並生成時間戳,本地大語言模型逐句翻譯,Kokoro 朗讀每一句譯文,指令碼再把每句配音放回原來的時間段。最終你會得到一個配音後的 MP4、一份翻譯好的字幕檔案,以及一份可編輯的譯文,可以在最終生成前修改校對。

只為你擁有版權或已獲授權翻譯的影片配音。機器翻譯和合成語音都會出錯:重要影片請讓母語人士稽核,並按各平台的規定標註 AI 生成的配音。

步驟 1:安裝元件

按照用 Kokoro 生成 AI 配音的方法配置好 /opt/tts,如果需要日語,還要安裝日語所需的額外軟體包。翻譯由 Ollama 完成,請參閱使用 Ollama 執行本地大模型。然後新增語音識別元件和 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 能識別約一百種源語言;目標語言則必須是 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

每種目標語言都會在影片旁邊生成四個檔案,例如 promo-zh.english.mp4(配音後的影片)、.srt(翻譯字幕)、.wav(純配音音軌)和 .lines.txt(譯文,每個原始語音片段對應一行)。

步驟 4:稽核並修改譯文

  1. 對照 promo-zh.english.lines.txt 觀看配音後的影片。
  2. 直接在該檔案中修改翻譯錯誤或過長的句子,保持每個片段一行。
  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
  • 如果要去掉原聲人聲、保留背景音樂,可以先用 Demucs 等音源分離工具把人聲分離出來。它可以在 CPU 上執行,但速度較慢。
  • 譯文往往比原文長。可以在翻譯提示詞中要求措辭更簡短,或在稽核時縮短句子。
  • 字幕基於自動語音識別生成,原影片中的人名和型號可能會被聽錯,請在稽核時重點檢查。

常見問題

能對上口型嗎?

不能。這是旁白式配音,適合產品演示、錄屏和有旁白講解的影片。對口型需要使用其他模型,通常還需要 GPU,而且畫面中出現真人時,還涉及是否取得本人同意的問題。

翻譯可以用雲端 AI 嗎?

可以。為任意相容 OpenAI 的 API 設定 AI_BASE_URL、AI_API_KEY 和 AI_MODEL 即可。這樣轉寫文本就會離開你的伺服器,因此請僅在服務商支援的地區、按其服務條款使用。

處理一條影片需要多長時間?

語音識別、翻譯和配音生成都在 CPU 上執行。使用 small 版 Whisper 模型和 7B 翻譯模型時,一分鐘的影片預計需要幾分鐘;其中翻譯通常是最慢的環節。

如果按以上步驟操作後問題仍未解決,請提交工單聯絡 IMIDC 7×24 技術支援,並附上伺服器 IP、系統版本和 Python 版本、執行過的命令以及完整報錯資訊,方便工程師快速定位。

這篇文章有幫助嗎?

相關教程