Cómo hacer videos con voz en off mediante código
Resumen
Un video = texto finalizado → TTS por oración → verificación ASR → línea de tiempo → animación HTML → captura fotograma a fotograma → composición con ffmpeg.
Principio fundamental: la línea de tiempo proviene del audio. Primero se graba cada frase, se verifica, se mide el tiempo real de inicio y fin de cada segmento de subtítulo, y luego se colocan todas las animaciones en esos tiempos. Las animaciones no tienen segundos fijos; si cambia la locución, solo se vuelve a ejecutar la línea de tiempo. La renderización consiste en capturar fotogramas de la página con __seek(t), el resultado es determinista y reproducible.
Adecuado para: explicaciones/tutoriales (2–5 minutos), videos promocionales de productos (20–40 segundos), videos educativos con subtítulos y capítulos. No adecuado para: edición con personas reales, videos que requieren material grabado real, videos musicales puros.
Flujo de trabajo
Cada paso tiene un punto de control; no pasar al siguiente si el anterior no se ha superado.
| # | Paso | Resultado | Punto de control | Referencia |
|---|---|---|---|---|
| 0 | Inicialización | Directorio de trabajo | bash scripts/init.sh <dir> exitoso | abajo |
| 1 | Elección del tema, verificación, texto | script.md, audio/make_script.py | Hechos verificados (experimentos primero); el usuario acepta explícitamente el texto antes de empezar la locución | references/script.md |
| 2 | Locución | audio/raw/NN.wav → audio/clips/NN.wav (1.3x) | Cada frase pasa ASR; las problemáticas se regrabar | references/voice.md |
| 3 | Línea de tiempo | build/narration.wav, build/timeline.json | La segmentación de subtítulos coincide con la voz | references/timeline.md |
| 4 | Material | Capturas de interfaz real, salida de terminal real | Coherente con la locución; usar example.com para dominios de ejemplo | references/capture.md |
| 5 | Composición | comp/index.html | Revisar cada composición de fotogramas | references/composition.md |
| 6 | Renderización | dist/video.mp4 | Duración = línea de tiempo, sonoridad -14 LUFS | references/render.md |
| 7 | Portada, texto | dist/cover-bili.png, dist/cover-douyin.png, dist/copy.txt | Portada dentro del área de recorte segura de cada plataforma | references/publish.md |
| 8 | Entrega, limpieza | Enviar al usuario; archivar fuentes | El usuario confirma que ha guardado antes de limpiar | abajo |
Inicialización
bash scripts/init.sh ~/work/<proyecto>/video-<número-episodio> copia template/ como directorio de trabajo, copia scripts/ a tools/, e instala playwright-core. La plantilla es una demo de 30 segundos que funciona directamente; antes de empezar, reemplaza estos contenidos de ejemplo:
script.md,audio/make_script.py: texto del episodio actual; los capítulos solo se definen una vez enCHAPTERSdemake_script.py, y las pausas entre capítulos, la barra de capítulo y las marcas de tiempo se derivan de ahí.audio/direction.txt: [Escena] de la instrucción del director de TTS.asr-prompt.txt: glosario de términos del episodio actual (escritura estándar, con pronunciación si es necesario).- Las escenas y animaciones de
comp/index.html, y el contenido de las portadas encomp/cover-*.html.
Comandos comunes
Ejecutar en el directorio de trabajo:
| Comando | Función |
|---|---|
python3 audio/make_script.py | Genera audio/script.json y audio/lines.json |
python3 tools/tts.py audio/lines.json audio/raw | Locución por oración; reutiliza audio existente según el texto; volver a ejecutar tras cambiar el texto |
bash tools/speed.sh audio/raw audio/clips 1.3 | Cambia velocidad sin cambiar tono |
python3 tools/asr.py audio/clips --en | Reconocimiento por oración: plain / con sugerencias de términos / modo inglés |
python3 tools/takes.py 3,7 3 | Graba 3 tomas de las oraciones 3 y 7, y las puntúa |
python3 tools/takes.py audio/var.json 2 | Graba 2 tomas de cada variante de la misma oración |
python3 tools/tts.py audio/lines.json audio/raw --adopt 3 audio/takes/03-0-1.raw.wav | Adopta una toma específica |
python3 tools/build_audio.py audio/clips build audio/script.json | Une la locución y genera la línea de tiempo |
python3 tools/align_chunks.py audio/clips build/timeline.json audio/script.json | Alinea los segmentos de subtítulos con la pronunciación real |
node tools/render.mjs --timeline=build/timeline.json --stills=2,8.5,20 | Captura fotogramas clave en build/stills (limpia antes cada vez) |
bash tools/contact.sh build/stills build/sheets | Crea collage 2x2; revisa con Read |
bash tools/render-par.sh build/timeline.json build/narration.wav dist/video.mp4 8 | Renderiza y mezcla el video completo con 8 procesos en paralelo |
node tools/render.mjs --html=comp/cover-4x3.html --width=1440 --height=1080 --png=dist/cover-bili.png | Genera portada |
python3 tools/bili_kw.py "configurar certificado SSL nginx" "baota https" | Investigación de palabras clave B站: top 6 por reproducciones |
Dependencias: ffmpeg, Node 18+, Google Chrome local, Python 3 + requests + openai-whisper (modelo small). TTS usa MiMo; la clave se lee de la variable de entorno MIMO_API_KEY o del archivo apuntado por MIMO_ENV_FILE; si ninguna está disponible, se le pide al usuario. Se puede usar otro TTS siempre que genere un WAV por oración.
Áreas propensas a errores
- Locucionar sin aprobación. Primero envía el texto al usuario; "continuar/empezar/está bien" cuenta como aprobación. Si cambias la redacción por la pronunciación, informa al usuario cada cambio en la entrega.
- Escribir subtítulos y locución como una sola cosa. Los subtítulos son para que los vea la gente (
Nginx,443,1.3), la locución es para que la lea el TTS (Engine X,cuatro cuatro tres,uno punto tres). Enmake_script.py, usaP(subtítulo, pronunciación)para separarlos; la concatenación de las pronunciaciones debe ser igual al texto completo del TTS. - Escuchar solo la mezcla completa. El reconocimiento de la mezcla completa identifica mal los términos técnicos; debe hacerse por oración. Para seleccionar tomas, solo mirar la puntuación plain y el resultado
--en; el modo prime con sugerencias de términos "corrige" incluso lo mal pronunciado. Las reglas de decisión están en voice.md. - Poner segundos fijos en las animaciones. Todas las animaciones después del inicio de la primera oración deben escribirse con
T(oración, segmento); después de cambios de velocidad o regrabaciones, los tiempos cambian. - Dejar la página vacía al inicio. Si la tarjeta aparece con 2–3 segundos de retraso, la página parece congelada; el contenido debe aparecer en el primer segmento de subtítulo del capítulo.
- Múltiples animaciones
fill: bothen el mismo elemento, o IDs duplicados. WAAPI no da error, el elemento puede desaparecer o quedarse en un estado incorrecto; ver composition.md. - Abrir la página con
file://para renderizar. Los CSS mask etc. fallan;render.mjsincluye un servidor HTTP local. - Portada recortada por la plataforma. B站 recorta una misma imagen a 4:3 y 16:9; TikTok 9:16 se recorta a 3:4; el contenido solo en el área segura.
- Redirección no permitida. No poner URLs, códigos QR o información de contacto que apunten a sitios reales en imágenes, portada o texto; no escribir "puedes verlo/buscarlo"; los dominios de ejemplo como
example.compueden aparecer en interfaces de demostración y terminales. Los videos publicados en múltiples plataformas no deben usar una plataforma de contenido como ejemplo; los videos promocionales no mencionan precios. - Anunciar éxito antes de ver el resultado. Enviar mensajes de progreso solo después de que el comando termine y se haya visto la salida.
Entrega y limpieza
- Entrega: video final, portada 4:3 para B站, portada 9:16 para TikTok,
dist/copy.txt(título, etiquetas, descripción, marcas de tiempo de capítulos). El video de B站 se publica por defecto también en TikTok y otras plataformas, por lo que se generan tanto la portada vertical como el texto de TikTok; si el usuario especifica una sola plataforma, solo se genera ese conjunto. Listar todos los cambios de redacción por pronunciación, y los puntos de tiempo dudosos para que el usuario los verifique. - Cuando el archivo supere el límite de la herramienta de chat (comúnmente 30MB), primero enviar una vista previa con
crf 22y explicar la ubicación de la versión de alta calidad. Si el envío falla por tiempo de espera y se reenvía, recordar al usuario que puede recibir dos copias. - Después de que el usuario confirme que ha guardado los materiales: archivar los archivos fuente reutilizables (
comp/y los materiales usados,audio/make_script.py, las especificaciones de variantes,script.md,copy.txt,direction.txt,asr-prompt.txt, el script de captura de pantalla,lab/) en la biblioteca de plantillas del usuario (si no hay acuerdo, preguntar al usuario dónde guardarlos, por ejemplo~/work/<proyecto>/video-template/<número-episodio>/), verificar condiff -rque sean idénticos, y luego eliminar el directorio de trabajo, el perfil temporal del navegador, el contenedor, el servidor de desarrollo (matar por PID, verificar la línea de comandos primero) y las dependencias frontend instaladas solo para las capturas. Conservar la caché del modelo whisper y las imágenes Docker.