Communitygithub.com

calesthio/OpenMontage

Traduce y dobla videos existentes a múltiples idiomas preservando la sincronización de labios y los patrones de habla naturales, mediante la API de HeyGen.

¿Qué es OpenMontage?

OpenMontage es una habilidad de agente de IA que permite traducir y doblar videos a varios idiomas utilizando la API de HeyGen /v2/video_translate, manteniendo la sincronización de labios y la voz del orador original. Es parte de OpenMontage, un estudio de video agéntico de código abierto con más de 61,000 estrellas en GitHub. La habilidad soporta traducción básica con sincronización de labios, traducción solo de audio para mayor velocidad, manejo de múltiples oradores y opciones avanzadas como vocabulario personalizado, subtítulos SRT y traducción a varios idiomas en una sola solicitud. Funciona con plataformas como Claude Code, Cursor y Codex, y se integra fácilmente mediante la clave de API de HeyGen. Los usuarios pueden enviar una URL de video o un ID de video de HeyGen, y el agente se encarga de crear el trabajo de traducción, consultar el estado hasta que esté completo y descargar el video traducido. La habilidad también incluye manejo de errores, control de música de fondo y mejora del habla.

Compatible con~Claude Code~Codex CLI~Cursor✓Antigravity
npx skills add https://github.com/calesthio/OpenMontage/tree/main/.agents/skills/video-translate

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Traducción de Video (HeyGen)

Traduce y dobla videos existentes a múltiples idiomas, preservando la sincronización de labios y los patrones de habla naturales. Proporciona una URL de video o un ID de video de HeyGen — no es necesario crear el video en HeyGen primero.

Autenticación

Todas las solicitudes requieren el encabezado X-Api-Key. Establece la variable de entorno HEYGEN_API_KEY.

curl -X POST "https://api.heygen.com/v2/video_translate" \
  -H "X-Api-Key: $HEYGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"video_url": "https://example.com/video.mp4", "output_language": "es-ES"}'

Flujo de Trabajo Predeterminado

  1. Proporciona una URL de video o un ID de video de HeyGen
  2. Llama a POST /v2/video_translate con el idioma de destino
  3. Consulta GET /v2/video_translate/{translate_id} hasta que el estado sea completed
  4. Descarga el video traducido desde la URL devuelta

Creando un Trabajo de Traducción

Campos de la Solicitud

CampoTipoOblig.Descripción
video_urlcadenaS*URL del video a traducir (*o video_id)
video_idcadenaS*ID de video de HeyGen (*o video_url)
output_languagecadenaSCódigo del idioma de destino (ej. "es-ES")
titlecadenaNombre para el video traducido
translate_audio_onlybooleanoSolo audio, sin sincronización de labios (más rápido)
speaker_numnúmeroNúmero de oradores en el video
callback_idcadenaID personalizado para seguimiento por webhook
callback_urlcadenaURL para notificación de finalización

Debe proporcionarse video_url o video_id.

curl

curl -X POST "https://api.heygen.com/v2/video_translate" \
  -H "X-Api-Key: $HEYGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "video_url": "https://example.com/original-video.mp4",
    "output_language": "es-ES",
    "title": "Versión en Español"
  }'

TypeScript

interface VideoTranslateRequest {
  video_url?: string;
  video_id?: string;
  output_language: string;
  title?: string;
  translate_audio_only?: boolean;
  speaker_num?: number;
  callback_id?: string;
  callback_url?: string;
}

interface VideoTranslateResponse {
  error: null | string;
  data: {
    video_translate_id: string;
  };
}

async function translateVideo(config: VideoTranslateRequest): Promise<string> {
  const response = await fetch("https://api.heygen.com/v2/video_translate", {
    method: "POST",
    headers: {
      "X-Api-Key": process.env.HEYGEN_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(config),
  });

  const json: VideoTranslateResponse = await response.json();

  if (json.error) {
    throw new Error(json.error);
  }

  return json.data.video_translate_id;
}

Python

import requests
import os

def translate_video(config: dict) -> str:
    response = requests.post(
        "https://api.heygen.com/v2/video_translate",
        headers={
            "X-Api-Key": os.environ["HEYGEN_API_KEY"],
            "Content-Type": "application/json"
        },
        json=config
    )

    data = response.json()
    if data.get("error"):
        raise Exception(data["error"])

    return data["data"]["video_translate_id"]

Idiomas Soportados

IdiomaCódigoNotas
Inglés (EE. UU.)en-USFuente predeterminada
Español (España)es-ESEspañol europeo
Español (México)es-MXLatinoamericano
Francésfr-FRFrancés estándar
Alemánde-DEAlemán estándar
Italianoit-ITItaliano estándar
Portugués (Brasil)pt-BRPortugués brasileño
Japonésja-JPJaponés estándar
Coreanoko-KRCoreano estándar
Chino (Mandarín)zh-CNChino simplificado
Hindihi-INHindi estándar
Árabear-SAÁrabe moderno estándar

Opciones de Traducción

Traducción Básica (con sincronización de labios)

const config = {
  video_url: "https://example.com/original.mp4",
  output_language: "es-ES",
  title: "Traducción al Español",
};

Traducción Solo de Audio (más rápida, sin sincronización de labios)

const config = {
  video_url: "https://example.com/original.mp4",
  output_language: "es-ES",
  translate_audio_only: true,
};

Videos con Múltiples Oradores

const config = {
  video_url: "https://example.com/interview.mp4",
  output_language: "fr-FR",
  speaker_num: 2,
};

Opciones Avanzadas (API v4)

Para un mayor control sobre la traducción:

interface VideoTranslateV4Request {
  input_video_id?: string;
  google_url?: string;
  output_languages: string[];        // Varios idiomas en una sola llamada
  name: string;
  srt_key?: string;                  // Subtítulos SRT personalizados
  instruction?: string;
  vocabulary?: string[];             // Términos a preservar tal cual
  brand_voice_id?: string;
  speaker_num?: number;
  keep_the_same_format?: boolean;
  input_language?: string;
  enable_video_stretching?: boolean;
  disable_music_track?: boolean;
  enable_speech_enhancement?: boolean;
  srt_role?: "input" | "output";
  translate_audio_only?: boolean;
}

Múltiples Idiomas de Salida

const config = {
  input_video_id: "original_video_id",
  output_languages: ["es-ES", "fr-FR", "de-DE"],
  name: "Traducciones multilingües",
};

Vocabulario Personalizado (preserva términos específicos)

const config = {
  video_url: "https://example.com/product-demo.mp4",
  output_language: "ja-JP",
  vocabulary: ["SuperWidget", "Pro Max", "TechCorp"],
};

Subtítulos SRT Personalizados

const config = {
  video_url: "https://example.com/video.mp4",
  output_language: "es-ES",
  srt_key: "path/to/custom-subtitles.srt",
  srt_role: "input",
};

Consultando el Estado de la Traducción

curl

curl -X GET "https://api.heygen.com/v2/video_translate/{translate_id}" \
  -H "X-Api-Key: $HEYGEN_API_KEY"

TypeScript

interface TranslateStatusResponse {
  error: null | string;
  data: {
    id: string;
    status: "pending" | "processing" | "completed" | "failed";
    video_url?: string;
    message?: string;
  };
}

async function getTranslateStatus(translateId: string): Promise<TranslateStatusResponse["data"]> {
  const response = await fetch(
    `https://api.heygen.com/v2/video_translate/${translateId}`,
    { headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
  );

  const json: TranslateStatusResponse = await response.json();

  if (json.error) {
    throw new Error(json.error);
  }

  return json.data;
}

Consulta Periódica para Finalización

Las traducciones tardan más que la generación de video estándar — permite hasta 30 minutos.

async function waitForTranslation(
  translateId: string,
  maxWaitMs = 1800000,
  pollIntervalMs = 30000
): Promise<string> {
  const startTime = Date.now();

  while (Date.now() - startTime < maxWaitMs) {
    const status = await getTranslateStatus(translateId);

    switch (status.status) {
      case "completed":
        return status.video_url!;
      case "failed":
        throw new Error(status.message || "La traducción falló");
      default:
        console.log(`Estado: ${status.status}...`);
        await new Promise((r) => setTimeout(r, pollIntervalMs));
    }
  }

  throw new Error("La traducción agotó el tiempo de espera");
}

Flujo de Trabajo Completo

async function translateAndDownload(
  videoUrl: string,
  targetLanguage: string
): Promise<string> {
  console.log(`Iniciando traducción a ${targetLanguage}...`);
  const translateId = await translateVideo({
    video_url: videoUrl,
    output_language: targetLanguage,
  });
  console.log(`ID de traducción: ${translateId}`);

  console.log("Procesando traducción...");
  const translatedVideoUrl = await waitForTranslation(translateId);
  console.log(`Traducción completa: ${translatedVideoUrl}`);

  return translatedVideoUrl;
}

const spanishVideo = await translateAndDownload(
  "https://example.com/my-video.mp4",
  "es-ES"
);

Traducción por Lotes

Traduce a varios idiomas en paralelo:

async function translateToMultipleLanguages(
  sourceVideoUrl: string,
  targetLanguages: string[]
): Promise<Record<string, string>> {
  const results: Record<string, string> = {};

  const translatePromises = targetLanguages.map(async (lang) => {
    const translateId = await translateVideo({
      video_url: sourceVideoUrl,
      output_language: lang,
    });
    return { lang, translateId };
  });

  const translationJobs = await Promise.all(translatePromises);

  for (const job of translationJobs) {
    try {
      const videoUrl = await waitForTranslation(job.translateId);
      results[job.lang] = videoUrl;
    } catch (error) {
      results[job.lang] = `error: ${error.message}`;
    }
  }

  return results;
}

const translations = await translateToMultipleLanguages(
  "https://example.com/original.mp4",
  ["es-ES", "fr-FR", "de-DE", "ja-JP"]
);

Características

  • Sincronización de Labios — Ajusta automáticamente los movimientos labiales del orador para que coincidan con el audio traducido
  • Clonación de Voz — El audio traducido coincide con las características de voz del orador original
  • Control de Pista Musical — Opcionalmente elimina la música de fondo con disable_music_track: true
  • Mejora del Habla — Mejora la calidad del audio con enable_speech_enhancement: true

Mejores Prácticas

  1. La calidad de la fuente importa — Usa videos fuente de alta calidad para mejores resultados
  2. Audio claro — Los videos con habla clara se traducen mejor
  3. Orador único — Mejores resultados con contenido de un solo orador
  4. Ritmo moderado — El habla muy rápida puede afectar la calidad
  5. Prueba primero — Prueba con clips cortos antes de traducir videos largos
  6. Permite tiempo extra — La traducción toma más tiempo que la generación de video (hasta 30 min)

Manejo de Errores

Errores comunes y cómo manejarlos:

async function safeTranslate(
  videoUrl: string,
  targetLanguage: string
): Promise<{ success: boolean; result?: string; error?: string }> {
  try {
    const url = await translateAndDownload(videoUrl, targetLanguage);
    return { success: true, result: url };
  } catch (error) {
    if (error.message.includes("quota")) {
      return { success: false, error: "Créditos insuficientes" };
    }
    if (error.message.includes("duration")) {
      return { success: false, error: "Video demasiado largo" };
    }
    if (error.message.includes("format")) {
      return { success: false, error: "Formato de video no soportado" };
    }
    return { success: false, error: error.message };
  }
}

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Skills relacionados