video-sync
Combina shots.json + video original en un solo video: en un lado la imagen, la información de la toma actual en el otro lado, la información cambia cuando cambia la toma, la tabla de tomas se desplaza automáticamente y resalta la toma actual.
El diseño solo depende de la relación de aspecto, nada más:
| Video original | Diseño | Método de composición |
|---|---|---|
| Horizontal (ancho > alto) o cuadrado | Imagen arriba, información abajo | vstack |
| Vertical (alto > ancho) | Imagen a la izquierda, información a la derecha | hstack |
En ambos casos, se garantiza que la imagen se escala sin recortes ni estiramientos, y el panel ocupa el espacio restante.
Solo se capturan tres imágenes, el movimiento se lo dejamos a ffmpeg. Lo único que cambia en el panel son dos cosas: la posición de desplazamiento y qué fila está resaltada.
Así que medimos la altura de las filas una vez, capturamos una imagen base y dos imágenes largas desplegadas de la tabla de tomas (una oscurecida y otra completamente iluminada).
El desplazamiento = recortar una ventana de la imagen larga oscurecida, el resaltado = recortar una fila de la imagen larga iluminada y superponerla, las posiciones se controlan con un sendcmd
que envía una pequeña expresión en cada punto de corte. Para un video de 53 tomas, las capturas se reducen de 53 a 3 (12 segundos).
La toma actual se fija en la segunda fila: las tomas 1 y 2 no se desplazan (el resaltado se mueve una fila hacia abajo), a partir de la toma 3, cada corte desplaza una fila hacia arriba.
El movimiento ocurre solo en los cortes, no dentro de la toma: en el punto de corte, el desplazamiento tarda --ease (por defecto 0.45) segundos en llegar y luego se detiene.
Si la lista se moviese continuamente durante una toma larga de 16 segundos, el espectador tendría que estar mirando fijamente el movimiento durante 16 segundos.
La altura de la fila la determina el contenido: si la descripción es larga, la fila será más alta. La barra de resaltado se divide según la altura de la fila (la altura de crop no se puede cambiar durante la ejecución).
{baseDir} = directorio donde se encuentra este archivo. Script {baseDir}/scripts/video-sync.mjs, sin dependencias, se ejecuta directamente con node.
Límites (lo que no hace): No hace análisis de tomas (eso es trabajo de video-shots, este skill consume su shots.json),
no edita, no añade transiciones, no pone música, no quema subtítulos en el video, no hace renderizado cuadro por cuadro (los efectos de movimiento los hace ffmpeg recortando ventanas por tiempo).
Paso 0 — Primero, tener los datos de análisis
Necesitas un archivo shots.json (salida de video-shots) y el video original correspondiente. Si tienes frames/ (un fotograma clave por toma), mejor:
la tabla de tomas usará el primer fotograma como miniatura; si no, se deja un espacio en blanco.
shots.json debe coincidir con este video: si meta.durationSeconds no coincide con el video original, no combines; primero corrige el análisis.
Paso 1 — Primero ver la geometría, luego procesar el video
node {baseDir}/scripts/video-sync.mjs plan shots.json --video <video>
Solo calcula, no combina. Resultado en segundos: diseño, área de video, área de panel, tamaño de salida. Verifica que estos tres números sean correctos antes de continuar — combinar un video de 3 minutos lleva uno o dos minutos, si te equivocas y tienes que repetir, no vale la pena.
Si no te gusta, ajústalo: --panel <proporción> (en horizontal es alto del panel ÷ alto del video, por defecto 0.8; en vertical es ancho del panel ÷ ancho del video, por defecto 1.6),
--width / --height (límite superior del área de video, por defecto 1920 / 1080).
Si el video original es demasiado pequeño, agranda el área de video: el panel se calcula a partir del área de video. Para un video de 640×360, el panel sería solo 640×288,
con cuatro columnas apretadas ilegibles. --scale 2 agranda el área de video a 1280×720 y el panel a 1280×576, manteniendo la relación de aspecto.
El valor por defecto es 1 (solo reduce, no agranda). plan / panels / compose / export deben usar el mismo --scale.
Paso 2 — Renderizar el panel
node {baseDir}/scripts/video-sync.mjs panels shots.json --video <video> \
--frames frames --lang zh|en [--out panels] [--chrome <ruta>]
Genera static.png (base), list-dim.png / list-lit.png (imágenes largas desplegadas de toda la tabla de tomas,
una oscurecida y otra iluminada), layout.json (posiciones de cada fila, para calcular el desplazamiento durante la composición), y además un archivo panels/panel.html —
si lo abres en un navegador, puedes previsualizar el panel, cambia #S07 para ver el efecto de diferentes tomas (la animación de desplazamiento en la vista previa es igual que la de la exportación).
Para cambiar el diseño, modifica {baseDir}/scripts/panel.css, luego vuelve a ejecutar panels para obtener el nuevo diseño,
sin tocar el script ni replantear la lógica de captura. Convenciones de estilo en {baseDir}/references/panel-style.md.
Paso 3 — Composición
node {baseDir}/scripts/video-sync.mjs compose shots.json --video <video> \
[--panels panels] [-o out.mp4] [--crf 20] [--ease 0.45]
La pista de audio se copia directamente del video original (si es mudo, no se añade pista de audio). Al terminar, stderr mostrará el tamaño de salida, duración y diseño. Verifícalo.
También puedes hacer los dos pasos en uno:
node {baseDir}/scripts/video-sync.mjs export shots.json --video <video> -o out.mp4 --frames frames
Paso 4 — Verificar extrayendo fotogramas ⛔ No se puede saltar
No entregues solo porque la composición no dio error. Extrae al menos tres fotogramas para verificar que la información coincida con la imagen:
for t in 5 30 60; do ffmpeg -v error -y -ss $t -i out.mp4 -frames:v 1 -q:v 3 /tmp/check-$t.jpg; done
Verifica tres cosas: si este fotograma pertenece a la toma correcta, si el número de toma en el panel es el mismo, si la fila de resaltado sigue correctamente.
Si no coinciden, generalmente es porque shots.json no es del mismo video, o --video apunta al video incorrecto.
Reporta en una frase clara: ruta de salida, tamaño, duración, diseño, número de tomas, qué imágenes del panel no se generaron (si las hay).
Límites
- El desplazamiento solo ocurre en los puntos de corte: se desplaza durante
--easesegundos y luego se detiene. Durante la toma, el panel está estático. Se podría hacer que la lista se moviese continuamente durante toda la toma (modificando la segmentación demotionPlan), pero en tomas largas de 16 segundos el espectador tendría que estar mirando fijamente el movimiento todo el tiempo. - Requiere un navegador sin cabeza (Chrome / Chromium / Edge). Es el precio de poder cambiar el diseño libremente:
si usáramos
drawtextde ffmpeg para el panel, habría que implementar nosotros mismos el salto de línea para chino, las miniaturas, el resaltado, y cambiar el diseño implicaría modificar el filtro. - El vocabulario (plano / tipo / movimiento / transición) viene incluido en este skill, no importa desde otro directorio —
el skill debe poder copiarse entero. Si
video-shotsañade nuevos términos, también hay que añadirlos aquí; de lo contrario, se mostrarán como claves de enumeración. - Renderizar el panel siempre son tres capturas, independientemente del número de tomas: 53 tomas ≈ 12 segundos. La composición depende de la duración del video, 203 segundos ≈ 75 segundos.
Autocomprobación
node {baseDir}/scripts/selftest.mjs
122 aserciones, sin usar ffmpeg ni abrir navegador: geometría (horizontal/cuadrado/vertical, tamaño de lados pares, límites superior e inferior, controles deslizantes), contrato de datos de la página del panel (envío de vocabulario, roles de ritmo, si hay miniatura o no, escapado), comandos de animación (cada uno debe ser corto — si la expresión es demasiado larga, ffmpeg falla directamente al configurarla), parámetros de ffmpeg. Ejecuta esto después de modificar el script.