Casos de uso y soluciones

Doblar videos a otro idioma con IA: transcripción con Whisper, traducción con LLM y voz con Kokoro

5 pasos 20 min de lectura 11 vistas 0
Contenido

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.

Doble solo videos que sean suyos o que tenga permiso para traducir. La traducción automática y las voces sintéticas cometen errores: pida a un hablante nativo que revise los videos importantes y etiquete la voz generada por IA según las normas de cada plataforma.

Paso 1: Instalar los componentes

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.

Paso 2: Crear el script de doblaje

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.

Paso 3: Doblar un video

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).

Paso 4: Revisar y corregir la traducción

  1. Vea el video doblado junto con promo-zh.english.lines.txt.
  2. Corrija las líneas erróneas o demasiado largas directamente en ese archivo, manteniendo una línea por segmento.
  3. Vuelva a ejecutar el mismo comando: el script usa sus líneas corregidas en lugar de traducir de nuevo y vuelve a generar la voz, los subtítulos y el video.

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.

Paso 5: Toques finales

# 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
  • Para eliminar la voz original pero conservar la música de fondo, separe primero las voces con una herramienta de separación de fuentes como Demucs. Funciona en CPU, pero es lenta.
  • Las traducciones suelen ser más largas que el original. Pida una redacción más breve en el prompt de traducción o acorte las líneas durante la revisión.
  • Los subtítulos se crean con reconocimiento automático de voz, por lo que los nombres y los números de modelo del original pueden transcribirse mal; compruébelos durante la revisión.

Preguntas frecuentes

¿Sincroniza el movimiento de los labios?

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.

¿Puedo usar una IA en la nube para la traducción?

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.

¿Cuánto tarda un video?

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.

¿Fue útil la respuesta?

Tutoriales relacionados