Un video de demostración de producto, un tutorial o un video de marca grabado en chino puede llegar a compradores de otros mercados cuando habla su idioma. El doblaje profesional es caro y lento. Esta guía crea un flujo automático en su propio servidor: faster-whisper transcribe la voz original con marcas de tiempo, un modelo de lenguaje local la traduce línea por línea, Kokoro locuta cada línea traducida y el script encaja cada línea en su intervalo de tiempo original. Obtendrá un MP4 doblado, un archivo de subtítulos traducido y una traducción editable que puede corregir antes de la ejecución final.
Configure /opt/tts como se describe en locución con IA con Kokoro, incluidos los paquetes adicionales para japonés si necesita ese idioma. La traducción se ejecuta con Ollama; consulte ejecutar un LLM local con Ollama. Después añada el reconocimiento de voz y el cliente de la 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 entiende unos cien idiomas de origen; el idioma de destino debe ser uno que Kokoro pueda hablar (consulte la tabla de voces de la guía de locución). Para otros idiomas de destino, traduzca solo los subtítulos.
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
Cada línea traducida empieza en el momento en que empezaba la línea original. Si es demasiado larga para su intervalo, se pronuncia hasta un 30 % más rápido; si aun así es demasiado larga, la línea siguiente empieza un poco más tarde, de modo que las líneas nunca se solapan. El audio original se mantiene de fondo a bajo volumen, lo que conserva la música y el sonido ambiente.
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
Para cada idioma de destino obtendrá cuatro archivos junto al video, por ejemplo promo-zh.english.mp4 (video doblado), .srt (subtítulos traducidos), .wav (solo la pista de voz) y .lines.txt (la traducción, una línea por segmento original).
promo-zh.english.lines.txt.Ajustes útiles: ORIGINAL_VOLUME=0 elimina por completo el audio original, MAX_SPEED=1.2 mantiene un habla más pausada y WHISPER_MODEL=medium reconoce la voz original con más precisión, a costa de más tiempo.
# 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
No. Se trata de un doblaje de tipo voz en off, que funciona bien para demostraciones de producto, grabaciones de pantalla y videos narrados. La sincronización labial requiere otros modelos, normalmente una GPU, y plantea cuestiones de consentimiento cuando aparecen personas reales en pantalla.
Sí. Configure AI_BASE_URL, AI_API_KEY y AI_MODEL para cualquier API compatible con OpenAI. En ese caso, la transcripción sale de su servidor, así que use cada proveedor solo en las regiones donde ofrece servicio y conforme a sus condiciones.
La transcripción, la traducción y la generación de voz se ejecutan en la CPU. Calcule varios minutos para un video de un minuto con el modelo small de Whisper y un modelo de traducción de 7B; la traducción suele ser la parte más lenta.
¿Sigue sin resolverlo después de seguir estos pasos? Abra un ticket de soporte y el equipo técnico de IMIDC, disponible 24/7, le ayudará. Incluya la IP del servidor, el sistema operativo y la versión de Python, el comando que ejecutó y la salida completa del error para que podamos localizar el problema más rápido.